using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Core;
using Theriapolis.Core.Tactical;

namespace Theriapolis.Game.Rendering;

/// <summary>
/// Holds one 32×32 sprite per <see cref="TacticalSurface"/> and
/// <see cref="TacticalDeco"/> value, with optional per-variant alternates.
///
/// On construction, looks for PNGs under <c>&lt;gfxRoot&gt;/surface/&lt;name&gt;.png</c>
/// and <c>&lt;gfxRoot&gt;/deco/&lt;name&gt;.png</c> (lowercase enum names). For
/// variants, drop in <c>&lt;name&gt;_0.png</c>, <c>&lt;name&gt;_1.png</c>, … —
/// the chunk's per-tile <c>Variant</c> nibble picks one. Missing files fall
/// back to a procedurally generated solid-color placeholder so the renderer
/// always has something to draw, even with no art on disk.
/// </summary>
public sealed class TacticalAtlas : IDisposable
{
    private const int Px = C.TACTICAL_TILE_SPRITE_PX;

    private readonly GraphicsDevice _gd;
    private readonly Dictionary<TacticalSurface, Texture2D[]> _surfaces = new();
    private readonly Dictionary<TacticalDeco,    Texture2D[]> _decos    = new();
    private readonly Dictionary<TacticalSurface, Color>       _surfaceAvg = new();
    private readonly List<Texture2D> _owned = new();
    private bool _disposed;

    public TacticalAtlas(GraphicsDevice gd, string? gfxRoot = null)
    {
        _gd = gd;
        LoadAll(gfxRoot);
    }

    /// <summary>Sprite for the given surface + per-tile variant. Always non-null.</summary>
    public Texture2D GetSurface(TacticalSurface s, byte variant)
    {
        if (!_surfaces.TryGetValue(s, out var arr) || arr.Length == 0)
            arr = _surfaces[TacticalSurface.None];
        return arr[variant % arr.Length];
    }

    /// <summary>Sprite for the given deco + variant, or null for <see cref="TacticalDeco.None"/>.</summary>
    public Texture2D? GetDeco(TacticalDeco d, byte variant)
    {
        if (d == TacticalDeco.None) return null;
        if (!_decos.TryGetValue(d, out var arr) || arr.Length == 0) return null;
        return arr[variant % arr.Length];
    }

    /// <summary>
    /// Average opaque-pixel RGB of a surface, cached on first request. Used by
    /// the renderer's edge-blend pass to soften the seam between adjacent
    /// dissimilar surfaces (Option B autotiling — see <see cref="TacticalRenderer"/>).
    /// </summary>
    public Color GetSurfaceAverageColor(TacticalSurface s)
    {
        if (_surfaceAvg.TryGetValue(s, out var cached)) return cached;
        var tex = GetSurface(s, 0);
        var pixels = new Color[tex.Width * tex.Height];
        tex.GetData(pixels);
        long r = 0, g = 0, b = 0, n = 0;
        foreach (var p in pixels)
        {
            if (p.A < 128) continue;
            r += p.R; g += p.G; b += p.B; n++;
        }
        Color avg = n == 0 ? Color.Transparent : new Color((byte)(r / n), (byte)(g / n), (byte)(b / n));
        _surfaceAvg[s] = avg;
        return avg;
    }

    private void LoadAll(string? gfxRoot)
    {
        // Always create a magenta sentinel for missing surfaces.
        _surfaces[TacticalSurface.None] = new[] { MakeSolid(new Color(255, 0, 255)) };

        foreach (TacticalSurface s in Enum.GetValues<TacticalSurface>())
        {
            if (s == TacticalSurface.None) continue;
            _surfaces[s] = LoadVariants(gfxRoot, "surface", s.ToString().ToLowerInvariant(),
                                         () => MakeSurfacePlaceholder(s));
        }
        foreach (TacticalDeco d in Enum.GetValues<TacticalDeco>())
        {
            if (d == TacticalDeco.None) continue;
            _decos[d] = LoadVariants(gfxRoot, "deco", d.ToString().ToLowerInvariant(),
                                      () => MakeDecoPlaceholder(d));
        }
    }

