using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;

namespace Barotrauma.Items.Components
{
    partial class Connection
    {
        //how many wires can be linked to connectors by default
        private const int DefaultMaxWires = 5;

        //how many wires a player can link to this connection
        public readonly int MaxPlayerConnectableWires = 5;

        //how many wires can be linked to this connection in total
        public readonly int MaxWires = 5;

        public readonly int DisplayOrder;

        public readonly string Name;
        private readonly LocalizedString _displayName;
        public LocalizedString DisplayName
        {
            get => DisplayNameOverride ?? _displayName;
            private init => _displayName = value;
        }

        public LocalizedString DisplayNameOverride;

        private readonly HashSet<Wire> wires;
        public IReadOnlyCollection<Wire> Wires => wires;

        /// <summary>
        /// Circuit box input and output connections that are linked to this connection.
        /// </summary>
        /// <remarks>
        /// We don't want to create a wire between the circuit boxes connection panel and the
        /// connection panel of the item inside the circuit box so we use this to bridge the gap.
        /// </remarks>
        public List<CircuitBoxConnection> CircuitBoxConnections = new();

        private bool enumeratingWires;
        private readonly HashSet<Wire> removedWires = new HashSet<Wire>();

        private readonly Item item;

        public readonly bool IsOutput;

        public readonly List<StatusEffect> Effects;

        public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires;

        //The grid the connection is a part of
        public GridInfo Grid;

        //Priority in which power output will be handled - load is unaffected
        public PowerPriority Priority = PowerPriority.Default;

        public Signal LastSentSignal { get; private set; }
        public Signal LastReceivedSignal {get; private set;}

        public bool IsPower
        {
            get;
            private set;
        }

        private bool recipientsDirty = true;
        private readonly List<Connection> recipients = new List<Connection>();
        public List<Connection> Recipients
        {
            get
            {
                if (recipientsDirty) { RefreshRecipients(); }
                return recipients;
            }
        }

        public Item Item
        {
            get { return item; }
        }

        public ConnectionPanel ConnectionPanel
        {
            get;
            private set;
        }

        public override string ToString()
        {
            return "Connection (" + item.Name + ", " + Name + ")";
        }

        public Connection(ContentXElement element, int connectionIndex, ConnectionPanel connectionPanel, IdRemap idRemap, bool isItemSwap)
        {

#if CLIENT
            if (connector == null)
            {
                connector = GUIStyle.GetComponentStyle("ConnectionPanelConnector").GetDefaultSprite();
                wireVertical = GUIStyle.GetComponentStyle("ConnectionPanelWire").GetDefaultSprite();
                connectionSprite = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetDefaultSprite();
                connectionSpriteHighlight = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetSprite(GUIComponent.ComponentState.Hover);
                screwSprites = GUIStyle.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList();
            }
#endif
            ConnectionPanel = connectionPanel;
            item = connectionPanel.Item;

            MaxWires = element.GetAttributeInt("maxwires", DefaultMaxWires);
            MaxWires = Math.Max(element.Elements().Count(e => e.Name.ToString().Equals("link", StringComparison.OrdinalIgnoreCase)), MaxWires);

            MaxPlayerConnectableWires = element.GetAttributeInt("maxplayerconnectablewires", MaxWires);
            wires = new HashSet<Wire>();

            IsOutput = element.Name.ToString() == "output";
            Name = element.GetAttributeString("name", IsOutput ? "output" : "input");

            int displayOrder;
            if (element.GetAttribute("displayorderoverride") is not { } displayOrderAttr)
            {
                var sameElements = connectionPanel.Connections.Where(c => c.IsOutput == IsOutput);
                displayOrder = !sameElements.Any() ? 0 : sameElements.Max(static c => c.DisplayOrder) + 1;
            }
            else
            {
                displayOrder = displayOrderAttr.GetAttributeInt(0);
            }

            DisplayOrder = displayOrder;

            string displayNameTag = "", fallbackTag = "";
            //if displayname is not present, attempt to find it from the prefab
            if (element.GetAttribute("displayname") == null)
            {
                foreach (var subElement in item.Prefab.ConfigElement.Elements())
                {
                    if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }                    
                    int prefabConnectionIndex = 0;
                    foreach (XElement connectionElement in subElement.Elements())
                    {
                        string prefabConnectionName = connectionElement.GetAttributeString("name", null);
                        if (prefabConnectionName.IsNullOrEmpty()) { continue; }

                        string[] aliases = connectionElement.GetAttributeStringArray("aliases", Array.Empty<string>());
                        if (prefabConnectionName == Name || aliases.Contains(Name) ||
                            //when swapping items, we move wires based on the order of the connections, not the names
                            //= we should find a connection based on the index if the name doesn't match
                            (isItemSwap && connectionIndex == prefabConnectionIndex))
                        {
                            displayNameTag = connectionElement.GetAttributeString("displayname", "");
                            fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", "");
                        }
                        prefabConnectionIndex++;
                    }
                }                
            }
            else
            {
                displayNameTag = element.GetAttributeString("displayname", "");
                fallbackTag = element.GetAttributeString("fallbackdisplayname", null);
            }

