using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;

namespace Theriapolis.Tools.Commands;

/// <summary>
/// Phase 6 M0 — settlement-render exports a stamped settlement to PNG.
/// Lets us visually QA building layouts before they're playtested in-game.
///
/// Usage:
///   dotnet run --project Theriapolis.Tools -- settlement-render \
///     --seed 12345 --settlement millhaven --out millhaven.png
///
///   --settlement: anchor name ("millhaven", "thornfield", ...) or numeric
///                 settlement id. If omitted, render the first Tier-1 anchor.
///   --pad N:      include N extra chunks around the settlement window.
///   --data-dir:   Content/Data root. Defaults to ./Content/Data.
/// </summary>
public static class SettlementRender
{
    public static int Run(string[] args)
    {
        ulong  seed       = 12345UL;
        string settlement = "";
        int    pad        = 1;
        string outPath    = "settlement.png";
        string dataDir    = ResolveDataDir();

        for (int i = 0; i < args.Length; i++)
        {
            switch (args[i].ToLowerInvariant())
            {
                case "--seed":
                    if (i + 1 < args.Length)
                    {
                        string raw = args[++i];
                        seed = raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
                            ? Convert.ToUInt64(raw[2..], 16)
                            : ulong.Parse(raw);
                    }
                    break;
                case "--settlement":
                    if (i + 1 < args.Length) settlement = args[++i];
                    break;
                case "--pad":
                    if (i + 1 < args.Length) pad = int.Parse(args[++i]);
                    break;
                case "--out":
                    if (i + 1 < args.Length) outPath = args[++i];
                    break;
                case "--data-dir":
                    if (i + 1 < args.Length) dataDir = args[++i];
                    break;
            }
        }

        if (!Directory.Exists(dataDir))
        {
            Console.Error.WriteLine($"Data directory not found: {dataDir}");
            return 1;
        }

        Console.WriteLine($"[settlement-render] seed=0x{seed:X} settlement='{settlement}' pad={pad}");
        var ctx = new WorldGenContext(seed, dataDir) { Log = msg => Console.WriteLine(msg) };
        WorldGenerator.RunAll(ctx);

        var content = new ContentResolver(new ContentLoader(dataDir));

        var s = ResolveSettlement(ctx.World, settlement);
        if (s is null)
        {
            Console.Error.WriteLine(string.IsNullOrEmpty(settlement)
                ? "No Tier-1 anchor settlement found in this world."
                : $"Settlement '{settlement}' not found.");
            return 1;
        }

        Console.WriteLine($"[settlement-render] resolved -> id={s.Id} name='{s.Name}' tier={s.Tier} tile=({s.TileX},{s.TileY}) anchor={s.Anchor}");

        // Compute chunk window covering the settlement plus padding.
        int cxPx = (int)s.WorldPixelX;
        int cyPx = (int)s.WorldPixelY;
        int radiusPx = s.Tier switch { 1 => 32, 2 => 26, 3 => 20, 4 => 16, _ => 12 };
        int minTx = cxPx - radiusPx;
        int minTy = cyPx - radiusPx;
        int maxTx = cxPx + radiusPx;
        int maxTy = cyPx + radiusPx;

        int minCx = (int)Math.Floor(minTx / (double)C.TACTICAL_CHUNK_SIZE) - pad;
        int minCy = (int)Math.Floor(minTy / (double)C.TACTICAL_CHUNK_SIZE) - pad;
        int maxCx = (int)Math.Floor(maxTx / (double)C.TACTICAL_CHUNK_SIZE) + pad;
        int maxCy = (int)Math.Floor(maxTy / (double)C.TACTICAL_CHUNK_SIZE) + pad;

        int gridW = maxCx - minCx + 1;
        int gridH = maxCy - minCy + 1;
        int sideX = gridW * C.TACTICAL_CHUNK_SIZE;
        int sideY = gridH * C.TACTICAL_CHUNK_SIZE;

        Console.WriteLine($"[settlement-render] chunk window {minCx}..{maxCx} x {minCy}..{maxCy} ({sideX}x{sideY} px)");

        using var img = new Image<Rgba32>(sideX, sideY);
        for (int gy = 0; gy < gridH; gy++)
        for (int gx = 0; gx < gridW; gx++)
        {
            var cc = new ChunkCoord(minCx + gx, minCy + gy);
            var chunk = TacticalChunkGen.Generate(seed, cc, ctx.World, content.Settlements);
            int ox = gx * C.TACTICAL_CHUNK_SIZE;
            int oy = gy * C.TACTICAL_CHUNK_SIZE;
            BlitChunk(img, chunk, ox, oy);
        }

        // Building summary line.
        SettlementStamper_EnsureBuildingsResolved(ctx.World, s, content);
        Console.WriteLine($"[settlement-render] {s.Buildings.Count} buildings stamped");
        foreach (var b in s.Buildings)
            Console.WriteLine($"    [{b.Id:00}] {b.TemplateId,-16} ({b.MinX,4},{b.MinY,4})..({b.MaxX,4},{b.MaxY,4})  doors={b.Doors.Length}  residents={b.Residents.Length}");

        img.SaveAsPng(outPath);
        Console.WriteLine($"[settlement-render] wrote {outPath}");
        return 0;
    }

