// Assets/_Project/Scripts/UI/Minimap/MinimapView.cs
using UnityEngine;
using UnityEngine.UIElements;
using TD.Core;
using TD.Gameplay;
using TD.Levels;

namespace TD.UI.Minimap
{
    /// <summary>
    /// UI Toolkit minimap controller. Manages two stacked sub-elements (terrain + entities)
    /// inside a host <see cref="VisualElement"/>, handles click-to-jump, drag-to-pan,
    /// right-click-to-move-builder, and scroll-wheel zoom against a <see cref="CameraController"/>
    /// and the local <see cref="SelectionState"/>. Refreshes the entity overlay at a throttled
    /// rate so 500+ enemies don't dominate frame time.
    /// </summary>
    /// <remarks>
    /// <para><b>Coordinate spaces.</b> Three are in play:
    /// <list type="bullet">
    /// <item><b>World</b> — XZ plane positions of game-side entities. Z+ is north.</item>
    /// <item><b>UI local</b> — pixel coordinates within the minimap container. Y+ is DOWN.</item>
    /// <item><b>Texture</b> — handled entirely by the baker; not exposed here.</item>
    /// </list>
    /// World↔UI conversion uses the currently-visible world rectangle, which is the full map
    /// at zoom 1 and a smaller rect centered on <see cref="viewCenter"/> at zoom &gt; 1.</para>
    ///
    /// <para><b>Zoom model.</b> Zoom is anchored to the cursor position: the world point under
    /// the cursor before the scroll stays under the cursor after, so the user can dial in on a
    /// region by hovering over it and scrolling. Zoom defaults to 1 (fully zoomed out) and is
    /// clamped to <see cref="MaxZoom"/>. When zoom returns to 1 the visible rect snaps back to
    /// the full map.</para>
    ///
    /// <para><b>Terrain rendering.</b> The terrain sub-element is sized and positioned so the
    /// currently-visible world rect maps to the container's contentRect. At zoom 1 the element
    /// fills the container exactly; at higher zoom it grows (in pixels) past the container and
    /// the container's <c>overflow: hidden</c> clips the off-view portion. The texture itself
    /// is baked once and never re-baked.</para>
    /// </remarks>
    public class MinimapView
    {
        // ----- Tuning -----------------------------------------------------

        /// <summary>Entity overlay refresh rate. 20Hz handles 500+ enemies comfortably without
        /// burning the frame budget on Painter2D. Revisit if testing shows it's not enough.</summary>
        private const float EntityRepaintHz = 20f;

        private const float MinZoom = 1f;
        private const float MaxZoom = 4f;
        /// <summary>Multiplicative factor applied per scroll-wheel tick. 1.2 ≈ 6 ticks to span
        /// 1x → 4x, which feels neither sluggish nor twitchy in playtest.</summary>
        private const float ZoomStepFactor = 1.2f;

        // Pixel-size floor for point-like icons (builders, enemies). Ensures they stay visible
        // even at full zoom-out on a large map. Towers use their actual footprint instead and
        // do not need a floor — they're always at least a couple of pixels by virtue of being
        // 2×2 or larger.
        private const float BuilderMinRadiusPx = 2.4f;
        private const float EnemyMinRadiusPx   = 1.5f;

        // Outline added to builder icons so they read against same-color zone fill.
        private static readonly Color BuilderOutline = new Color(1f, 1f, 1f, 0.85f);

        // Viewport trapezoid (the "what the player sees" rectangle on the minimap).
        // Drawn on top of all entities so it's always readable; matches WC3 visual style.
        private static readonly Color ViewportColor = new Color(1f, 1f, 1f, 0.85f);
        private const float ViewportLineWidth = 1.5f;

        // Reused buffer for the camera's four world-space view corners. Heap-allocated
        // once and refilled every repaint to avoid per-frame GC.
        private readonly Vector3[] viewCornersBuf = new Vector3[4];

        // ----- Refs -------------------------------------------------------

        private readonly VisualElement container;
        private readonly VisualElement terrainLayer;
        private readonly VisualElement entityLayer;
        private readonly CameraController cameraController;

        // ----- Bake state -------------------------------------------------

        // World-space rectangle the texture covers (the full map). Cached once at bake.
        private Vector2 worldMin;
        private Vector2 worldMax;

        // Owned texture — kept so we can destroy it cleanly. Null until first successful bake.
        private Texture2D bakedTerrain;

        // True once terrain has been baked successfully.
        private bool ready;

        // ----- View state -------------------------------------------------

        // Current zoom in [MinZoom, MaxZoom]. 1 = full map visible.
        private float zoom = 1f;

