using Theriapolis.Core.Util;

namespace Theriapolis.Core.World.Generation.Stages;

/// <summary>
/// Stage 3 — ElevationGen
/// Generates a multi-octave noise heightmap, applies a fractal continent mask
/// with distance-warp and domain-warped coordinates, and clamps the result into
/// the per-tile macro cell's elevation range.
///
/// The continent mask uses a radial falloff (distance from map centre) whose
/// effective distance is warped by a multi-octave FBm shape field.  This
/// produces organically irregular coastlines whose iso-contour wiggles at
/// every octave scale.  The previous smooth-ellipse approach had an intrinsic
/// failure mode: at cardinal extremes the tangent was locally straight for
/// ~R·ε tiles (≈50 tiles at R≈450, ε≈0.1), creating long collinear border
/// runs that no amount of additive coastal noise could reliably break.
///
/// Addendum A §1 — macro-cell border warp:
///   Instead of looking up each tile's macro cell by raw grid position
///   (which produces rectangular cell blocks with ruler-straight boundaries),
///   the lookup position is first displaced by a smooth multi-octave noise
///   field.  Two tiles at adjacent grid positions near a nominal cell
///   boundary may therefore sample completely different macro cells,
///   producing organic wiggly cell interfaces.  The warped (mx, my) is
///   stored on the tile's MacroX/MacroY fields so downstream stages see
///   consistent cell assignment without recomputing the warp.
/// </summary>
public sealed class ElevationGenStage : IWorldGenStage
{
    public string Name => "ElevationGen";

