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

namespace TD.UI.Minimap
{
    /// <summary>
    /// Builds the static terrain layer of the WC3-style minimap as a <see cref="Texture2D"/>
    /// from the baked <see cref="LevelData"/>. One pixel per tile; the UI element scales the
    /// texture up with Point filtering so the result reads as crisp, chunky map abstraction
    /// rather than a tiny blurred photograph.
    /// </summary>
    /// <remarks>
    /// The terrain layer represents what cannot change during a match: zone ownership, spawner
    /// locations, goal locations, and the map shape itself. Towers and enemies are drawn on a
    /// separate dynamic overlay by <see cref="MinimapView"/>, so this texture is baked once at
    /// level load and never re-baked.
    /// </remarks>
    public static class MinimapTerrainBaker
    {
        // ----- Palette ----------------------------------------------------
        // Picked for legibility at low resolution. Aim: each tile category reads as a distinct
        // color block when scaled up 4–8×, even on a Steam Deck screen at running brightness.

        /// <summary>Tiles outside the playable map area. Fully transparent so the panel
        /// background shows through.</summary>
        private static readonly Color32 OutOfMap = new Color32(0, 0, 0, 0);

        /// <summary>In-map tiles that are not owned by any player and not part of a goal /
        /// spawner. Dark neutral grey — reads as "playable but unallocated".</summary>
        private static readonly Color32 NeutralInMap = new Color32(46, 46, 51, 255);

        /// <summary>Spawner tiles. Bright cyan — pops against every player zone color.</summary>
        private static readonly Color32 SpawnerTile = new Color32(102, 217, 255, 255);

        /// <summary>Goal tiles. Matches <see cref="PlayerColors.Goal"/> (gold).</summary>
        private static readonly Color32 GoalTile = new Color32(224, 176, 32, 255);

        // ----- Public API -------------------------------------------------

        /// <summary>
        /// Bakes the current level into a Texture2D and returns it. Returns null if the loader
        /// is not ready. The returned texture has FilterMode.Point and is non-readable after
        /// bake (CPU-side pixel data is released).
        /// </summary>
        /// <remarks>
        /// Caller is responsible for the texture lifetime: assign it to the minimap element and
        /// destroy it when no longer needed. Currently <see cref="MinimapView"/> holds the only
        /// reference and lets it live for the duration of the match.
        /// </remarks>
        public static Texture2D Bake(LevelLoader loader)
        {
            if (loader == null || !loader.IsLoaded)
            {
                Debug.LogWarning("[MinimapTerrainBaker] LevelLoader not ready; cannot bake terrain.");
                return null;
            }

            var data = loader.LevelData;
            int w = data.GridSize.x;
            int h = data.GridSize.y;
            if (w <= 0 || h <= 0)
            {
                Debug.LogWarning($"[MinimapTerrainBaker] Invalid grid size {data.GridSize}; cannot bake.");
                return null;
            }

            // RGBA32 supports alpha for out-of-map transparency. mipChain off — the texture
            // is rendered at near-1:1 in the UI and mipmaps would only blur the abstract look.
            var tex = new Texture2D(w, h, TextureFormat.RGBA32, mipChain: false, linear: false)
            {
                filterMode = FilterMode.Point,
                wrapMode = TextureWrapMode.Clamp,
                name = "MinimapTerrain"
            };

            var pixels = new Color32[w * h];

            // Pass 1: base tile color from MapArea + Owner.
            // Texture y=0 is the BOTTOM row in Unity convention. World z and grid y both
            // increase northward, so a tile at grid-y=0 should land at the bottom of the
            // texture. That's exactly what `pixels[y * w + x] = ...` gives us, since SetPixels32
            // treats index 0 as bottom-left. No flip needed during bake — the flip happens at
            // display time (UI y-axis points down, world z points up; MinimapView handles it).
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < w; x++)
                {
                    int idx = y * w + x;
                    pixels[idx] = ResolveBaseColor(data, idx);
                }
            }

            // Pass 2: stamp special tile categories on top — spawners and goals override base.
            // PlayerZones[].LeakExits intentionally NOT painted: leak exits are conceptually
            // part of the source player's zone (an enemy boundary, not a player-visible
            // landmark), and visually highlighting them would clutter the minimap.
            StampZoneSpawners(pixels, data);
            StampGoals(pixels, data);

            tex.SetPixels32(pixels);
            tex.Apply(updateMipmaps: false, makeNoLongerReadable: true);

