using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

namespace Theriapolis.Tools.Commands;

/// <summary>
/// tile-analyze --dir &lt;path&gt; [--sheet &lt;out.png&gt;]
/// tile-analyze --files &lt;f1.png&gt; &lt;f2.png&gt; ... [--sheet &lt;out.png&gt;]
///
/// Reports per-tile diagnostics for a folder of 32×32 PNG tiles
/// (typically a Pixellab tiles_pro download). Used to vet tile quality
/// before saving picks into <c>Content/Gfx/tactical/</c>.
///
/// What it checks:
///
/// • <b>Border edges (0–4)</b>: how many of the four perimeter rows/cols are
///   ≥80% "dark uniform" pixels (max channel ≤ 95 AND max - min ≤ 25). Catches
///   the hard near-black or dark-grey frames that the regular create_tiles_pro
///   path bakes around every tile. Should be 0 for a clean surface tile.
///
/// • <b>Opaque %</b>: fraction of pixels with α ≥ 128. Catches the failure
///   mode where a "marsh" or other prompt produces transparent decoration
///   sprites instead of edge-to-edge surface tiles. Surface tiles want ~100%;
///   decoration sprites are typically 30–70%.
///
/// • <b>Shadow scores (top/bot/lef/rig)</b>: brightness drop on each edge
///   versus the interior average. Positive = edge is darker than interior.
///   The pseudo-3D shading that <c>tile_view: "low top-down"</c> bakes in
///   shows up here as a +50 to +90 drop on the bottom and right edges that
///   the border detector misses (the colors aren't black-uniform, just
///   darker). Anything &gt; 30 is a red flag for tiling: adjacent tiles will
///   show a visible diagonal grid of dark seams.
///
/// Optionally writes a labeled 4×-upscaled contact sheet so you can present
/// the batch to a user for picks. Labels turn ORANGE for any tile that fails
/// any check (borders &gt; 0, opaque &lt; 95%, or shadow &gt; threshold).
///
/// See <c>theriapolis-tile-generation-handoff.md</c> for the full Pixellab
/// MCP workflow this tool slots into.
/// </summary>
public static class TileAnalyze
{
    public static int Run(string[] args)
    {
        string? dir = null;
        string? sheetOut = null;
        var explicitFiles = new List<string>();
        int shadowThreshold = 30;
        int upscale = 4;

        for (int i = 0; i < args.Length; i++)
        {
            switch (args[i].ToLowerInvariant())
            {
                case "--dir":
                    if (i + 1 < args.Length) dir = args[++i];
                    break;
                case "--sheet":
                    if (i + 1 < args.Length) sheetOut = args[++i];
                    break;
                case "--shadow-threshold":
                    if (i + 1 < args.Length) shadowThreshold = int.Parse(args[++i]);
                    break;
                case "--upscale":
                    if (i + 1 < args.Length) upscale = int.Parse(args[++i]);
                    break;
                case "--files":
                    while (i + 1 < args.Length && !args[i + 1].StartsWith("--"))
                        explicitFiles.Add(args[++i]);
                    break;
            }
        }

        List<string> files;
        if (dir is not null)
        {
            if (!Directory.Exists(dir))
            {
                Console.Error.WriteLine($"Directory not found: {dir}");
                return 1;
            }
            // Sort numerically when the filename is tile_N.png — common Pixellab layout.
            files = Directory.EnumerateFiles(dir, "*.png")
                .OrderBy(NumericOrderKey)
                .ToList();
        }
        else if (explicitFiles.Count > 0)
        {
            files = explicitFiles;
        }
        else
        {
            PrintHelp();
            return 1;
        }
        if (files.Count == 0) { Console.Error.WriteLine("No PNGs found."); return 1; }

        var stats = new List<TileStat>(files.Count);
        Console.WriteLine($"Analyzing {files.Count} tiles (shadow threshold {shadowThreshold})\n");
        Console.WriteLine($"{"file",-32} | brd | op%  | int | top  bot  lef  rig | verdict");
        Console.WriteLine(new string('-', 90));
        foreach (var path in files)
        {
            using var img = Image.Load<Rgba32>(path);
            var st = AnalyzeTile(path, img);
            stats.Add(st);
            int maxShadow = Math.Max(Math.Max(Math.Abs(st.TopDiff), Math.Abs(st.BotDiff)),
                                     Math.Max(Math.Abs(st.LefDiff), Math.Abs(st.RigDiff)));
            string verdict = (st.BorderEdges == 0 && st.OpaquePct >= 95 && maxShadow <= shadowThreshold)
                ? "ok"
                : "⚠";
            Console.WriteLine(
                $"{Path.GetFileNameWithoutExtension(path),-32} |  {st.BorderEdges}  | {st.OpaquePct,3}% | {st.InteriorBrightness,3} |" +
                $" {st.TopDiff,4} {st.BotDiff,4} {st.LefDiff,4} {st.RigDiff,4} | {verdict}");
        }

        if (sheetOut is not null)
        {
            BuildContactSheet(files, stats, sheetOut, upscale, shadowThreshold);
            Console.WriteLine($"\nContact sheet: {sheetOut}");
        }
        return 0;
    }