    private static Settlement? ResolveSettlement(WorldState world, string raw)
    {
        if (string.IsNullOrEmpty(raw))
            return world.Settlements.FirstOrDefault(s => s.Tier == 1 && !s.IsPoi)
                ?? world.Settlements.FirstOrDefault(s => s.Tier <= 2 && !s.IsPoi);

        if (int.TryParse(raw, out int id))
            return world.Settlements.FirstOrDefault(s => s.Id == id);

        // Match anchor name (case-insensitive) first, then settlement.Name.
        return world.Settlements.FirstOrDefault(
                   s => s.Anchor is { } a && string.Equals(a.ToString(), raw, StringComparison.OrdinalIgnoreCase))
            ?? world.Settlements.FirstOrDefault(
                   s => string.Equals(s.Name, raw, StringComparison.OrdinalIgnoreCase));
    }

    /// <summary>
    /// Forces the settlement's <see cref="Settlement.Buildings"/> list to
    /// resolve so the dump line at the end of <see cref="Run"/> can describe
    /// what got stamped — useful even before we render the chunks (e.g. if a
    /// chunk-window calculation goes wrong, the buildings list still tells
    /// the user what *would* have stamped).
    /// </summary>
    private static void SettlementStamper_EnsureBuildingsResolved(WorldState world, Settlement s, ContentResolver content)
    {
        if (s.BuildingsResolved) return;
        Theriapolis.Core.World.Settlements.SettlementStamper.EnsureBuildingsResolved(world.WorldSeed, s, content.Settlements);
    }

    private static void BlitChunk(Image<Rgba32> img, TacticalChunk chunk, int ox, int oy)
    {
        for (int ly = 0; ly < C.TACTICAL_CHUNK_SIZE; ly++)
        for (int lx = 0; lx < C.TACTICAL_CHUNK_SIZE; lx++)
        {
            ref var t = ref chunk.Tiles[lx, ly];
            img[ox + lx, oy + ly] = ColorFor(t);
        }
    }

    private static Rgba32 ColorFor(TacticalTile t)
    {
        // Decoration overrides for visual identification.
        if (t.Deco == TacticalDeco.Door)    return new Rgba32(255, 200, 80);    // bright yellow doors
        if (t.Deco == TacticalDeco.Counter) return new Rgba32(180, 130, 80);
        if (t.Deco == TacticalDeco.Bed)     return new Rgba32(160, 100, 140);
        if (t.Deco == TacticalDeco.Hearth)  return new Rgba32(220, 90,  40);
        if (t.Deco == TacticalDeco.Sign)    return new Rgba32(220, 220, 100);
        if (t.Deco == TacticalDeco.Tree)    return new Rgba32(20,  80,  30);
        if (t.Deco == TacticalDeco.Bush)    return new Rgba32(70,  110, 50);
        if (t.Deco == TacticalDeco.Boulder) return new Rgba32(110, 100, 90);
        if (t.Deco == TacticalDeco.Rock)    return new Rgba32(140, 130, 110);
        if (t.Deco == TacticalDeco.Flower)  return new Rgba32(220, 180, 210);

        return t.Surface switch
        {
            TacticalSurface.DeepWater    => new Rgba32(20,  60, 130),
            TacticalSurface.ShallowWater => new Rgba32(60,  120, 180),
            TacticalSurface.Marsh        => new Rgba32(70,  100, 80),
            TacticalSurface.Mud          => new Rgba32(100, 80,  60),
            TacticalSurface.Sand         => new Rgba32(220, 200, 150),
            TacticalSurface.Snow         => new Rgba32(230, 235, 240),
            TacticalSurface.Rock         => new Rgba32(120, 115, 110),
            TacticalSurface.Cobble       => new Rgba32(170, 150, 120),
            TacticalSurface.Gravel       => new Rgba32(150, 140, 110),
            TacticalSurface.TroddenDirt  => new Rgba32(140, 110, 80),
            TacticalSurface.Wall         => new Rgba32(60,  55,  50),
            TacticalSurface.Floor        => new Rgba32(220, 200, 165),
            TacticalSurface.Dirt         => new Rgba32(120, 95,  60),
            TacticalSurface.TallGrass    => new Rgba32(80,  140, 60),
            TacticalSurface.Grass        => new Rgba32(110, 160, 70),
            _                             => new Rgba32(255, 0,   255),
        };
    }

    private static string ResolveDataDir()
    {
        string local = Path.Combine(AppContext.BaseDirectory, "Data");
        if (Directory.Exists(local)) return local;
        string? dir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
        for (int i = 0; i < 6; i++)
        {
            if (dir is null) break;
            string candidate = Path.Combine(dir, "Content", "Data");
            if (Directory.Exists(candidate)) return candidate;
            dir = Path.GetDirectoryName(dir);
        }
        return local;
    }
}
