using System;

using Gamecraft.Wires;
using Svelto.ECS;
using Svelto.ECS.Experimental;

using GamecraftModdingAPI.Utility;

namespace GamecraftModdingAPI.Blocks
{
    public class Wire
    {
        internal static SignalEngine signalEngine;

        protected EGID startPortEGID;

        protected EGID endPortEGID;

        protected EGID startBlockEGID;

        protected EGID endBlockEGID;

        protected EGID wireEGID;

        protected bool inputToOutput;
        
        protected byte startPort;

        protected byte endPort;

        public static Wire Connect(SignalingBlock start, byte startPort, SignalingBlock end, byte endPort)
        {
            WireEntityStruct wire = signalEngine.CreateNewWire(start.Id, startPort, end.Id, endPort);
            return new Wire(wire);
        }

        /// <summary>
        /// An existing wire connection ending at the specified input.
        /// If multiple exist, this will return the first one found.
        /// </summary>
        /// <param name="end">Destination block.</param>
        /// <param name="endPort">Port number.</param>
        /// <returns>The wire, where the end of the wire is the block port specified, or null if does not exist.</returns>
        public static Wire ConnectedToInputPort(SignalingBlock end, byte endPort)
        {
            EGID port = signalEngine.MatchBlockInputToPort(end, endPort, out bool exists);
            if (!exists) return null;
            WireEntityStruct wire = signalEngine.MatchPortToWire(port, end.Id, out exists);
            if (exists)
            {
                return new Wire(new Block(wire.sourceBlockEGID), end, wire.sourcePortUsage, endPort);
            }
            return null;
        }

        /// <summary>
        /// An existing wire connection starting at the specified output.
        /// If multiple exist, this will return the first one found.
        /// </summary>
        /// <param name="start">Source block entity ID.</param>
        /// <param name="startPort">Port number.</param>
        /// <returns>The wire, where the start of the wire is the block port specified, or null if does not exist.</returns>
        public static Wire ConnectedToOutputPort(SignalingBlock start, byte startPort)
        {
            EGID port = signalEngine.MatchBlockOutputToPort(start, startPort, out bool exists);
            if (!exists) return null;
            WireEntityStruct wire = signalEngine.MatchPortToWire(port, start.Id, out exists);
            if (exists)
            {
                return new Wire(start, new Block(wire.destinationBlockEGID), startPort, wire.destinationPortUsage);
            }
            return null;
        }

        /// <summary>
        /// Construct a wire object from an existing connection.
        /// </summary>
        /// <param name="start">Starting block ID.</param>
        /// <param name="end">Ending block ID.</param>
        /// <param name="startPort">Starting port number, or guess if omitted.</param>
        /// <param name="endPort">Ending port number, or guess if omitted.</param>
        /// <exception cref="WireInvalidException">Guessing failed or wire does not exist.</exception>
        public Wire(Block start, Block end, byte startPort = Byte.MaxValue, byte endPort = Byte.MaxValue)
        {
            startBlockEGID = start.Id;
            endBlockEGID = end.Id;
            // find block ports
            WireEntityStruct wire = signalEngine.MatchBlocksToWire(start.Id, end.Id, out bool exists, startPort, endPort);
            if (exists)
            {
                wireEGID = wire.ID;
                endPortEGID = signalEngine.MatchBlockInputToPort(end, wire.destinationPortUsage, out exists);
                if (!exists) throw new WireInvalidException("Wire end port not found");
                startPortEGID = signalEngine.MatchBlockOutputToPort(start, wire.sourcePortUsage, out exists);
                if (!exists) throw new WireInvalidException("Wire start port not found");
                inputToOutput = false;
                endPort = wire.destinationPortUsage;
                startPort = wire.sourcePortUsage;
            }
            else
            {
                // flip I/O around and try again
                wire = signalEngine.MatchBlocksToWire(end.Id, start.Id, out exists, endPort, startPort);
                if (exists)
                {
                    wireEGID = wire.ID;
                    endPortEGID = signalEngine.MatchBlockOutputToPort(end, wire.sourcePortUsage, out exists);
                    if (!exists) throw new WireInvalidException("Wire end port not found");
                    startPortEGID = signalEngine.MatchBlockInputToPort(start, wire.destinationPortUsage, out exists);
                    if (!exists) throw new WireInvalidException("Wire start port not found");
                    inputToOutput = true; // end is actually the source
                    // NB: start and end are handled exactly as they're received as params.
                    // This makes wire traversal easier, but makes logic in this class a bit more complex
                    endPort = wire.sourcePortUsage;
                    startPort = wire.destinationPortUsage;
                }
                else
                {
                    throw new WireInvalidException("Wire not found");
                }
            }
        }

