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

namespace Theriapolis.Core.Tactical;

/// <summary>
/// Deterministic per-chunk generator. Pure function of (worldSeed, ChunkCoord, WorldState).
///
/// Pipeline:
///   1. Ground layer — biome → surface variant per tactical tile
///   2. Polyline burn-in — rivers and roads stamp water/road tiles
///   3. Settlement burn-in — settlement footprints stamp cobble + walls
///   4. Scatter — biome decorations seeded from a per-chunk sub-seed
///   5. Spawn list — encounter density + sub-seed
///
/// Every step takes its randomness from a sub-seed derived from the chunk
/// coord, so adjacent chunks see independent (but globally deterministic)
/// scatters.
/// </summary>
public static class TacticalChunkGen
{
    public static TacticalChunk Generate(ulong worldSeed, ChunkCoord cc, WorldState world)
        => Generate(worldSeed, cc, world, settlementContent: null);

    /// <summary>
    /// Phase 6 M0 — overload accepting building/layout content. When
    /// <paramref name="settlementContent"/> is non-null, Pass 3 stamps
    /// templated buildings; when null, falls back to the Phase-4
    /// cobble-plaza + outer-wall-ring placeholder so headless tools and
    /// older tests keep working unchanged.
    /// </summary>
    public static TacticalChunk Generate(
        ulong worldSeed,
        ChunkCoord cc,
        WorldState world,
        Theriapolis.Core.Data.SettlementContent? settlementContent)
    {
        var chunk = new TacticalChunk(cc);
        ulong chunkHash = Hash(cc);

        Pass1_Ground(worldSeed, chunk, world, chunkHash);
        Pass2_Polylines(chunk, world);
        Theriapolis.Core.World.Settlements.SettlementStamper.Stamp(worldSeed, chunk, world, settlementContent);
        Pass4_Scatter(worldSeed, chunk, world, chunkHash);
        Pass5_Spawns(worldSeed, chunk, world, chunkHash);

        return chunk;
    }

    // ── Pass 1 — ground layer ─────────────────────────────────────────────

    private static void Pass1_Ground(ulong seed, TacticalChunk chunk, WorldState world, ulong chunkHash)
    {
        var rng = SeededRng.ForSubsystem(seed, C.RNG_TACTICAL_GROUND ^ chunkHash);
        int origX = chunk.OriginX;
        int origY = chunk.OriginY;

        for (int ly = 0; ly < C.TACTICAL_CHUNK_SIZE; ly++)
        for (int lx = 0; lx < C.TACTICAL_CHUNK_SIZE; lx++)
        {
            int tx = origX + lx;             // tactical-tile coord (= world pixel)
            int ty = origY + ly;
            int wx = tx / C.TACTICAL_PER_WORLD_TILE;
            int wy = ty / C.TACTICAL_PER_WORLD_TILE;
            wx = Math.Clamp(wx, 0, C.WORLD_WIDTH_TILES  - 1);
            wy = Math.Clamp(wy, 0, C.WORLD_HEIGHT_TILES - 1);
            ref var src = ref world.TileAt(wx, wy);

            ref var dst = ref chunk.Tiles[lx, ly];
            (dst.Surface, dst.Variant) = PickGround(src, rng);
        }
    }

