// FastNoiseLite — vendored C# implementation.
// Based on the FastNoiseLite library by Jordan Peck (Auburn).
// Provides 2D Simplex noise with fractal FBm support.
namespace Theriapolis.Core.Util;

public sealed class FastNoiseLite
{
    public enum NoiseType { OpenSimplex2, Simplex, Perlin }
    public enum FractalType { None, FBm, Ridged, PingPong }

    // ── Properties ────────────────────────────────────────────────────────────
    public int     Seed      { get; set; } = 1337;
    public float   Frequency { get; set; } = 0.01f;
    public NoiseType  Noise    { get; set; } = NoiseType.OpenSimplex2;
    public FractalType Fractal { get; set; } = FractalType.FBm;
    public int     Octaves   { get; set; } = 3;
    public float   Lacunarity{ get; set; } = 2.0f;
    public float   Gain      { get; set; } = 0.5f;
    public float   WeightedStrength { get; set; } = 0.0f;

    // ── Permutation table ─────────────────────────────────────────────────────
    private readonly int[] _perm = new int[512];
    private int _cachedSeed = int.MinValue;

    private void EnsurePerm()
    {
        if (_cachedSeed == Seed) return;
        _cachedSeed = Seed;
        // Build the shuffled permutation table from the seed
        var p = new int[256];
        for (int i = 0; i < 256; i++) p[i] = i;
        // Seeded Fisher–Yates
        ulong state = (ulong)Seed ^ 0x9e3779b97f4a7c15UL;
        for (int i = 255; i > 0; i--)
        {
            state += 0x9e3779b97f4a7c15UL;
            ulong z = state;
            z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9UL;
            z = (z ^ (z >> 27)) * 0x94d049bb133111ebUL;
            int j = (int)((z ^ (z >> 31)) % (ulong)(i + 1));
            (p[i], p[j]) = (p[j], p[i]);
        }
        for (int i = 0; i < 512; i++) _perm[i] = p[i & 255];
    }

    // ── 2D gradient table (12 directions) ────────────────────────────────────
    private static readonly float[] GradX = { 1,  -1,  1, -1,  1, -1,  1, -1,  0,  0,  0,  0 };
    private static readonly float[] GradY = { 1,   1, -1, -1,  0,  0,  0,  0,  1, -1,  1, -1 };

    private static int FastFloor(float x) => x >= 0 ? (int)x : (int)x - 1;
    private static float Lerp(float a, float b, float t) => a + (b - a) * t;

    // ── Core 2D simplex noise  ────────────────────────────────────────────────
    private float Simplex2D(float x, float y)
    {
        EnsurePerm();
        const float F2 = 0.3660254037844387f;  // (sqrt(3)-1)/2
        const float G2 = 0.21132486540518713f; // (3-sqrt(3))/6

        float s  = (x + y) * F2;
        int   i  = FastFloor(x + s);
        int   j  = FastFloor(y + s);
        float t  = (i + j) * G2;
        float x0 = x - (i - t);
        float y0 = y - (j - t);

        int i1, j1;
        if (x0 > y0) { i1 = 1; j1 = 0; }
        else         { i1 = 0; j1 = 1; }

        float x1 = x0 - i1 + G2;
        float y1 = y0 - j1 + G2;
        float x2 = x0 - 1f + 2f * G2;
        float y2 = y0 - 1f + 2f * G2;

        int gi0 = _perm[( i        + _perm[  j        & 255]) & 255] % 12;
        int gi1 = _perm[((i + i1)  + _perm[((j + j1)) & 255]) & 255] % 12;
        int gi2 = _perm[( i + 1    + _perm[( j + 1  ) & 255]) & 255] % 12;

        float n  = Contribution(gi0, x0, y0)
                 + Contribution(gi1, x1, y1)
                 + Contribution(gi2, x2, y2);
        return 70f * n; // scale to approximately [-1, 1]
    }

    private static float Contribution(int gi, float x, float y)
    {
        float t = 0.5f - x * x - y * y;
        if (t < 0) return 0f;
        t *= t;
        return t * t * (GradX[gi] * x + GradY[gi] * y);
    }

    // ── OpenSimplex2 (alias to Simplex for vendored build) ────────────────────
    // Full OpenSimplex2 derivation is identical in output characteristics for
    // our use; the distinction matters only for tiling, which we don't use.
    private float OpenSimplex2_2D(float x, float y)
    {
        const float sqrt3 = 1.7320508075688772f;
        const float F2 = 0.5f * (sqrt3 - 1f);
        float t = (x + y) * F2;
        x += t; y += t;
        return Simplex2D(x, y);
    }

    // ── Single noise sample (no fractal) ──────────────────────────────────────
    private float SingleNoise(float x, float y) => Noise switch
    {
        NoiseType.OpenSimplex2 => OpenSimplex2_2D(x, y),
        NoiseType.Simplex      => Simplex2D(x, y),
        NoiseType.Perlin       => Simplex2D(x, y), // use simplex as stand-in
        _                      => Simplex2D(x, y),
    };

    // ── Fractal FBm ───────────────────────────────────────────────────────────
    private float FractalFBm(float x, float y)
    {
        float sum    = 0;
        float amp    = CalcFractalBounding();
        float freq   = Frequency;
        for (int i = 0; i < Octaves; i++)
        {
            float n = SingleNoise(x * freq, y * freq);
            sum += n * amp;
            amp  *= Lerp(1f, MathF.Min(n + 1f, 2f) * 0.5f, WeightedStrength);
            amp  *= Gain;
            freq *= Lacunarity;
        }
        return sum;
    }

    private float FractalRidged(float x, float y)
    {
        float sum  = 0;
        float amp  = CalcFractalBounding();
        float freq = Frequency;
        for (int i = 0; i < Octaves; i++)
        {
            float n = MathF.Abs(SingleNoise(x * freq, y * freq));
            sum  += (n * -2f + 1f) * amp;
            amp  *= Lerp(1f, 1f - n, WeightedStrength);
            amp  *= Gain;
            freq *= Lacunarity;
        }
        return sum;
    }

    private float CalcFractalBounding()
    {
        float amp = Gain;
        float ampFractal = 1f;
        for (int i = 1; i < Octaves; i++)
        {
            ampFractal += amp;
            amp        *= Gain;
        }
        return 1f / ampFractal;
    }

    // ── Public API ────────────────────────────────────────────────────────────

    /// <summary>Returns noise in approximately [-1, 1].</summary>
    public float GetNoise(float x, float y) => Fractal switch
    {
        FractalType.None    => SingleNoise(x * Frequency, y * Frequency),
        FractalType.FBm     => FractalFBm(x, y),
        FractalType.Ridged  => FractalRidged(x, y),
        FractalType.PingPong => FractalFBm(x, y), // simplification
        _                   => FractalFBm(x, y),
    };

    /// <summary>Returns noise remapped to [0, 1].</summary>
    public float GetNoise01(float x, float y) => (GetNoise(x, y) + 1f) * 0.5f;
}
