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

namespace Editor.GON.WorldEditor
{
    public sealed class WorldMapCanvas : VisualElement
    {
        private const float MinCellPixels = 16f;
        private const float MaxCellPixels = 96f;

        private readonly WorldEditorWindow owner;
        private readonly WorldEditorState state;

        private WorldLayoutSO worldLayout;
        private Vector2 pan = new(60f, 60f);
        private float cellPixels = 36f;
        private bool isPanning;
        private bool isPaintingStage;
        private bool isMovingStage;
        private bool hasHoverMouse;
        private Vector2 hoverMousePosition;
        private Vector2 panStartMouse;
        private Vector2 panStartValue;
        private JWorldTile paintStartTile;
        private JWorldTile paintCurrentTile;
        private JWorldTile moveStartTile;
        private JWorldTile moveCurrentTile;
        private string movingStageId;

        public WorldMapCanvas(WorldEditorWindow owner, WorldEditorState state)
        {
            this.owner = owner;
            this.state = state;

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

            generateVisualContent += OnGenerateVisualContent;
            RegisterCallback<MouseDownEvent>(OnMouseDown);
            RegisterCallback<MouseMoveEvent>(OnMouseMove);
            RegisterCallback<MouseUpEvent>(OnMouseUp);
            RegisterCallback<MouseLeaveEvent>(OnMouseLeave);
            RegisterCallback<WheelEvent>(OnWheel);
            RegisterCallback<KeyDownEvent>(OnKeyDown);
        }

        public void SetLayout(WorldLayoutSO newLayout)
        {
            worldLayout = newLayout;
            worldLayout?.EnsureDefaults();
            MarkDirtyRepaint();
        }

        public void RequestRepaint()
        {
            MarkDirtyRepaint();
        }

        public void FrameSelection()
        {
            if (worldLayout is null)
            {
                return;
            }

            if (!string.IsNullOrEmpty(state.selectedEndpointId))
            {
                var endpoint = worldLayout.FindEndpoint(state.selectedEndpointId);
                if (endpoint is not null && TryGetEndpointGridCenter(endpoint, out var endpointCenter))
                {
                    FrameGridCenter(endpointCenter);
                }

                return;
            }

            if (!string.IsNullOrEmpty(state.selectedConnectionId))
            {
                if (TryGetConnectionGridCenter(state.selectedConnectionId, out var connectionCenter))
                {
                    FrameGridCenter(connectionCenter);
                }

                return;
            }

            var stage = worldLayout.FindStage(state.selectedStageId);
            if (stage is not null && TryGetStageGridCenter(stage, out var stageCenter))
            {
                FrameGridCenter(stageCenter);
            }
        }

        private void OnMouseDown(MouseDownEvent evt)
        {
            Focus();
            if (state.pendingStagePlacementKind.HasValue && (evt.button == 1 || evt.button == 2))
            {
                owner.CancelStagePlacement();
                evt.StopPropagation();
                return;
            }

            if (evt.button == 1 || evt.button == 2)
            {
                isPanning = true;
                panStartMouse = evt.localMousePosition;
                panStartValue = pan;
                this.CaptureMouse();
                evt.StopPropagation();
                return;
            }

            if (evt.button != 0 || worldLayout is null)
            {
                return;
            }

            if (!TryScreenToTile(evt.localMousePosition, out var tile))
            {
                return;
            }

            if (state.pendingStagePlacementKind.HasValue)
            {
                DeployPendingStage(tile);
                evt.StopPropagation();
                return;
            }

            if (state.activeTool == WorldEditorTool.Anchor && !string.IsNullOrEmpty(state.pendingAnchorEndpointId))
            {
                ApplyTool(tile, evt.localMousePosition);
                evt.StopPropagation();
                return;
            }

            if (state.activeTool == WorldEditorTool.Select && TrySelectEndpointAtMouse(tile, evt.localMousePosition))
            {
                evt.StopPropagation();
                return;
            }

            if (SelectStageOnFirstClick(tile))
            {
                evt.StopPropagation();
                return;
            }

            if (state.activeTool is WorldEditorTool.PaintStage or WorldEditorTool.EraseStage)
            {
                BeginStagePaint(tile);
                evt.StopPropagation();
                return;
            }

            if (state.activeTool == WorldEditorTool.Select && TryBeginStageMove(tile))
            {
                evt.StopPropagation();
                return;
            }

            ApplyTool(tile, evt.localMousePosition);
            evt.StopPropagation();
        }

        private void OnKeyDown(KeyDownEvent evt)
        {
            if (evt.keyCode != KeyCode.Escape || state.pendingStagePlacementKind is null)
            {
                return;
            }

            owner.CancelStagePlacement();
            evt.StopPropagation();
        }

        private bool SelectStageOnFirstClick(JWorldTile tile)
        {
            var stageAtTile = worldLayout.FindStageAt(state.activeLayer, tile);
            if (stageAtTile is null || state.selectedStageId == stageAtTile.id)
            {
                return false;
            }

            state.selectedStageId = stageAtTile.id;
            state.selectedEndpointId = null;
            state.selectedConnectionId = null;
            owner.RequestSelectionRebuild();
            MarkDirtyRepaint();
            return true;
        }

        private bool TrySelectEndpointAtMouse(JWorldTile tile, Vector2 mouse)
        {
            if (TryFindEndpointAtTile(tile, JEndpointKind.Anchor, out var anchor) &&
                Vector2.Distance(mouse, TileRect(tile).center) <= Mathf.Max(8f, cellPixels * 0.24f))
            {
                SelectEndpoint(anchor);
                return true;
            }

            if (TryFindEndpointAtTile(tile, JEndpointKind.Lift, out var lift) &&
                Vector2.Distance(mouse, TileRect(tile).center) <= Mathf.Max(10f, cellPixels * 0.32f))
            {
                SelectEndpoint(lift);
                return true;
            }

            var stageAtTile = worldLayout.FindStageAt(state.activeLayer, tile);
            if (stageAtTile is null)
            {
                return false;
            }

            var face = GetNearestFace(mouse, tile);
            if (DistanceToFace(mouse, TileRect(tile), face) > Mathf.Max(8f, cellPixels * 0.25f))
            {
                return false;
            }

            var socket = worldLayout.FindSocketAt(stageAtTile.id, tile, face);
            if (socket is null)
            {
                return false;
            }

            SelectEndpoint(socket);
            return true;
        }

        private void SelectEndpoint(WorldEndpointAuthoring endpoint)
        {
            owner.SelectEndpoint(endpoint.id);
        }