        // World-XZ point the visible rect is centered on. At zoom 1 this is forced to the
        // world center; at higher zoom the user's scrolling determines it (clamped so the
        // visible rect never leaves the map).
        private Vector2 viewCenter;

        // Visible world rect, derived from zoom + viewCenter in ApplyView().
        private Vector2 visibleWorldMin;
        private Vector2 visibleWorldMax;

        // ----- Input state ------------------------------------------------

        private bool isDragging;
        private int dragPointerId;

        // ----- Repaint throttle -------------------------------------------

        private float lastEntityRepaintTime;

        // ----- Construction -----------------------------------------------

        public MinimapView(VisualElement container, CameraController cameraController)
        {
            this.container = container;
            this.cameraController = cameraController;

            // Two stacked sub-elements. Terrain is painted via background-image; the entity
            // overlay uses generateVisualContent + Painter2D. Both are PickingMode.Ignore so
            // pointer events bubble back to the container, where we capture them.
            terrainLayer = new VisualElement { name = "minimap-terrain" };
            terrainLayer.AddToClassList("minimap-terrain");
            terrainLayer.pickingMode = PickingMode.Ignore;
            container.Add(terrainLayer);

            entityLayer = new VisualElement { name = "minimap-entities" };
            entityLayer.AddToClassList("minimap-entities");
            entityLayer.pickingMode = PickingMode.Ignore;
            entityLayer.generateVisualContent += DrawEntities;
            container.Add(entityLayer);

            // Pointer events on the container drive click-to-jump, drag-to-pan, right-click-move.
            container.RegisterCallback<PointerDownEvent>(OnPointerDown);
            container.RegisterCallback<PointerMoveEvent>(OnPointerMove);
            container.RegisterCallback<PointerUpEvent>(OnPointerUp);
            container.RegisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
            container.RegisterCallback<WheelEvent>(OnWheel);

            // Recompute terrain layout when the container resizes — the absolute pixel
            // positions we set on terrainLayer are container-relative.
            container.RegisterCallback<GeometryChangedEvent>(_ => { if (ready) ApplyView(); });
        }

        // ----- Lifecycle --------------------------------------------------

        /// <summary>
        /// Called from <c>HUDController.Update</c>. Performs lazy bake on the first frame
        /// after <see cref="LevelLoader"/> finishes loading, then triggers throttled entity
        /// overlay repaints.
        /// </summary>
        public void Tick()
        {
            if (!ready)
            {
                TryBake();
                if (!ready) return;
            }

            float now = Time.unscaledTime;
            if (now - lastEntityRepaintTime >= 1f / EntityRepaintHz)
            {
                lastEntityRepaintTime = now;
                entityLayer.MarkDirtyRepaint();
            }
        }

        /// <summary>Releases the baked texture. Call from <c>HUDController.OnDestroy</c>.</summary>
        public void Dispose()
        {
            if (bakedTerrain != null)
            {
                Object.Destroy(bakedTerrain);
                bakedTerrain = null;
            }
            ready = false;
        }

        // ----- Bake -------------------------------------------------------

        private void TryBake()
        {
            var loader = LevelLoader.Instance;
            if (loader == null || !loader.IsLoaded) return;

            bakedTerrain = MinimapTerrainBaker.Bake(loader);
            if (bakedTerrain == null) return;

            terrainLayer.style.backgroundImage =
                new StyleBackground(Background.FromTexture2D(bakedTerrain));

            // World extents of the baked rectangle. Tile (n) spans world [n, n+1] (edge-aligned),
            // so the rectangle covers [GridOriginTile, GridOriginTile + GridSize] on each axis.
            var data = loader.LevelData;
            float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE;
            float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE;
            float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE;
            float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE;
            worldMin = new Vector2(minX, minZ);
            worldMax = new Vector2(maxX, maxZ);

            // Default view: centered, fully zoomed out.
            zoom = MinZoom;
            viewCenter = (worldMin + worldMax) * 0.5f;
            ApplyView();

            ready = true;
        }

        // ----- View math --------------------------------------------------

