using System;
using Svelto.ECS;
using Svelto.DataStructures;
using Gamecraft.Wires;

using GamecraftModdingAPI.Engines;

namespace GamecraftModdingAPI.Blocks
{
    /// <summary>
    /// Engine which executes signal actions
    /// </summary>
    public class SignalEngine : IApiEngine, IFactoryEngine
    {
		public const float POSITIVE_HIGH = 1.0f;
		public const float NEGATIVE_HIGH = -1.0f;
		public const float HIGH = 1.0f;
		public const float ZERO = 0.0f;

        public string Name { get; } = "GamecraftModdingAPISignalGameEngine";

        public EntitiesDB entitiesDB { set; private get; }
        
        public IEntityFactory Factory { get; set; }
        
		public bool isRemovable => false;

		public bool IsInGame = false;

        public void Dispose()
        {
            IsInGame = false;
        }

        public void Ready()
        {
            IsInGame = true;
        }

        // implementations for block wiring

        public WireEntityStruct CreateNewWire(EGID startBlock, byte startPort, EGID endBlock, byte endPort)
        {
	        EGID wireEGID = new EGID(WiresExclusiveGroups.NewWireEntityId, NamedExclusiveGroup<WiresGroup>.Group);
	        EntityComponentInitializer wireInitializer = Factory.BuildEntity<WireEntityDescriptor>(wireEGID);
			wireInitializer.Init(new WireEntityStruct
			{
				sourceBlockEGID = startBlock,
				sourcePortUsage = startPort,
				destinationBlockEGID = endBlock,
				destinationPortUsage = endPort,
				ID = wireEGID
			});
	        return wireInitializer.Get<WireEntityStruct>();
        }

        public ref WireEntityStruct GetWire(EGID wire)
        {
	        if (!entitiesDB.Exists<WireEntityStruct>(wire))
	        {
		        throw new WiringException($"Wire {wire} does not exist");
	        }
	        return ref entitiesDB.QueryEntity<WireEntityStruct>(wire);
        }

        public ref PortEntityStruct GetPort(EGID port)
        {
	        if (!entitiesDB.Exists<PortEntityStruct>(port))
	        {
		        throw new WiringException($"Port {port} does not exist (yet?)");
	        }
	        return ref entitiesDB.QueryEntity<PortEntityStruct>(port);
        }

        public ref PortEntityStruct GetPortByOffset(BlockPortsStruct bps, byte portNumber, bool input)
        {
	        ExclusiveGroup group = input
		        ? NamedExclusiveGroup<InputPortsGroup>.Group
		        : NamedExclusiveGroup<OutputPortsGroup>.Group;
	        uint id = (input ? bps.firstInputID : bps.firstOutputID) + portNumber;
	        EGID egid = new EGID(id, group);
	        if (!entitiesDB.Exists<PortEntityStruct>(egid))
	        {
		        throw new WiringException("Port does not exist");
	        }
	        return ref entitiesDB.QueryEntity<PortEntityStruct>(egid);
        }
        
        public ref PortEntityStruct GetPortByOffset(Block block, byte portNumber, bool input)
        {
	        BlockPortsStruct bps = GetFromDbOrInitData<BlockPortsStruct>(block, block.Id, out bool exists);
	        if (!exists)
	        {
		        throw new BlockException("Block does not exist");
	        }
	        return ref GetPortByOffset(bps, portNumber, input);
        }

        public ref T GetComponent<T>(EGID egid) where T : unmanaged, IEntityComponent
        {
	        return ref entitiesDB.QueryEntity<T>(egid);
        }

        public bool Exists<T>(EGID egid) where T : struct, IEntityComponent
        {
	        return entitiesDB.Exists<T>(egid);
        }

        public bool SetSignal(EGID blockID, float signal, out uint signalID, bool input = true)
        {
            signalID = GetSignalIDs(blockID, input)[0];
            return SetSignal(signalID, signal);
        }

        public bool SetSignal(uint signalID, float signal, bool input = true)
        {
	        var array = GetSignalStruct(signalID, out uint index, input);
	        if (array.count > 0) array[index].valueAsFloat = signal;
            return false;
        }

        public float AddSignal(EGID blockID, float signal, out uint signalID, bool clamp = true, bool input = true)
        {
            signalID = GetSignalIDs(blockID, input)[0];
            return AddSignal(signalID, signal, clamp, input);
        }

        public float AddSignal(uint signalID, float signal, bool clamp = true, bool input = true)
        {
	        var array = GetSignalStruct(signalID, out uint index, input);
	        if (array.count > 0)
	        {
		        ref var channelData = ref array[index];
		        channelData.valueAsFloat += signal;
		        if (clamp)
		        {
			        if (channelData.valueAsFloat > POSITIVE_HIGH)
			        {
				        channelData.valueAsFloat = POSITIVE_HIGH;
			        }
			        else if (channelData.valueAsFloat < NEGATIVE_HIGH)
			        {
				        channelData.valueAsFloat = NEGATIVE_HIGH;
			        }

			        return channelData.valueAsFloat;
		        }
	        }

	        return signal;
        }