    private Texture2D[] LoadVariants(string? root, string subdir, string name, Func<Texture2D> placeholder)
    {
        var found = new List<Texture2D>();
        if (root is not null)
        {
            string dir = Path.Combine(root, subdir);
            if (Directory.Exists(dir))
            {
                // Variant suffix files: name_0.png, name_1.png, ...
                for (int i = 0; ; i++)
                {
                    string p = Path.Combine(dir, $"{name}_{i}.png");
                    if (!File.Exists(p)) break;
                    found.Add(LoadFile(p));
                }
                // Fallback to a single name.png if no _N variants exist.
                if (found.Count == 0)
                {
                    string p = Path.Combine(dir, $"{name}.png");
                    if (File.Exists(p)) found.Add(LoadFile(p));
                }
            }
        }
        if (found.Count == 0) found.Add(placeholder());
        return found.ToArray();
    }

    private Texture2D LoadFile(string path)
    {
        using var stream = File.OpenRead(path);
        var tex = Texture2D.FromStream(_gd, stream);
        StripBorderPixels(tex);
        _owned.Add(tex);
        return tex;
    }

    /// <summary>
    /// Many AI-generated tile sources (Pixellab in particular) bake a uniform
    /// dark border into each tile — sometimes pure black, sometimes a dark
    /// purple/grey, and 1–3 pixels deep. Adjacent tiles in the world then
    /// show as grid-lined rectangles instead of seamless terrain.
    ///
    /// This pass detects each side's border depth (rows/cols where ≥80% of
    /// pixels are uniformly dark) and replaces those rows/cols with a copy of
    /// the first interior row/col, restoring the seamless look without
    /// touching tile interiors. No-ops on tiles that don't have a detectable
    /// border, so it's safe to run on any input.
    /// </summary>
    private static void StripBorderPixels(Texture2D tex)
    {
        const int BorderChannelMax = 80;   // every channel ≤ this counts as "dark"
        const float BorderRowFrac  = 0.80f; // fraction of dark pixels for a row to be a border

        int w = tex.Width, h = tex.Height;
        if (w < 3 || h < 3) return;
        var pixels = new Color[w * h];
        tex.GetData(pixels);

        // Only opaque dark pixels count — otherwise transparent perimeter
        // (the norm for decoration sprites with see-through backgrounds)
        // would be misread as a border and trigger a damaging strip.
        bool IsDark(Color p) => p.A >= 128 && p.R <= BorderChannelMax && p.G <= BorderChannelMax && p.B <= BorderChannelMax;
        bool RowIsBorder(int y)
        {
            int dark = 0;
            for (int x = 0; x < w; x++) if (IsDark(pixels[y * w + x])) dark++;
            return dark >= w * BorderRowFrac;
        }
        bool ColIsBorder(int x)
        {
            int dark = 0;
            for (int y = 0; y < h; y++) if (IsDark(pixels[y * w + x])) dark++;
            return dark >= h * BorderRowFrac;
        }

        int top = 0;       while (top < h / 2 && RowIsBorder(top)) top++;
        int bot = h - 1;   while (bot > h / 2 && RowIsBorder(bot)) bot--;
        int lef = 0;       while (lef < w / 2 && ColIsBorder(lef)) lef++;
        int rig = w - 1;   while (rig > w / 2 && ColIsBorder(rig)) rig--;

        if (top == 0 && bot == h - 1 && lef == 0 && rig == w - 1) return; // no border

        // Replace top/bottom border rows with the first interior row's pixels.
        for (int y = 0; y < top; y++)
            for (int x = 0; x < w; x++) pixels[y * w + x] = pixels[top * w + x];
        for (int y = bot + 1; y < h; y++)
            for (int x = 0; x < w; x++) pixels[y * w + x] = pixels[bot * w + x];
        // Replace left/right columns from each row's first interior pixel
        // (after the top/bottom rows have been refreshed, so corners inherit
        // the cleaned-up content).
        for (int y = 0; y < h; y++)
        {
            var fillL = pixels[y * w + lef];
            for (int x = 0; x < lef; x++) pixels[y * w + x] = fillL;
            var fillR = pixels[y * w + rig];
            for (int x = rig + 1; x < w; x++) pixels[y * w + x] = fillR;
        }
        tex.SetData(pixels);
    }

