using Theriapolis.Core;
using Theriapolis.Core.Util;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
using Theriapolis.Core.World.Polylines;

namespace Theriapolis.Tools.Commands;

/// <summary>
/// tile-inspect --seed &lt;n&gt; --tile X,Y [--radius N] [--data-dir &lt;dir&gt;]
///
/// Runs the full pipeline headless and prints every road / rail / river polyline
/// and every bridge whose geometry passes within <c>radius</c> tiles of (X, Y),
/// along with the raw point sequence. Intended for diagnosing drawing bugs
/// reported from the in-game debug overlay.
/// </summary>
public static class TileInspect
{
    public static int Run(string[] args)
    {
        ulong seed    = 12345;
        string dataDir = ResolveDataDir();
        int tx = -1, ty = -1;
        int radius = 3;
        bool dumpAll = false;
        int stopAtStage = -1;

        for (int i = 0; i < args.Length; i++)
        {
            switch (args[i].ToLowerInvariant())
            {
                case "--dump-all":
                    dumpAll = true;
                    break;
                case "--stop-at-stage":
                    if (i + 1 < args.Length && int.TryParse(args[++i], out int s)) stopAtStage = s;
                    break;
                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 "--tile":
                    if (i + 1 < args.Length)
                    {
                        var parts = args[++i].Split(',');
                        if (parts.Length == 2 && int.TryParse(parts[0], out tx) && int.TryParse(parts[1], out ty)) { }
                    }
                    break;
                case "--radius":
                    if (i + 1 < args.Length && int.TryParse(args[++i], out int r)) radius = r;
                    break;
                case "--data-dir":
                    if (i + 1 < args.Length) dataDir = args[++i];
                    break;
            }
        }

        if (tx < 0 || ty < 0)
        {
            Console.Error.WriteLine("tile-inspect: --tile X,Y is required");
            return 1;
        }

        // Optional --crop-png outPath argument — writes a crop of the biome+feature map.
        string? cropPath = null;
        for (int i = 0; i < args.Length - 1; i++)
            if (args[i].Equals("--crop-png", StringComparison.OrdinalIgnoreCase))
                cropPath = args[i + 1];

        Console.WriteLine($"[tile-inspect] seed={seed} tile=({tx},{ty}) radius={radius}" + (stopAtStage >= 0 ? $" stopAtStage={stopAtStage}" : ""));
        var ctx = new WorldGenContext(seed, dataDir);
        if (stopAtStage >= 0)
            WorldGenerator.RunThrough(ctx, stopAtStage);
        else
            WorldGenerator.RunAll(ctx);
        var world = ctx.World;

        int px = C.WORLD_TILE_PIXELS;
        float rpx = radius * px;
        float rpxSq = rpx * rpx;
        Vec2 target = new(tx * px + px * 0.5f, ty * px + px * 0.5f);

        // ── Tile state ────────────────────────────────────────────────────────
        Console.WriteLine();
        Console.WriteLine("== Tile state ==");
        for (int dy = -1; dy <= 1; dy++)
        for (int dx = -1; dx <= 1; dx++)
        {
            int nx = tx + dx, ny = ty + dy;
            if ((uint)nx >= C.WORLD_WIDTH_TILES || (uint)ny >= C.WORLD_HEIGHT_TILES) continue;
            ref var t = ref world.TileAt(nx, ny);
            Console.WriteLine($"  ({nx,4},{ny,4}) biome={t.Biome,-20} flags={t.Features,-40} riverDir={DirName(t.RiverFlowDir)} railDir={DirName(t.RailDir)}");
        }

        // ── Polylines passing near the tile ───────────────────────────────────
        PrintPolylines("Rivers", world.Rivers, target, rpxSq, px, dumpAll);
        PrintPolylines("Roads",  world.Roads,  target, rpxSq, px, dumpAll);
        PrintPolylines("Rails",  world.Rails,  target, rpxSq, px, dumpAll);

        // ── River bounding boxes (to find rivers visible but not indexed near the tile) ──
        Console.WriteLine();
        Console.WriteLine($"== All {world.Rivers.Count} rivers (bounding box in tiles) ==");
        foreach (var r in world.Rivers)
        {
            if (r.Points.Count < 2) continue;
            float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
            foreach (var p in r.Points) { if (p.X<minX)minX=p.X; if (p.Y<minY)minY=p.Y; if (p.X>maxX)maxX=p.X; if (p.Y>maxY)maxY=p.Y; }
            Console.WriteLine($"  [id={r.Id}] class={r.RiverClassification} flow={r.FlowAccumulation} pts={r.Points.Count}  tiles x=[{(int)(minX/px),4}..{(int)(maxX/px),4}] y=[{(int)(minY/px),4}..{(int)(maxY/px),4}]  first=({r.Points[0].X:F0},{r.Points[0].Y:F0}) last=({r.Points[^1].X:F0},{r.Points[^1].Y:F0})");
        }

        // ── Tiles flagged HasRiver near target, with source polyline ──────────
        Console.WriteLine();
        Console.WriteLine($"== HasRiver tiles within {radius} of ({tx},{ty}) ==");
        int hits = 0;
        for (int yy = Math.Max(0, ty - radius); yy <= Math.Min(C.WORLD_HEIGHT_TILES - 1, ty + radius); yy++)
        for (int xx = Math.Max(0, tx - radius); xx <= Math.Min(C.WORLD_WIDTH_TILES  - 1, tx + radius); xx++)
        {
            if ((world.Tiles[xx, yy].Features & FeatureFlags.HasRiver) != 0)
            {
                Console.WriteLine($"  ({xx,4},{yy,4})");
                hits++;
            }
        }
        if (hits == 0) Console.WriteLine("  (none)");

        // ── Settlements within radius ────────────────────────────────────────
        Console.WriteLine();
        Console.WriteLine($"== Settlements within {radius} tiles ==");
        foreach (var s in world.Settlements)
        {
            int dt = Math.Max(Math.Abs(s.TileX - tx), Math.Abs(s.TileY - ty));
            if (dt <= radius)
                Console.WriteLine($"  id={s.Id,4} tier={s.Tier} poi={s.IsPoi} tile=({s.TileX,4},{s.TileY,4}) dt={dt}");
        }

        // ── Bridges near the tile ─────────────────────────────────────────────
        Console.WriteLine();
        Console.WriteLine($"== Bridges (all {world.Bridges.Count}) ==");
        foreach (var b in world.Bridges)
        {
            int btx = (int)(b.WorldPixelX / px);
            int bty = (int)(b.WorldPixelY / px);
            int dt = Math.Max(Math.Abs(btx - tx), Math.Abs(bty - ty));
            if (dt <= radius + 2)
                Console.WriteLine($"  roadId={b.RoadId,4} tile=({btx,4},{bty,4})  deck=({b.Start.X:F1},{b.Start.Y:F1})-({b.End.X:F1},{b.End.Y:F1})");
        }

        return 0;
    }