        public float GetSignal(EGID blockID, out uint signalID, bool input = true)
        {
			signalID = GetSignalIDs(blockID, input)[0];
            return GetSignal(signalID, input);
        }

        public float GetSignal(uint signalID, bool input = true)
        {
	        var array = GetSignalStruct(signalID, out uint index, input);
	        return array.count > 0 ? array[index].valueAsFloat : 0f;
        }

        public uint[] GetSignalIDs(EGID blockID, bool input = true)
		{
			ref BlockPortsStruct bps = ref entitiesDB.QueryEntity<BlockPortsStruct>(blockID);
			uint[] signals;
			if (input) {
				signals = new uint[bps.inputCount];
				for (uint i = 0u; i < bps.inputCount; i++)
				{
					signals[i] = bps.firstInputID + i;
				}
			} else {
				signals = new uint[bps.outputCount];
                for (uint i = 0u; i < bps.outputCount; i++)
                {
                    signals[i] = bps.firstOutputID + i;
                }
			}
			return signals;
		}

        public EGID[] GetSignalInputs(EGID blockID)
		{
			BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(blockID);
			EGID[] inputs = new EGID[ports.inputCount];
			for (uint i = 0; i < ports.inputCount; i++)
			{
				inputs[i] = new EGID(i + ports.firstInputID, NamedExclusiveGroup<InputPortsGroup>.Group);
			}
			return inputs;
		}

        public EGID[] GetSignalOutputs(EGID blockID)
        {
            BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(blockID);
            EGID[] outputs = new EGID[ports.outputCount];
            for (uint i = 0; i < ports.outputCount; i++)
            {
                outputs[i] = new EGID(i + ports.firstOutputID, NamedExclusiveGroup<OutputPortsGroup>.Group);
            }
            return outputs;
        }

		public EGID MatchBlockInputToPort(Block block, byte portUsage, out bool exists)
		{
			BlockPortsStruct ports = GetFromDbOrInitData<BlockPortsStruct>(block, block.Id, out exists);
			return new EGID(ports.firstInputID + portUsage, NamedExclusiveGroup<InputPortsGroup>.Group);
		}
		
		public EGID MatchBlockInputToPort(EGID block, byte portUsage, out bool exists)
		{
			if (!entitiesDB.Exists<BlockPortsStruct>(block))
			{
				exists = false;
				return default;
			}
			exists = true;
			BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(block);
			return new EGID(ports.firstInputID + portUsage, NamedExclusiveGroup<InputPortsGroup>.Group);
		}
		
		public EGID MatchBlockOutputToPort(Block block, byte portUsage, out bool exists)
		{
			BlockPortsStruct ports = GetFromDbOrInitData<BlockPortsStruct>(block, block.Id, out exists);
			return new EGID(ports.firstOutputID + portUsage, NamedExclusiveGroup<OutputPortsGroup>.Group);
		}
		
		public EGID MatchBlockOutputToPort(EGID block, byte portUsage, out bool exists)
		{
			if (!entitiesDB.Exists<BlockPortsStruct>(block))
			{
				exists = false;
				return default;
			}
			exists = true;
			BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(block);
			return new EGID(ports.firstOutputID + portUsage, NamedExclusiveGroup<OutputPortsGroup>.Group);
		}

		public ref WireEntityStruct MatchPortToWire(EGID portID, EGID blockID, out bool exists)
		{
			ref PortEntityStruct port = ref entitiesDB.QueryEntity<PortEntityStruct>(portID);
			var wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<WiresGroup>.Group);
			for (uint i = 0; i < wires.count; i++)
			{
				if ((wires[i].destinationPortUsage == port.usage && wires[i].destinationBlockEGID == blockID)
				    || (wires[i].sourcePortUsage == port.usage && wires[i].sourceBlockEGID == blockID))
				{
					exists = true;
					return ref wires[i];
				}
			}
			exists = false;
			WireEntityStruct[] defRef = new WireEntityStruct[1];
            return ref defRef[0];
		}

