#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;

namespace Barotrauma.Items.Components
{
    internal sealed partial class CircuitBox
    {
        public CircuitBoxUI? UI;
        public readonly Dictionary<Character, CircuitBoxCursor> ActiveCursors = new Dictionary<Character, CircuitBoxCursor>();
        public Option<ItemPrefab> HeldComponent = Option.None;

        private const float CursorUpdateInterval = 1f;
        private float cursorUpdateTimer;

        private readonly Vector2[] recordedCursorPositions = new Vector2[10];
        private Option<Vector2> recordedDragStart = Option.None;
        private Option<ItemPrefab> recordedHeldPrefab = Option.None;

        /// <summary>
        /// If the circuit box was initialized by the server instead of from the save file.
        /// Used to ensure the wires the server sends are properly connected up when we load in.
        /// </summary>
        private bool wasInitializedByServer;

        public Sprite? WireSprite { get; private set; }
        public Sprite? ConnectionSprite { get; private set; }
        public Sprite? WireConnectorSprite { get; private set; }
        public Sprite? ConnectionScrewSprite { get; private set; }
        public UISprite? NodeFrameSprite { get; private set; }
        public UISprite? NodeTopSprite { get; private set; }

        protected override void CreateGUI()
        {
            base.CreateGUI();
            GuiFrame.ClearChildren();
            UI?.CreateGUI(GuiFrame);
        }

        partial void InitProjSpecific(ContentXElement element)
        {
            UI = new CircuitBoxUI(this);
            IsActive = true;
            CreateGUI();

            foreach (var subElement in element.Elements())
            {
                switch (subElement.Name.ToString().ToLowerInvariant())
                {
                    case "wiresprite":
                        WireSprite = new Sprite(subElement);
                        break;
                    case "connectionsprite":
                        ConnectionSprite = new Sprite(subElement);
                        break;
                    case "wireconnectorsprite":
                        WireConnectorSprite = new Sprite(subElement);
                        break;
                    case "connectionscrewsprite":
                        ConnectionScrewSprite = new Sprite(subElement);
                        break;
                }
            }

            if (GUIStyle.GetComponentStyle("CircuitBoxTop") is {  } topStyle)
            {
                NodeTopSprite = topStyle.Sprites[GUIComponent.ComponentState.None][0];
            }

            if (GUIStyle.GetComponentStyle("CircuitBoxFrame") is { } compStyle)
            {
                NodeFrameSprite = compStyle.Sprites[GUIComponent.ComponentState.None][0];
            }
        }

        public override bool ShouldDrawHUD(Character character)
            => character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item);

        public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
        {
            if (UI is null) { return; }

            UI.Update(deltaTime);

            if (GameMain.NetworkMember is null) { return; }

            foreach (var (cursorChar, cursor) in ActiveCursors)
            {
                if (!cursor.IsActive) { continue; }

                ActiveCursors[cursorChar].Update(deltaTime);
            }

            Vector2 cursorPos = UI.GetCursorPosition();
            int lastCursorPosIndex = recordedCursorPositions.Length - 1;

            if (cursorUpdateTimer < CursorUpdateInterval)
            {
                cursorUpdateTimer += deltaTime;
                int cursorIndex = (int)MathF.Floor(cursorUpdateTimer * lastCursorPosIndex);
                RecordCursorPosition(cursorIndex);
            }
            else
            {
                RecordCursorPosition(lastCursorPosIndex);
                SendCursorState(recordedCursorPositions, recordedDragStart, recordedHeldPrefab.Select(static c => c.Identifier));

                recordedDragStart = Option.None;
                recordedHeldPrefab = Option.None;
                cursorUpdateTimer = 0f;
            }

            void RecordCursorPosition(int index)
            {
                var dragStart = UI.GetDragStart();
                if (dragStart.IsSome()) { recordedDragStart = dragStart; }

                var heldComponent = HeldComponent;
                if (heldComponent.IsSome()) { recordedHeldPrefab = heldComponent; }

                if (index >= 0 && index < recordedCursorPositions.Length) { recordedCursorPositions[index] = cursorPos; }
            }
        }