    private static (TacticalSurface, byte) PickGround(in WorldTile w, SeededRng rng)
    {
        byte v = (byte)rng.NextInt(0, 4);
        return w.Biome switch
        {
            BiomeId.Ocean              => (TacticalSurface.DeepWater,    0),
            BiomeId.Wetland            => (TacticalSurface.Marsh,        v),
            BiomeId.MarshEdge          => (rng.NextBool(0.4) ? TacticalSurface.Mud : TacticalSurface.Grass, v),
            BiomeId.Mangrove           => (rng.NextBool(0.5) ? TacticalSurface.ShallowWater : TacticalSurface.Mud, v),
            BiomeId.TidalFlat          => (TacticalSurface.Mud,          v),
            BiomeId.Beach              => (TacticalSurface.Sand,         v),
            BiomeId.Coastal            => (rng.NextBool(0.3) ? TacticalSurface.Sand : TacticalSurface.Grass, v),
            BiomeId.Tundra             => (rng.NextBool(0.6) ? TacticalSurface.Snow : TacticalSurface.Dirt, v),
            BiomeId.MountainAlpine     => (rng.NextBool(0.5) ? TacticalSurface.Rock : TacticalSurface.Snow, v),
            BiomeId.MountainForested   => (rng.NextBool(0.4) ? TacticalSurface.Rock : TacticalSurface.Dirt, v),
            BiomeId.Cliff              => (TacticalSurface.Rock,         v),
            BiomeId.Foothills          => (rng.NextBool(0.3) ? TacticalSurface.Rock : TacticalSurface.Grass, v),
            BiomeId.DesertCold         => (rng.NextBool(0.4) ? TacticalSurface.Sand : TacticalSurface.Dirt, v),
            BiomeId.Scrubland          => (rng.NextBool(0.5) ? TacticalSurface.Dirt : TacticalSurface.Grass, v),
            BiomeId.Boreal             => (rng.NextBool(0.4) ? TacticalSurface.Dirt : TacticalSurface.Grass, v),
            BiomeId.SubtropicalForest  => (rng.NextBool(0.3) ? TacticalSurface.TallGrass : TacticalSurface.Grass, v),
            BiomeId.TemperateDeciduous => (rng.NextBool(0.2) ? TacticalSurface.Dirt : TacticalSurface.Grass, v),
            BiomeId.TemperateGrassland => (rng.NextBool(0.4) ? TacticalSurface.TallGrass : TacticalSurface.Grass, v),
            BiomeId.RiverValley        => (TacticalSurface.Grass,        v),
            BiomeId.ForestEdge         => (TacticalSurface.Grass,        v),
            _                           => (TacticalSurface.Grass,        v),
        };
    }

    // ── Pass 2 — polyline burn-in ─────────────────────────────────────────

    private static void Pass2_Polylines(TacticalChunk chunk, WorldState world)
    {
        // World-pixel AABB for the chunk (since 1 tactical tile == 1 world pixel,
        // local (0,0) = world pixel (origX, origY)).
        int x0 = chunk.OriginX;
        int y0 = chunk.OriginY;
        int x1 = x0 + C.TACTICAL_CHUNK_SIZE;
        int y1 = y0 + C.TACTICAL_CHUNK_SIZE;

        // Roads first, then rivers — rivers stamp deeper water and override
        // road surface where they cross (bridges are stamped later from
        // World.Bridges).
        foreach (var road in world.Roads)
        {
            var (surf, hw) = RoadStyle(road);
            BurnPolyline(chunk, road, x0, y0, x1, y1, hw, surf, TacticalFlags.Road);
        }

        foreach (var river in world.Rivers)
            BurnPolyline(chunk, river, x0, y0, x1, y1, RiverHalfWidth(river), TacticalSurface.ShallowWater, TacticalFlags.River);

        // Bridges: short segments that re-stamp Cobble+Bridge across the river.
        foreach (var b in world.Bridges)
        {
            BurnSegment(chunk, x0, y0, x1, y1,
                new Vec2(b.Start.X, b.Start.Y), new Vec2(b.End.X, b.End.Y),
                halfWidth: 2.5f,
                surface: TacticalSurface.Cobble,
                flag:    TacticalFlags.Road | TacticalFlags.Bridge);
        }
    }

    /// <summary>
    /// Maps road class to a (surface, half-width) pair so each road tier
    /// reads as a distinct material in tactical, not just a different width
    /// of the same cobble. Footpaths use a narrower stamp than dirt roads
    /// because they're walking trails, not cart routes.
    /// </summary>
    private static (TacticalSurface surface, float halfWidth) RoadStyle(Polyline p) =>
        p.RoadClassification switch
        {
            RoadType.Highway  => (TacticalSurface.Cobble,      2.5f),
            RoadType.PostRoad => (TacticalSurface.Cobble,      1.5f),
            RoadType.DirtRoad => (TacticalSurface.TroddenDirt, 1.0f),
            _                  => (TacticalSurface.Gravel,      0.7f), // Footpath
        };