    // Numeric key so "tile_2" sorts before "tile_10".
    private static (int, string) NumericOrderKey(string path)
    {
        var name = Path.GetFileNameWithoutExtension(path);
        int u = name.LastIndexOf('_');
        if (u >= 0 && int.TryParse(name[(u + 1)..], out var n)) return (n, name);
        return (int.MaxValue, name);
    }

    // ── Detector logic ────────────────────────────────────────────────────

    public readonly record struct TileStat(
        int BorderEdges,
        int OpaquePct,
        int InteriorBrightness,
        int TopDiff,
        int BotDiff,
        int LefDiff,
        int RigDiff);

    public static TileStat AnalyzeTile(string path, Image<Rgba32> img)
    {
        int border = CountBorderEdges(img);
        int opaque = CountOpaqueFraction(img);
        var (interior, top, bot, lef, rig) = ShadowScores(img);
        return new TileStat(border, opaque, interior, top, bot, lef, rig);
    }

    /// <summary>
    /// "Border" pixel: alpha ≥ 128 AND max channel ≤ 95 AND (max - min) ≤ 25.
    /// Catches the dark-uniform frames Pixellab bakes around tiles.
    /// </summary>
    private static bool IsBorderPixel(Rgba32 p)
    {
        if (p.A < 128) return false;
        int max = Math.Max(p.R, Math.Max(p.G, p.B));
        int min = Math.Min(p.R, Math.Min(p.G, p.B));
        return max <= 95 && (max - min) <= 25;
    }

    /// <summary>Number of edges (0–4) where ≥80% of perimeter pixels are border-pixels.</summary>
    public static int CountBorderEdges(Image<Rgba32> img)
    {
        int w = img.Width, h = img.Height;
        int top = 0, bot = 0, lef = 0, rig = 0;
        for (int x = 0; x < w; x++)
        {
            if (IsBorderPixel(img[x, 0]))     top++;
            if (IsBorderPixel(img[x, h - 1])) bot++;
        }
        for (int y = 0; y < h; y++)
        {
            if (IsBorderPixel(img[0, y]))     lef++;
            if (IsBorderPixel(img[w - 1, y])) rig++;
        }
        int edges = 0;
        if (top >= w * 0.8) edges++;
        if (bot >= w * 0.8) edges++;
        if (lef >= h * 0.8) edges++;
        if (rig >= h * 0.8) edges++;
        return edges;
    }

    /// <summary>Percent (0–100) of pixels with alpha ≥ 128.</summary>
    public static int CountOpaqueFraction(Image<Rgba32> img)
    {
        int total = img.Width * img.Height, opaque = 0;
        for (int y = 0; y < img.Height; y++)
            for (int x = 0; x < img.Width; x++)
                if (img[x, y].A >= 128) opaque++;
        return (int)(opaque * 100.0 / total);
    }