    private Texture2D MakeSolid(Color c)
    {
        var tex = new Texture2D(_gd, Px, Px);
        var p = new Color[Px * Px];
        Array.Fill(p, c);
        tex.SetData(p);
        _owned.Add(tex);
        return tex;
    }

    private Texture2D MakeSurfacePlaceholder(TacticalSurface s)
    {
        // Solid fill — no border. (Earlier versions drew a 1-px darker edge
        // as a debug aid, but it baked visible grid lines into adjacent
        // placeholder tiles in-game.)
        var c = SurfaceColor(s);
        var tex = new Texture2D(_gd, Px, Px);
        var pixels = new Color[Px * Px];
        Array.Fill(pixels, c);
        tex.SetData(pixels);
        _owned.Add(tex);
        return tex;
    }

    private Texture2D MakeDecoPlaceholder(TacticalDeco d)
    {
        var (color, fillFraction) = DecoStyle(d);
        var tex = new Texture2D(_gd, Px, Px);
        var pixels = new Color[Px * Px];
        float cx = (Px - 1) * 0.5f;
        float r  = Px * 0.5f * fillFraction;
        for (int y = 0; y < Px; y++)
        for (int x = 0; x < Px; x++)
        {
            float dx = x - cx, dy = y - cx;
            pixels[y * Px + x] = (dx * dx + dy * dy) <= r * r ? color : Color.Transparent;
        }
        tex.SetData(pixels);
        _owned.Add(tex);
        return tex;
    }

    private static Color SurfaceColor(TacticalSurface s) => s switch
    {
        TacticalSurface.DeepWater    => new Color(20,  60, 130),
        TacticalSurface.ShallowWater => new Color(60, 120, 180),
        TacticalSurface.Marsh        => new Color(70, 100,  80),
        TacticalSurface.Mud          => new Color(100, 80,  60),
        TacticalSurface.Sand         => new Color(220, 200, 150),
        TacticalSurface.Snow         => new Color(230, 235, 240),
        TacticalSurface.Rock         => new Color(120, 115, 110),
        TacticalSurface.Cobble       => new Color(170, 150, 120),
        TacticalSurface.Gravel       => new Color(150, 140, 110),
        TacticalSurface.Wall         => new Color(60,  55,  50),
        TacticalSurface.Floor        => new Color(180, 160, 130),
        TacticalSurface.Dirt         => new Color(120, 95,  60),
        TacticalSurface.TroddenDirt  => new Color(140, 110, 70),  // worn / lighter than wild dirt
        TacticalSurface.TallGrass    => new Color(80, 140,  60),
        TacticalSurface.Grass        => new Color(110, 160, 70),
        _                             => new Color(255, 0, 255),
    };

    private static (Color color, float fillFraction) DecoStyle(TacticalDeco d) => d switch
    {
        TacticalDeco.Tree     => (new Color(20,  80, 30),  0.85f),
        TacticalDeco.Bush     => (new Color(70, 110, 50),  0.55f),
        TacticalDeco.Boulder  => (new Color(110,100, 90),  0.65f),
        TacticalDeco.Rock     => (new Color(140,130,110),  0.35f),
        TacticalDeco.Flower   => (new Color(220,180,210),  0.25f),
        TacticalDeco.Crop     => (new Color(180,160, 60),  0.40f),
        TacticalDeco.Reed     => (new Color(120,140, 60),  0.40f),
        TacticalDeco.Snag     => (new Color(80,  60, 40),  0.45f),
        _                      => (Color.Magenta, 0.5f),
    };

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        foreach (var t in _owned) t.Dispose();
        _owned.Clear();
        _surfaces.Clear();
        _decos.Clear();
    }
}