        private void DeployPendingStage(JWorldTile tile)
        {
            if (worldLayout is null || state.pendingStagePlacementKind is null)
            {
                return;
            }

            var stageKind = state.pendingStagePlacementKind.Value;
            var occupyingStage = worldLayout.FindStageAt(state.activeLayer, tile);
            if (stageKind == WorldStageKind.Stage && occupyingStage is not null)
            {
                MarkDirtyRepaint();
                return;
            }

            owner.MutateLayout(stageKind == WorldStageKind.SavePoint ? "Place save point" : "Place world stage", delegate
            {
                if (stageKind == WorldStageKind.SavePoint)
                {
                    RemoveOccupiedTileForSavePoint(tile);
                }

                var isSavePoint = stageKind == WorldStageKind.SavePoint;
                var stage = new WorldStageAuthoring
                {
                    id = worldLayout.CreateId("stage"),
                    displayName = isSavePoint
                        ? "Save Point " + (worldLayout.stages.Count + 1)
                        : "Stage " + (worldLayout.stages.Count + 1),
                    stageKind = stageKind,
                    worldLayer = state.activeLayer,
                    color = isSavePoint
                        ? new Color(0.95f, 0.25f, 0.85f, 0.82f)
                        : Random.ColorHSV(0f, 1f, 0.35f, 0.75f, 0.55f, 0.85f, 0.75f, 0.9f)
                };
                stage.AddTile(tile);
                worldLayout.stages.Add(stage);
                worldLayout.EnforceSavePointRules(stage);

                state.selectedStageId = stage.id;
                state.selectedEndpointId = null;
                state.selectedConnectionId = null;
                state.pendingStagePlacementKind = null;
                state.activeTool = state.toolBeforeStagePlacement;
            });

            MarkDirtyRepaint();
        }

        private void RemoveOccupiedTileForSavePoint(JWorldTile tile)
        {
            var occupyingStage = worldLayout.FindStageAt(state.activeLayer, tile);
            if (occupyingStage is null)
            {
                return;
            }

            worldLayout.RemoveEndpointsAtTile(occupyingStage.id, tile);
            occupyingStage.RemoveTile(tile);
            if (occupyingStage.occupiedTiles.Count == 0)
            {
                worldLayout.RemoveStageAndOwnedData(occupyingStage.id);
            }
        }

        private void OnMouseMove(MouseMoveEvent evt)
        {
            hasHoverMouse = true;
            hoverMousePosition = evt.localMousePosition;
            UpdateHoverTile(evt.localMousePosition);

            if (isPaintingStage)
            {
                if (TryScreenToTile(evt.localMousePosition, out var tile))
                {
                    paintCurrentTile = tile;
                    MarkDirtyRepaint();
                }

                evt.StopPropagation();
                return;
            }

            if (isMovingStage)
            {
                if (TryScreenToTile(evt.localMousePosition, out var tile))
                {
                    moveCurrentTile = tile;
                    MarkDirtyRepaint();
                }

                evt.StopPropagation();
                return;
            }

            if (!isPanning)
            {
                return;
            }

            pan = panStartValue + evt.localMousePosition - panStartMouse;
            MarkDirtyRepaint();
            evt.StopPropagation();
        }

        private void OnMouseLeave(MouseLeaveEvent evt)
        {
            hasHoverMouse = false;
            owner.SetHoverTile(null);
            MarkDirtyRepaint();
        }

        private void OnMouseUp(MouseUpEvent evt)
        {
            if (isPaintingStage)
            {
                if (TryScreenToTile(evt.localMousePosition, out var tile))
                {
                    paintCurrentTile = tile;
                }

                ApplyStagePaint();
                EndPointerCapture();
                evt.StopPropagation();
                return;
            }

            if (isMovingStage)
            {
                if (TryScreenToTile(evt.localMousePosition, out var tile))
                {
                    moveCurrentTile = tile;
                }

                ApplyStageMove();
                EndPointerCapture();
                evt.StopPropagation();
                return;
            }

            if (!isPanning)
            {
                return;
            }

            isPanning = false;
            EndPointerCapture();
            evt.StopPropagation();
        }