    public void Run(WorldGenContext ctx)
    {
        ulong seed = ctx.World.WorldSeed ^ C.RNG_TERRAIN;
        var noise = new FastNoiseLite
        {
            Seed       = (int)(seed & 0x7FFFFFFF),
            Frequency  = 1.8f / C.WORLD_WIDTH_TILES,
            Noise      = FastNoiseLite.NoiseType.OpenSimplex2,
            Fractal    = FastNoiseLite.FractalType.FBm,
            Octaves    = 7,
            Lacunarity = 2.1f,
            Gain       = 0.48f,
        };

        // Second noise layer for macro-region variety
        var noise2 = new FastNoiseLite
        {
            Seed       = (int)((seed >> 16) & 0x7FFFFFFF),
            Frequency  = 0.6f / C.WORLD_WIDTH_TILES,
            Noise      = FastNoiseLite.NoiseType.OpenSimplex2,
            Fractal    = FastNoiseLite.FractalType.FBm,
            Octaves    = 4,
            Lacunarity = 2.0f,
            Gain       = 0.5f,
        };

        // Domain-warp noise for the continent mask (large-scale coastal excursions).
        ulong warpSeed = ctx.World.WorldSeed ^ C.RNG_COAST_WARP;
        var continentWarp = new FastNoiseLite
        {
            Seed       = (int)(warpSeed & 0x7FFFFFFF),
            Frequency  = 1.5f / C.WORLD_WIDTH_TILES,
            Noise      = FastNoiseLite.NoiseType.OpenSimplex2,
            Fractal    = FastNoiseLite.FractalType.FBm,
            Octaves    = 4,
            Lacunarity = 2.0f,
            Gain       = 0.5f,
        };

        // Fractal continent shape (Addendum A §1 — organic coastlines).
        // Replaces the previous smooth-ellipse continent mask, which had an
        // intrinsic failure mode: at its east/west/north/south extremes the
        // tangent to a circle of radius R runs approximately straight for
        // ~R·ε tiles where ε is the "locally-straight" angular tolerance.
        // For our continent (R ≈ 470 tiles, ε ≈ 0.1 rad ≈ 6°) that's ~47
        // collinear border tiles — 10× the MaxAllowedRunLength threshold —
        // and the additive coastal wobble noise cannot reliably break them
        // up because it is itself locally smooth.
        //
        // A fractal shape field has no such tangent alignment: the 0.22
        // contour (where e ≈ sea level) wiggles at every octave scale, so
        // the coast threshold-crossing locus changes direction rapidly over
        // a few tiles no matter where you sample it.  Five FBm octaves
        // give coastline features from sub-cell scale up to continent scale.
        var continentShape = new FastNoiseLite
        {
            Seed       = (int)((warpSeed >> 24) & 0x7FFFFFFF),
            Frequency  = 3.0f / C.WORLD_WIDTH_TILES,
            Noise      = FastNoiseLite.NoiseType.OpenSimplex2,
            Fractal    = FastNoiseLite.FractalType.FBm,
            Octaves    = 5,
            Lacunarity = 2.0f,
            Gain       = 0.55f,
        };

        // Macro-cell border warp (Addendum A §1): displaces the per-tile macro
        // lookup position so that macro cell boundaries become wiggly curves.
        // Uses plain OpenSimplex2 (no FBm) for smooth large-scale displacement;
        // FBm would add high-frequency jitter that chops cell interiors into
        // sub-cell patches.
        ulong macroWarpSeed = ctx.World.WorldSeed ^ C.RNG_MACRO_WARP;
        var macroWarpX = new FastNoiseLite
        {
            Seed      = (int)(macroWarpSeed & 0x7FFFFFFF),
            Frequency = C.MACRO_WARP_FREQUENCY,
            Noise     = FastNoiseLite.NoiseType.OpenSimplex2,
        };
        var macroWarpY = new FastNoiseLite
        {
            Seed      = (int)((macroWarpSeed >> 16) & 0x7FFFFFFF),
            Frequency = C.MACRO_WARP_FREQUENCY,
            Noise     = FastNoiseLite.NoiseType.OpenSimplex2,
        };

        int W = C.WORLD_WIDTH_TILES;
        int H = C.WORLD_HEIGHT_TILES;
        int cellW = W / C.MACRO_GRID_WIDTH;
        int cellH = H / C.MACRO_GRID_HEIGHT;

        // Warm up permutation tables on the calling thread before the parallel loop.
        // FastNoiseLite.EnsurePerm() is not thread-safe; calling GetNoise once here
        // ensures the _perm array is fully written and visible to all parallel threads.
        _ = noise.GetNoise01(0f, 0f);
        _ = noise2.GetNoise01(0f, 0f);
        _ = continentWarp.GetNoise(0f, 0f);
        _ = continentShape.GetNoise01(0f, 0f);
        _ = macroWarpX.GetNoise(0f, 0f);
        _ = macroWarpY.GetNoise(0f, 0f);

        Parallel.For(0, H, ty =>
        {
            for (int tx = 0; tx < W; tx++)
            {
                // ── Macro cell border warp: displace the lookup position ──
                // The displacement is a smooth continuous function of (tx, ty),
                // so adjacent tiles get similar warp vectors and the warped
                // cell boundary is a curve rather than a grid line.
                float mwx = macroWarpX.GetNoise((float)tx, (float)ty) * C.MACRO_WARP_AMPLITUDE;
                float mwy = macroWarpY.GetNoise((float)tx, (float)ty) * C.MACRO_WARP_AMPLITUDE;
                int wtx = Math.Clamp((int)(tx + mwx), 0, W - 1);
                int wty = Math.Clamp((int)(ty + mwy), 0, H - 1);
                byte mx = (byte)Math.Clamp(wtx / cellW, 0, C.MACRO_GRID_WIDTH  - 1);
                byte my = (byte)Math.Clamp(wty / cellH, 0, C.MACRO_GRID_HEIGHT - 1);

                ctx.World.Tiles[tx, ty].MacroX = mx;
                ctx.World.Tiles[tx, ty].MacroY = my;

                var cell = ctx.World.MacroGrid![mx, my];
                float floor = cell.ElevationFloor;
                float ceil  = cell.ElevationCeiling;

                // ── Base elevation from noise + continent mask ──
                float raw = noise.GetNoise01((float)tx, (float)ty) * 0.7f
                          + noise2.GetNoise01((float)tx, (float)ty) * 0.3f;

                // Continent mask: radial falloff with fractal distance warp.
                //
                // Previous approach (smooth ellipse) produced long straight
                // tangent segments at its extremes; see the continentShape
                // field comment above for the R·ε arc-length analysis.
                //
                // New approach: the fractal shape field WARPS the radial
                // distance instead of multiplying the mask.  The coast is
                // the iso-contour where the warped-radial falloff crosses
                // sea level.  Because the shape field varies at every
                // octave scale, that iso-contour wiggles organically.
                //
                // Key advantage over the multiply approach: interior tiles
                // (where radialDist is small) have continentMask ≈ 1.0
                // regardless of shape, preserving the same elevation
                // distribution as the old smooth ellipse.  Only tiles near
                // the coast transition (radialDist ≈ 0.7–1.0) are affected
                // by the fractal warp, which is exactly where we need it.
                float wx = continentWarp.GetNoise((float)tx,         (float)ty)         * C.COAST_WARP_AMP;
                float wy = continentWarp.GetNoise((float)tx + 3000f, (float)ty + 3000f) * C.COAST_WARP_AMP;

                // Domain-warp the radial coordinates (same as the old ellipse)
                // to preserve interior elevation variety.  Without this, all
                // interior tiles have continentMask ≈ 1.0 and pile near the
                // macro cell ceiling, crushing biome diversity.
                float rcx = ((float)tx + wx - W * 0.5f) / (W * 0.72f);
                float rcy = ((float)ty + wy - H * 0.5f) / (H * 0.72f);
                float radialDist = MathF.Sqrt(rcx * rcx + rcy * rcy);

                // Warp the radial distance by the fractal shape field.
                // shape is in [0,1]; centred to [-0.5,0.5] and scaled by
                // 0.35 so the coast edge displaces by up to ±0.175 in
                // normalised-radius units (≈ ±74 tiles at W*0.72=737).
                float shape = continentShape.GetNoise01((float)tx + wx, (float)ty + wy);
                float warpedDist = radialDist + (shape - 0.5f) * 0.20f;

                float continentMask = Math.Clamp(1f - warpedDist, 0f, 1f);
                continentMask = continentMask * continentMask * (3f - 2f * continentMask);

                // Additive continent bias: raw * 0.3 (local variation) +
                // continentMask * 0.9 (determines "how land-like" this tile
                // is).  Interior tiles get e ≈ 1.2 clamped to 1.0; edge tiles
                // get e ≈ 0.
                float e = raw * 0.3f + continentMask * 0.9f;
                e = Math.Clamp(e, 0f, 1f);

                // ── Soft macro clamp (Addendum A §1 — coastline organics) ──
                // The previous hard clamp `floor + e * (ceil - floor)` forced
                // every tile in a macro cell into the cell's elevation range.
                // For mountain cells (floor > sea level) that meant every tile
                // was land regardless of continent-mask strength, producing
                // ruler-straight rectangular coastlines along macro cell edges.
                // For submerged cells (ceiling < sea level — e.g. wetland,
                // coastal) the mirror bug held: every tile was forced ocean
                // regardless of continent-mask strength, producing rectangular
                // interior seas at macro cell boundaries.
                //
                // Soft clamp rules:
                //   • The continent mask drives the land/ocean threshold.
                //     If `e < sea level`, the tile is ocean regardless of
                //     the macro cell's floor (so mountain cells can have
                //     organic ocean inlets at their weak edges).
                //   • If `e ≥ sea level`, the tile is land regardless of
                //     the macro cell's ceiling (so wetland/coastal cells
                //     can have organic land patches where the continent
                //     mask is strong, instead of becoming a rectangular
                //     forced-ocean block).
                //   • Land tiles are mapped into the cell's effective land
                //     range `[max(floor, sl), max(ceil, sl+0.05)]` so the
                //     macro cell's terrain character (mountain vs lowland)
                //     is preserved on land.
                if (e >= WorldState.SeaLevel)
                {
                    float landFloor = MathF.Max(floor, WorldState.SeaLevel);
                    float landCeil  = MathF.Max(ceil,  landFloor + 0.05f);
                    float tLand     = (e - WorldState.SeaLevel) / (1f - WorldState.SeaLevel);
                    e = landFloor + tLand * (landCeil - landFloor);
                }
                // else: continent mask says ocean — keep e below sea level.

                e = Math.Clamp(e, 0f, 1f);
                ctx.World.Tiles[tx, ty].Elevation = e;
            }
        });

        ctx.World.StageHashes["ElevationGen"] = ctx.World.HashElevation();
        ctx.LogMessage($"[ElevationGen] Elevation hash: 0x{ctx.World.StageHashes["ElevationGen"]:X16}");
    }
}