    private static float RiverHalfWidth(Polyline p) => p.RiverClassification switch
    {
        RiverClass.MajorRiver => 4.0f,
        RiverClass.River      => 2.5f,
        _                      => 1.0f,
    };

    private static void BurnPolyline(
        TacticalChunk chunk, Polyline p,
        int x0, int y0, int x1, int y1,
        float halfWidth, TacticalSurface surface, TacticalFlags flag)
    {
        var pts = p.Points;
        if (pts.Count < 2) return;

        // Cheap AABB rejection per polyline.
        bool any = false;
        for (int i = 0; i < pts.Count && !any; i++)
            if (pts[i].X >= x0 - 8 && pts[i].X <= x1 + 8 &&
                pts[i].Y >= y0 - 8 && pts[i].Y <= y1 + 8) any = true;
        if (!any) return;

        for (int i = 0; i < pts.Count - 1; i++)
            BurnSegment(chunk, x0, y0, x1, y1, pts[i], pts[i + 1], halfWidth, surface, flag);
    }

    private static void BurnSegment(
        TacticalChunk chunk,
        int x0, int y0, int x1, int y1,
        Vec2 a, Vec2 b,
        float halfWidth, TacticalSurface surface, TacticalFlags flag)
    {
        // Walk the segment's AABB intersected with the chunk and test each pixel
        // for distance ≤ halfWidth. CHUNK_SIZE is small (64) so this is cheap.
        float minX = MathF.Min(a.X, b.X) - halfWidth;
        float maxX = MathF.Max(a.X, b.X) + halfWidth;
        float minY = MathF.Min(a.Y, b.Y) - halfWidth;
        float maxY = MathF.Max(a.Y, b.Y) + halfWidth;

        int sx = Math.Max(x0, (int)MathF.Floor(minX));
        int sy = Math.Max(y0, (int)MathF.Floor(minY));
        int ex = Math.Min(x1 - 1, (int)MathF.Ceiling(maxX));
        int ey = Math.Min(y1 - 1, (int)MathF.Ceiling(maxY));
        if (sx > ex || sy > ey) return;

        Vec2 d   = b - a;
        float L2 = d.LengthSquared;
        float hw2 = halfWidth * halfWidth;

        for (int ty = sy; ty <= ey; ty++)
        for (int tx = sx; tx <= ex; tx++)
        {
            // Tile centre in world pixels. Each tactical tile is 1 world pixel
            // wide so the centre is at (tx + 0.5, ty + 0.5).
            Vec2 q = new(tx + 0.5f, ty + 0.5f);
            float t = L2 < 1e-6f ? 0f : Math.Clamp(Vec2.Dot(q - a, d) / L2, 0f, 1f);
            Vec2 closest = a + d * t;
            if (Vec2.DistSq(q, closest) > hw2) continue;

            int lx = tx - chunk.OriginX;
            int ly = ty - chunk.OriginY;
            ref var dst = ref chunk.Tiles[lx, ly];

            // For a river-on-water cell we want DeepWater near the centre line
            // and ShallowWater near the bank. For a road, just stamp surface.
            if (flag == TacticalFlags.River)
            {
                float dist = MathF.Sqrt(Vec2.DistSq(q, closest));
                bool deep  = dist < halfWidth * 0.5f;
                if ((dst.Flags & (byte)TacticalFlags.Bridge) == 0)
                    dst.Surface = deep ? TacticalSurface.DeepWater : TacticalSurface.ShallowWater;
                dst.Flags |= (byte)flag;
            }
            else
            {
                // Roads override river surface only when bridge flag is set
                // (BurnSegment is called with Bridge for actual bridge spans).
                if ((flag & TacticalFlags.Bridge) != 0 || (dst.Flags & (byte)TacticalFlags.River) == 0)
                {
                    dst.Surface = surface;
                }
                dst.Flags |= (byte)flag;
                // Bridge lookup forgets the underlying river flag for walkability.
                if ((flag & TacticalFlags.Bridge) != 0)
                    dst.Flags &= unchecked((byte)~(byte)TacticalFlags.River);
            }
            // Polyline stamps clear scatter.
            dst.Deco = TacticalDeco.None;
        }
    }

