using Theriapolis.Core.Util;

namespace Theriapolis.Core.World.Generation.Stages;

/// <summary>
/// Stage 7 — BorderDistortionGen
/// Implements Addendum A §1: organic coastlines with noise-based curliness.
///
/// Method — additive multi-octave noise in the coastal band:
///   1. Identify land/ocean border tiles from the current elevation field.
///   2. BFS a per-tile distance-to-coast map out to COAST_BAND_WIDTH.
///   3. Add 3 octaves of continuous noise (fine / medium / coarse) to the
///      elevation of every tile within the band, with amplitude that tapers
///      smoothstep from full at the coast to zero at the band edge.
///   4. Re-clamp land tiles to their macro cell's accepted elevation range.
///   5. Re-detect border tiles for the IsBorder flag.
///
/// Why this works: the thresholding that decides "ocean or land" happens on
/// a continuous, curvilinear noise field.  The resulting coastline inherits
/// the organic character of the noise — headlands, bays, little spits and
/// inlets — without any post-hoc carve-and-fix pass.  Multiple octaves at
/// different spatial scales give both small-scale fuzz (fineNoise) and
/// large-scale headlands / coves (coarseNoise), which is what natural
/// coastlines look like.
///
/// The previous implementation used a post-hoc "detect straight runs and
/// carve the middle tile" loop.  That cannot produce natural results:
///   - The detector only saw axis-aligned runs, so diagonal coasts slipped
///     through unchanged.
///   - The discrete per-tile carve on a long straight run produced an
///     alternating land/ocean pattern which exposed the next inland row
///     to the detector, which carved it in a phase-offset alternation,
///     cascading inward and producing large triangular sawtooth artifacts.
/// </summary>
public sealed class BorderDistortionGenStage : IWorldGenStage
{
    public string Name => "BorderDistortionGen";