        public void RemoveComponents(IReadOnlyCollection<CircuitBoxComponent> node)
        {
            if (IsLocked()) { return; }
            var ids = node.Select(static n => n.ID).ToImmutableArray();

            if (GameMain.NetworkMember is null)
            {
                CreateRefundItemsForUsedResources(ids, Character.Controlled);
                RemoveComponentInternal(ids);
                return;
            }

            if (!node.Any()) { return; }

            CreateClientEvent(new CircuitBoxRemoveComponentEvent(ids));
        }

        public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two)
        {
            if (IsLocked()) { return; }
            if (GameMain.NetworkMember is null)
            {
                Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab);
                return;
            }

            if (!VerifyConnection(one, two)) { return; }

            CreateClientEvent(new CircuitBoxClientAddWireEvent(Color.White, CircuitBoxConnectorIdentifier.FromConnection(one), CircuitBoxConnectorIdentifier.FromConnection(two), CircuitBoxWire.SelectedWirePrefab.UintIdentifier));
        }

        public void RemoveWires(IReadOnlyCollection<CircuitBoxWire> wires)
        {
            if (IsLocked()) { return; }
            var ids = wires.Select(static w => w.ID).ToImmutableArray();
            if (GameMain.NetworkMember is null)
            {
                RemoveWireInternal(ids);
                return;
            }

            if (!ids.Any()) { return; }
            CreateClientEvent(new CircuitBoxRemoveWireEvent(ids));
        }

        public void SelectComponents(IReadOnlyCollection<CircuitBoxNode> moveables, bool overwrite)
        {
            if (Character.Controlled is not { ID: var controlledId }) { return; }

            var ids = ImmutableArray.CreateBuilder<ushort>();
            var ios = ImmutableArray.CreateBuilder<CircuitBoxInputOutputNode.Type>();
            var labelIds = ImmutableArray.CreateBuilder<ushort>();

            foreach (var moveable in moveables)
            {
                if (moveable is { IsSelected: true, IsSelectedByMe: false }) { continue; }

                switch (moveable)
                {
                    case CircuitBoxComponent node:
                        ids.Add(node.ID);
                        break;
                    case CircuitBoxInputOutputNode io:
                        ios.Add(io.NodeType);
                        break;
                    case CircuitBoxLabelNode label:
                        labelIds.Add(label.ID);
                        break;
                }
            }

            if (GameMain.NetworkMember is null)
            {
                SelectComponentsInternal(ids, controlledId, overwrite);
                SelectInputOutputInternal(ios, controlledId, overwrite);
                SelectLabelsInternal(labelIds, controlledId, overwrite);
                return;
            }

            if (!ids.Any() && !ios.Any() && !labelIds.Any() && !overwrite) { return; }

            CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), overwrite, controlledId));
        }

        public void SelectWires(IReadOnlyCollection<CircuitBoxWire> wires, bool overwrite)
        {
            if (Character.Controlled is not { ID: var controlledId }) { return; }

            var ids = (from wire in wires where !wire.IsSelected || wire.IsSelectedByMe select wire.ID).ToImmutableArray();

            if (GameMain.NetworkMember is null)
            {
                SelectWiresInternal(ids, controlledId, overwrite);
                return;
            }

            if (!ids.Any() && !overwrite) { return; }

            CreateClientEvent(new CircuitBoxSelectWiresEvent(ids, overwrite, Character.Controlled.ID));
        }

        public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection<CircuitBoxNode> moveables)
        {
            if (IsLocked()) { return; }
            var ids = ImmutableArray.CreateBuilder<ushort>();
            var ios = ImmutableArray.CreateBuilder<CircuitBoxInputOutputNode.Type>();
            var labelIds = ImmutableArray.CreateBuilder<ushort>();

            foreach (CircuitBoxNode move in moveables)
            {
                switch (move)
                {
                    case CircuitBoxComponent node:
                        ids.Add(node.ID);
                        break;
                    case CircuitBoxInputOutputNode io:
                        ios.Add(io.NodeType);
                        break;
                    case CircuitBoxLabelNode label:
                        labelIds.Add(label.ID);
                        break;
                }
            }

            if (GameMain.NetworkMember is null)
            {
                MoveNodesInternal(ids, ios, labelIds, moveAmount);
                return;
            }

            if (!ids.Any() && !ios.Any() && !labelIds.Any()) { return; }


            CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), moveAmount));
        }

        public void AddComponent(ItemPrefab prefab, Vector2 pos)
        {
            if (IsLocked()) { return; }
            if (GameMain.NetworkMember is null)
            {
                ItemPrefab resource;

                if (IsFull) { return; }

                if (IsInGame())
                {
                    if (!GetApplicableResourcePlayerHas(prefab, Character.Controlled).TryUnwrap(out var r)) { return; }
                    resource = r.Prefab;
                    RemoveItem(r);
                }
                else
                {
                    resource = ItemPrefab.Prefabs[Tags.FPGACircuit];
                }

                AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, Character.Controlled, onItemSpawned: null);
                return;
            }

            CreateClientEvent(new CircuitBoxAddComponentEvent(prefab.UintIdentifier, pos));
        }

        public void RenameLabel(CircuitBoxLabelNode label, Color color, NetLimitedString header, NetLimitedString body)
        {
            if (IsLocked()) { return; }
            if (GameMain.NetworkMember is null)
            {
                label.EditText(header, body);
                label.Color = color;
                return;
            }

            CreateClientEvent(new CircuitBoxRenameLabelEvent(label.ID, color, header, body));
        }

        public void SetConnectionLabelOverrides(CircuitBoxInputOutputNode node, Dictionary<string, string> newOverrides)
        {
            if (GameMain.NetworkMember is null)
            {
                node.ReplaceAllConnectionLabelOverrides(newOverrides);
                return;
            }

            CreateClientEvent(new CircuitBoxRenameConnectionLabelsEvent(node.NodeType, newOverrides.ToNetDictionary()));
        }

        public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount)
        {
            if (IsLocked()) { return; }
            var resize = node.ResizeBy(dir, amount);
            if (GameMain.NetworkMember is null)
            {
                node.ApplyResize(resize.Size, resize.Pos);
                return;
            }

            // TODO this needs to be refactored at some point, probably not now
            // the problem here is that the circuit  box supports resizing all nodes
            // but we limit the resizing to only labels on the client
            // and on the server we only have a network message that targets labels
            // so if we ever want the ability to resize other nodes (could be useful) the network message
            // needs to know what type of ID it's targeting
            if (node is not ICircuitBoxIdentifiable identifiable)
            {
                DebugConsole.ThrowError("Tried to resize a node that doesn't have an ID.");
                return;
            }

            CreateClientEvent(new CircuitBoxResizeLabelEvent(identifiable.ID, resize.Pos, resize.Size));
        }

        public void AddLabel(Vector2 pos)
        {
            if (IsLocked()) { return; }
            if (GameMain.NetworkMember is null)
            {
                AddLabelInternal(ICircuitBoxIdentifiable.FindFreeID(Labels), GUIStyle.Blue, pos, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty);
                return;
            }

            CreateClientEvent(new CircuitBoxAddLabelEvent(pos, GUIStyle.Blue, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty));
        }

        public void RemoveLabel(IReadOnlyCollection<CircuitBoxLabelNode> labels)
        {
            if (IsLocked()) { return; }
            if (!labels.Any()) { return; }

            var ids = labels.Select(static n => n.ID).ToImmutableArray();

            if (GameMain.NetworkMember is null)
            {
                RemoveLabelInternal(ids);
                return;
            }

            CreateClientEvent(new CircuitBoxRemoveLabelEvent(ids));
        }

        public partial void OnViewUpdateProjSpecific()
        {
            UI?.MouseSnapshotHandler.UpdateConnections();
            UI?.UpdateComponentList();
        }

        protected override void OnResolutionChanged()
        {
            base.OnResolutionChanged();
            CreateGUI();
        }

        // Remove selection when the circuit box is deselected
        public partial void OnDeselected(Character c)
        {
            cursorUpdateTimer = 0f;

            // Server will broadcast the deselection, we don't need to do it ourselves
            if (GameMain.NetworkMember is not null) { return; }
            ClearAllSelectionsInternal(c.ID);
        }

        public void ClientRead(INetSerializableStruct data)
        {
            switch (data)
            {
                case NetCircuitBoxCursorInfo cursorInfo:
                {
                    ClientReadCursor(cursorInfo);
                    break;
                }
                case CircuitBoxErrorEvent errorData:
                {
                    DebugConsole.ThrowError($"The server responded with an error: {errorData.Message}");
                    break;
                }
                default:
                    throw new ArgumentOutOfRangeException(nameof(data), data, "This data cannot be handled using direct network messages.");
            }
        }

        public void SendMessage(CircuitBoxOpcode opcode, INetSerializableStruct data)
        {
            IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.CIRCUITBOX);

            msg.WriteNetSerializableStruct(new NetCircuitBoxHeader(
                Opcode: opcode,
                ItemID: item.ID,
                ComponentIndex: (byte)item.GetComponentIndex(this)));

            msg.WriteNetSerializableStruct(data);

            DeliveryMethod deliveryMethod =
                UnrealiableOpcodes.Contains(opcode)
                    ? DeliveryMethod.Unreliable
                    : DeliveryMethod.Reliable;

            GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
        }

        private void SendCursorState(Vector2[] cursorPositions, Option<Vector2> dragStart, Option<Identifier> heldComponent)
        {
            if (!IsRoundRunning()) { return; }

            var msg = new NetCircuitBoxCursorInfo(
                RecordedPositions: cursorPositions,
                DragStart: dragStart,
                HeldItem: heldComponent);

            SendMessage(CircuitBoxOpcode.Cursor, msg);
        }

        public void ClientReadCursor(NetCircuitBoxCursorInfo info)
        {
            if (Entity.FindEntityByID(info.CharacterID) is not Character character) { return; }

            if (!ActiveCursors.ContainsKey(character))
            {
                var newCursor = new CircuitBoxCursor(info);
                ActiveCursors.Add(character, newCursor);
                return;
            }

            var activeCursor = ActiveCursors[character];
            activeCursor.UpdateInfo(info);
            activeCursor.ResetTimers();
        }

        public void CreateClientEvent(INetSerializableStruct data)
            => item.CreateClientEvent(this, new CircuitBoxEventData(data));

        public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null)
        {
            if (extraData is null) { return; }
            var eventData = ExtractEventData<CircuitBoxEventData>(extraData);
            msg.WriteByte((byte)eventData.Opcode);
            msg.WriteNetSerializableStruct(eventData.Data);
        }

        public void ClientEventRead(IReadMessage msg, float sendingTime)
        {
            var header = (CircuitBoxOpcode)msg.ReadByte();

            switch (header)
            {
                case CircuitBoxOpcode.AddComponent:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxServerCreateComponentEvent>(msg);
                    AddComponentFromData(data);
                    break;
                }
                case CircuitBoxOpcode.DeleteComponent:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxRemoveComponentEvent>(msg);
                    RemoveComponentInternal(data.TargetIDs);
                    break;
                }
                case CircuitBoxOpcode.MoveComponent:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxMoveComponentEvent>(msg);
                    MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount);
                    break;
                }
                case CircuitBoxOpcode.UpdateSelection:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxServerUpdateSelection>(msg);

                    var nodeDict = data.ComponentIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy);
                    var wireDict = data.WireIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy);
                    var ioDict = data.InputOutputs.ToImmutableDictionary(static s => s.Type, static s => s.SelectedBy);
                    var labelDict = data.LabelIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy);

                    UpdateSelections(nodeDict, wireDict, ioDict, labelDict);
                    break;
                }
                case CircuitBoxOpcode.AddWire:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxServerCreateWireEvent>(msg);
                    AddWireFromData(data);
                    break;
                }
                case CircuitBoxOpcode.RemoveWire:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxRemoveWireEvent>(msg);
                    RemoveWireInternal(data.TargetIDs);
                    break;
                }
                case CircuitBoxOpcode.ServerInitialize:
                {
                    Components.Clear();
                    Wires.Clear();
                    Labels.Clear();

                    var data = INetSerializableStruct.Read<CircuitBoxInitializeStateFromServerEvent>(msg);
                    foreach (var compData in data.Components) { AddComponentFromData(compData); }
                    foreach (var wireData in data.Wires) { AddWireFromData(wireData); }

                    foreach (var labelData in data.Labels)
                    {
                        AddLabelInternal(labelData.ID, labelData.Color, labelData.Position, labelData.Header, labelData.Body);
                        ResizeLabelInternal(labelData.ID, labelData.Position, labelData.Size);
                    }

                    foreach (var node in InputOutputNodes)
                    {
                        node.Position = node.NodeType switch
                        {
                            CircuitBoxInputOutputNode.Type.Input => data.InputPos,
                            CircuitBoxInputOutputNode.Type.Output => data.OutputPos,
                            _ => node.Position
                        };
                    }

                    foreach (var labelOverride in data.LabelOverrides)
                    {
                        RenameConnectionLabelsInternal(labelOverride.Type, labelOverride.Override.ToDictionary());
                    }

                    wasInitializedByServer = true;
                    break;
                }
                case CircuitBoxOpcode.RenameLabel:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxRenameLabelEvent>(msg);
                    RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody);
                    break;
                }
                case CircuitBoxOpcode.AddLabel:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxServerAddLabelEvent>(msg);
                    AddLabelInternal(data.ID, data.Color, data.Position, data.Header, data.Body);
                    ResizeLabelInternal(data.ID, data.Position, data.Size);
                    break;
                }
                case CircuitBoxOpcode.RemoveLabel:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxRemoveLabelEvent>(msg);
                    RemoveLabelInternal(data.TargetIDs);
                    break;
                }
                case CircuitBoxOpcode.ResizeLabel:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxResizeLabelEvent>(msg);
                    ResizeLabelInternal(data.ID, data.Position, data.Size);
                    break;
                }
                case CircuitBoxOpcode.RenameConnections:
                {
                    var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
                    RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
                    break;
                }
                default:
                    throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events");
            }
        }

        public void AddComponentFromData(CircuitBoxServerCreateComponentEvent data)
        {
            if (ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.UsedResource) is not { } prefab)
            {
                throw new Exception($"No item prefab found for \"{data.UsedResource}\"");
            }

            AddComponentInternalUnsafe(data.ComponentId, FindItemByID(data.BackingItemId), prefab, data.Position);
        }

        public void AddWireFromData(CircuitBoxServerCreateWireEvent data)
        {
            var (req, wireId, possibleItemId) = data;
            var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == req.SelectedWirePrefabIdentifier);
            if (prefab is null)
            {
                throw new Exception($"No prefab found for \"{req.SelectedWirePrefabIdentifier}\"");
            }

            if (!req.Start.FindConnection(this).TryUnwrap(out var start))
            {
                throw new Exception($"No connection found for ({req.Start})");
            }

            if (!req.End.FindConnection(this).TryUnwrap(out var end))
            {
                throw new Exception($"No connection found for ({req.Start})");
            }

            if (possibleItemId.TryUnwrap(out var backingItem))
            {
                CreateWireWithItem(start, end, wireId, FindItemByID(backingItem));
            }
            else
            {
                CreateWireWithoutItem(start, end, wireId, prefab);
            }
        }

        public static Item FindItemByID(ushort id)
            => Entity.FindEntityByID(id) as Item ?? throw new Exception($"No item with ID {id} exists.");

        public override void AddToGUIUpdateList(int order = 0)
        {
            base.AddToGUIUpdateList(order);
            UI?.AddToGUIUpdateList();
        }
    }
}