		public ref WireEntityStruct MatchBlocksToWire(EGID startBlock, EGID endBlock, out bool exists, byte startPort = byte.MaxValue,
			byte endPort = byte.MaxValue)
		{
			EGID[] startPorts;
			if (startPort == byte.MaxValue)
			{
				// search all output ports on source block
				startPorts = GetSignalOutputs(startBlock);
			}
			else
			{
				BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(startBlock);
				startPorts = new EGID[] {new EGID(ports.firstOutputID + startPort, NamedExclusiveGroup<OutputPortsGroup>.Group) };
			}

			EGID[] endPorts;
			if (startPort == byte.MaxValue)
			{
				// search all input ports on destination block
				endPorts = GetSignalInputs(endBlock);
			}
			else
			{
				BlockPortsStruct ports = entitiesDB.QueryEntity<BlockPortsStruct>(endBlock);
				endPorts = new EGID[] {new EGID(ports.firstInputID + endPort, NamedExclusiveGroup<InputPortsGroup>.Group) };
			}
			
			EntityCollection<WireEntityStruct> wires = entitiesDB.QueryEntities<WireEntityStruct>(NamedExclusiveGroup<WiresGroup>.Group);
			for (int endIndex = 0; endIndex < endPorts.Length; endIndex++)
			{
				PortEntityStruct endPES = entitiesDB.QueryEntity<PortEntityStruct>(endPorts[endIndex]);
				for (int startIndex = 0; startIndex < startPorts.Length; startIndex++)
				{
					PortEntityStruct startPES = entitiesDB.QueryEntity<PortEntityStruct>(startPorts[startIndex]);
					for (int w = 0; w < wires.count; w++)
					{
						if ((wires[w].destinationPortUsage == endPES.usage && wires[w].destinationBlockEGID == endBlock)
						    && (wires[w].sourcePortUsage == startPES.usage && wires[w].sourceBlockEGID == startBlock))
						{
							exists = true;
							return ref wires[w];
						}
					}
				}
			}
			
			exists = false;
			WireEntityStruct[] defRef = new WireEntityStruct[1];
			return ref defRef[0];
		}

        public ref ChannelDataStruct GetChannelDataStruct(EGID portID, out bool exists)
		{
			ref PortEntityStruct port = ref entitiesDB.QueryEntity<PortEntityStruct>(portID);
			var channels = entitiesDB.QueryEntities<ChannelDataStruct>(NamedExclusiveGroup<ChannelDataGroup>.Group);
			if (port.firstChannelIndexCachedInSim < channels.count)
			{
				exists = true;
				return ref channels[port.firstChannelIndexCachedInSim];
			}
			exists = false;
			ChannelDataStruct[] defRef = new ChannelDataStruct[1];
			return ref defRef[0];
		}

        public EGID[] GetElectricBlocks()
        {
	        var res = new FasterList<EGID>();
            foreach (var (coll, _) in entitiesDB.QueryEntities<BlockPortsStruct>())
            foreach (ref BlockPortsStruct s in coll)
	            res.Add(s.ID);
            return res.ToArray();
        }

        public EGID[] WiredToInput(EGID block, byte port)
        {
	        WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<WiresGroup>.Group,
		        (WireEntityStruct wes) => wes.destinationPortUsage == port && wes.destinationBlockEGID == block);
	        EGID[] result = new EGID[wireEntityStructs.Length];
	        for (uint i = 0; i < wireEntityStructs.Length; i++)
	        {
		        result[i] = wireEntityStructs[i].ID;
	        }

	        return result;
        }
        
        public EGID[] WiredToOutput(EGID block, byte port)
        {
	        WireEntityStruct[] wireEntityStructs = Search(NamedExclusiveGroup<WiresGroup>.Group,
		        (WireEntityStruct wes) => wes.sourcePortUsage == port && wes.sourceBlockEGID == block);
	        EGID[] result = new EGID[wireEntityStructs.Length];
	        for (uint i = 0; i < wireEntityStructs.Length; i++)
	        {
		        result[i] = wireEntityStructs[i].ID;
	        }

	        return result;
        }

        private T[] Search<T>(ExclusiveGroup group, Func<T, bool> isMatch) where T : struct, IEntityComponent
        {
	        FasterList<T> results = new FasterList<T>();
	        EntityCollection<T> components = entitiesDB.QueryEntities<T>(group);
	        for (uint i = 0; i < components.count; i++)
	        {
		        if (isMatch(components[i]))
		        {
			        results.Add(components[i]);
		        }
	        }
	        return results.ToArray();
        }

        private ref T GetFromDbOrInitData<T>(Block block, EGID id, out bool exists) where T : unmanaged, IEntityComponent
        {
	        T[] defRef = new T[1];
	        if (entitiesDB.Exists<T>(id))
	        {
		        exists = true;
		        return ref entitiesDB.QueryEntity<T>(id);
	        }
	        if (block == null || block.InitData.Group == null)
	        {
		        exists = false;
		        return ref defRef[0];
	        }
	        EntityComponentInitializer initializer = new EntityComponentInitializer(block.Id, block.InitData.Group);
	        if (initializer.Has<T>())
	        {
		        exists = true;
		        return ref initializer.Get<T>();
	        }
	        exists = false;
	        return ref defRef[0];
        }

        private EntityCollection<ChannelDataStruct> GetSignalStruct(uint signalID, out uint index, bool input = true)
        {
	        ExclusiveGroup group = input
		        ? NamedExclusiveGroup<InputPortsGroup>.Group
		        : NamedExclusiveGroup<OutputPortsGroup>.Group;
	        if (entitiesDB.Exists<PortEntityStruct>(signalID, group))
	        {
		        index = entitiesDB.QueryEntity<PortEntityStruct>(signalID, group).anyChannelIndex;
		        var channelData =
			        entitiesDB.QueryEntities<ChannelDataStruct>(NamedExclusiveGroup<ChannelDataGroup>.Group);
		        return channelData;
	        }

	        index = 0;
	        return default; //count: 0
        }
    }
}