            LogBakeSummary(data, w, h);
            return tex;
        }

        // ----- Diagnostics ------------------------------------------------

        /// <summary>
        /// One-shot log of the bake breakdown. Useful for diagnosing apparent asymmetries in
        /// the rendered minimap — a symmetric authoring should produce mirrored counts on
        /// opposite edges. Per-player min/max columns reveal exactly where each zone starts
        /// and ends across the grid.
        /// </summary>
        private static void LogBakeSummary(LevelData data, int w, int h)
        {
            int outOfMap = 0, neutralInMap = 0, spawnerTiles = 0, goalTiles = 0;
            // Per-slot owned-tile bounds (1..9).
            int[] ownedCount = new int[10];
            int[] minX = new int[10]; int[] maxX = new int[10];
            int[] minY = new int[10]; int[] maxY = new int[10];
            for (int i = 0; i < 10; i++)
            {
                minX[i] = int.MaxValue; minY[i] = int.MaxValue;
                maxX[i] = int.MinValue; maxY[i] = int.MinValue;
            }

            for (int y = 0; y < h; y++)
            for (int x = 0; x < w; x++)
            {
                int idx = y * w + x;
                bool inMap = data.MapAreaGrid != null && data.MapAreaGrid[idx];
                if (!inMap) { outOfMap++; continue; }

                PlayerSlot s = data.OwnerGrid != null ? data.OwnerGrid[idx] : PlayerSlot.None;
                if (s == PlayerSlot.None) { neutralInMap++; continue; }

                int si = (int)s;
                if (si < 0 || si > 9) continue;
                ownedCount[si]++;
                if (x < minX[si]) minX[si] = x;
                if (x > maxX[si]) maxX[si] = x;
                if (y < minY[si]) minY[si] = y;
                if (y > maxY[si]) maxY[si] = y;
            }

            if (data.PlayerZones != null)
                foreach (var z in data.PlayerZones)
                    if (z?.Spawners != null)
                        foreach (var sp in z.Spawners)
                            if (sp?.TileArea != null) spawnerTiles += sp.TileArea.Length;
            if (data.Goals != null)
                foreach (var g in data.Goals)
                    if (g?.TileArea != null) goalTiles += g.TileArea.Length;

            var sb = new System.Text.StringBuilder(256);
            sb.AppendFormat("[MinimapTerrainBaker] '{0}' baked. Grid {1}×{2} origin {3}. ",
                data.MapName, w, h, data.GridOriginTile);
            sb.AppendFormat("Tiles: outOfMap={0}, neutralInMap={1}, spawner={2}, goal={3}.",
                outOfMap, neutralInMap, spawnerTiles, goalTiles);
            for (int i = 1; i <= 9; i++)
            {
                if (ownedCount[i] == 0) continue;
                sb.AppendFormat(" P{0}={1} (x:{2}–{3}, y:{4}–{5})",
                    i, ownedCount[i], minX[i], maxX[i], minY[i], maxY[i]);
            }
            Debug.Log(sb.ToString());
        }

        // ----- Pass implementations ---------------------------------------

        private static Color32 ResolveBaseColor(LevelData data, int idx)
        {
            // MapAreaGrid may be null on levels baked before that field existed; treat as
            // "not in map" so we render transparent rather than a misleading neutral block.
            bool inMap = data.MapAreaGrid != null
                         && idx < data.MapAreaGrid.Length
                         && data.MapAreaGrid[idx];
            if (!inMap) return OutOfMap;

            PlayerSlot owner = (data.OwnerGrid != null && idx < data.OwnerGrid.Length)
                ? data.OwnerGrid[idx]
                : PlayerSlot.None;

            if (owner == PlayerSlot.None) return NeutralInMap;
            return ToZoneColor(PlayerColors.Get(owner));
        }

        private static void StampZoneSpawners(Color32[] pixels, LevelData data)
        {
            if (data.PlayerZones == null) return;
            foreach (var zone in data.PlayerZones)
            {
                if (zone == null || zone.Spawners == null) continue;
                foreach (var spawner in zone.Spawners)
                {
                    if (spawner == null || spawner.TileArea == null) continue;
                    foreach (var tile in spawner.TileArea)
                        PaintTile(pixels, data, tile, SpawnerTile);
                }
            }
        }

        private static void StampGoals(Color32[] pixels, LevelData data)
        {
            if (data.Goals == null) return;
            foreach (var goal in data.Goals)
            {
                if (goal == null || goal.TileArea == null) continue;
                foreach (var tile in goal.TileArea)
                    PaintTile(pixels, data, tile, GoalTile);
            }
        }

        private static void PaintTile(Color32[] pixels, LevelData data, Vector2Int worldTile, Color32 color)
        {
            int x = worldTile.x - data.GridOriginTile.x;
            int y = worldTile.y - data.GridOriginTile.y;
            if (x < 0 || x >= data.GridSize.x || y < 0 || y >= data.GridSize.y) return;
            pixels[y * data.GridSize.x + x] = color;
        }

        // ----- Color helpers ----------------------------------------------

        /// <summary>
        /// Translates a saturated player gizmo color into the dimmer, slightly desaturated tone
        /// used for zone fill on the minimap. The full-saturation original is reserved for the
        /// player's icons (towers, builders) so units pop against their own zone tint.
        /// </summary>
        private static Color32 ToZoneColor(Color full)
        {
            // Desaturate ~45% toward equal-luminance grey, then darken to ~70% brightness.
            float lum = full.r * 0.299f + full.g * 0.587f + full.b * 0.114f;
            float r = Mathf.Lerp(full.r, lum, 0.45f) * 0.70f;
            float g = Mathf.Lerp(full.g, lum, 0.45f) * 0.70f;
            float b = Mathf.Lerp(full.b, lum, 0.45f) * 0.70f;
            return new Color32(
                (byte)Mathf.RoundToInt(r * 255f),
                (byte)Mathf.RoundToInt(g * 255f),
                (byte)Mathf.RoundToInt(b * 255f),
                255);
        }
    }
}
