using System;
using Intrepid.Gameplay.Journey;
using UnityEngine;
using UnityEngine.UIElements;

namespace Editor.GON.StageEditor
{
    public sealed class StageMapCanvas : VisualElement
    {
        private const float MinCellPixels = 3f;
        private const float MaxCellPixels = 96f;
        private const float ResizeHandlePixels = 12f;

        private enum DragMode
        {
            None,
            Pan,
            Paint,
            MoveObject,
            ResizeObject
        }

        private readonly StageEditorWindow owner;
        private readonly StageEditorState state;

        private StageDefinition stage;
        private Vector2 pan = new(40f, 40f);
        private float cellPixels = 32f;

        private DragMode dragMode = DragMode.None;
        private Vector2 panStartMouse;
        private Vector2 panStartValue;
        private Vector2Int dragStartCell;
        private Vector2Int dragCurrentCell;
        private Vector2Int? hoverCell;

        private StageObjectDefinition draggedObject;
        private int objectStartX;
        private int objectStartY;
        private int objectStartWidth;
        private int objectStartHeight;

        public StageMapCanvas(StageEditorWindow owner, StageEditorState state)
        {
            this.owner = owner;
            this.state = state;

            focusable = true;
            style.flexGrow = 1f;
            style.overflow = Overflow.Hidden;
            style.backgroundColor = StageEditorColors.Background;

            generateVisualContent += OnGenerateVisualContent;

            RegisterCallback<MouseDownEvent>(OnMouseDown);
            RegisterCallback<MouseMoveEvent>(OnMouseMove);
            RegisterCallback<MouseUpEvent>(OnMouseUp);
            RegisterCallback<MouseLeaveEvent>(OnMouseLeave);
            RegisterCallback<WheelEvent>(OnWheel);
        }
        