        /// <summary>
        /// Construct a wire object from an existing wire connection.
        /// </summary>
        /// <param name="start">Starting block ID.</param>
        /// <param name="end">Ending block ID.</param>
        /// <param name="startPort">Starting port number.</param>
        /// <param name="endPort">Ending port number.</param>
        /// <param name="wire">The wire ID.</param>
        /// <param name="inputToOutput">Whether the wire direction goes input -> output (true) or output -> input (false, preferred).</param>
        public Wire(Block start, Block end, byte startPort, byte endPort, EGID wire, bool inputToOutput)
        {
            this.startBlockEGID = start.Id;
            this.endBlockEGID = end.Id;
            this.inputToOutput = inputToOutput;
            this.wireEGID = wire;
            if (inputToOutput)
            {
                endPortEGID = signalEngine.MatchBlockOutputToPort(start, startPort, out bool exists); 
                if (!exists) throw new WireInvalidException("Wire end port not found");
                startPortEGID = signalEngine.MatchBlockInputToPort(end, endPort, out exists);
                if (!exists) throw new WireInvalidException("Wire start port not found");
            }
            else
            {
                endPortEGID = signalEngine.MatchBlockInputToPort(end, endPort, out bool exists);
                if (!exists) throw new WireInvalidException("Wire end port not found");
                startPortEGID = signalEngine.MatchBlockOutputToPort(start, startPort, out exists);
                if (!exists) throw new WireInvalidException("Wire start port not found");
            }
            this.startPort = startPort;
            this.endPort = endPort;
        }

        /// <summary>
        /// Construct a wire object from an existing wire connection.
        /// </summary>
        /// <param name="wireEgid">The wire ID.</param>
        public Wire(EGID wireEgid)
        {
            this.wireEGID = wireEgid;
            WireEntityStruct wire = signalEngine.GetWire(wireEGID);
            this.startBlockEGID = wire.sourceBlockEGID;
            this.endBlockEGID = wire.destinationBlockEGID;
            this.inputToOutput = false;
            endPortEGID = signalEngine.MatchBlockInputToPort(wire.destinationBlockEGID, wire.destinationPortUsage, out bool exists);
            if (!exists) throw new WireInvalidException("Wire end port not found");
            startPortEGID = signalEngine.MatchBlockOutputToPort(wire.sourceBlockEGID, wire.sourcePortUsage, out exists);
            if (!exists) throw new WireInvalidException("Wire start port not found");
            this.endPort = wire.destinationPortUsage;
            this.startPort = wire.sourcePortUsage;
        }

        internal Wire(WireEntityStruct wire)
        {
            this.wireEGID = wire.ID;
            this.startBlockEGID = wire.sourceBlockEGID;
            this.endBlockEGID = wire.destinationBlockEGID;
            inputToOutput = false;
            endPortEGID = signalEngine.MatchBlockInputToPort(wire.destinationBlockEGID, wire.destinationPortUsage, out bool exists);
            if (!exists) throw new WireInvalidException("Wire end port not found");
            startPortEGID = signalEngine.MatchBlockOutputToPort(wire.sourceBlockEGID, wire.sourcePortUsage, out exists);
            if (!exists) throw new WireInvalidException("Wire start port not found");
            this.endPort = wire.destinationPortUsage;
            this.startPort = wire.sourcePortUsage;
        }

