using System;
using System.Collections.Generic;
using Editor.GON.WorldEditor;
using Intrepid.Gameplay.Journey;
using Intrepid.Utilities;
using UnityEngine;

namespace Editor.GON.StageEditor
{
    public enum StageLogicalLayer
    {
        Structural = 0,
        Design = 1,
        Camera = 2,
        Markers = 3
    }

    public enum StructuralCellKind
    {
        Empty = 0,
        
        Ground = 10,
        
        Ramp_Front = 11,
        Ramp_Left = 12,
        Ramp_Right = 13,
        
        Stair_Front = 14,
        Stair_Left = 15,
        Stair_Right = 16,
        
        Wall = 50,
    }

    public enum StageObjectKind
    {
        Trap_FallingFloor = 0,
        
        SocketEast = 10,
        SocketNorth = 11,
        SocketSouth = 12,
        SocketWest = 13,
        
        CameraBoundary = 40,
        
        Spawn = 50,
        // Elevator = 6,
        // Marker = 7,
        // Custom = 100
    }

    [CreateAssetMenu(menuName = "GON/Editor/Stage Definition", fileName = "StageDefinition")]
    public sealed class StageDefinition : SOIdentification
    {
        public float cellSize = 1f;
        public float floorHeight = 6f;
        public int activeFloorIndex;
        public StageTag stageTag;

        public StageWorldSyncData worldSync = new();

        public List<StageFloorDefinition> floors = new();
        public List<StageObjectDefinition> objects = new();

        private void OnValidate()
        {
            if (stageTag.IsNotNull() && !string.IsNullOrWhiteSpace(worldSync.worldStageId))
            {
                stageTag.StageId = worldSync.worldStageId;
            }
        }

        public StageFloorDefinition ActiveFloor
        {
            get
            {
                EnsureDefaults();
                if (activeFloorIndex < 0) activeFloorIndex = 0;
                if (activeFloorIndex >= floors.Count) activeFloorIndex = floors.Count - 1;
                return floors[activeFloorIndex];
            }
        }

        public void EnsureDefaults()
        {
            worldSync ??= new StageWorldSyncData();
            worldSync.EnsureDefaults();

            floors ??= new List<StageFloorDefinition>();
            objects ??= new List<StageObjectDefinition>();

            if (floors.Count == 0)
            {
                floors.Add(new StageFloorDefinition
                {
                    floorId = "F0"
                });
            }

            if (cellSize <= 0f)
            {
                cellSize = 1f;
            }

            if (floorHeight <= 0f)
            {
                floorHeight = 6f;
            }
        }

        public StageFloorDefinition GetFloor(string floorId)
        {
            EnsureDefaults();
            foreach (var stageFloorDefinition in floors)
            {
                if (stageFloorDefinition != null && stageFloorDefinition.floorId == floorId)
                {
                    return stageFloorDefinition;
                }
            }

            return null;
        }

        public bool ContainsFloor(string floorId)
        {
            return GetFloor(floorId) is not null;
        }

        public string CreateUniqueObjectId(StageObjectKind kind)
        {
            objects ??= new List<StageObjectDefinition>();

            var prefix = kind.ToString().ToLowerInvariant();
            var index = 1;
            while (true)
            {
                var candidate = prefix + "_" + index.ToString("000");
                var exists = false;
                foreach (var stageObjectDefinition in objects)
                {
                    if (stageObjectDefinition != null && stageObjectDefinition.id == candidate)
                    {
                        exists = true;
                        break;
                    }
                }

                if (!exists)
                {
                    return candidate;
                }

                index++;
            }
        }
    }

    [Serializable]
    public sealed class StageWorldSyncData
    {
        public string worldLayoutId;
        public string worldStageId;
        public string worldStageDisplayName;
        public WorldStageKind worldStageKind;
        public int worldLayer;
        public float worldTileSize = 24f;
        public float worldLayerHeight = 24f;
        public List<JWorldTile> worldTiles = new();
        public List<StageWorldSyncEndpoint> expectedEndpoints = new();
        public string syncedAt;
        public int contractVersion = 1;
        public string contractHash;

