using System;
using System.Collections.Generic;
using System.Linq;
using Assets.Scripts;
using Assets.Scripts.Networks;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.Util;
using JetBrains.Annotations;
using RemoteControl.Message;
using Swan;
using UnityEngine;
using GameDevice = Assets.Scripts.Objects.Pipes.Device;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable MemberCanBePrivate.Global
namespace RemoteControl
{
    public class DataNetwork
    {
        public long ReferenceId { get; private set; }
        internal readonly List<CableAnalyser> Probes = new ();
        internal CableNetwork Network { get; private set; }
        
        private readonly HashSet<long> _knownIds = new();


        internal readonly Dictionary<long, Device> DeviceCache = new();
    
        public DataNetwork(CableNetwork network)
        {
    
            ReferenceId = network.ReferenceId;
            Network = network;
        }

        internal void RescanNetworkComposition()
        {
            _knownIds.Clear();
            _knownIds.UnionWith(Network.DataDeviceList.Select(device => device.ReferenceId));
            DeviceCache.Keys.Except(_knownIds).ToList().ForEach(device => DeviceCache.Remove(device));

            _knownIds.Except(DeviceCache.Keys).ToList().ForEach(device =>
            {
                var newDev = new Device();
                var dev = Referencable.Find<GameDevice>(device);
                if (dev == null) return;
                newDev.ReferenceId = device;
                newDev.PrefabHash = dev.PrefabHash;
                newDev.PrefabName = dev.PrefabName;
                DeviceCache.Add(device, newDev);
            });
            UpdateAllDevices();
        }

        private void UpdateAllDevices()
        {
            foreach (var kvp in DeviceCache)
            {
                var device = kvp.Value;
                var gameObj = Referencable.Find<GameDevice>(kvp.Key);
                if (gameObj == null) continue;
                device.LogicValues.Clear();
                foreach (var type in EnumCollections.LogicTypes.Values)
                {
                    if (gameObj.CanLogicRead(type))
                    {
                        device.LogicValues[type] = gameObj.GetLogicValue(type);
                    }
                }

                var slotCount = gameObj.TotalSlots;
                while (device.Slots.Count > slotCount)
                {
                    device.Slots.RemoveAt(device.Slots.Count - 1);
                } 
                for (int slotIndex = 0; slotIndex < slotCount; slotIndex++)
                {
                    if (device.Slots.Count <= slotIndex)
                    {
                        device.Slots.Add(new Dictionary<LogicSlotType, double>());
                    }

                    foreach (var type in EnumCollections.LogicSlotTypes.Values)
                    {
                        if (gameObj.CanLogicRead(type, slotIndex))
                        {
                            device.Slots[slotIndex][type] = gameObj.GetLogicValue(type, slotIndex);
                        }
                    }
                }
            }
        }
    

        public DataNetwork Reset(CableNetwork cnet)
        {
            ReferenceId = cnet.ReferenceId;
            Network = cnet;
            Probes.Clear();
            DeviceCache.Clear();
            _knownIds.Clear();
            return this;
        }
    }

    public readonly struct LogicUpdate : IEquatable<LogicUpdate>
    {
        public readonly long TargetReferenceId;
        public readonly LogicType LogicType;

        public LogicUpdate(long targetReferenceId, LogicType logicType)
        {
            TargetReferenceId = targetReferenceId;
            LogicType = logicType;
        }

        public bool Equals(LogicUpdate other)
        {
            return TargetReferenceId == other.TargetReferenceId && LogicType == other.LogicType;
        }

        public override bool Equals(object obj)
        {
            return obj is LogicUpdate other && Equals(other);
        }

        public override int GetHashCode()
        {
            return CompositeHashCode.Using(TargetReferenceId, LogicType);
        }
    }

    public readonly struct SlotUpdate : IEquatable<SlotUpdate>
    {
        public readonly long ReferenceId;
        public readonly int Slot;
        public readonly LogicSlotType Type;

        public SlotUpdate(long referenceID, int slot, LogicSlotType type)
        {
            ReferenceId = referenceID;
            Slot = slot;
            Type = type;
        }

        public bool Equals(SlotUpdate other)
        {
            return ReferenceId == other.ReferenceId && Slot == other.Slot && Type == other.Type;
        }

        public override bool Equals(object obj)
        {
            return obj is SlotUpdate other && Equals(other);
        }

        public override int GetHashCode()
        {
            return CompositeHashCode.Using(ReferenceId, Slot, Type);
        }
    }
    
    public static class SubscriptionManager
    {
        public static readonly object Lock = new();

        private static readonly Dictionary<long, DataNetwork> DataNetworks = new();
        // private Dictionary<string, List<RemoteControlLimpet>> _probesByName = new();
        // private Dictionary<long, RemoteControlLimpet> _probesById = new();
        private static readonly Dictionary<LogicUpdate, double> PendingUpdates = new();
        private static readonly Dictionary<SlotUpdate, double> PendingSlotUpdates = new();

        [CanBeNull]
        public static Device FindCachedDevice(string probeName, long referenceID)
        {
            foreach (var analyzer in GetDataNetwork(probeName))
            {
                if (analyzer.DeviceCache.TryGetValue(referenceID, out var device))
                {
                    return device;
                }
            }

            return null;
        }