            if (!string.IsNullOrEmpty(displayNameTag))
            {
                //extract the tag parts in case the tags contains variables
                string tagWithoutVariables = displayNameTag?.Split('~')?.FirstOrDefault();
                string fallbackTagWithoutVariables = fallbackTag?.Split('~')?.FirstOrDefault();
                //use displayNameTag if found, otherwise fallBack
                if (TextManager.ContainsTag(tagWithoutVariables))
                {
                    DisplayName = TextManager.GetServerMessage(displayNameTag);
                }
                else if (TextManager.ContainsTag(fallbackTagWithoutVariables))
                {
                    DisplayName = TextManager.GetServerMessage(fallbackTag);
                }
            }

            if (DisplayName.IsNullOrEmpty())
            {
#if DEBUG
                DebugConsole.ThrowError($"Could not find a display name for the connection {Name} in the item {item.Name} (submarine: {item.Submarine?.Info?.Name ?? "none"})");
#endif
                DisplayName = Name;
            }

            IsPower = element.GetAttributeBool("ispower", Name is "power_in" or "power" or "power_out");

            LoadedWires = new List<(ushort wireId, int? connectionIndex)>();
            foreach (var subElement in element.Elements())
            {
                switch (subElement.Name.ToString().ToLowerInvariant())
                {
                    case "link":
                        int id = subElement.GetAttributeInt("w", 0);
                        int? i = null;                        
                        if (subElement.GetAttribute("i") != null)
                        {
                            i = subElement.GetAttributeInt("i", 0); 
                        }                       
                        if (id < 0) { id = 0; }
                        if (LoadedWires.Count < MaxWires) { LoadedWires.Add((idRemap.GetOffsetId(id), i)); }
                        break;
                    case "statuseffect":
                        Effects ??= new List<StatusEffect>();
                        Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name));
                        break;
                }
            }
        }

        /// <summary>
        /// Checks if the the connection is connected to a wire or a circuit box connection
        /// </summary>
        public bool IsConnectedToSomething()
            => wires.Count > 0 || CircuitBoxConnections.Count > 0;

        public void SetRecipientsDirty()
        {
            recipientsDirty = true;
            if (IsPower) { Powered.ChangedConnections.Add(this); }
        }

        private void RefreshRecipients()
        {
            recipients.Clear();
            foreach (var wire in wires)
            {
                Connection recipient = wire.OtherConnection(this);
                if (recipient != null) { recipients.Add(recipient); }
            }
            recipientsDirty = false;
        }

        public Wire FindWireByItem(Item it)
            => Wires.FirstOrDefault(w => w.Item == it);

        public bool WireSlotsAvailable()
            => wires.Count < MaxWires;
        
        public bool TryAddLink(Wire wire)
        {
            if (wire is null
                || wires.Contains(wire)
                || !WireSlotsAvailable())
            {
                return false;
            }
            wires.Add(wire);
            return true;
        }

        public void DisconnectWire(Wire wire)
        {
            if (wire == null || !wires.Contains(wire)) { return; }

            var prevOtherConnection = wire.OtherConnection(this);
            if (prevOtherConnection != null)
            {
                //Change the connection grids or flag them for updating
                if (IsPower && prevOtherConnection.IsPower && Grid != null)
                {
                    //Check if both connections belong to a larger grid
                    if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1)
                    {
                        Powered.ChangedConnections.Add(prevOtherConnection);
                        Powered.ChangedConnections.Add(this);
                    }
                    else if (recipients.Count > 1)
                    {
                        //This wire was the only one at the other grid
                        prevOtherConnection.Grid?.RemoveConnection(prevOtherConnection);
                        prevOtherConnection.Grid = null;
                    }
                    else if (prevOtherConnection.recipients.Count > 1)
                    {
                        Grid?.RemoveConnection(this);
                        Grid = null;
                    }
                    else if (Grid.Connections.Count == 2)
                    {
                        //Delete the grid as these were the only 2 devices
                        Powered.Grids.Remove(Grid.ID);
                        Grid = null;
                        prevOtherConnection.Grid = null;
                    }
                }
                prevOtherConnection.recipientsDirty = true;
            }
            if (enumeratingWires)
            {
                removedWires.Add(wire);
            }
            else
            {
                wires.Remove(wire);
            }
            recipientsDirty = true;
        }
        
        public void ConnectWire(Wire wire)
        {
            if (wire == null || !TryAddLink(wire)) { return; }
            ConnectionPanel.DisconnectedWires.Remove(wire);
            var otherConnection = wire.OtherConnection(this);
            if (otherConnection != null)
            {
                //Set the other connection grid if a grid exists already
                if (Powered.ValidPowerConnection(this, otherConnection))
                {
                    if (Grid == null && otherConnection.Grid != null)
                    {
                        otherConnection.Grid.AddConnection(this);
                        Grid = otherConnection.Grid;
                    }
                    else if (Grid != null && otherConnection.Grid == null)
                    {
                        Grid.AddConnection(otherConnection);
                        otherConnection.Grid = Grid;
                    }
                    else
                    {
                        //Flag change so that proper grids can be formed
                        Powered.ChangedConnections.Add(this);
                        Powered.ChangedConnections.Add(otherConnection);
                    }
                }

                otherConnection.recipientsDirty = true;
            }
            recipientsDirty = true;
        }

        public void SendSignal(Signal signal)
        {
            LastSentSignal = signal;
            enumeratingWires = true;
            foreach (var wire in wires)
            {
                Connection recipient = wire.OtherConnection(this);
                if (recipient == null) { continue; }
                if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; }

                signal.source?.LastSentSignalRecipients.Add(recipient);
#if CLIENT
                wire.RegisterSignal(signal, source: this);
#endif
                SendSignalIntoConnection(signal, recipient);
                GameMain.LuaCs.Hook.Call("signalReceived", signal, recipient);
                GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient);
            }

            foreach (CircuitBoxConnection connection in CircuitBoxConnections)
            {
                connection.ReceiveSignal(signal);
                GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection);
                GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection);
            }
            enumeratingWires = false;
            foreach (var removedWire in removedWires)
            {
                wires.Remove(removedWire);
            }
            removedWires.Clear();
        }

        public static void SendSignalIntoConnection(Signal signal, Connection conn)
        {
            conn.LastReceivedSignal = signal;

            foreach (ItemComponent ic in conn.item.Components)
            {
                ic.ReceiveSignal(signal, conn);
            }

            if (conn.Effects == null || signal.value == "0") { return; }

            foreach (StatusEffect effect in conn.Effects)
            {
                conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
            }
        }

        public void ClearConnections()
        {
            if (IsPower && Grid != null)
            {
                Powered.ChangedConnections.Add(this);
                foreach (Connection c in recipients)
                {
                    Powered.ChangedConnections.Add(c);
                }
            }
            foreach (var wire in wires)
            {
                wire.RemoveConnection(this);
                recipientsDirty = true;
            }

            if (enumeratingWires)
            {
                foreach (var wire in wires)
                {
                    removedWires.Add(wire);
                }
            }
            else
            {
                wires.Clear();
            }
        }
        
        public void InitializeFromLoaded()
        {
            if (LoadedWires.Count == 0) { return; }
            
            foreach ((ushort wireId, int? connectionIndex) in LoadedWires)
            {
                if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; }

                var wire = wireItem.GetComponent<Wire>();
                if (wire != null && TryAddLink(wire))
                {
                    if (wire.Item.body != null) { wire.Item.body.Enabled = false; }
                    if (connectionIndex.HasValue)
                    {
                        wire.Connect(this, connectionIndex.Value, addNode: false, sendNetworkEvent: false);
                    }
                    else
                    {
                        wire.TryConnect(this, addNode: false, sendNetworkEvent: false);
                    }
                    wire.FixNodeEnds();
                    recipientsDirty = true;
                }
            }
            LoadedWires.Clear();
        }


        public void Save(XElement parentElement)
        {
            XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));

            foreach (var wire in wires.OrderBy(w => w.Item.ID))
            {
                newElement.Add(new XElement("link",
                    new XAttribute("w", wire.Item.ID.ToString()),
                    new XAttribute("i", wire.Connections[0] == this ? 0 : 1)));
            }

            parentElement.Add(newElement);
        }
    }
}