        private RectInt GetVisibleCellBounds(int margin = 1)
        {
            var r = contentRect;

            var minX = Mathf.FloorToInt((r.xMin - pan.x) / cellPixels) - margin;
            var maxX = Mathf.CeilToInt((r.xMax - pan.x) / cellPixels) + margin;
            var minY = Mathf.FloorToInt((r.yMin - pan.y) / cellPixels) - margin;
            var maxY = Mathf.CeilToInt((r.yMax - pan.y) / cellPixels) + margin;

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

        private static bool ContainsCell(RectInt rect, int x, int y)
        {
            return x >= rect.xMin && x < rect.xMax &&
                   y >= rect.yMin && y < rect.yMax;
        }

        public void SetStage(StageDefinition newStage)
        {
            stage = newStage;
            stage?.EnsureDefaults();
            MarkDirtyRepaint();
        }

        public void RequestRepaint()
        {
            MarkDirtyRepaint();
        }

        private void OnWheel(WheelEvent evt)
        {
            if (stage is null)
            {
                return;
            }

            UpdateHoverCell(evt.localMousePosition);

            var mouse = evt.localMousePosition;
            var before = ScreenToGridFloat(mouse);

            var delta = -evt.delta.y * 0.08f;
            cellPixels = Mathf.Clamp(cellPixels * (1f + delta), MinCellPixels, MaxCellPixels);

            var after = ScreenToGridFloat(mouse);
            pan += (after - before) * cellPixels;

            evt.StopPropagation();
            MarkDirtyRepaint();
        }

        private void OnMouseDown(MouseDownEvent evt)
        {
            Focus();
            UpdateHoverCell(evt.localMousePosition);

            if (stage is null)
            {
                return;
            }

            // Pan is no longer a palette tool. It is always available through RMB/MMB.
            if (evt.button == 2 || evt.button == 1)
            {
                BeginPan(evt.localMousePosition);
                evt.StopPropagation();
                return;
            }

            if (evt.button != 0)
            {
                return;
            }

            if (!TryScreenToCell(evt.localMousePosition, out var cell))
            {
                return;
            }

            // If a resizable selected object has its handle clicked, resize wins over move/select.
            if (TryBeginResizeSelectedObject(evt.localMousePosition, cell))
            {
                evt.StopPropagation();
                MarkDirtyRepaint();
                return;
            }

            // LMB always tries to select an existing logical object first.
            // If the user drags after selecting/hitting it, the same interaction moves it.
            var hit = FindObjectAt(cell, false);
            if (hit != null)
            {
                state.SelectObject(hit);
                BeginMoveObject(hit, cell);
                evt.StopPropagation();
                MarkDirtyRepaint();
                return;
            }

            state.SelectObject(null);

            // Empty space uses the active palette item to paint/place.
            BeginPaint(cell);
            evt.StopPropagation();
            MarkDirtyRepaint();
        }

        private void OnMouseMove(MouseMoveEvent evt)
        {
            UpdateHoverCell(evt.localMousePosition);

            switch (dragMode)
            {
                case DragMode.Pan:
                    pan = panStartValue + evt.localMousePosition - panStartMouse;
                    evt.StopPropagation();
                    MarkDirtyRepaint();
                    return;

                case DragMode.Paint:
                    if (TryScreenToCell(evt.localMousePosition, out var paintCell))
                    {
                        dragCurrentCell = paintCell;
                        MarkDirtyRepaint();
                    }

                    evt.StopPropagation();
                    return;

                case DragMode.MoveObject:
                    if (TryScreenToCell(evt.localMousePosition, out var moveCell))
                    {
                        ApplyMoveObject(moveCell);
                    }

                    evt.StopPropagation();
                    return;

                case DragMode.ResizeObject:
                    if (TryScreenToCell(evt.localMousePosition, out var resizeCell))
                    {
                        ApplyResizeObject(resizeCell);
                    }

                    evt.StopPropagation();
                    return;
                case DragMode.None:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void OnMouseUp(MouseUpEvent evt)
        {
            switch (dragMode)
            {
                case DragMode.None:
                    return;
                case DragMode.Paint when evt.button == 0:
                {
                    var rect = GetDragRect();
                    ApplyPalette(rect);
                    break;
                }
                case DragMode.Pan:
                case DragMode.MoveObject:
                case DragMode.ResizeObject:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            EndDrag();
            evt.StopPropagation();
            MarkDirtyRepaint();
        }

        private void OnMouseLeave(MouseLeaveEvent evt)
        {
            if (!this.HasMouseCapture())
            {
                hoverCell = null;
                owner.SetMouseCell(null);
                MarkDirtyRepaint();
            }
        }

        private void BeginPan(Vector2 mouse)
        {
            dragMode = DragMode.Pan;
            panStartMouse = mouse;
            panStartValue = pan;
            this.CaptureMouse();
        }

        private void BeginPaint(Vector2Int cell)
        {
            dragMode = DragMode.Paint;
            dragStartCell = cell;
            dragCurrentCell = cell;
            this.CaptureMouse();
        }

        private void BeginMoveObject(StageObjectDefinition obj, Vector2Int cell)
        {
            dragMode = DragMode.MoveObject;
            draggedObject = obj;
            dragStartCell = cell;
            dragCurrentCell = cell;
            objectStartX = obj.x;
            objectStartY = obj.y;
            objectStartWidth = obj.width;
            objectStartHeight = obj.height;
            this.CaptureMouse();
        }

        private bool TryBeginResizeSelectedObject(Vector2 mouse, Vector2Int cell)
        {
            var obj = state.selectedObject;
            if (obj is null || !CanEditObjectOnActiveFloor(obj) || !obj.IsResizable)
            {
                return false;
            }

            if (!GetResizeHandleRect(obj).Contains(mouse))
            {
                return false;
            }

            dragMode = DragMode.ResizeObject;
            draggedObject = obj;
            dragStartCell = cell;
            dragCurrentCell = cell;
            objectStartX = obj.x;
            objectStartY = obj.y;
            objectStartWidth = obj.width;
            objectStartHeight = obj.height;
            this.CaptureMouse();
            return true;
        }

        private void ApplyMoveObject(Vector2Int cell)
        {
            if (draggedObject is null || !CanEditObjectOnActiveFloor(draggedObject))
            {
                return;
            }

            dragCurrentCell = cell;

            var dx = cell.x - dragStartCell.x;
            var dy = cell.y - dragStartCell.y;
            var newX = objectStartX + dx;
            var newY = objectStartY + dy;

            if (draggedObject.x == newX && draggedObject.y == newY)
            {
                return;
            }

            owner.MutateStage("Move object", delegate
            {
                draggedObject.x = newX;
                draggedObject.y = newY;
            });
        }

        private void ApplyResizeObject(Vector2Int cell)
        {
            if (draggedObject is null || !CanEditObjectOnActiveFloor(draggedObject) || !draggedObject.IsResizable)
            {
                return;
            }

            dragCurrentCell = cell;

            var min = draggedObject.MinSize;
            var max = draggedObject.MaxSize;
            var newWidth = Mathf.Clamp(cell.x - objectStartX + 1, Mathf.Max(1, min.x), Mathf.Max(1, max.x));
            var newHeight = Mathf.Clamp(cell.y - objectStartY + 1, Mathf.Max(1, min.y), Mathf.Max(1, max.y));

            if (draggedObject.width == newWidth && draggedObject.height == newHeight)
            {
                return;
            }

            owner.MutateStage("Resize object", delegate
            {
                draggedObject.width = newWidth;
                draggedObject.height = newHeight;
                draggedObject.NormalizeSize();
            });
        }

        private void EndDrag()
        {
            dragMode = DragMode.None;
            draggedObject = null;
            if (this.HasMouseCapture())
            {
                this.ReleaseMouse();
            }
        }

        private void ApplyPalette(RectInt rect)
        {
            var floor = stage?.ActiveFloor;
            if (floor is null)
            {
                return;
            }

            var entry = state.activePaletteEntry;
            if (entry is null)
            {
                return;
            }

            if (entry.paintsStructural)
            {
                owner.MutateStage("Paint " + entry.displayName,
                    delegate { floor.FillRect(rect, entry.structuralKind); });
                return;
            }

            var objectRect = GetPaletteObjectRect(entry, rect);

            owner.MutateStage("Place " + entry.displayName, delegate
            {
                var obj = new StageObjectDefinition
                {
                    id = stage.CreateUniqueObjectId(entry.objectKind),
                    layer = entry.layer,
                    kind = entry.objectKind,
                    typeId = entry.typeId,
                    prefabId = entry.prefabId,
                    floorId = floor.floorId,
                    x = objectRect.x,
                    y = objectRect.y,
                    width = Mathf.Max(1, objectRect.width),
                    height = Mathf.Max(1, objectRect.height),
                    cutsGround = entry.cutsGround,
                    blocksMovement = entry.blocksMovement
                };

                obj.NormalizeSize();
                stage.objects.Add(obj);
                state.SelectObject(obj);
            });
        }

        private RectInt GetPaletteObjectRect(StagePaletteEntry entry, RectInt paintedRect)
        {
            var defaultSize = entry.defaultSize;
            if (defaultSize.x <= 0 || defaultSize.y <= 0)
            {
                defaultSize = StageObjectDefinition.GetDefaultSize(entry.objectKind);
            }

            if (!StageObjectDefinition.IsKindResizable(entry.objectKind))
            {
                return new RectInt(paintedRect.x, paintedRect.y, Mathf.Max(1, defaultSize.x),
                    Mathf.Max(1, defaultSize.y));
            }

            var objectRect = paintedRect;
            if (paintedRect is { width: 1, height: 1 })
            {
                objectRect = new RectInt(paintedRect.x, paintedRect.y, Mathf.Max(1, defaultSize.x),
                    Mathf.Max(1, defaultSize.y));
            }

            var min = StageObjectDefinition.GetMinSize(entry.objectKind);
            var max = StageObjectDefinition.GetMaxSize(entry.objectKind);
            objectRect.width = Mathf.Clamp(objectRect.width, Mathf.Max(1, min.x), Mathf.Max(1, max.x));
            objectRect.height = Mathf.Clamp(objectRect.height, Mathf.Max(1, min.y), Mathf.Max(1, max.y));
            return objectRect;
        }

        private StageObjectDefinition FindObjectAt(Vector2Int cell, bool activeLayerOnly)
        {
            if (stage is null)
            {
                return null;
            }

            var floorId = stage.ActiveFloor.floorId;
            for (var i = stage.objects.Count - 1; i >= 0; i--)
            {
                var obj = stage.objects[i];
                if (obj is null || obj.floorId != floorId)
                {
                    continue;
                }

                if (activeLayerOnly && obj.layer != state.activeLayer)
                {
                    continue;
                }

                if (obj.ContainsCell(cell.x, cell.y))
                {
                    return obj;
                }
            }

            return null;
        }

        private bool CanEditObjectOnActiveFloor(StageObjectDefinition obj)
        {
            return stage is not null && obj is not null && stage.ActiveFloor is not null &&
                   obj.floorId == stage.ActiveFloor.floorId;
        }

        private void UpdateHoverCell(Vector2 localMousePosition)
        {
            if (stage is null || !TryScreenToCell(localMousePosition, out var cell))
            {
                hoverCell = null;
                owner.SetMouseCell(null);
                return;
            }

            if (hoverCell.HasValue && hoverCell.Value == cell)
            {
                return;
            }

            hoverCell = cell;
            owner.SetMouseCell(cell);
            MarkDirtyRepaint();
        }

        private void OnGenerateVisualContent(MeshGenerationContext mgc)
        {
            var p = mgc.painter2D;
            FillRect(p, contentRect, StageEditorColors.Background);

            if (stage is null)
            {
                return;
            }

            stage.EnsureDefaults();
            var floor = stage.ActiveFloor;
            if (floor is null)
            {
                return;
            }

            DrawInfiniteGrid(p);

            DrawWorldSyncUnderlay(p);

            DrawLowerFloors(p);
            
            DrawCells(p, floor, 1f, ShouldDrawCellIcons(floor));
            DrawObjects(p, floor.floorId, 1f, true);

            DrawOriginAxes(p);
            DrawHoverCell(p);
            DrawPreview(p);
        }
        
        private bool ShouldDrawCellIcons(StageFloorDefinition floor)
        {
            if (cellPixels < 24f)
            {
                return false;
            }

            return true;
        }

        private void DrawWorldSyncUnderlay(Painter2D p)
        {
            var sync = stage.worldSync;
            if (!state.showWorldSyncUnderlay || sync is null || !sync.HasSync)
            {
                return;
            }

            sync.EnsureDefaults();
            var cellsPerWorldTile = sync.worldTileSize / Mathf.Max(0.01f, stage.cellSize);
            if (cellsPerWorldTile <= 0f)
            {
                return;
            }

            sync.TryGetWorldBounds(out var worldBounds);
            var worldCenter = worldBounds.size == Vector2Int.zero
                ? Vector2.zero
                : new Vector2(worldBounds.xMin + worldBounds.width * 0.5f,
                    worldBounds.yMin + worldBounds.height * 0.5f);

            foreach (var tile in sync.worldTiles)
            {
                DrawHatching(p, WorldTileRect(tile, worldCenter, cellsPerWorldTile),
                    StageEditorColors.WorldSyncFootprint);
            }

            foreach (var endpoint in sync.expectedEndpoints)
            {
                if (endpoint is null)
                {
                    continue;
                }

                var rect = WorldTileRect(endpoint.worldTile, worldCenter, cellsPerWorldTile);
                switch (endpoint.kind)
                {
                    case JEndpointKind.Anchor:
                        DrawHatching(p, rect.Expanded(-3f), StageEditorColors.WorldSyncAnchor);
                        break;
                    case JEndpointKind.Lift:
                        DrawHatching(p, rect.Expanded(-6f), StageEditorColors.WorldSyncLift);
                        break;
                    case JEndpointKind.Socket:
                        DrawSocketFaceHint(p, rect, endpoint.socketFace);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
        
        private void DrawLowerFloors(Painter2D p)
        {
            if (stage?.floors is null)
            {
                return;
            }

            var activeIndex = Mathf.Clamp(stage.activeFloorIndex, 0, stage.floors.Count - 1);

            for (var i = 0; i < activeIndex; i++)
            {
                var lowerFloor = stage.floors[i];
                if (lowerFloor is null)
                {
                    continue;
                }

                var alpha = GetLowerFloorAlpha(i, activeIndex);

                DrawCells(p, lowerFloor, alpha, false);
                DrawObjects(p, lowerFloor.floorId, alpha, false);
            }
        }

        private float GetLowerFloorAlpha(int floorIndex, int activeFloorIndex)
        {
            var distance = activeFloorIndex - floorIndex;

            // Floor imediatamente abaixo aparece mais forte.
            // Floors mais distantes ficam mais apagados.
            return Mathf.Clamp(0.38f - ((distance - 1) * 0.10f), 0.10f, 0.38f);
        }
        
        private void FillTriangle(Painter2D p, Vector2 a, Vector2 b, Vector2 c, Color color)
        {
            p.fillColor = color;
            p.BeginPath();
            p.MoveTo(a);
            p.LineTo(b);
            p.LineTo(c);
            p.ClosePath();
            p.Fill();
        }

        private void StrokeTriangle(Painter2D p, Vector2 a, Vector2 b, Vector2 c, Color color, float width)
        {
            p.strokeColor = color;
            p.lineWidth = width;
            p.BeginPath();
            p.MoveTo(a);
            p.LineTo(b);
            p.LineTo(c);
            p.ClosePath();
            p.Stroke();
        }

        private void DrawCells(Painter2D p, StageFloorDefinition floor, float alphaMultiplier, bool drawIcons)
        {
            var visible = GetVisibleCellBounds();

            foreach (var cell in floor.Cells)
            {
                if (!ContainsCell(visible, cell.x, cell.y))
                {
                    continue;
                }

                var color = StageEditorColors.Structural(cell.structural);
                if (color.a <= 0f)
                {
                    continue;
                }

                color = color.WithAlpha(color.a * alphaMultiplier);

                var rect = CellRect(cell.x, cell.y);
                FillRect(p, rect, color);

                if (drawIcons)
                {
                    DrawStructuralIcon(p, cell.structural, rect);
                }
            }
        }

        private void DrawStructuralIcon(Painter2D p, StructuralCellKind kind, Rect rect)
        {
            if (cellPixels < 18f)
            {
                return;
            }

            var pad = Mathf.Clamp(rect.width * 0.18f, 3f, 8f);
            var inner = rect.Expanded(-pad);
            var ink = Color.black.WithAlpha(0.55f);
            var light = Color.white.WithAlpha(0.45f);
            var line = Mathf.Clamp(cellPixels * 0.08f, 1.5f, 4f);

            switch (kind)
            {
                case StructuralCellKind.Ground:
                    StrokeRect(p, inner, ink, line);
                    DrawLine(p, new Vector2(inner.xMin, inner.center.y), new Vector2(inner.xMax, inner.center.y), light,
                        line);
                    break;

                case StructuralCellKind.Ramp_Front:
                    FillTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMax),
                        new Vector2(inner.xMax, inner.yMax),
                        new Vector2(inner.center.x, inner.yMin),
                        light
                    );
                    StrokeTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMax),
                        new Vector2(inner.xMax, inner.yMax),
                        new Vector2(inner.center.x, inner.yMin),
                        ink,
                        line
                    );
                    break;

                case StructuralCellKind.Ramp_Left:
                    FillTriangle(
                        p,
                        new Vector2(inner.xMax, inner.yMin),
                        new Vector2(inner.xMin, inner.center.y),
                        new Vector2(inner.xMax, inner.yMax),
                        light
                    );
                    StrokeTriangle(
                        p,
                        new Vector2(inner.xMax, inner.yMin),
                        new Vector2(inner.xMin, inner.center.y),
                        new Vector2(inner.xMax, inner.yMax),
                        ink,
                        line
                    );
                    break;

                case StructuralCellKind.Ramp_Right:
                    FillTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMin),
                        new Vector2(inner.xMax, inner.center.y),
                        new Vector2(inner.xMin, inner.yMax),
                        light
                    );
                    StrokeTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMin),
                        new Vector2(inner.xMax, inner.center.y),
                        new Vector2(inner.xMin, inner.yMax),
                        ink,
                        line
                    );
                    break;
                
                case StructuralCellKind.Stair_Front:
                    FillTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMax),
                        new Vector2(inner.xMax, inner.yMax),
                        new Vector2(inner.center.x, inner.yMin),
                        light
                    );
                    StrokeTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMax),
                        new Vector2(inner.xMax, inner.yMax),
                        new Vector2(inner.center.x, inner.yMin),
                        ink,
                        line
                    );
                    break;

                case StructuralCellKind.Stair_Left:
                    FillTriangle(
                        p,
                        new Vector2(inner.xMax, inner.yMin),
                        new Vector2(inner.xMin, inner.center.y),
                        new Vector2(inner.xMax, inner.yMax),
                        light
                    );
                    StrokeTriangle(
                        p,
                        new Vector2(inner.xMax, inner.yMin),
                        new Vector2(inner.xMin, inner.center.y),
                        new Vector2(inner.xMax, inner.yMax),
                        ink,
                        line
                    );
                    break;

                case StructuralCellKind.Stair_Right:
                    FillTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMin),
                        new Vector2(inner.xMax, inner.center.y),
                        new Vector2(inner.xMin, inner.yMax),
                        light
                    );
                    StrokeTriangle(
                        p,
                        new Vector2(inner.xMin, inner.yMin),
                        new Vector2(inner.xMax, inner.center.y),
                        new Vector2(inner.xMin, inner.yMax),
                        ink,
                        line
                    );
                    break;

                case StructuralCellKind.Wall:
                    StrokeRect(p, inner, ink, line);
                    DrawLine(p, new Vector2(inner.xMin, inner.yMin), new Vector2(inner.xMax, inner.yMax), ink, line);
                    DrawLine(p, new Vector2(inner.xMax, inner.yMin), new Vector2(inner.xMin, inner.yMax), ink, line);
                    break;
                case StructuralCellKind.Empty:
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
            }
        }

        private void DrawObjects(Painter2D p, string floorId, float alphaMultiplier, bool allowSelection)
        {
            foreach (var obj in stage.objects)
            {
                if (obj is null || obj.floorId != floorId)
                {
                    continue;
                }

                var rect = GridRect(obj.Bounds);
                var color = StageEditorColors.Layer(obj.layer);
                color = color.WithAlpha(color.a * alphaMultiplier);

                FillRect(p, rect, color);
                StrokeRect(p, rect, color.WithAlpha(Mathf.Min(1f, color.a + 0.25f * alphaMultiplier)), 2f);
                DrawObjectIcon(p, obj.kind, rect);
                
                if (allowSelection && state.selectedObject == obj)
                {
                    StrokeRect(p, rect.Expanded(2f), StageEditorColors.SelectionStroke, 3f);

                    if (obj.IsResizable)
                    {
                        DrawResizeHandle(p, obj);
                    }
                }
            }
        }

        private void DrawObjectIcon(Painter2D p, StageObjectKind kind, Rect rect)
        {
            if (cellPixels < 18f)
            {
                return;
            }

            var pad = Mathf.Clamp(Mathf.Min(rect.width, rect.height) * 0.2f, 4f, 12f);
            var inner = rect.Expanded(-pad);
            var ink = Color.black.WithAlpha(0.65f);
            var light = Color.white.WithAlpha(0.7f);
            var line = Mathf.Clamp(cellPixels * 0.08f, 1.5f, 4f);

            switch (kind)
            {
                case StageObjectKind.Trap_FallingFloor:
                    DrawLine(p, new Vector2(inner.xMin, inner.center.y), new Vector2(inner.center.x, inner.yMin), ink,
                        line);
                    DrawLine(p, new Vector2(inner.center.x, inner.yMin), new Vector2(inner.xMax, inner.yMax), ink,
                        line);
                    break;

                case StageObjectKind.SocketEast:
                case StageObjectKind.SocketWest:
                    DrawLine(p, new Vector2(inner.xMin, inner.center.y), new Vector2(inner.xMax, inner.center.y), light,
                        line);
                    DrawLine(p, new Vector2(inner.xMin, inner.yMin), new Vector2(inner.xMin, inner.yMax), ink, line);
                    DrawLine(p, new Vector2(inner.xMax, inner.yMin), new Vector2(inner.xMax, inner.yMax), ink, line);
                    break;

                case StageObjectKind.SocketNorth:
                case StageObjectKind.SocketSouth:
                    DrawLine(p, new Vector2(inner.center.x, inner.yMin), new Vector2(inner.center.x, inner.yMax), light,
                        line);
                    DrawLine(p, new Vector2(inner.xMin, inner.yMin), new Vector2(inner.xMax, inner.yMin), ink, line);
                    DrawLine(p, new Vector2(inner.xMin, inner.yMax), new Vector2(inner.xMax, inner.yMax), ink, line);
                    break;

                case StageObjectKind.CameraBoundary:
                    DrawCornerBrackets(p, inner, ink, line);
                    break;
            }
        }

        private void DrawCornerBrackets(Painter2D p, Rect rect, Color color, float width)
        {
            var l = Mathf.Min(rect.width, rect.height) * 0.25f;

            DrawLine(p, new Vector2(rect.xMin, rect.yMin), new Vector2(rect.xMin + l, rect.yMin), color, width);
            DrawLine(p, new Vector2(rect.xMin, rect.yMin), new Vector2(rect.xMin, rect.yMin + l), color, width);

            DrawLine(p, new Vector2(rect.xMax, rect.yMin), new Vector2(rect.xMax - l, rect.yMin), color, width);
            DrawLine(p, new Vector2(rect.xMax, rect.yMin), new Vector2(rect.xMax, rect.yMin + l), color, width);

            DrawLine(p, new Vector2(rect.xMin, rect.yMax), new Vector2(rect.xMin + l, rect.yMax), color, width);
            DrawLine(p, new Vector2(rect.xMin, rect.yMax), new Vector2(rect.xMin, rect.yMax - l), color, width);

            DrawLine(p, new Vector2(rect.xMax, rect.yMax), new Vector2(rect.xMax - l, rect.yMax), color, width);
            DrawLine(p, new Vector2(rect.xMax, rect.yMax), new Vector2(rect.xMax, rect.yMax - l), color, width);
        }

        private void DrawResizeHandle(Painter2D p, StageObjectDefinition obj)
        {
            var handle = GetResizeHandleRect(obj);
            FillRect(p, handle, StageEditorColors.SelectionStroke);
            StrokeRect(p, handle, Color.black.WithAlpha(0.75f), 1f);
        }

        private void DrawInfiniteGrid(Painter2D p)
        {
            var r = contentRect;
            var minX = Mathf.FloorToInt((r.xMin - pan.x) / cellPixels) - 1;
            var maxX = Mathf.CeilToInt((r.xMax - pan.x) / cellPixels) + 1;
            var minY = Mathf.FloorToInt((r.yMin - pan.y) / cellPixels) - 1;
            var maxY = Mathf.CeilToInt((r.yMax - pan.y) / cellPixels) + 1;

            for (var x = minX; x <= maxX; x++)
            {
                var sx = pan.x + x * cellPixels;
                var c = x % 4 == 0 ? StageEditorColors.GridMajor : StageEditorColors.GridMinor;
                DrawLine(p, new Vector2(sx, r.yMin), new Vector2(sx, r.yMax), c, 1f);
            }

            for (var y = minY; y <= maxY; y++)
            {
                var sy = pan.y + y * cellPixels;
                var c = y % 4 == 0 ? StageEditorColors.GridMajor : StageEditorColors.GridMinor;
                DrawLine(p, new Vector2(r.xMin, sy), new Vector2(r.xMax, sy), c, 1f);
            }
        }

        private void DrawOriginAxes(Painter2D p)
        {
            var r = contentRect;
            var sx = pan.x;
            var sy = pan.y;

            if (sx >= r.xMin && sx <= r.xMax)
            {
                DrawLine(p, new Vector2(sx, r.yMin), new Vector2(sx, r.yMax), StageEditorColors.GridBorder, 2f);
            }

            if (sy >= r.yMin && sy <= r.yMax)
            {
                DrawLine(p, new Vector2(r.xMin, sy), new Vector2(r.xMax, sy), StageEditorColors.GridBorder, 2f);
            }
        }

        private void DrawHoverCell(Painter2D p)
        {
            if (!hoverCell.HasValue)
            {
                return;
            }

            var cell = hoverCell.Value;
            StrokeRect(p, CellRect(cell.x, cell.y).Expanded(-2f), StageEditorColors.PreviewStroke.WithAlpha(0.45f), 1f);
        }

        private void DrawPreview(Painter2D p)
        {
            if (dragMode != DragMode.Paint || state.activePaletteEntry is null)
            {
                return;
            }

            var rect = GetDragRect();
            var entry = state.activePaletteEntry;

            if (!entry.paintsStructural)
            {
                rect = GetPaletteObjectRect(entry, rect);
            }

            var screenRect = GridRect(rect);
            FillRect(p, screenRect, StageEditorColors.PreviewFill);
            StrokeRect(p, screenRect, StageEditorColors.PreviewStroke, 2f);
        }

        private RectInt GetDragRect()
        {
            var minX = Mathf.Min(dragStartCell.x, dragCurrentCell.x);
            var maxX = Mathf.Max(dragStartCell.x, dragCurrentCell.x);
            var minY = Mathf.Min(dragStartCell.y, dragCurrentCell.y);
            var maxY = Mathf.Max(dragStartCell.y, dragCurrentCell.y);
            return new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1);
        }

        private bool TryScreenToCell(Vector2 screen, out Vector2Int cell)
        {
            var grid = ScreenToGridFloat(screen);
            cell = new Vector2Int(Mathf.FloorToInt(grid.x), Mathf.FloorToInt(grid.y));
            return stage?.ActiveFloor is not null;
        }

        private Vector2 ScreenToGridFloat(Vector2 screen)
        {
            return new Vector2((screen.x - pan.x) / cellPixels, (screen.y - pan.y) / cellPixels);
        }

        private Rect CellRect(int x, int y)
        {
            return new Rect(pan.x + x * cellPixels, pan.y + y * cellPixels, cellPixels, cellPixels);
        }

        private Rect GridRect(RectInt rect)
        {
            return new Rect(pan.x + rect.x * cellPixels, pan.y + rect.y * cellPixels, rect.width * cellPixels,
                rect.height * cellPixels);
        }

        private Rect WorldTileRect(JWorldTile tile, Vector2 worldCenter, float cellsPerWorldTile)
        {
            var localX = tile.x - worldCenter.x;
            var localY = tile.y - worldCenter.y;
            return new Rect(
                pan.x + localX * cellsPerWorldTile * cellPixels,
                pan.y + localY * cellsPerWorldTile * cellPixels,
                cellsPerWorldTile * cellPixels,
                cellsPerWorldTile * cellPixels);
        }

        private Rect GetResizeHandleRect(StageObjectDefinition obj)
        {
            var rect = GridRect(obj.Bounds);
            var size = Mathf.Min(ResizeHandlePixels, Mathf.Max(8f, cellPixels * 0.35f));
            return new Rect(rect.xMax - size, rect.yMax - size, size, size);
        }

        private void FillRect(Painter2D p, Rect rect, Color color)
        {
            p.fillColor = color;
            p.BeginPath();
            p.MoveTo(new Vector2(rect.xMin, rect.yMin));
            p.LineTo(new Vector2(rect.xMax, rect.yMin));
            p.LineTo(new Vector2(rect.xMax, rect.yMax));
            p.LineTo(new Vector2(rect.xMin, rect.yMax));
            p.ClosePath();
            p.Fill();
        }

        private void StrokeRect(Painter2D p, Rect rect, Color color, float width)
        {
            p.strokeColor = color;
            p.lineWidth = width;
            p.BeginPath();
            p.MoveTo(new Vector2(rect.xMin, rect.yMin));
            p.LineTo(new Vector2(rect.xMax, rect.yMin));
            p.LineTo(new Vector2(rect.xMax, rect.yMax));
            p.LineTo(new Vector2(rect.xMin, rect.yMax));
            p.ClosePath();
            p.Stroke();
        }

        private void DrawLine(Painter2D p, Vector2 a, Vector2 b, Color color, float width)
        {
            p.strokeColor = color;
            p.lineWidth = width;
            p.BeginPath();
            p.MoveTo(a);
            p.LineTo(b);
            p.Stroke();
        }

        private void DrawHatching(Painter2D p, Rect rect, Color color)
        {
            if (rect.width <= 0f || rect.height <= 0f)
            {
                return;
            }

            StrokeRect(p, rect, color.WithAlpha(Mathf.Min(0.85f, color.a + 0.12f)), 1.5f);

            var spacing = Mathf.Clamp(cellPixels * 2f, 10f, 28f);
            var width = Mathf.Clamp(cellPixels * 0.08f, 1.25f, 3f);
            for (var d = -rect.height; d < rect.width; d += spacing)
            {
                var start = d < 0f
                    ? new Vector2(rect.xMin, rect.yMin - d)
                    : new Vector2(rect.xMin + d, rect.yMin);
                var end = d + rect.height < rect.width
                    ? new Vector2(rect.xMin + d + rect.height, rect.yMax)
                    : new Vector2(rect.xMax, rect.yMin + rect.width - d);
                DrawLine(p, start, end, color, width);
            }
        }

        private void DrawSocketFaceHint(Painter2D p, Rect rect, JSocketFace face)
        {
            var color = StageEditorColors.WorldSyncSocket;
            var inset = Mathf.Clamp(cellPixels * 0.25f, 4f, 14f);
            var width = Mathf.Clamp(cellPixels * 0.16f, 3f, 8f);

            switch (face)
            {
                case JSocketFace.North:
                    DrawLine(p, new Vector2(rect.xMin + inset, rect.yMin), new Vector2(rect.xMax - inset, rect.yMin),
                        color, width);
                    break;
                case JSocketFace.South:
                    DrawLine(p, new Vector2(rect.xMin + inset, rect.yMax), new Vector2(rect.xMax - inset, rect.yMax),
                        color, width);
                    break;
                case JSocketFace.East:
                    DrawLine(p, new Vector2(rect.xMax, rect.yMin + inset), new Vector2(rect.xMax, rect.yMax - inset),
                        color, width);
                    break;
                case JSocketFace.West:
                    DrawLine(p, new Vector2(rect.xMin, rect.yMin + inset), new Vector2(rect.xMin, rect.yMax - inset),
                        color, width);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(face), face, null);
            }
        }
    }

    internal static class StageEditorRectExtensions
    {
        public static Rect Expanded(this Rect rect, float amount)
        {
            return new Rect(rect.x - amount, rect.y - amount, rect.width + amount * 2f, rect.height + amount * 2f);
        }

        public static Color WithAlpha(this Color color, float alpha)
        {
            color.a = alpha;
            return color;
        }
    }
}