        /// <summary>
        /// Recomputes <see cref="visibleWorldMin"/>/<see cref="visibleWorldMax"/> from the
        /// current <see cref="zoom"/> and <see cref="viewCenter"/>, clamps the view to stay
        /// within the map, and resizes/positions the terrain sub-element so its rendered
        /// region matches the visible world rect.
        /// </summary>
        private void ApplyView()
        {
            var rect = container.contentRect;
            if (rect.width <= 0f || rect.height <= 0f) return;

            // Half-extents of the visible world rect.
            float halfWorldX = (worldMax.x - worldMin.x) * 0.5f / zoom;
            float halfWorldZ = (worldMax.y - worldMin.y) * 0.5f / zoom;

            // Clamp viewCenter so the visible rect doesn't leave the map. At zoom 1 the
            // clamp range collapses to a single point (the world center), forcing the view.
            viewCenter.x = Mathf.Clamp(viewCenter.x,
                worldMin.x + halfWorldX, worldMax.x - halfWorldX);
            viewCenter.y = Mathf.Clamp(viewCenter.y,
                worldMin.y + halfWorldZ, worldMax.y - halfWorldZ);

            visibleWorldMin = new Vector2(viewCenter.x - halfWorldX, viewCenter.y - halfWorldZ);
            visibleWorldMax = new Vector2(viewCenter.x + halfWorldX, viewCenter.y + halfWorldZ);

            // Position the terrain element so visibleWorldMin maps to container (0, 0) — in
            // UI coords where y is top-down and the texture is flipped to put north at top.
            // Terrain at zoom z is rect.size * z big, and we offset it so the visible window
            // lands inside the container.
            float worldRangeX = worldMax.x - worldMin.x;
            float worldRangeZ = worldMax.y - worldMin.y;

            float terrainW = rect.width * zoom;
            float terrainH = rect.height * zoom;
            float left = -(visibleWorldMin.x - worldMin.x) / worldRangeX * terrainW;
            // Top: container's top corresponds to visibleWorldMax.z. Distance from texture's
            // top (worldMax.z) to visibleWorldMax.z, as a fraction of world range, times height.
            float top  = -(worldMax.y - visibleWorldMax.y) / worldRangeZ * terrainH;

            terrainLayer.style.width  = terrainW;
            terrainLayer.style.height = terrainH;
            terrainLayer.style.left = left;
            terrainLayer.style.top  = top;
            terrainLayer.style.right  = StyleKeyword.Auto;
            terrainLayer.style.bottom = StyleKeyword.Auto;

            // Entity overlay needs a redraw to reflect the new visible rect.
            entityLayer.MarkDirtyRepaint();
        }

        // ----- Pointer handling -------------------------------------------

        private void OnPointerDown(PointerDownEvent evt)
        {
            if (!ready) return;

            if (evt.button == 0)
            {
                // Left button: jump camera + start drag-to-pan.
                if (cameraController == null) return;
                isDragging = true;
                dragPointerId = evt.pointerId;
                container.CapturePointer(evt.pointerId);
                cameraController.BeginDrag();
                cameraController.JumpTo(UIToWorld(evt.localPosition));
                evt.StopPropagation();
            }
            else if (evt.button == 1)
            {
                // Right button: move-and-pause command on the selected builder. Fires
                // immediately; no drag/capture semantics (single-shot like an RTS).
                HandleRightClickMove(evt.localPosition);
                evt.StopPropagation();
            }
        }

        private void OnPointerMove(PointerMoveEvent evt)
        {
            if (!isDragging || evt.pointerId != dragPointerId) return;
            cameraController.JumpTo(UIToWorld(evt.localPosition));
            evt.StopPropagation();
        }

        private void OnPointerUp(PointerUpEvent evt)
        {
            if (!isDragging || evt.pointerId != dragPointerId) return;
            EndDragging(evt.pointerId);
            evt.StopPropagation();
        }

        // Lost capture — end drag cleanly so we don't leave CameraController stuck.
        private void OnPointerCaptureOut(PointerCaptureOutEvent evt)
        {
            if (!isDragging || evt.pointerId != dragPointerId) return;
            EndDragging(evt.pointerId);
        }

        private void EndDragging(int pointerId)
        {
            isDragging = false;
            cameraController?.EndDrag();
            if (container.HasPointerCapture(pointerId))
                container.ReleasePointer(pointerId);
        }

        private void HandleRightClickMove(Vector2 uiLocal)
        {
            // Right-click on the minimap = "send my builder there". Only meaningful when
            // the LOCAL BUILDER is selected; a tower or enemy selection has no move action.
            var selection = SelectionState.Instance;
            var builder = selection?.SelectedBuilder;
            if (builder == null) return;

            Vector3 worldTarget = UIToWorld(uiLocal);
            // Same RPC the world right-click uses; server validates and side-effects the queue.
            builder.RequestMoveAndPauseRpc(worldTarget);
        }

        // ----- Zoom -------------------------------------------------------