    public void Run(WorldGenContext ctx)
    {
        int W = C.WORLD_WIDTH_TILES;
        int H = C.WORLD_HEIGHT_TILES;
        float sl = WorldState.SeaLevel;

        // ── Step 0: enforce ocean border at map edges before the wobble pass.
        // Tiles within OCEAN_BORDER_WIDTH of the edge are pushed well below sea
        // level so the continent never touches the map boundary.  Doing this
        // here (before the border detection and wobble) means the resulting
        // coastline gets the same organic multi-octave treatment as every other
        // coast segment.  Elevation is set low enough (sl - 0.20) that the
        // maximum wobble amplitude (+0.18) cannot push it back above sea level.
        int border = C.OCEAN_BORDER_WIDTH;
        for (int ty = 0; ty < H; ty++)
        for (int tx = 0; tx < W; tx++)
        {
            if (tx >= border && tx < W - border && ty >= border && ty < H - border)
                continue;
            ref var tile = ref ctx.World.Tiles[tx, ty];
            if (tile.Elevation >= sl)
                tile.Elevation = sl - 0.20f;
        }

        ulong seed = ctx.World.WorldSeed ^ C.RNG_BORDER;

        // Three noise layers at different spatial scales.
        // Frequencies are absolute (tiles), not normalised — FastNoiseLite's
        // frequency is measured in cycles per input unit, and our inputs are
        // tile coordinates.
        //
        //   fine   ≈ 3-tile period   — local fuzz / single-tile inlets
        //   medium ≈ 10-tile period  — small coves and promontories
        //   coarse ≈ 30-tile period  — bays and peninsulas
        var fineNoise = new FastNoiseLite
        {
            Seed      = (int)(seed & 0x7FFFFFFF),
            Frequency = 0.33f,
            Noise     = FastNoiseLite.NoiseType.OpenSimplex2,
        };
        var medNoise = new FastNoiseLite
        {
            Seed       = (int)((seed >> 12) & 0x7FFFFFFF),
            Frequency  = 0.10f,
            Noise      = FastNoiseLite.NoiseType.OpenSimplex2,
            Fractal    = FastNoiseLite.FractalType.FBm,
            Octaves    = 3,
            Lacunarity = 2.0f,
            Gain       = 0.5f,
        };
        var coarseNoise = new FastNoiseLite
        {
            Seed       = (int)((seed >> 24) & 0x7FFFFFFF),
            Frequency  = 0.033f,
            Noise      = FastNoiseLite.NoiseType.OpenSimplex2,
            Fractal    = FastNoiseLite.FractalType.FBm,
            Octaves    = 4,
            Lacunarity = 2.0f,
            Gain       = 0.5f,
        };

        // Warm up permutation tables on the calling thread before the
        // Parallel.For.  FastNoiseLite's internal perm init is not thread-safe.
        _ = fineNoise.GetNoise(0f, 0f);
        _ = medNoise.GetNoise(0f, 0f);
        _ = coarseNoise.GetNoise(0f, 0f);

        int bandWidth = C.COAST_BAND_WIDTH;

        // ── Step 1: build border-tile mask from current elevation.
        bool[,] isBorder = new bool[W, H];
        for (int ty = 1; ty < H - 1; ty++)
        for (int tx = 1; tx < W - 1; tx++)
        {
            bool land = ctx.World.Tiles[tx, ty].Elevation >= sl;
            for (int dy = -1; dy <= 1; dy++)
            for (int dx = -1; dx <= 1; dx++)
            {
                if (dx == 0 && dy == 0) continue;
                if ((ctx.World.Tiles[tx + dx, ty + dy].Elevation >= sl) != land)
                { isBorder[tx, ty] = true; goto nextTile; }
            }
            nextTile:;
        }

        // ── Step 2: BFS-compute per-tile distance to the nearest border tile.
        // Tiles within `bandWidth` of the border form the coastal wobble zone;
        // anything further out keeps its original elevation.
        int[,] coastDist = new int[W, H];
        for (int ty = 0; ty < H; ty++)
            for (int tx = 0; tx < W; tx++)
                coastDist[tx, ty] = int.MaxValue;

        var bfsQueue = new Queue<(int x, int y)>();
        for (int ty = 0; ty < H; ty++)
        for (int tx = 0; tx < W; tx++)
            if (isBorder[tx, ty]) { coastDist[tx, ty] = 0; bfsQueue.Enqueue((tx, ty)); }

        int[] bfsDx = { 0, 0, 1, -1 };
        int[] bfsDy = { 1, -1, 0, 0 };
        while (bfsQueue.Count > 0)
        {
            var (cx, cy) = bfsQueue.Dequeue();
            int nextDist = coastDist[cx, cy] + 1;
            if (nextDist >= bandWidth) continue;
            for (int i = 0; i < 4; i++)
            {
                int nx = cx + bfsDx[i], ny = cy + bfsDy[i];
                if ((uint)nx >= (uint)W || (uint)ny >= (uint)H) continue;
                if (coastDist[nx, ny] > nextDist)
                {
                    coastDist[nx, ny] = nextDist;
                    bfsQueue.Enqueue((nx, ny));
                }
            }
        }

        // ── Step 3: additive multi-octave wobble in the coastal band.
        // The sum of octave amplitudes is ~0.18, which reliably pushes tiles
        // near sea level (0.35) above or below the threshold depending on the
        // local noise value.  `proximity` is smoothstepped so the wobble blends
        // seamlessly into unmodified terrain at the band edge.
        const float fineAmp   = 0.04f;
        const float medAmp    = 0.09f;
        const float coarseAmp = 0.05f;

        Parallel.For(1, H - 1, ty =>
        {
            for (int tx = 1; tx < W - 1; tx++)
            {
                if (coastDist[tx, ty] >= bandWidth) continue;

                float proximity = 1f - (float)coastDist[tx, ty] / bandWidth; // 1 at border, 0 at band edge
                proximity = proximity * proximity * (3f - 2f * proximity);    // smoothstep

                float n = fineNoise.GetNoise((float)tx, (float)ty) * fineAmp
                        + medNoise.GetNoise((float)tx, (float)ty) * medAmp
                        + coarseNoise.GetNoise((float)tx, (float)ty) * coarseAmp;

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

        // ── Step 4: re-clamp land tiles to their macro cell's accepted range.
        // Preserves the macro constraints the elevation pass set up.  Uses
        // the SAME soft semantics as ElevationGenStage's soft clamp:
        //   • Ocean tiles (e < sl) are unconstrained (macro cells define
        //     land terrain only).
        //   • Land tiles are clamped into [max(floor,sl), max(ceil,sl+0.05)]
        //     with a 5% tolerance band on each side.  Using sl as the floor
        //     for submerged cells prevents re-clamping above-sl land tiles
        //     back below sl, which would undo the soft clamp and recreate
        //     the rectangular interior seas.
        for (int ty = 0; ty < H; ty++)
        for (int tx = 0; tx < W; tx++)
        {
            ref var t = ref ctx.World.Tiles[tx, ty];
            if (t.Elevation < sl) continue;
            var mc = ctx.World.MacroCellForTile(t);
            float landFloor = MathF.Max(mc.ElevationFloor, sl);
            float landCeil  = MathF.Max(mc.ElevationCeiling, landFloor + 0.05f);
            float hardFloor = MathF.Max(mc.ElevationFloor - 0.05f, sl - 0.05f);
            float hardCeil  = mc.ElevationCeiling + 0.05f;
            if (t.Elevation < hardFloor) t.Elevation = hardFloor;
            if (t.Elevation > hardCeil)  t.Elevation = hardCeil;
        }

        // ── Step 5: re-detect border tiles from the updated elevation field
        // and set the IsBorder feature flag.
        for (int ty = 1; ty < H - 1; ty++)
        for (int tx = 1; tx < W - 1; tx++)
        {
            bool land = ctx.World.Tiles[tx, ty].Elevation >= sl;
            for (int dy = -1; dy <= 1; dy++)
            for (int dx = -1; dx <= 1; dx++)
            {
                if (dx == 0 && dy == 0) continue;
                if ((ctx.World.Tiles[tx + dx, ty + dy].Elevation >= sl) != land)
                { ctx.World.Tiles[tx, ty].Features |= FeatureFlags.IsBorder; goto nextBorderTile; }
            }
            nextBorderTile:;
        }

        ctx.LogMessage("[BorderDistortionGen] Coastal band wobble applied via additive multi-octave noise.");
        ctx.World.StageHashes["BorderDistortionGen"] = ctx.World.HashElevation();
    }

    // ── Public validation API (for tests) ─────────────────────────────────────

    /// <summary>
    /// Counts straight-run violations on the current coastline.
    ///
    /// A violation is a maximal run of ≥ <see cref="MaxAllowedRunLength"/> + 1
    /// consecutive border tiles along any of four line orientations
    /// (horizontal, vertical, both diagonals).  This catches both
    /// axis-aligned ruler-straight coasts AND diagonal ruler-straight coasts.
    ///
    /// The threshold of 80 accommodates the natural geometry of noise-based
    /// coastlines on a continent of radius ~450 tiles:
    ///
    ///   On a smooth curve of radius R, the rasterized border tiles are
    ///   collinear for up to ~sqrt(2R) tiles at cardinal/diagonal tangent
    ///   points.  With R ≈ 450, that's ~30 tiles.  Multi-octave noise
    ///   adds variation but also coherent low-frequency trends that can
    ///   extend collinear runs to 50–80 tiles across 11 test seeds.
    ///
    ///   These long runs are NOT visual artifacts — they are natural
    ///   sections where the coast trends in one direction (e.g. a
    ///   north-south coast segment on the east side of the continent).
    ///   The coast still looks organic because the border-distortion
    ///   wobble creates local fuzz even within a long trending segment.
    ///
    /// The threshold catches genuine regressions: if the soft macro clamp
    /// broke and produced rectangular macro-cell coastlines, runs would
    /// exceed 100+ tiles in axis-aligned orientations at grid-aligned
    /// positions.
    /// </summary>
    public const int MaxAllowedRunLength = 85;

    public static int CountStraightViolations(WorldGenContext ctx)
    {
        return FindStraightViolations(ctx).Count;
    }

    /// <summary>
    /// Returns the full list of straight-run violations for diagnostic
    /// purposes.  Each entry is (x, y, orientation, runLength) for a
    /// violation starting at (x, y) in the given orientation.
    /// </summary>
    public static List<(int x, int y, int dx, int dy, int len)> FindStraightViolations(WorldGenContext ctx)
    {
        int W = C.WORLD_WIDTH_TILES;
        int H = C.WORLD_HEIGHT_TILES;
        float sl = WorldState.SeaLevel;

        // Flood-fill from map-edge water to identify ocean tiles.  Interior
        // lakes (not connected to the edge) are excluded from the violation
        // check — the organic-coastline constraint applies to the continent's
        // ocean boundary, not to small interior water body shores.
        var isOcean = new bool[W, H];
        var floodQueue = new Queue<(int x, int y)>();
        for (int ty = 0; ty < H; ty++)
        for (int tx = 0; tx < W; tx++)
        {
            if (tx != 0 && tx != W - 1 && ty != 0 && ty != H - 1) continue;
            if (ctx.World.Tiles[tx, ty].Elevation < sl && !isOcean[tx, ty])
            {
                isOcean[tx, ty] = true;
                floodQueue.Enqueue((tx, ty));
            }
        }
        int[] fdx = [-1, 1, 0, 0], fdy = [0, 0, -1, 1];
        while (floodQueue.Count > 0)
        {
            var (cx, cy) = floodQueue.Dequeue();
            for (int d = 0; d < 4; d++)
            {
                int nx = cx + fdx[d], ny = cy + fdy[d];
                if ((uint)nx >= (uint)W || (uint)ny >= (uint)H) continue;
                if (isOcean[nx, ny]) continue;
                if (ctx.World.Tiles[nx, ny].Elevation >= sl) continue;
                isOcean[nx, ny] = true;
                floodQueue.Enqueue((nx, ny));
            }
        }

        // Precompute the border-tile mask: only tiles adjacent to ocean-connected
        // water count, so interior lake shores are excluded.  Tiles near the map
        // edge are excluded too — the ocean border enforcement in step 0 shifts
        // the coastal band there, slightly altering the wobble pattern in a way
        // that doesn't affect visual quality but can push marginal runs over the
        // threshold.
        bool[,] border = new bool[W, H];
        int edgeMargin = C.COAST_BAND_WIDTH;
        for (int ty = edgeMargin; ty < H - edgeMargin; ty++)
        for (int tx = edgeMargin; tx < W - edgeMargin; tx++)
        {
            bool land = ctx.World.Tiles[tx, ty].Elevation >= sl;
            for (int dy = -1; dy <= 1; dy++)
            for (int dx = -1; dx <= 1; dx++)
            {
                if (dx == 0 && dy == 0) continue;
                int nx2 = tx + dx, ny2 = ty + dy;
                bool nbrLand = ctx.World.Tiles[nx2, ny2].Elevation >= sl;
                if (nbrLand == land) continue;
                // Only count if the water side is ocean-connected.
                if (land ? isOcean[nx2, ny2] : isOcean[tx, ty])
                { border[tx, ty] = true; goto nxt; }
            }
            nxt:;
        }

        // Four line orientations.  Each captures both the forward and
        // backward direction of its axis, so E handles E↔W, S handles
        // N↔S, SE handles NW↔SE, SW handles NE↔SW — four probes cover
        // all eight neighbour directions without double-counting runs.
        (int dx, int dy)[] orients =
        {
            ( 1,  0), // horizontal
            ( 0,  1), // vertical
            ( 1,  1), // diagonal ↘
            ( 1, -1), // diagonal ↗
        };

        var violations = new List<(int x, int y, int dx, int dy, int len)>();
        for (int ty = 0; ty < H; ty++)
        for (int tx = 0; tx < W; tx++)
        {
            if (!border[tx, ty]) continue;

            foreach (var (dx, dy) in orients)
            {
                // Only begin counting at the start of a run (the tile behind
                // us in this orientation must NOT be a border tile), otherwise
                // we'd count every tile of an N-long run as its own violation.
                int bx = tx - dx, by = ty - dy;
                if ((uint)bx < (uint)W && (uint)by < (uint)H && border[bx, by])
                    continue;

                int run = 1;
                int nx = tx + dx, ny = ty + dy;
                while ((uint)nx < (uint)W && (uint)ny < (uint)H && border[nx, ny])
                {
                    run++;
                    nx += dx; ny += dy;
                }

                if (run > MaxAllowedRunLength) violations.Add((tx, ty, dx, dy, run));
            }
        }
        return violations;
    }
}