    // ── Pass 3 — settlement stamping handed off to SettlementStamper ─────
    // Phase 6 M0 moved the settlement burn-in into
    // World/Settlements/SettlementStamper.cs, which falls back to the
    // legacy plaza+wall-ring stamp when no content is supplied.

    // ── Pass 4 — scatter ──────────────────────────────────────────────────

    private static void Pass4_Scatter(ulong seed, TacticalChunk chunk, WorldState world, ulong chunkHash)
    {
        var rng = SeededRng.ForSubsystem(seed, C.RNG_TACTICAL_SCATTER ^ chunkHash);

        for (int ly = 0; ly < C.TACTICAL_CHUNK_SIZE; ly++)
        for (int lx = 0; lx < C.TACTICAL_CHUNK_SIZE; lx++)
        {
            ref var dst = ref chunk.Tiles[lx, ly];
            // Don't scatter on impassable, water, road, settlement, or river tiles.
            if (dst.Surface == TacticalSurface.DeepWater) continue;
            if (dst.Surface == TacticalSurface.Wall) continue;
            if ((dst.Flags & ((byte)TacticalFlags.Road | (byte)TacticalFlags.Settlement | (byte)TacticalFlags.River)) != 0) continue;

            int wx = (chunk.OriginX + lx) / C.TACTICAL_PER_WORLD_TILE;
            int wy = (chunk.OriginY + ly) / C.TACTICAL_PER_WORLD_TILE;
            wx = Math.Clamp(wx, 0, C.WORLD_WIDTH_TILES  - 1);
            wy = Math.Clamp(wy, 0, C.WORLD_HEIGHT_TILES - 1);
            ref var w = ref world.TileAt(wx, wy);

            float treeP = TreeDensity(w.Biome);
            float bushP = BushDensity(w.Biome);
            float rockP = RockDensity(w.Biome);

            double r = rng.NextDouble();
            TacticalDeco candidate = TacticalDeco.None;
            if (r < treeP)               candidate = TacticalDeco.Tree;
            else if (r < treeP + bushP)  candidate = TacticalDeco.Bush;
            else if (r < treeP + bushP + rockP)
                candidate = rng.NextBool(0.15) ? TacticalDeco.Boulder : TacticalDeco.Rock;
            else if (rng.NextBool(0.02))
                candidate = TacticalDeco.Flower;

            // Rule: no blocking decorations within 1 tactical tile of a road.
            // Trees/boulders adjacent to a road would visually clutter the
            // road edge and (since both are impassable) effectively narrow
            // the road by half a tile on each side. Bushes, rocks, and
            // flowers are walkable so they're allowed adjacent.
            bool blocking = candidate == TacticalDeco.Tree || candidate == TacticalDeco.Boulder;
            if (blocking && HasRoadNeighbour(chunk, lx, ly)) continue;

            dst.Deco = candidate;
        }
    }

    /// <summary>
    /// Returns true if any of the 4 cardinal neighbours of (lx, ly) within
    /// this chunk has the Road flag set. Cross-chunk neighbours aren't
    /// inspected (would require streamer access during generation, breaking
    /// determinism + adding latency); the worst-case artefact is one tile of
    /// blocking deco at a chunk boundary, which is rare and barely visible.
    /// </summary>
    private static bool HasRoadNeighbour(TacticalChunk chunk, int lx, int ly)
    {
        const byte ROAD = (byte)TacticalFlags.Road;
        if (lx > 0                            && (chunk.Tiles[lx - 1, ly].Flags & ROAD) != 0) return true;
        if (lx + 1 < C.TACTICAL_CHUNK_SIZE    && (chunk.Tiles[lx + 1, ly].Flags & ROAD) != 0) return true;
        if (ly > 0                            && (chunk.Tiles[lx, ly - 1].Flags & ROAD) != 0) return true;
        if (ly + 1 < C.TACTICAL_CHUNK_SIZE    && (chunk.Tiles[lx, ly + 1].Flags & ROAD) != 0) return true;
        return false;
    }