        private void OnWheel(WheelEvent evt)
        {
            if (!ready) return;

            // Cursor-anchored zoom: capture the world point under the cursor before the
            // zoom change, then move viewCenter so that same world point lands at the same
            // UI position afterward. Result: the cursor "drills in" on whatever it's over.
            Vector3 cursorWorldBefore = UIToWorld(evt.localMousePosition);

            // WheelEvent.delta.y: positive = scroll down (typically zoom out), negative = up.
            // Multiplicative steps feel natural and keep the rate consistent across zoom levels.
            float steps = -evt.delta.y;
            float newZoom = Mathf.Clamp(zoom * Mathf.Pow(ZoomStepFactor, steps), MinZoom, MaxZoom);
            if (Mathf.Approximately(newZoom, zoom))
            {
                evt.StopPropagation();
                return;
            }

            zoom = newZoom;

            // Compute viewCenter that keeps cursorWorldBefore under the cursor at the new zoom.
            // visibleWorldMin = viewCenter - halfWorld; visibleWorldMax = viewCenter + halfWorld
            // UIToWorld(uiLocal) = visibleWorldMin + frac * (visibleWorldMax - visibleWorldMin)
            //                   = viewCenter - halfWorld + frac * 2 * halfWorld
            //                   = viewCenter + (2*frac - 1) * halfWorld
            // Solve viewCenter from desired cursorWorldBefore at the cursor's frac.
            var rect = container.contentRect;
            float fx = Mathf.Clamp01(evt.localMousePosition.x / rect.width);
            float fyTopDown = Mathf.Clamp01(evt.localMousePosition.y / rect.height);
            float fzBottomUp = 1f - fyTopDown;

            float halfWorldX = (worldMax.x - worldMin.x) * 0.5f / zoom;
            float halfWorldZ = (worldMax.y - worldMin.y) * 0.5f / zoom;
            viewCenter.x = cursorWorldBefore.x - (2f * fx - 1f) * halfWorldX;
            viewCenter.y = cursorWorldBefore.z - (2f * fzBottomUp - 1f) * halfWorldZ;

            ApplyView();   // clamps viewCenter and updates terrain element
            evt.StopPropagation();
        }

        // ----- Coordinate transforms --------------------------------------

        // UI local (y down) → world (z up). Clamps to visible map rect so dragging past the
        // edge doesn't fly the camera off the map.
        private Vector3 UIToWorld(Vector2 uiLocal)
        {
            var rect = container.contentRect;
            if (rect.width <= 0f || rect.height <= 0f) return Vector3.zero;

            float fx = Mathf.Clamp01(uiLocal.x / rect.width);
            float fyTopDown = Mathf.Clamp01(uiLocal.y / rect.height);
            float fzBottomUp = 1f - fyTopDown;

            float worldX = Mathf.Lerp(visibleWorldMin.x, visibleWorldMax.x, fx);
            float worldZ = Mathf.Lerp(visibleWorldMin.y, visibleWorldMax.y, fzBottomUp);
            return new Vector3(worldX, GridCoordinates.BUILDABLE_PLANE_Y, worldZ);
        }

        // World → UI local. Used for placing entity icons.
        //
        // IMPORTANT: this must NOT clamp. Mathf.InverseLerp clamps to [0,1], which would
        // pin off-screen entities to the minimap edge — making zoomed-in views show
        // ghost icons stuck against every border. We compute the fraction manually so
        // out-of-view entities get UI coords outside the container's rect, where the
        // bounds check in DrawOneEntity culls them.
        private Vector2 WorldToUI(Vector3 world)
        {
            var rect = container.contentRect;
            float rangeX = visibleWorldMax.x - visibleWorldMin.x;
            float rangeZ = visibleWorldMax.y - visibleWorldMin.y;
            if (rangeX <= 0.0001f || rangeZ <= 0.0001f) return Vector2.zero;

            float fx = (world.x - visibleWorldMin.x) / rangeX;
            float fzBottomUp = (world.z - visibleWorldMin.y) / rangeZ;
            float fyTopDown = 1f - fzBottomUp;
            return new Vector2(fx * rect.width, fyTopDown * rect.height);
        }

        /// <summary>Pixels per world unit at the current zoom. Used to scale entity icons so
        /// e.g. a 2×2 tower footprint reads as a 2-tile square on the minimap.</summary>
        private float PixelsPerWorldUnit
        {
            get
            {
                var rect = container.contentRect;
                float visibleWorldWidth = visibleWorldMax.x - visibleWorldMin.x;
                return visibleWorldWidth > 0.0001f ? rect.width / visibleWorldWidth : 1f;
            }
        }

        // ----- Entity overlay drawing -------------------------------------