    private static void PrintPolylines(string label, List<Polyline> polys, Vec2 target, float rpxSq, int px, bool dumpAll)
    {
        Console.WriteLine();
        Console.WriteLine($"== {label} near tile ==");
        for (int pi = 0; pi < polys.Count; pi++)
        {
            var p = polys[pi];
            if (p.Points.Count < 2) continue;

            // Find closest segment
            float bestSq = float.MaxValue;
            int bestIdx = -1;
            for (int i = 0; i < p.Points.Count - 1; i++)
            {
                float dSq = NearestPointOnSegment(target, p.Points[i], p.Points[i + 1], out _);
                if (dSq < bestSq) { bestSq = dSq; bestIdx = i; }
            }
            if (bestSq > rpxSq) continue;

            string endpointInfo = p.Type == PolylineType.Road ? $" from={p.FromSettlementId} to={p.ToSettlementId}" : "";
            Console.WriteLine($"  [id={p.Id}] type={p.Type} class={(p.Type == PolylineType.Road ? p.RoadClassification.ToString() : p.RiverClassification.ToString())} pts={p.Points.Count}{endpointInfo}  closestSeg=#{bestIdx} dist={MathF.Sqrt(bestSq) / px:F2}tiles");
            int ctxStart = dumpAll ? 0 : Math.Max(0, bestIdx - 2);
            int ctxEnd   = dumpAll ? p.Points.Count - 1 : Math.Min(p.Points.Count - 1, bestIdx + 3);
            for (int i = ctxStart; i <= ctxEnd; i++)
            {
                int t0x = (int)(p.Points[i].X / px);
                int t0y = (int)(p.Points[i].Y / px);
                string marker = i == bestIdx ? " <-" : "";
                Console.WriteLine($"      pt[{i,3}] world=({p.Points[i].X,7:F1},{p.Points[i].Y,7:F1}) tile=({t0x,4},{t0y,4}){marker}");
            }
        }
    }

    private static float NearestPointOnSegment(Vec2 p, Vec2 a, Vec2 b, out Vec2 nearest)
    {
        Vec2 ab = b - a;
        float lenSq = ab.LengthSquared;
        if (lenSq < 1e-8f) { nearest = a; return Vec2.DistSq(p, a); }
        float t = Math.Clamp(Vec2.Dot(p - a, ab) / lenSq, 0f, 1f);
        nearest = a + ab * t;
        return Vec2.DistSq(p, nearest);
    }

    private static string DirName(byte d) => d switch
    {
        Dir.None => "—",
        Dir.N    => "N",
        Dir.NE   => "NE",
        Dir.E    => "E",
        Dir.SE   => "SE",
        Dir.S    => "S",
        Dir.SW   => "SW",
        Dir.W    => "W",
        Dir.NW   => "NW",
        _        => $"?{d}",
    };

    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;
    }
}