        /// <summary>
        /// The wire's in-game id.
        /// </summary>
        public EGID Id
        {
            get => wireEGID;
        }

        /// <summary>
        /// The wire's signal value, as a float.
        /// </summary>
        public float Float
        {
            get
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return 0f;
                return cds.valueAsFloat;
            }

            set
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return;
                cds.valueAsFloat = value;
            }
        }

        /// <summary>
        /// The wire's string signal.
        /// </summary>
        public string String
        {
            get
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return "";
                return cds.valueAsEcsString;
            }

            set
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return;
                cds.valueAsEcsString.Set(value);
            }
        }
        
        /// <summary>
        /// The wire's raw string signal.
        /// </summary>
        public ECSString ECSString
        {
            get
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return default;
                return cds.valueAsEcsString;
            }

            set
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return;
                cds.valueAsEcsString = value;
            }
        }

        /// <summary>
        /// The wire's signal id.
        /// I'm 50% sure this is useless.
        /// </summary>
        public uint SignalId
        {
            get
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return uint.MaxValue;
                return cds.valueAsID;
            }
            
            set
            {
                ref ChannelDataStruct cds = ref signalEngine.GetChannelDataStruct(startPortEGID, out bool exists);
                if (!exists) return;
                cds.valueAsID = value;
            }
        }

        /// <summary>
        /// The block at the beginning of the wire.
        /// </summary>
        public SignalingBlock Start
        {
            get => new SignalingBlock(startBlockEGID);
        }

        /// <summary>
        /// The port number that the beginning of the wire connects to.
        /// </summary>
        public byte StartPort
        {
            get => startPort;
        }
        
        /// <summary>
        /// The block at the end of the wire.
        /// </summary>
        public SignalingBlock End
        {
            get => new SignalingBlock(endBlockEGID);
        }
        
        /// <summary>
        /// The port number that the end of the wire connects to.
        /// </summary>
        public byte EndPort
        {
            get => endPort;
        }

        /// <summary>
        /// Create a copy of the wire object where the direction of the wire is guaranteed to be from a block output to a block input.
        /// This is simply a different memory configuration and does not affect the in-game wire (which is always output -> input).
        /// </summary>
        /// <returns>A copy of the wire object.</returns>
        public Wire OutputToInputCopy()
        {
            return new Wire(wireEGID);
        }
        
        /// <summary>
        /// Convert the wire object to the direction the signal flows.
        /// Signals on wires always flow from a block output port to a block input port.
        /// This is simply a different memory configuration and does not affect the in-game wire (which is always output -> input).
        /// </summary>
        public void OutputToInputInPlace()
        {
            if (inputToOutput)
            {
                inputToOutput = false;
                // swap inputs and outputs
                EGID temp = endBlockEGID;
                endBlockEGID = startBlockEGID;
                startBlockEGID = temp;
                temp = endPortEGID;
                endPortEGID = startPortEGID;
                startPortEGID = temp;
                byte tempPortNumber = endPort;
                endPort = startPort;
                startPort = tempPortNumber;
            }
        }

        public override string ToString()
        {
            if (signalEngine.Exists<WireEntityStruct>(wireEGID))
            {
                return $"{nameof(Id)}: {Id}, Start{nameof(Start.Id)}: {Start.Id}, End{nameof(End.Id)}: {End.Id}, ({Start.Type}::{StartPort} aka {Start.PortName(StartPort, inputToOutput)}) -> ({End.Type}::{EndPort} aka {End.PortName(EndPort, !inputToOutput)})";
            }
            return $"{nameof(Id)}: {Id}, Start{nameof(Start.Id)}: {Start.Id}, End{nameof(End.Id)}: {End.Id}, ({Start.Type}::{StartPort} -> {End.Type}::{EndPort})";
        }

        internal static void Init() { }
    }
}