        private void OnWheel(WheelEvent evt)
        {
            UpdateHoverTile(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 bool TryBeginStageMove(JWorldTile tile)
        {
            var stageAtTile = worldLayout.FindStageAt(state.activeLayer, tile);
            if (stageAtTile is null)
            {
                state.selectedStageId = null;
                state.selectedEndpointId = null;
                state.selectedConnectionId = null;
                owner.RequestSelectionRebuild();
                return false;
            }

            if (state.selectedStageId != stageAtTile.id)
            {
                state.selectedStageId = stageAtTile.id;
                state.selectedEndpointId = null;
                state.selectedConnectionId = null;
                owner.RequestSelectionRebuild();
                MarkDirtyRepaint();
                return false;
            }

            state.selectedStageId = stageAtTile.id;
            movingStageId = stageAtTile.id;
            moveStartTile = tile;
            moveCurrentTile = tile;
            isMovingStage = true;
            this.CaptureMouse();
            state.selectedEndpointId = null;
            state.selectedConnectionId = null;
            owner.RequestSelectionRebuild();
            MarkDirtyRepaint();
            return true;
        }

        private void UpdateHoverTile(Vector2 mouse)
        {
            if (TryScreenToTile(mouse, out var tile))
            {
                owner.SetHoverTile(tile);
            }
        }

        private void ApplyTool(JWorldTile tile, Vector2 mouse)
        {
            var stageAtTile = worldLayout.FindStageAt(state.activeLayer, tile);
            if (state.activeTool == WorldEditorTool.Select)
            {
                state.selectedStageId = stageAtTile?.id;
                state.selectedEndpointId = null;
                state.selectedConnectionId = null;
                owner.RequestSelectionRebuild();
                return;
            }

            if (state.activeTool == WorldEditorTool.Anchor && !string.IsNullOrEmpty(state.pendingAnchorEndpointId))
            {
                ConnectPendingAnchorAtTile(tile);
                return;
            }

            var selectedStage = worldLayout.FindStage(state.selectedStageId);
            if (selectedStage is null)
            {
                return;
            }

            if (selectedStage.worldLayer != state.activeLayer)
            {
                return;
            }

            switch (state.activeTool)
            {
                case WorldEditorTool.PaintStage:
                    if (!worldLayout.IsTileOccupiedByOther(selectedStage.id, state.activeLayer, tile))
                    {
                        owner.MutateLayout("Paint world tile", delegate { selectedStage.AddTile(tile); });
                    }
                    break;
                case WorldEditorTool.EraseStage:
                    owner.MutateLayout("Erase world tile", delegate { selectedStage.RemoveTile(tile); });
                    break;
                case WorldEditorTool.Anchor:
                    AddOrConnectAnchor(selectedStage, tile);
                    break;
                case WorldEditorTool.Lift:
                    AddOrConnectLift(selectedStage, tile);
                    break;
                case WorldEditorTool.Socket:
                    AddOrConnectSocket(tile, GetNearestFace(mouse, tile));
                    break;
            }
        }

        private void AddOrConnectLift(WorldStageAuthoring stage, JWorldTile tile)
        {
            if (stage.stageKind == WorldStageKind.SavePoint)
            {
                return;
            }

            if (!stage.ContainsTile(tile))
            {
                return;
            }

            if (worldLayout.FindEndpointAt(stage.id, tile, JEndpointKind.Anchor) != null)
            {
                return;
            }

            owner.MutateLayout("Add journey lift", delegate
            {
                var lift = worldLayout.FindEndpointAt(stage.id, tile, JEndpointKind.Lift);
                if (lift is null)
                {
                    lift = new WorldEndpointAuthoring
                    {
                        id = worldLayout.CreateId("endpoint"),
                        stageId = stage.id,
                        worldTile = tile,
                        kind = JEndpointKind.Lift,
                        required = true
                    };
                    worldLayout.endpoints.Add(lift);
                }

                state.selectedStageId = stage.id;
                state.selectedEndpointId = lift.id;
                state.selectedConnectionId = null;
                TryConnectLift(lift);
            });
        }

        private void TryConnectLift(WorldEndpointAuthoring lift)
        {
            var liftStage = worldLayout.FindStage(lift.stageId);
            if (liftStage is null)
            {
                return;
            }

            foreach (var candidate in worldLayout.endpoints)
            {
                if (candidate is null ||
                    candidate.id == lift.id ||
                    candidate.kind != JEndpointKind.Lift ||
                    candidate.worldTile.x != lift.worldTile.x ||
                    candidate.worldTile.y != lift.worldTile.y ||
                    worldLayout.FindConnectionBetween(lift.id, candidate.id) is not null)
                {
                    continue;
                }

                var candidateStage = worldLayout.FindStage(candidate.stageId);
                if (candidateStage is null || Mathf.Abs(candidateStage.worldLayer - liftStage.worldLayer) != 1)
                {
                    continue;
                }

                worldLayout.connections.Add(new WorldConnectionAuthoring
                {
                    id = worldLayout.CreateId("connection"),
                    endpointAId = lift.id,
                        endpointBId = candidate.id,
                        directionality = JConnectionDirectionality.Bidirectional
                    });
            }
        }

        private void ConnectPendingAnchorAtTile(JWorldTile tile)
        {
            if (!TryFindAnchorAtTile(tile, out var anchor))
            {
                return;
            }

            if (state.pendingAnchorEndpointId == anchor.id)
            {
                state.pendingAnchorEndpointId = null;
                SelectEndpoint(anchor);
                MarkDirtyRepaint();
                return;
            }

            var firstAnchor = worldLayout.FindEndpoint(state.pendingAnchorEndpointId);
            if (firstAnchor is null || firstAnchor.kind != JEndpointKind.Anchor)
            {
                state.pendingAnchorEndpointId = anchor.id;
                SelectEndpoint(anchor);
                MarkDirtyRepaint();
                return;
            }

            owner.MutateLayout("Connect journey anchors", delegate
            {
                var connection = worldLayout.FindConnectionBetween(firstAnchor.id, anchor.id);
                if (connection is null)
                {
                    connection = new WorldConnectionAuthoring
                    {
                        id = worldLayout.CreateId("connection"),
                        endpointAId = firstAnchor.id,
                        endpointBId = anchor.id,
                        directionality = JConnectionDirectionality.Bidirectional
                    };
                    worldLayout.connections.Add(connection);
                }

                state.pendingAnchorEndpointId = null;
                state.selectedStageId = anchor.stageId;
                state.selectedEndpointId = null;
                state.selectedConnectionId = connection.id;
            });
        }

        private void AddOrConnectAnchor(WorldStageAuthoring stage, JWorldTile tile)
        {
            if (stage.stageKind == WorldStageKind.SavePoint)
            {
                return;
            }

            if (!stage.ContainsTile(tile))
            {
                return;
            }

            var anchor = worldLayout.FindEndpointAt(stage.id, tile, JEndpointKind.Anchor);
            if (anchor is null)
            {
                if (!string.IsNullOrEmpty(state.pendingAnchorEndpointId))
                {
                    return;
                }

                if (worldLayout.FindEndpointAt(stage.id, tile, JEndpointKind.Lift) != null)
                {
                    return;
                }

                owner.MutateLayout("Add journey anchor", delegate
                {
                    anchor = new WorldEndpointAuthoring
                    {
                        id = worldLayout.CreateId("endpoint"),
                        stageId = stage.id,
                        worldTile = tile,
                        kind = JEndpointKind.Anchor,
                        required = true
                    };
                    worldLayout.endpoints.Add(anchor);
                    state.selectedStageId = stage.id;
                    state.selectedEndpointId = anchor.id;
                    state.selectedConnectionId = null;
                    state.pendingAnchorEndpointId = anchor.id;
                });
                return;
            }

            if (string.IsNullOrEmpty(state.pendingAnchorEndpointId))
            {
                state.pendingAnchorEndpointId = anchor.id;
                SelectEndpoint(anchor);
                MarkDirtyRepaint();
                return;
            }

            if (state.pendingAnchorEndpointId == anchor.id)
            {
                state.pendingAnchorEndpointId = null;
                SelectEndpoint(anchor);
                MarkDirtyRepaint();
                return;
            }

            var firstAnchor = worldLayout.FindEndpoint(state.pendingAnchorEndpointId);
            if (firstAnchor is null || firstAnchor.kind != JEndpointKind.Anchor)
            {
                state.pendingAnchorEndpointId = anchor.id;
                SelectEndpoint(anchor);
                MarkDirtyRepaint();
                return;
            }

            owner.MutateLayout("Connect journey anchors", delegate
            {
                var connection = worldLayout.FindConnectionBetween(firstAnchor.id, anchor.id);
                if (connection is null)
                {
                    connection = new WorldConnectionAuthoring
                    {
                        id = worldLayout.CreateId("connection"),
                        endpointAId = firstAnchor.id,
                        endpointBId = anchor.id,
                        directionality = JConnectionDirectionality.Bidirectional
                    };
                    worldLayout.connections.Add(connection);
                }

                state.pendingAnchorEndpointId = null;
                state.selectedStageId = anchor.stageId;
                state.selectedEndpointId = null;
                state.selectedConnectionId = connection.id;
            });
        }

        private void AddOrConnectSocket(JWorldTile tile, JSocketFace face)
        {
            var stage = worldLayout.FindStageAt(state.activeLayer, tile);
            if (stage is null)
            {
                return;
            }

            var adjacentTile = Adjacent(tile, face);
            var adjacentStage = worldLayout.FindStageAt(state.activeLayer, adjacentTile);
            if (adjacentStage != null && adjacentStage.id == stage.id)
            {
                return;
            }

            state.selectedStageId = stage.id;
            var oppositeFace = Opposite(face);

            owner.MutateLayout("Create journey socket connection", delegate
            {
                var socket = GetOrCreateSocket(stage, tile, face);
                state.selectedStageId = stage.id;
                state.selectedEndpointId = socket.id;
                state.selectedConnectionId = null;
                if (adjacentStage is null)
                {
                    return;
                }

                var adjacentSocket = GetOrCreateSocket(adjacentStage, adjacentTile, oppositeFace);
                if (worldLayout.FindConnectionBetween(socket.id, adjacentSocket.id) is null)
                {
                    worldLayout.connections.Add(new WorldConnectionAuthoring
                    {
                        id = worldLayout.CreateId("connection"),
                        endpointAId = socket.id,
                        endpointBId = adjacentSocket.id,
                        directionality = JConnectionDirectionality.Bidirectional
                    });
                }
            });
        }

        private WorldEndpointAuthoring GetOrCreateSocket(WorldStageAuthoring stage, JWorldTile tile, JSocketFace face)
        {
            var socket = worldLayout.FindSocketAt(stage.id, tile, face);
            if (socket != null)
            {
                return socket;
            }

            socket = new WorldEndpointAuthoring
            {
                id = worldLayout.CreateId("endpoint"),
                stageId = stage.id,
                worldTile = tile,
                kind = JEndpointKind.Socket,
                socketFace = face,
                required = true
            };
            worldLayout.endpoints.Add(socket);
            return socket;
        }

        private void BeginStagePaint(JWorldTile tile)
        {
            paintStartTile = tile;
            paintCurrentTile = tile;
            isPaintingStage = true;
            this.CaptureMouse();
            MarkDirtyRepaint();
        }

        private void ApplyStagePaint()
        {
            if (worldLayout is null)
            {
                isPaintingStage = false;
                return;
            }

            var selectedStage = worldLayout.FindStage(state.selectedStageId);
            if (selectedStage is null && state.activeTool == WorldEditorTool.PaintStage)
            {
                selectedStage = CreateStageForPaint();
            }

            if (selectedStage is null || selectedStage.worldLayer != state.activeLayer)
            {
                isPaintingStage = false;
                MarkDirtyRepaint();
                return;
            }

            var rect = GetTileRect(paintStartTile, paintCurrentTile);
            if (state.activeTool == WorldEditorTool.PaintStage)
            {
                if (selectedStage.stageKind == WorldStageKind.SavePoint)
                {
                    isPaintingStage = false;
                    MarkDirtyRepaint();
                    return;
                }

                owner.MutateLayout("Paint world stage footprint", delegate
                {
                    for (var y = rect.yMin; y < rect.yMax; y++)
                    {
                        for (var x = rect.xMin; x < rect.xMax; x++)
                        {
                            var tile = new JWorldTile(x, y);
                            if (!worldLayout.IsTileOccupiedByOther(selectedStage.id, state.activeLayer, tile))
                            {
                                selectedStage.AddTile(tile);
                            }
                        }
                    }
                });
            }
            else if (state.activeTool == WorldEditorTool.EraseStage)
            {
                owner.MutateLayout("Erase world stage footprint", delegate
                {
                    if (selectedStage.stageKind == WorldStageKind.SavePoint)
                    {
                        RemoveSavePointStage(selectedStage, rect);
                        return;
                    }

                    var singleTile = rect.width == 1 && rect.height == 1;
                    if (singleTile && RemoveEndpointsInRect(selectedStage, rect))
                    {
                        return;
                    }

                    for (var y = rect.yMin; y < rect.yMax; y++)
                    {
                        for (var x = rect.xMin; x < rect.xMax; x++)
                        {
                            selectedStage.RemoveTile(new JWorldTile(x, y));
                        }
                    }

                    RemoveEndpointsInRect(selectedStage, rect);
                });
            }

            isPaintingStage = false;
            MarkDirtyRepaint();
        }

        private WorldStageAuthoring CreateStageForPaint()
        {
            var stage = new WorldStageAuthoring
            {
                id = worldLayout.CreateId("stage"),
                displayName = "Stage " + (worldLayout.stages.Count + 1),
                stageKind = WorldStageKind.Stage,
                worldLayer = state.activeLayer,
                color = Random.ColorHSV(0f, 1f, 0.35f, 0.75f, 0.55f, 0.85f, 0.75f, 0.9f)
            };
            worldLayout.stages.Add(stage);
            state.selectedStageId = stage.id;
            state.selectedEndpointId = null;
            state.selectedConnectionId = null;
            return stage;
        }

        private void ApplyStageMove()
        {
            if (worldLayout is null)
            {
                isMovingStage = false;
                return;
            }

            var stage = worldLayout.FindStage(movingStageId);
            if (stage is null || stage.worldLayer != state.activeLayer)
            {
                isMovingStage = false;
                MarkDirtyRepaint();
                return;
            }

            var dx = moveCurrentTile.x - moveStartTile.x;
            var dy = moveCurrentTile.y - moveStartTile.y;
            if (dx == 0 && dy == 0)
            {
                isMovingStage = false;
                MarkDirtyRepaint();
                return;
            }

            if (!CanMoveStage(stage, dx, dy))
            {
                isMovingStage = false;
                MarkDirtyRepaint();
                return;
            }

            owner.MutateLayout("Move world stage", delegate
            {
                var movedTiles = new List<JWorldTile>();
                foreach (var tile in stage.occupiedTiles)
                {
                    movedTiles.Add(new JWorldTile(tile.x + dx, tile.y + dy));
                }

                stage.occupiedTiles = movedTiles;

                foreach (var endpoint in worldLayout.endpoints)
                {
                    if (endpoint != null && endpoint.stageId == stage.id)
                    {
                        endpoint.worldTile = new JWorldTile(endpoint.worldTile.x + dx, endpoint.worldTile.y + dy);
                    }
                }

                RemoveInvalidConnections();
            });

            isMovingStage = false;
            MarkDirtyRepaint();
        }

        private bool CanMoveStage(WorldStageAuthoring stage, int dx, int dy)
        {
            foreach (var tile in stage.occupiedTiles)
            {
                var movedTile = new JWorldTile(tile.x + dx, tile.y + dy);
                if (worldLayout.IsTileOccupiedByOther(stage.id, stage.worldLayer, movedTile))
                {
                    return false;
                }
            }

            return true;
        }

        private void RemoveInvalidConnections()
        {
            for (var i = worldLayout.connections.Count - 1; i >= 0; i--)
            {
                var connection = worldLayout.connections[i];
                if (connection is null)
                {
                    worldLayout.connections.RemoveAt(i);
                    continue;
                }

                var endpointA = worldLayout.FindEndpoint(connection.endpointAId);
                var endpointB = worldLayout.FindEndpoint(connection.endpointBId);
                if (endpointA is null || endpointB is null)
                {
                    ClearSelectedConnectionIf(connection.id);
                    worldLayout.connections.RemoveAt(i);
                    continue;
                }

                if (endpointA.kind != endpointB.kind)
                {
                    continue;
                }

                if (endpointA.kind == JEndpointKind.Socket &&
                    !AreValidSocketConnectionEndpoints(endpointA, endpointB))
                {
                    ClearSelectedConnectionIf(connection.id);
                    worldLayout.connections.RemoveAt(i);
                }

                if (endpointA.kind == JEndpointKind.Lift &&
                    !AreValidLiftConnectionEndpoints(endpointA, endpointB))
                {
                    ClearSelectedConnectionIf(connection.id);
                    worldLayout.connections.RemoveAt(i);
                }
            }
        }

        private bool AreValidSocketConnectionEndpoints(WorldEndpointAuthoring endpointA, WorldEndpointAuthoring endpointB)
        {
            var stageA = worldLayout.FindStage(endpointA.stageId);
            var stageB = worldLayout.FindStage(endpointB.stageId);
            if (stageA is null || stageB is null || stageA.worldLayer != stageB.worldLayer || stageA.id == stageB.id)
            {
                return false;
            }

            return Opposite(endpointA.socketFace) == endpointB.socketFace &&
                   SameTile(Adjacent(endpointA.worldTile, endpointA.socketFace), endpointB.worldTile);
        }

        private bool AreValidLiftConnectionEndpoints(WorldEndpointAuthoring endpointA, WorldEndpointAuthoring endpointB)
        {
            var stageA = worldLayout.FindStage(endpointA.stageId);
            var stageB = worldLayout.FindStage(endpointB.stageId);
            if (stageA is null || stageB is null || stageA.id == stageB.id)
            {
                return false;
            }

            return SameTile(endpointA.worldTile, endpointB.worldTile) &&
                   Mathf.Abs(stageA.worldLayer - stageB.worldLayer) == 1;
        }

        private static bool SameTile(JWorldTile a, JWorldTile b)
        {
            return a.x == b.x && a.y == b.y;
        }

        private void RemoveSavePointStage(WorldStageAuthoring stage, RectInt rect)
        {
            if (stage is null || stage.stageKind != WorldStageKind.SavePoint)
            {
                return;
            }

            var touched = false;
            foreach (var tile in stage.occupiedTiles)
            {
                if (rect.Contains(new Vector2Int(tile.x, tile.y)))
                {
                    touched = true;
                    break;
                }
            }

            if (!touched)
            {
                return;
            }

            worldLayout.RemoveStageAndOwnedData(stage.id);

            if (state.selectedStageId == stage.id)
            {
                state.selectedStageId = null;
                state.selectedEndpointId = null;
                state.selectedConnectionId = null;
            }
        }

        private bool RemoveEndpointsInRect(WorldStageAuthoring stage, RectInt rect)
        {
            var removedEndpointIds = new HashSet<string>();
            for (var i = worldLayout.endpoints.Count - 1; i >= 0; i--)
            {
                var endpoint = worldLayout.endpoints[i];
                if (endpoint == null || endpoint.stageId != stage.id)
                {
                    continue;
                }

                if (!rect.Contains(new Vector2Int(endpoint.worldTile.x, endpoint.worldTile.y)))
                {
                    continue;
                }

                removedEndpointIds.Add(endpoint.id);
                if (state.selectedEndpointId == endpoint.id)
                {
                    state.selectedEndpointId = null;
                }

                worldLayout.endpoints.RemoveAt(i);
            }

            if (removedEndpointIds.Count == 0)
            {
                return false;
            }

            for (var i = worldLayout.connections.Count - 1; i >= 0; i--)
            {
                var connection = worldLayout.connections[i];
                if (connection == null ||
                    removedEndpointIds.Contains(connection.endpointAId) ||
                    removedEndpointIds.Contains(connection.endpointBId))
                {
                    if (connection != null)
                    {
                        ClearSelectedConnectionIf(connection.id);
                    }

                    worldLayout.connections.RemoveAt(i);
                }
            }

            return true;
        }

        private void EndPointerCapture()
        {
            if (this.HasMouseCapture())
            {
                this.ReleaseMouse();
            }
        }

        private void AddEndpoint(WorldStageAuthoring stage, JWorldTile tile, JEndpointKind kind, JSocketFace face)
        {
            if (!stage.ContainsTile(tile))
            {
                return;
            }

            if ((kind == JEndpointKind.Anchor && worldLayout.FindEndpointAt(stage.id, tile, JEndpointKind.Lift) != null) ||
                (kind == JEndpointKind.Lift && worldLayout.FindEndpointAt(stage.id, tile, JEndpointKind.Anchor) != null))
            {
                return;
            }

            if (stage.stageKind == WorldStageKind.SavePoint &&
                kind is JEndpointKind.Anchor or JEndpointKind.Lift)
            {
                return;
            }

            if (worldLayout.FindEndpointAt(stage.id, tile, kind) != null)
            {
                return;
            }

            owner.MutateLayout("Add journey endpoint", delegate
            {
                worldLayout.endpoints.Add(new WorldEndpointAuthoring
                {
                    id = worldLayout.CreateId("endpoint"),
                    stageId = stage.id,
                    worldTile = tile,
                    kind = kind,
                    socketFace = face,
                    required = true
                });
            });
        }

        private void OnGenerateVisualContent(MeshGenerationContext mgc)
        {
            var p = mgc.painter2D;
            DrawGrid(p);
            DrawStages(p);
            DrawPendingStagePlacement(p);
            DrawStagePaintPreview(p);
            DrawStageMovePreview(p);
            DrawAnchorConnections(p);
            DrawOtherLayerLiftIndicators(p);
            DrawPendingAnchorConnection(p);
            DrawEndpoints(p);
            DrawSelectedEndpointIndicator(p);
        }

        private void DrawGrid(Painter2D p)
        {
            var rect = contentRect;
            var min = ScreenToGridFloat(new Vector2(rect.xMin, rect.yMin));
            var max = ScreenToGridFloat(new Vector2(rect.xMax, rect.yMax));
            var minX = Mathf.FloorToInt(min.x) - 1;
            var maxX = Mathf.CeilToInt(max.x) + 1;
            var minY = Mathf.FloorToInt(min.y) - 1;
            var maxY = Mathf.CeilToInt(max.y) + 1;

            for (var x = minX; x <= maxX; x++)
            {
                DrawLine(p, GridToScreen(new Vector2(x, minY)), GridToScreen(new Vector2(x, maxY)),
                    x == 0 ? WorldEditorColors.GridAxis : WorldEditorColors.Grid, x == 0 ? 2f : 1f);
            }

            for (var y = minY; y <= maxY; y++)
            {
                DrawLine(p, GridToScreen(new Vector2(minX, y)), GridToScreen(new Vector2(maxX, y)),
                    y == 0 ? WorldEditorColors.GridAxis : WorldEditorColors.Grid, y == 0 ? 2f : 1f);
            }
        }

        private void DrawStages(Painter2D p)
        {
            if (worldLayout is null)
            {
                return;
            }

            worldLayout.EnsureDefaults();
            foreach (var stage in worldLayout.stages)
            {
                if (stage == null || stage.worldLayer != state.activeLayer)
                {
                    continue;
                }

                foreach (var tile in stage.occupiedTiles)
                {
                    var rect = TileRect(tile);
                    FillRect(p, rect, stage.color);
                    if (stage.stageKind == WorldStageKind.SavePoint)
                    {
                        DrawSavePointSymbol(p, rect);
                    }
                }

                foreach (var tile in stage.occupiedTiles)
                {
                    DrawStageTileFreeFaces(p, stage, tile,
                        stage.id == state.selectedStageId ? WorldEditorColors.Selected : Color.black, 2f);
                }
            }
        }

        private void DrawStageTileFreeFaces(
            Painter2D p,
            WorldStageAuthoring stage,
            JWorldTile tile,
            Color color,
            float width)
        {
            var rect = TileRect(tile);

            if (!stage.ContainsTile(new JWorldTile(tile.x, tile.y - 1)))
            {
                DrawLine(p, new Vector2(rect.xMin, rect.yMin), new Vector2(rect.xMax, rect.yMin), color, width);
            }

            if (!stage.ContainsTile(new JWorldTile(tile.x, tile.y + 1)))
            {
                DrawLine(p, new Vector2(rect.xMin, rect.yMax), new Vector2(rect.xMax, rect.yMax), color, width);
            }

            if (!stage.ContainsTile(new JWorldTile(tile.x + 1, tile.y)))
            {
                DrawLine(p, new Vector2(rect.xMax, rect.yMin), new Vector2(rect.xMax, rect.yMax), color, width);
            }

            if (!stage.ContainsTile(new JWorldTile(tile.x - 1, tile.y)))
            {
                DrawLine(p, new Vector2(rect.xMin, rect.yMin), new Vector2(rect.xMin, rect.yMax), color, width);
            }
        }

        private void DrawSavePointSymbol(Painter2D p, Rect rect)
        {
            var a = new Vector2(rect.xMin + rect.width * 0.32f, rect.yMin + rect.height * 0.25f);
            var b = new Vector2(rect.xMin + rect.width * 0.68f, rect.yMin + rect.height * 0.25f);
            var c = new Vector2(rect.xMin + rect.width * 0.32f, rect.yMin + rect.height * 0.50f);
            var d = new Vector2(rect.xMin + rect.width * 0.68f, rect.yMin + rect.height * 0.50f);
            var e = new Vector2(rect.xMin + rect.width * 0.32f, rect.yMin + rect.height * 0.75f);
            var f = new Vector2(rect.xMin + rect.width * 0.68f, rect.yMin + rect.height * 0.75f);
            var width = Mathf.Max(2.5f, cellPixels * 0.08f);

            DrawSegmentedSavePointSymbol(p, a, b, c, d, e, f, new Color(0f, 0f, 0f, 0.65f), width + 2f);
            DrawSegmentedSavePointSymbol(p, a, b, c, d, e, f, Color.white, width);
        }

        private void DrawSegmentedSavePointSymbol(
            Painter2D p,
            Vector2 a,
            Vector2 b,
            Vector2 c,
            Vector2 d,
            Vector2 e,
            Vector2 f,
            Color color,
            float width)
        {
            DrawLine(p, a, b, color, width);
            DrawLine(p, a, c, color, width);
            DrawLine(p, c, d, color, width);
            DrawLine(p, d, f, color, width);
            DrawLine(p, e, f, color, width);
        }

        private void DrawPendingStagePlacement(Painter2D p)
        {
            if (worldLayout is null || state.pendingStagePlacementKind is null || !state.hoverTile.HasValue)
            {
                return;
            }

            var tile = state.hoverTile.Value;
            var rect = TileRect(tile);
            var stageKind = state.pendingStagePlacementKind.Value;
            var occupied = worldLayout.FindStageAt(state.activeLayer, tile) is not null;
            var blocked = stageKind == WorldStageKind.Stage && occupied;
            var fill = blocked
                ? new Color(1f, 0.15f, 0.12f, 0.25f)
                : stageKind == WorldStageKind.SavePoint
                    ? new Color(0.95f, 0.25f, 0.85f, 0.28f)
                    : new Color(1f, 0.86f, 0.32f, 0.22f);
            var stroke = blocked
                ? new Color(1f, 0.15f, 0.12f, 1f)
                : stageKind == WorldStageKind.SavePoint
                    ? WorldEditorColors.EndpointSelected
                    : WorldEditorColors.Selected;

            FillRect(p, rect, fill);
            StrokeRect(p, rect, stroke, 2f);
        }

        private void DrawEndpoints(Painter2D p)
        {
            if (worldLayout is null)
            {
                return;
            }

            foreach (var endpoint in worldLayout.endpoints)
            {
                var stage = worldLayout.FindStage(endpoint.stageId);
                if (stage is null || stage.worldLayer != state.activeLayer)
                {
                    continue;
                }

                var rect = TileRect(endpoint.worldTile);
                var center = rect.center;
                switch (endpoint.kind)
                {
                    case JEndpointKind.Anchor:
                        FillCircle(p, center, Mathf.Max(5f, cellPixels * 0.16f), WorldEditorColors.Anchor);
                        break;
                    case JEndpointKind.Lift:
                        FillMarker(p, center, Mathf.Max(8f, cellPixels * 0.28f), WorldEditorColors.Lift);
                        DrawLine(p, center + Vector2.down * 7f, center + Vector2.up * 7f, Color.white, 2f);
                        break;
                    case JEndpointKind.Socket:
                        DrawSocket(p, rect, endpoint.socketFace);
                        break;
                }
            }
        }

        private void DrawSelectedEndpointIndicator(Painter2D p)
        {
            if (worldLayout is null || string.IsNullOrEmpty(state.selectedEndpointId))
            {
                return;
            }

            var endpoint = worldLayout.FindEndpoint(state.selectedEndpointId);
            if (endpoint is null || !TryGetEndpointScreenPosition(endpoint, out var center))
            {
                return;
            }

            DrawDottedCircle(p, center, cellPixels * 0.5f, WorldEditorColors.EndpointSelected, 2f);
        }

        private void DrawOtherLayerLiftIndicators(Painter2D p)
        {
            if (worldLayout is null)
            {
                return;
            }

            foreach (var endpoint in worldLayout.endpoints)
            {
                if (endpoint is null || endpoint.kind != JEndpointKind.Lift)
                {
                    continue;
                }

                var stage = worldLayout.FindStage(endpoint.stageId);
                if (stage is null || stage.worldLayer == state.activeLayer)
                {
                    continue;
                }

                var rect = TileRect(endpoint.worldTile);
                var center = rect.center;
                FillMarker(p, center, Mathf.Max(8f, cellPixels * 0.28f), WorldEditorColors.LiftGhost);
                DrawLine(p, center + Vector2.down * 6f, center + Vector2.up * 6f, WorldEditorColors.Lift, 1f);
            }
        }

        private void DrawAnchorConnections(Painter2D p)
        {
            if (worldLayout is null)
            {
                return;
            }

            foreach (var connection in worldLayout.connections)
            {
                if (connection is null)
                {
                    continue;
                }

                var endpointA = worldLayout.FindEndpoint(connection.endpointAId);
                var endpointB = worldLayout.FindEndpoint(connection.endpointBId);
                if (endpointA is null || endpointB is null ||
                    endpointA.kind != JEndpointKind.Anchor ||
                    endpointB.kind != JEndpointKind.Anchor)
                {
                    continue;
                }

                if (!TryGetEndpointCenter(endpointA, out var a) || !TryGetEndpointCenter(endpointB, out var b))
                {
                    continue;
                }

                DrawLine(p, a, b, WorldEditorColors.Anchor, 2f);
            }
        }

        private void DrawPendingAnchorConnection(Painter2D p)
        {
            if (string.IsNullOrEmpty(state.pendingAnchorEndpointId) || !hasHoverMouse)
            {
                return;
            }

            var anchor = worldLayout?.FindEndpoint(state.pendingAnchorEndpointId);
            if (anchor is null || anchor.kind != JEndpointKind.Anchor ||
                !TryGetEndpointCenter(anchor, out var start))
            {
                return;
            }

            var color = IsHoveringConnectableAnchor(anchor)
                ? WorldEditorColors.Anchor
                : new Color(0.55f, 0.55f, 0.55f, 1f);
            DrawLine(p, start, hoverMousePosition, color, 2f);
        }

        private bool IsHoveringConnectableAnchor(WorldEndpointAuthoring pendingAnchor)
        {
            if (!state.hoverTile.HasValue)
            {
                return false;
            }

            return TryFindAnchorAtTile(state.hoverTile.Value, out var hoverAnchor) &&
                   hoverAnchor.id != pendingAnchor.id;
        }

        private bool TryFindAnchorAtTile(JWorldTile tile, out WorldEndpointAuthoring anchor)
        {
            return TryFindEndpointAtTile(tile, JEndpointKind.Anchor, out anchor);
        }

        private bool TryFindEndpointAtTile(
            JWorldTile tile,
            JEndpointKind kind,
            out WorldEndpointAuthoring foundEndpoint)
        {
            foundEndpoint = null;
            if (worldLayout is null)
            {
                return false;
            }

            foreach (var endpoint in worldLayout.endpoints)
            {
                if (endpoint is null ||
                    endpoint.kind != kind ||
                    endpoint.worldTile.x != tile.x ||
                    endpoint.worldTile.y != tile.y)
                {
                    continue;
                }

                var stage = worldLayout.FindStage(endpoint.stageId);
                if (stage is null || stage.worldLayer != state.activeLayer)
                {
                    continue;
                }

                foundEndpoint = endpoint;
                return true;
            }

            return false;
        }

        private bool TryGetEndpointCenter(WorldEndpointAuthoring endpoint, out Vector2 center)
        {
            center = default;
            var stage = worldLayout?.FindStage(endpoint.stageId);
            if (stage is null || stage.worldLayer != state.activeLayer)
            {
                return false;
            }

            center = TileRect(endpoint.worldTile).center;
            return true;
        }

        private void DrawStagePaintPreview(Painter2D p)
        {
            if (!isPaintingStage || state.activeTool is not (WorldEditorTool.PaintStage or WorldEditorTool.EraseStage))
            {
                return;
            }

            var rect = GetTileRect(paintStartTile, paintCurrentTile);
            var screenMin = GridToScreen(new Vector2(rect.xMin, rect.yMin));
            var screenMax = GridToScreen(new Vector2(rect.xMax, rect.yMax));
            var screenRect = new Rect(
                screenMin.x,
                screenMin.y,
                screenMax.x - screenMin.x,
                screenMax.y - screenMin.y);

            var fill = state.activeTool == WorldEditorTool.PaintStage
                ? new Color(1f, 0.86f, 0.32f, 0.18f)
                : new Color(1f, 0.25f, 0.2f, 0.18f);
            var stroke = state.activeTool == WorldEditorTool.PaintStage
                ? WorldEditorColors.Selected
                : new Color(1f, 0.25f, 0.2f, 1f);

            FillRect(p, screenRect, fill);
            StrokeRect(p, screenRect, stroke, 2f);
        }

        private void DrawStageMovePreview(Painter2D p)
        {
            if (!isMovingStage)
            {
                return;
            }

            var stage = worldLayout?.FindStage(movingStageId);
            if (stage is null)
            {
                return;
            }

            var dx = moveCurrentTile.x - moveStartTile.x;
            var dy = moveCurrentTile.y - moveStartTile.y;
            if (dx == 0 && dy == 0)
            {
                return;
            }

            var canMove = CanMoveStage(stage, dx, dy);
            var fill = canMove
                ? new Color(1f, 0.86f, 0.32f, 0.18f)
                : new Color(1f, 0.25f, 0.2f, 0.18f);
            var stroke = canMove ? WorldEditorColors.Selected : new Color(1f, 0.25f, 0.2f, 1f);

            foreach (var tile in stage.occupiedTiles)
            {
                var rect = TileRect(new JWorldTile(tile.x + dx, tile.y + dy));
                FillRect(p, rect, fill);
                StrokeRect(p, rect, stroke, 2f);
            }
        }

        private void DrawSocket(Painter2D p, Rect rect, JSocketFace face)
        {
            var center = rect.center;
            var half = cellPixels * 0.5f;
            var size = Mathf.Max(5f, cellPixels * 0.16f);
            var point = SocketPoint(rect, face);
            FillMarker(p, point, size * 2f, GetSocketColor(rect, face));
        }

        private bool TryGetEndpointScreenPosition(WorldEndpointAuthoring endpoint, out Vector2 position)
        {
            position = default;
            var stage = worldLayout.FindStage(endpoint.stageId);
            if (stage is null || stage.worldLayer != state.activeLayer)
            {
                return false;
            }

            var rect = TileRect(endpoint.worldTile);
            position = endpoint.kind == JEndpointKind.Socket
                ? SocketPoint(rect, endpoint.socketFace)
                : rect.center;
            return true;
        }

        private Vector2 SocketPoint(Rect rect, JSocketFace face)
        {
            var center = rect.center;
            var half = cellPixels * 0.5f;
            return face switch
            {
                JSocketFace.North => new Vector2(center.x, center.y - half),
                JSocketFace.South => new Vector2(center.x, center.y + half),
                JSocketFace.East => new Vector2(center.x + half, center.y),
                _ => new Vector2(center.x - half, center.y)
            };
        }

        private Color GetSocketColor(Rect rect, JSocketFace face)
        {
            var tile = ScreenRectToTile(rect);
            var stage = worldLayout.FindStageAt(state.activeLayer, tile);
            if (stage is null)
            {
                return WorldEditorColors.Socket;
            }

            var socket = worldLayout.FindSocketAt(stage.id, tile, face);
            if (socket is null)
            {
                return WorldEditorColors.Socket;
            }

            var adjacentTile = Adjacent(tile, face);
            var adjacentStage = worldLayout.FindStageAt(state.activeLayer, adjacentTile);
            if (adjacentStage is null || adjacentStage.id == stage.id)
            {
                return WorldEditorColors.Socket;
            }

            var adjacentSocket = worldLayout.FindSocketAt(adjacentStage.id, adjacentTile, Opposite(face));
            if (adjacentSocket is null)
            {
                return WorldEditorColors.SocketPaired;
            }

            return worldLayout.FindConnectionBetween(socket.id, adjacentSocket.id) is null
                ? WorldEditorColors.SocketPaired
                : WorldEditorColors.SocketConnected;
        }

        private JSocketFace GetNearestFace(Vector2 mouse, JWorldTile tile)
        {
            var rect = TileRect(tile);
            var left = Mathf.Abs(mouse.x - rect.xMin);
            var right = Mathf.Abs(mouse.x - rect.xMax);
            var top = Mathf.Abs(mouse.y - rect.yMin);
            var bottom = Mathf.Abs(mouse.y - rect.yMax);
            var min = Mathf.Min(left, right, top, bottom);
            if (Mathf.Approximately(min, top)) return JSocketFace.North;
            if (Mathf.Approximately(min, bottom)) return JSocketFace.South;
            if (Mathf.Approximately(min, right)) return JSocketFace.East;
            return JSocketFace.West;
        }

        private static float DistanceToFace(Vector2 mouse, Rect rect, JSocketFace face)
        {
            return face switch
            {
                JSocketFace.North => Mathf.Abs(mouse.y - rect.yMin),
                JSocketFace.South => Mathf.Abs(mouse.y - rect.yMax),
                JSocketFace.East => Mathf.Abs(mouse.x - rect.xMax),
                _ => Mathf.Abs(mouse.x - rect.xMin)
            };
        }

        private void ClearSelectedConnectionIf(string connectionId)
        {
            if (state.selectedConnectionId == connectionId)
            {
                state.selectedConnectionId = null;
            }
        }

        private void FrameGridCenter(Vector2 gridCenter)
        {
            var rect = contentRect;
            if (rect.width <= 1f || rect.height <= 1f)
            {
                return;
            }

            pan = rect.center - gridCenter * cellPixels;
            MarkDirtyRepaint();
        }

        private bool TryGetStageGridCenter(WorldStageAuthoring stage, out Vector2 center)
        {
            center = default;
            if (stage is null || stage.occupiedTiles is null || stage.occupiedTiles.Count == 0)
            {
                return false;
            }

            var minX = int.MaxValue;
            var minY = int.MaxValue;
            var maxX = int.MinValue;
            var maxY = int.MinValue;
            foreach (var tile in stage.occupiedTiles)
            {
                minX = Mathf.Min(minX, tile.x);
                minY = Mathf.Min(minY, tile.y);
                maxX = Mathf.Max(maxX, tile.x + 1);
                maxY = Mathf.Max(maxY, tile.y + 1);
            }

            center = new Vector2((minX + maxX) * 0.5f, (minY + maxY) * 0.5f);
            return true;
        }

        private bool TryGetEndpointGridCenter(WorldEndpointAuthoring endpoint, out Vector2 center)
        {
            center = default;
            var stage = worldLayout.FindStage(endpoint.stageId);
            if (stage is null || stage.worldLayer != state.activeLayer)
            {
                return false;
            }

            center = new Vector2(endpoint.worldTile.x + 0.5f, endpoint.worldTile.y + 0.5f);
            return true;
        }

        private bool TryGetConnectionGridCenter(string connectionId, out Vector2 center)
        {
            center = default;
            foreach (var connection in worldLayout.connections)
            {
                if (connection == null || connection.id != connectionId)
                {
                    continue;
                }

                var endpointA = worldLayout.FindEndpoint(connection.endpointAId);
                var endpointB = worldLayout.FindEndpoint(connection.endpointBId);
                var centerA = default(Vector2);
                var centerB = default(Vector2);
                var hasA = endpointA is not null && TryGetEndpointGridCenter(endpointA, out centerA);
                var hasB = endpointB is not null && TryGetEndpointGridCenter(endpointB, out centerB);

                if (hasA && hasB)
                {
                    center = (centerA + centerB) * 0.5f;
                    return true;
                }

                if (hasA)
                {
                    center = centerA;
                    return true;
                }

                if (hasB)
                {
                    center = centerB;
                    return true;
                }

                return false;
            }

            return false;
        }

        private Rect TileRect(JWorldTile tile)
        {
            var min = GridToScreen(new Vector2(tile.x, tile.y));
            return new Rect(min.x, min.y, cellPixels, cellPixels);
        }

        private JWorldTile ScreenRectToTile(Rect rect)
        {
            var grid = ScreenToGridFloat(new Vector2(rect.xMin + 1f, rect.yMin + 1f));
            return new JWorldTile(Mathf.FloorToInt(grid.x), Mathf.FloorToInt(grid.y));
        }

        private bool TryScreenToTile(Vector2 screen, out JWorldTile tile)
        {
            var grid = ScreenToGridFloat(screen);
            tile = new JWorldTile(Mathf.FloorToInt(grid.x), Mathf.FloorToInt(grid.y));
            return true;
        }

        private static RectInt GetTileRect(JWorldTile a, JWorldTile b)
        {
            var xMin = Mathf.Min(a.x, b.x);
            var yMin = Mathf.Min(a.y, b.y);
            var xMax = Mathf.Max(a.x, b.x) + 1;
            var yMax = Mathf.Max(a.y, b.y) + 1;
            return new RectInt(xMin, yMin, xMax - xMin, yMax - yMin);
        }

        private static JWorldTile Adjacent(JWorldTile tile, JSocketFace face)
        {
            return face switch
            {
                JSocketFace.North => new JWorldTile(tile.x, tile.y - 1),
                JSocketFace.South => new JWorldTile(tile.x, tile.y + 1),
                JSocketFace.East => new JWorldTile(tile.x + 1, tile.y),
                _ => new JWorldTile(tile.x - 1, tile.y)
            };
        }

        private static JSocketFace Opposite(JSocketFace face)
        {
            return face switch
            {
                JSocketFace.North => JSocketFace.South,
                JSocketFace.South => JSocketFace.North,
                JSocketFace.East => JSocketFace.West,
                _ => JSocketFace.East
            };
        }

        private Vector2 ScreenToGridFloat(Vector2 screen)
        {
            return (screen - pan) / cellPixels;
        }

        private Vector2 GridToScreen(Vector2 grid)
        {
            return pan + grid * cellPixels;
        }

        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 FillMarker(Painter2D p, Vector2 center, float size, Color color)
        {
            var half = size * 0.5f;
            FillRect(p, new Rect(center.x - half, center.y - half, size, size), color);
        }

        private void FillCircle(Painter2D p, Vector2 center, float radius, Color color)
        {
            const int segments = 24;
            p.fillColor = color;
            p.BeginPath();
            p.MoveTo(center + new Vector2(radius, 0f));
            for (var i = 1; i <= segments; i++)
            {
                var angle = Mathf.PI * 2f * i / segments;
                p.LineTo(center + new Vector2(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius));
            }
            p.ClosePath();
            p.Fill();
        }

        private void DrawDottedCircle(Painter2D p, Vector2 center, float radius, Color color, float width)
        {
            const int dots = 24;
            const float dotRadians = Mathf.PI * 2f / dots * 0.38f;
            for (var i = 0; i < dots; i++)
            {
                var angle = Mathf.PI * 2f * i / dots;
                var a = angle - dotRadians * 0.5f;
                var b = angle + dotRadians * 0.5f;
                DrawLine(
                    p,
                    center + new Vector2(Mathf.Cos(a) * radius, Mathf.Sin(a) * radius),
                    center + new Vector2(Mathf.Cos(b) * radius, Mathf.Sin(b) * radius),
                    color,
                    width);
            }
        }

        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();
        }
    }
}