        // Can be called from any thread
        public static bool SetLogic(string probeName, long referenceID, LogicType type, double value)
        {
            // lock (Lock)
            {
                var dev = FindCachedDevice(probeName, referenceID);
                if (dev == null)
                {
                    return false;
                }

                dev.LogicValues[type] = value;
                PendingUpdates[new LogicUpdate(referenceID, type)] = value;
            }

            return true;
        }

        // Can be called from any thread
        public static bool SetSlot(string probeName, long referenceID, int slot, LogicSlotType type, double value)
        {
            // lock (Lock)
            {
                
                var dev = FindCachedDevice(probeName, referenceID);
                if (dev != null && dev.Slots.Count > slot && dev.Slots[slot].ContainsKey(type))
                {
                    dev.Slots[slot][type] = value;
                    PendingSlotUpdates[new SlotUpdate(referenceID, slot, type)] = value;
                    return true;
                }
                else
                {
                    return false;
                }
            }

        }
        
        public static DataNetwork GetDataNetwork(CableNetwork network)
        {
            if (DataNetworks.TryGetValue(network.ReferenceId, out var dataNetwork))
            {
                return dataNetwork;
            }
            else
            {
                var ret =  new DataNetwork(network);
                DataNetworks.Add(network.ReferenceId, ret);
                return ret;
            }
        }

        public static IEnumerable<DataNetwork> GetDataNetwork(string probeName)
        {
            // lock (Lock)
            {
                List<DataNetwork> networks = new();
                GameDevice.AllDevices.ForEach(dev =>
                {
                    if (dev is CableAnalyser analyzer)
                    {
                        networks.Add(GetDataNetwork(analyzer.CableNetwork));
                    }
                });
                return networks;
            }
        } 
        
        
        public static GameDevice GetDevice(long referenceID)
        {
            var device = Referencable.Find<GameDevice>(referenceID);
            if (!DataNetworks.Values.Any((item) => item.Network.DataDeviceList.Contains(device)))
                return null;
            return device;

        }

        public static GameDevice GetDevice(string probeName, long referenceID)
        {
            var device = Referencable.Find<GameDevice>(referenceID);
            if (GetDataNetwork(probeName).Any(network => network.DeviceCache.ContainsKey(referenceID)))
            {
                return device;
            }
            return null;
        }

        /// <summary>
        /// Called from Unity thread pool before the logic tick
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        public static void ApplyUpdates()
        {
            lock (Lock)
            {
                foreach (var update in PendingUpdates)
                {
                    var device = GetDevice(update.Key.TargetReferenceId);

                    if (!device.CanLogicWrite(update.Key.LogicType))
                    {
                        device.SetLogicValue(update.Key.LogicType, update.Value);
                    }
                }

                foreach (var update in PendingSlotUpdates)
                {
                    var device = Referencable.Find<Assets.Scripts.Objects.Pipes.Device>(update.Key.ReferenceId);
                    if (!DataNetworks.Values.Any((item) => item.Network.DataDeviceList.Contains(device)))
                        continue;

                    if (!device.CanLogicWrite(update.Key.Type, update.Key.Slot))
                    {
                        device.SetLogicValue(update.Key.Type, update.Key.Slot, update.Value);
                    }
                }
                
                PendingUpdates.Clear();
                PendingSlotUpdates.Clear();
            }
        }

        public static void RescanNetworks()
        {
            using (new LogTimer("RescanNetworks"))
            {
                HashSet<CableAnalyser> scannedAnalyzers = new();
                HashSet<CableNetwork> scannedNetworks = new();

                scannedNetworks.Clear();
                GameDevice.AllDevices.ForEach(dev =>
                {
                    if (dev is CableAnalyser analyser)
                    {
                        scannedAnalyzers.Add(analyser);
                        scannedNetworks.Add(analyser.CableNetwork);
                    }
                });

                var removed =
                    new List<DataNetwork>(DataNetworks.Values.Where(dataNetwork =>
                        !scannedNetworks.Contains(dataNetwork.Network)));
                foreach (var dataNet in removed)
                {
                    DataNetworks.Remove(dataNet.ReferenceId);
                }

                foreach (var dataNet in DataNetworks.Values)
                {
                    dataNet.Probes.Clear();
                }

                foreach (var analyzer in scannedAnalyzers)
                {
                    if (DataNetworks.ContainsKey(analyzer.ReferenceId)) continue;

                    var dataNet = removed.Pop()?.Reset(analyzer.CableNetwork) ?? new DataNetwork(analyzer.CableNetwork);
                    dataNet.Probes.Add(analyzer);
                }


                // TODO: when we have our own device for tagging a network, make this triggered by on the DataNetworkChange method
                foreach (var dataNet in DataNetworks.Values)
                {
                    dataNet.RescanNetworkComposition();
                }
            }
        }
        
        public static IList<string> GetProbes()
        {
            List<string> probes = new();
            GameDevice.AllDevices.ForEach(dev =>
            {
                if (dev is CableAnalyser)
                {
                    probes.Add(dev.DisplayName);
                }
            });
            return probes;
        }

    }

    public class LogTimer : IDisposable
    {
        private readonly DateTime _startTime =  DateTime.Now;
        private string _action;

        public LogTimer(string action)
        {
            RemoteControl.Log($"Beginning {action}");
            _action = action;
        }

        public void Dispose()
        {
            var endTime = DateTime.Now;
            var elapsed =  endTime - _startTime;
            Debug.Log($"Time taken for {_action}: " + elapsed);
        }
    }
}