    /// <summary>
    /// Edge-vs-interior brightness drop for each side. Positive value = edge
    /// is darker than interior (= bad: baked-in shadow gradient).
    /// Interior excludes a 4-pixel margin so it's the "true" middle.
    /// </summary>
    public static (int interior, int topDiff, int botDiff, int lefDiff, int rigDiff) ShadowScores(Image<Rgba32> img)
    {
        int w = img.Width, h = img.Height;
        long ir = 0, ig = 0, ib = 0, ic = 0;
        for (int y = 4; y < h - 4; y++)
        for (int x = 4; x < w - 4; x++)
        {
            var p = img[x, y]; if (p.A < 128) continue;
            ir += p.R; ig += p.G; ib += p.B; ic++;
        }
        if (ic == 0) return (0, 0, 0, 0, 0);
        int avgR = (int)(ir / ic), avgG = (int)(ig / ic), avgB = (int)(ib / ic);
        int interior = (avgR + avgG + avgB) / 3;

        int EdgeBrightness(Func<int, (int x, int y)> sel, int len)
        {
            long er = 0, eg = 0, eb = 0;
            for (int i = 0; i < len; i++)
            {
                var (x, y) = sel(i);
                var p = img[x, y];
                er += p.R; eg += p.G; eb += p.B;
            }
            return ((int)(er / len) + (int)(eg / len) + (int)(eb / len)) / 3;
        }
        int top = EdgeBrightness(i => (i, 0),     w);
        int bot = EdgeBrightness(i => (i, h - 1), w);
        int lef = EdgeBrightness(i => (0, i),     h);
        int rig = EdgeBrightness(i => (w - 1, i), h);
        return (interior, interior - top, interior - bot, interior - lef, interior - rig);
    }

    // ── Contact sheet ─────────────────────────────────────────────────────

    private static void BuildContactSheet(
        List<string> files, List<TileStat> stats, string outPath,
        int upscale, int shadowThreshold)
    {
        int n = files.Count;
        int cols = (int)Math.Ceiling(Math.Sqrt(n));
        int rows = (int)Math.Ceiling((double)n / cols);
        int cell = 32, gap = 6, label = 30;
        int side = cell * upscale;
        int sheetW = side * cols + (cols + 1) * gap;
        int sheetH = (side + label) * rows + (rows + 1) * gap;
        var family = SystemFonts.Get("Consolas");
        var font = family.CreateFont(10f, FontStyle.Regular);

        using var sheet = new Image<Rgba32>(sheetW, sheetH, new Rgba32(40, 40, 40));
        for (int i = 0; i < n; i++)
        {
            using var t = Image.Load<Rgba32>(files[i]);
            t.Mutate(x => x.Resize(side, side, KnownResamplers.NearestNeighbor));
            int cx = (i % cols) * (side + gap) + gap;
            int cy = (i / cols) * (side + label + gap) + gap;
            sheet.Mutate(x => x.DrawImage(t, new Point(cx, cy), 1f));

            var st = stats[i];
            int maxShadow = Math.Max(Math.Max(Math.Abs(st.TopDiff), Math.Abs(st.BotDiff)),
                                     Math.Max(Math.Abs(st.LefDiff), Math.Abs(st.RigDiff)));
            bool ok = st.BorderEdges == 0 && st.OpaquePct >= 95 && maxShadow <= shadowThreshold;
            var col = ok ? Color.LightGreen : Color.Orange;
            string idx = Path.GetFileNameWithoutExtension(files[i]);
            sheet.Mutate(x => x.DrawText(
                $"{idx} b:{st.BorderEdges} op:{st.OpaquePct}%",
                font, col, new PointF(cx + 4, cy + side + 2)));
            sheet.Mutate(x => x.DrawText(
                $"sh t/b/l/r: {st.TopDiff,3} {st.BotDiff,3} {st.LefDiff,3} {st.RigDiff,3}",
                font, col, new PointF(cx + 4, cy + side + 14)));
        }
        sheet.Save(outPath);
    }

    private static void PrintHelp()
    {
        Console.WriteLine("Usage:");
        Console.WriteLine("  tile-analyze --dir <path>    [--sheet <out.png>] [--shadow-threshold N] [--upscale N]");
        Console.WriteLine("  tile-analyze --files <a.png> <b.png> ... [--sheet <out.png>]");
        Console.WriteLine();
        Console.WriteLine("Reports border edges, opaque %, and shadow gradients per tile,");
        Console.WriteLine("optionally writing a labeled contact sheet for visual review.");
    }
}