        private void DrawEntities(MeshGenerationContext mgc)
        {
            if (!ready) return;
            var painter = mgc.painter2D;
            float pxPerWorld = PixelsPerWorldUnit;

            // The local player's builder always draws on top. Cache the reference once
            // (Builder.Local does a dictionary lookup) and cast to the interface so the
            // ReferenceEquals compare against registry entries works without boxing.
            IMinimapEntity localBuilder = Builder.Local;

            // Pass 1: every entity except the local builder. Order within this pass is
            // arbitrary (registry is a HashSet); revisit if cross-entity layering between
            // non-local entities ever matters.
            MinimapEntityRegistry.ForEach(e =>
            {
                if (ReferenceEquals(e, localBuilder)) return;
                DrawOneEntity(painter, e, pxPerWorld);
            });

            // Pass 2: local builder on top. Skipped if the local builder isn't spawned
            // (e.g., on a dedicated server or before the local client's builder arrives).
            if (localBuilder != null)
                DrawOneEntity(painter, localBuilder, pxPerWorld);

            // Pass 3: the camera-viewport trapezoid sits on top of everything so the
            // player can always see where they're looking, regardless of zone tint or
            // unit density underneath.
            DrawViewportRect(painter);
        }

        // Draws a thin white outline matching the camera's footprint on the buildable
        // plane. Because the camera is angled, the on-plane footprint is a TRAPEZOID
        // (far edge wider than near edge) — that's the visual we want, since it tells
        // the player how much of the world they're actually seeing at the current pitch.
        private void DrawViewportRect(Painter2D painter)
        {
            if (cameraController == null) return;
            // Even when one or more corners can't be projected onto the plane (camera at
            // the horizon), the fallback puts them at a far point along the ray. The
            // resulting UI coords land outside the container and get clipped by
            // overflow:hidden — perfectly acceptable visual.
            cameraController.TryGetViewportWorldCorners(viewCornersBuf);

            Vector2 a = WorldToUI(viewCornersBuf[0]);
            Vector2 b = WorldToUI(viewCornersBuf[1]);
            Vector2 c = WorldToUI(viewCornersBuf[2]);
            Vector2 d = WorldToUI(viewCornersBuf[3]);

            painter.strokeColor = ViewportColor;
            painter.lineWidth = ViewportLineWidth;
            painter.BeginPath();
            painter.MoveTo(a);
            painter.LineTo(b);
            painter.LineTo(c);
            painter.LineTo(d);
            painter.ClosePath();
            painter.Stroke();
        }

        private void DrawOneEntity(Painter2D p, IMinimapEntity entity, float pxPerWorld)
        {
            if (entity == null) return;

            Vector2 ui = WorldToUI(entity.WorldPosition);

            // Skip if obviously off-screen — overflow:hidden clips it anyway, but skipping
            // saves Painter2D work for entities far outside the zoomed-in window.
            var rect = container.contentRect;
            if (ui.x < -8f || ui.y < -8f || ui.x > rect.width + 8f || ui.y > rect.height + 8f) return;

            Color col = entity.MinimapColor; col.a = 1f;
            p.fillColor = col;

            switch (entity.IconKind)
            {
                case MinimapIconKind.Enemy:
                {
                    float r = Mathf.Max(entity.MinimapWorldSize * 0.5f * pxPerWorld, EnemyMinRadiusPx);
                    DrawCircle(p, ui, r);
                    break;
                }
                case MinimapIconKind.Tower:
                {
                    // Tower size follows footprint exactly — no pixel floor. Adjacent towers
                    // visually touch on the minimap because their drawn squares span their
                    // full footprint extent in world units.
                    float halfPx = entity.MinimapWorldSize * 0.5f * pxPerWorld;
                    DrawSquare(p, ui, halfPx);
                    break;
                }
                case MinimapIconKind.Builder:
                {
                    float r = Mathf.Max(entity.MinimapWorldSize * 0.5f * pxPerWorld, BuilderMinRadiusPx);
                    DrawCircle(p, ui, r);
                    p.strokeColor = BuilderOutline;
                    p.lineWidth = 1f;
                    p.Stroke();
                    break;
                }
            }
        }

        private static void DrawCircle(Painter2D p, Vector2 center, float radius)
        {
            p.BeginPath();
            p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
            p.Fill();
        }

        private static void DrawSquare(Painter2D p, Vector2 center, float halfSize)
        {
            p.BeginPath();
            p.MoveTo(new Vector2(center.x - halfSize, center.y - halfSize));
            p.LineTo(new Vector2(center.x + halfSize, center.y - halfSize));
            p.LineTo(new Vector2(center.x + halfSize, center.y + halfSize));
            p.LineTo(new Vector2(center.x - halfSize, center.y + halfSize));
            p.ClosePath();
            p.Fill();
        }
    }
}