        public bool HasSync => !string.IsNullOrEmpty(worldStageId);

        public void EnsureDefaults()
        {
            worldTiles ??= new List<JWorldTile>();
            expectedEndpoints ??= new List<StageWorldSyncEndpoint>();

            if (worldTileSize <= 0f)
            {
                worldTileSize = 24f;
            }

            if (worldLayerHeight <= 0f)
            {
                worldLayerHeight = 24f;
            }

            if (contractVersion <= 0)
            {
                contractVersion = 1;
            }
        }

        public bool TryGetWorldBounds(out RectInt bounds)
        {
            EnsureDefaults();
            if (worldTiles.Count == 0)
            {
                bounds = default;
                return false;
            }

            var minX = worldTiles[0].x;
            var maxX = worldTiles[0].x;
            var minY = worldTiles[0].y;
            var maxY = worldTiles[0].y;

            for (var i = 1; i < worldTiles.Count; i++)
            {
                var tile = worldTiles[i];
                minX = Mathf.Min(minX, tile.x);
                maxX = Mathf.Max(maxX, tile.x);
                minY = Mathf.Min(minY, tile.y);
                maxY = Mathf.Max(maxY, tile.y);
            }

            bounds = new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1);
            return true;
        }
    }

    [Serializable]
    public sealed class StageWorldSyncEndpoint
    {
        public string endpointId;
        public JEndpointKind kind;
        public JWorldTile worldTile;
        public JSocketFace socketFace;
        public bool required;
        public string connectionId;
        public string connectedEndpointId;
        public string debugLabel;
    }

    [Serializable]
    public sealed class StageFloorDefinition
    {
        public string floorId = "F0";
        public float FloorHeight;
        
        [SerializeField] private List<LogicalCell> cells = new();
        [NonSerialized] private Dictionary<long, int> cellIndex;

        public List<LogicalCell> Cells
        {
            get
            {
                cells ??= new List<LogicalCell>();
                return cells;
            }
        }
        
        private static long CellKey(int x, int y)
        {
            return ((long)x << 32) ^ (uint)y;
        }

        private void EnsureCellIndex()
        {
            if (cellIndex != null)
            {
                return;
            }

            cellIndex = new Dictionary<long, int>(Cells.Count);

            for (var i = 0; i < Cells.Count; i++)
            {
                var cell = Cells[i];
                cellIndex[CellKey(cell.x, cell.y)] = i;
            }
        }

        public StructuralCellKind GetCell(int x, int y)
        {
            var index = FindCellIndex(x, y);
            if (index < 0)
            {
                return StructuralCellKind.Empty;
            }

            return Cells[index].structural;
        }

        public void SetCell(int x, int y, StructuralCellKind kind)
        {
            EnsureCellIndex();

            var key = CellKey(x, y);
            var exists = cellIndex.TryGetValue(key, out var index);

            if (kind == StructuralCellKind.Empty)
            {
                if (exists)
                {
                    RemoveCellAt(index);
                }

                return;
            }

            var cell = new LogicalCell
            {
                x = x,
                y = y,
                structural = kind
            };

            if (exists)
            {
                Cells[index] = cell;
            }
            else
            {
                cellIndex[key] = Cells.Count;
                Cells.Add(cell);
            }
        }
        
        private void RemoveCellAt(int index)
        {
            EnsureCellIndex();

            var removed = Cells[index];
            cellIndex.Remove(CellKey(removed.x, removed.y));

            var lastIndex = Cells.Count - 1;
            if (index != lastIndex)
            {
                var moved = Cells[lastIndex];
                Cells[index] = moved;
                cellIndex[CellKey(moved.x, moved.y)] = index;
            }

            Cells.RemoveAt(lastIndex);
        }

        public void FillRect(RectInt rect, StructuralCellKind kind)
        {
            for (var y = rect.yMin; y < rect.yMax; y++)
            {
                for (var x = rect.xMin; x < rect.xMax; x++)
                {
                    SetCell(x, y, kind);
                }
            }
        }

        public bool TryGetUsedBounds(out RectInt bounds)
        {
            if (Cells.Count == 0)
            {
                bounds = default;
                return false;
            }

            var minX = Cells[0].x;
            var maxX = Cells[0].x;
            var minY = Cells[0].y;
            var maxY = Cells[0].y;

            for (var i = 1; i < Cells.Count; i++)
            {
                var cell = Cells[i];
                minX = Mathf.Min(minX, cell.x);
                maxX = Mathf.Max(maxX, cell.x);
                minY = Mathf.Min(minY, cell.y);
                maxY = Mathf.Max(maxY, cell.y);
            }

            bounds = new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1);
            return true;
        }

        private int FindCellIndex(int x, int y)
        {
            for (var i = 0; i < Cells.Count; i++)
            {
                var cell = Cells[i];
                if (cell.x == x && cell.y == y)
                {
                    return i;
                }
            }

            return -1;
        }
    }

    [Serializable]
    public struct LogicalCell
    {
        public int x;
        public int y;
        public StructuralCellKind structural;
    }

    [Serializable]
    public sealed class StageObjectDefinition
    {
        public string id;
        public StageLogicalLayer layer;
        public StageObjectKind kind;
        public string typeId;
        public string prefabId;
        public string floorId;

        public int x;
        public int y;
        public int width = 1;
        public int height = 1;

        public bool cutsGround;
        public bool blocksMovement;
        public string notes;

        public bool IsResizable => IsKindResizable(kind);
        public Vector2Int DefaultSize => GetDefaultSize(kind);
        public Vector2Int MinSize => GetMinSize(kind);
        public Vector2Int MaxSize => GetMaxSize(kind);

        public RectInt Bounds => new(x, y, Mathf.Max(1, width), Mathf.Max(1, height));

        public bool ContainsCell(int cellX, int cellY)
        {
            var bounds = Bounds;
            return cellX >= bounds.xMin && cellX < bounds.xMax && cellY >= bounds.yMin && cellY < bounds.yMax;
        }

        public void NormalizeSize()
        {
            if (!IsResizable)
            {
                var fixedSize = DefaultSize;
                width = Mathf.Max(1, fixedSize.x);
                height = Mathf.Max(1, fixedSize.y);
                return;
            }

            var min = MinSize;
            var max = MaxSize;
            width = Mathf.Clamp(Mathf.Max(1, width), Mathf.Max(1, min.x), Mathf.Max(1, max.x));
            height = Mathf.Clamp(Mathf.Max(1, height), Mathf.Max(1, min.y), Mathf.Max(1, max.y));
        }

        public static bool IsKindResizable(StageObjectKind kind)
        {
            return kind switch
            {
                StageObjectKind.Trap_FallingFloor => true,
                StageObjectKind.CameraBoundary => true,
                _ => false
            };
        }

        public static Vector2Int GetDefaultSize(StageObjectKind kind)
        {
            return kind switch
            {
                StageObjectKind.Trap_FallingFloor => 2 * Vector2Int.one,
                StageObjectKind.SocketNorth => new Vector2Int(4, 1),
                StageObjectKind.SocketSouth => new Vector2Int(4, 1),
                StageObjectKind.SocketEast => new Vector2Int(1, 4),
                StageObjectKind.SocketWest => new Vector2Int(1, 4),
                StageObjectKind.CameraBoundary => new Vector2Int(8, 5),
                _ => Vector2Int.one
            };
        }

        public static Vector2Int GetMinSize(StageObjectKind kind)
        {
            return kind switch
            {
                StageObjectKind.CameraBoundary => new Vector2Int(1, 1),
                _ => GetDefaultSize(kind)
            };
        }

        public static Vector2Int GetMaxSize(StageObjectKind kind)
        {
            return kind switch
            {
                StageObjectKind.Trap_FallingFloor => new Vector2Int(64, 64), 
                StageObjectKind.CameraBoundary => new Vector2Int(64, 64),
                _ => GetDefaultSize(kind)
            };
        }
    }
}