    private static float TreeDensity(BiomeId b) => b switch
    {
        BiomeId.Boreal              => 0.25f,
        BiomeId.SubtropicalForest   => 0.30f,
        BiomeId.TemperateDeciduous  => 0.22f,
        BiomeId.MountainForested    => 0.18f,
        BiomeId.ForestEdge          => 0.10f,
        BiomeId.RiverValley         => 0.05f,
        BiomeId.Mangrove            => 0.20f,
        BiomeId.Wetland             => 0.04f,
        _                            => 0.02f,
    };
    private static float BushDensity(BiomeId b) => b switch
    {
        BiomeId.TemperateGrassland  => 0.04f,
        BiomeId.Scrubland           => 0.10f,
        BiomeId.Foothills           => 0.05f,
        BiomeId.ForestEdge          => 0.06f,
        _                            => 0.02f,
    };
    private static float RockDensity(BiomeId b) => b switch
    {
        BiomeId.MountainAlpine      => 0.20f,
        BiomeId.MountainForested    => 0.12f,
        BiomeId.Cliff               => 0.30f,
        BiomeId.Foothills           => 0.06f,
        BiomeId.Tundra              => 0.04f,
        BiomeId.DesertCold          => 0.05f,
        _                            => 0.01f,
    };

    // ── Pass 5 — spawn list ───────────────────────────────────────────────

    private static void Pass5_Spawns(ulong seed, TacticalChunk chunk, WorldState world, ulong chunkHash)
    {
        var rng = SeededRng.ForSubsystem(seed, C.RNG_TACTICAL_SPAWN ^ chunkHash);

        // Sample the encounter density at the chunk centre. Dense areas roll
        // a few candidates, sparse ones roll one or none.
        int cxw = (chunk.OriginX + C.TACTICAL_CHUNK_SIZE / 2) / C.TACTICAL_PER_WORLD_TILE;
        int cyw = (chunk.OriginY + C.TACTICAL_CHUNK_SIZE / 2) / C.TACTICAL_PER_WORLD_TILE;
        cxw = Math.Clamp(cxw, 0, C.WORLD_WIDTH_TILES  - 1);
        cyw = Math.Clamp(cyw, 0, C.WORLD_HEIGHT_TILES - 1);

        // Phase 5 M5: stamp the chunk's danger zone. Pass5 is the natural place
        // because chunks needing a zone are the same chunks needing spawns.
        chunk.DangerZone = (byte)World.DangerZone.Compute(cxw, cyw, world);

        float density = world.EncounterDensity?[cxw, cyw] ?? 0f;
        int candidates = density switch
        {
            < 0.1f => 0,
            < 0.3f => 1,
            < 0.6f => 2,
            _      => 3,
        };

        for (int i = 0; i < candidates; i++)
        {
            int lx = rng.NextInt(0, C.TACTICAL_CHUNK_SIZE);
            int ly = rng.NextInt(0, C.TACTICAL_CHUNK_SIZE);
            ref var t = ref chunk.Tiles[lx, ly];
            if (!t.IsWalkable) continue;
            if ((t.Flags & (byte)TacticalFlags.Settlement) != 0) continue;

            SpawnKind kind = rng.NextDouble() switch
            {
                < 0.55 => SpawnKind.WildAnimal,
                < 0.80 => SpawnKind.Brigand,
                < 0.92 => SpawnKind.Patrol,
                < 0.98 => SpawnKind.Merchant,
                _      => SpawnKind.PoiGuard,
            };
            chunk.Spawns.Add(new TacticalSpawn(kind, lx, ly));
        }
    }

    // ── Helper: mix the chunk coord into a sub-seed ───────────────────────

    private static ulong Hash(ChunkCoord cc)
    {
        // SplitMix-style avalanche of (X, Y) into 64 bits.
        ulong h = (ulong)(uint)cc.X | ((ulong)(uint)cc.Y << 32);
        h += 0x9e3779b97f4a7c15UL;
        h = (h ^ (h >> 30)) * 0xbf58476d1ce4e5b9UL;
        h = (h ^ (h >> 27)) * 0x94d049bb133111ebUL;
        return h ^ (h >> 31);
    }
}
