/**
 * SimplexNoise — компактная реализация 2D/3D Simplex noise.
 *
 * Простая Perlin/Simplex реализация на основе классического кода
 * Ken Perlin / Stefan Gustavson (public domain). Single-file без
 * зависимостей.
 *
 * Свойства:
 *   - Детерминированный по seed
 *   - 2D и 3D версии
 *   - fbm() для фрактального шума (несколько октав)
 *   - Возвращает значения в диапазоне ~[-1, +1]
 *
 * Использование:
 *   const noise = new SimplexNoise(12345);
 *   const v = noise.noise2(x * 0.01, z * 0.01);  // плавный noise
 *   const h = noise.fbm2(x, z, 4, 0.5, 2.0);     // фрактальный
 *
 * Производительность: noise2 ~ 0.001ms на вызов. Для генерации чанка
 * 32×32 это ~1мс на heightmap.
 */

// Pseudo-random number generator с seed (mulberry32). Возвращает float [0..1).
function mulberry32(seed) {
    let t = seed | 0;
    return function () {
        t = (t + 0x6D2B79F5) | 0;
        let r = Math.imul(t ^ (t >>> 15), 1 | t);
        r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
        return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
    };
}

// 2D градиенты (12 направлений + 4 дубля для удобства &15)
const GRAD2 = [
    1, 1, -1, 1, 1, -1, -1, -1,
    1, 0, -1, 0, 1, 0, -1, 0,
    0, 1, 0, -1, 0, 1, 0, -1,
    1, 1, 0, 0, -1, 1, 0, 0,    // padding до 16
];

// 3D градиенты (12 направлений)
const GRAD3 = [
    1, 1, 0,  -1, 1, 0,  1, -1, 0,  -1, -1, 0,
    1, 0, 1,  -1, 0, 1,  1, 0, -1,  -1, 0, -1,
    0, 1, 1,  0, -1, 1,  0, 1, -1,  0, -1, -1,
    1, 1, 0,  0, -1, 1,  -1, 1, 0,  0, -1, -1,    // padding до 16
];

const F2 = 0.5 * (Math.sqrt(3) - 1);   // skew factor 2D
const G2 = (3 - Math.sqrt(3)) / 6;     // unskew factor 2D
const F3 = 1 / 3;
const G3 = 1 / 6;

export class SimplexNoise {
    /**
     * @param {number} seed - 32-битное число
     */
    constructor(seed = 0) {
        this.seed = seed;
        // Permutation table (size 512 = 256 doubled для wrap-around)
        const rand = mulberry32(seed);
        const perm = new Uint8Array(512);
        const p = new Uint8Array(256);
        for (let i = 0; i < 256; i++) p[i] = i;
        // Fisher-Yates shuffle
        for (let i = 255; i > 0; i--) {
            const j = Math.floor(rand() * (i + 1));
            const tmp = p[i];
            p[i] = p[j];
            p[j] = tmp;
        }
        for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
        this.perm = perm;
    }

    /**
     * 2D simplex noise. Возвращает значение примерно в [-1, +1].
     */
    noise2(xin, yin) {
        const perm = this.perm;
        // Skew входных координат в simplex-сетку
        const s = (xin + yin) * F2;
        const i = Math.floor(xin + s);
        const j = Math.floor(yin + s);
        const t = (i + j) * G2;
        const X0 = i - t;
        const Y0 = j - t;
        const x0 = xin - X0;
        const y0 = yin - Y0;

        // Определяем какой треугольник (simplex) внутри ячейки
        let i1, j1;
        if (x0 > y0) { i1 = 1; j1 = 0; }
        else { i1 = 0; j1 = 1; }

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

        // Wraparound в perm-таблицу
        const ii = i & 255;
        const jj = j & 255;
        const gi0 = perm[ii + perm[jj]] & 15;
        const gi1 = perm[ii + i1 + perm[jj + j1]] & 15;
        const gi2 = perm[ii + 1 + perm[jj + 1]] & 15;

        // Вклады от 3 углов
        let n0 = 0, n1 = 0, n2 = 0;
        let t0 = 0.5 - x0 * x0 - y0 * y0;
        if (t0 >= 0) {
            t0 *= t0;
            n0 = t0 * t0 * (GRAD2[gi0 * 2] * x0 + GRAD2[gi0 * 2 + 1] * y0);
        }
        let t1 = 0.5 - x1 * x1 - y1 * y1;
        if (t1 >= 0) {
            t1 *= t1;
            n1 = t1 * t1 * (GRAD2[gi1 * 2] * x1 + GRAD2[gi1 * 2 + 1] * y1);
        }
        let t2 = 0.5 - x2 * x2 - y2 * y2;
        if (t2 >= 0) {
            t2 *= t2;
            n2 = t2 * t2 * (GRAD2[gi2 * 2] * x2 + GRAD2[gi2 * 2 + 1] * y2);
        }
        // Сумма вкладов; множитель 70 калибрует диапазон ~[-1..1]
        return 70 * (n0 + n1 + n2);
    }

    /**
     * 3D simplex noise. Возвращает [-1, +1].
     */
    noise3(xin, yin, zin) {
        const perm = this.perm;
        const s = (xin + yin + zin) * F3;
        const i = Math.floor(xin + s);
        const j = Math.floor(yin + s);
        const k = Math.floor(zin + s);
        const t = (i + j + k) * G3;
        const X0 = i - t;
        const Y0 = j - t;
        const Z0 = k - t;
        const x0 = xin - X0;
        const y0 = yin - Y0;
        const z0 = zin - Z0;

        let i1, j1, k1, i2, j2, k2;
        if (x0 >= y0) {
            if (y0 >= z0)      { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0; }
            else if (x0 >= z0) { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1; }
            else               { i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1; }
        } else {
            if (y0 < z0)       { i1 = 0; j1 = 0; k1 = 1; i2 = 0; j2 = 1; k2 = 1; }
            else if (x0 < z0)  { i1 = 0; j1 = 1; k1 = 0; i2 = 0; j2 = 1; k2 = 1; }
            else               { i1 = 0; j1 = 1; k1 = 0; i2 = 1; j2 = 1; k2 = 0; }
        }

        const x1 = x0 - i1 + G3;
        const y1 = y0 - j1 + G3;
        const z1 = z0 - k1 + G3;
        const x2 = x0 - i2 + 2 * G3;
        const y2 = y0 - j2 + 2 * G3;
        const z2 = z0 - k2 + 2 * G3;
        const x3 = x0 - 1 + 3 * G3;
        const y3 = y0 - 1 + 3 * G3;
        const z3 = z0 - 1 + 3 * G3;

        const ii = i & 255;
        const jj = j & 255;
        const kk = k & 255;
        const gi0 = perm[ii + perm[jj + perm[kk]]] % 12;
        const gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12;
        const gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12;
        const gi3 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12;

        let n0 = 0, n1 = 0, n2 = 0, n3 = 0;
        let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
        if (t0 >= 0) {
            t0 *= t0;
            n0 = t0 * t0 * (GRAD3[gi0*3]*x0 + GRAD3[gi0*3+1]*y0 + GRAD3[gi0*3+2]*z0);
        }
        let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
        if (t1 >= 0) {
            t1 *= t1;
            n1 = t1 * t1 * (GRAD3[gi1*3]*x1 + GRAD3[gi1*3+1]*y1 + GRAD3[gi1*3+2]*z1);
        }
        let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
        if (t2 >= 0) {
            t2 *= t2;
            n2 = t2 * t2 * (GRAD3[gi2*3]*x2 + GRAD3[gi2*3+1]*y2 + GRAD3[gi2*3+2]*z2);
        }
        let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
        if (t3 >= 0) {
            t3 *= t3;
            n3 = t3 * t3 * (GRAD3[gi3*3]*x3 + GRAD3[gi3*3+1]*y3 + GRAD3[gi3*3+2]*z3);
        }
        return 32 * (n0 + n1 + n2 + n3);
    }

    /**
     * Фрактальный 2D noise (FBM = Fractional Brownian Motion).
     * Сумма нескольких октав с уменьшающейся амплитудой.
     * Возвращает значение примерно в [-1, +1] (нормализовано).
     */
    fbm2(x, y, octaves = 4, persistence = 0.5, lacunarity = 2.0) {
        let total = 0;
        let amp = 1;
        let freq = 1;
        let norm = 0;
        for (let i = 0; i < octaves; i++) {
            total += this.noise2(x * freq, y * freq) * amp;
            norm += amp;
            amp *= persistence;
            freq *= lacunarity;
        }
        return total / norm;
    }

    /** Фрактальный 3D noise. */
    fbm3(x, y, z, octaves = 4, persistence = 0.5, lacunarity = 2.0) {
        let total = 0;
        let amp = 1;
        let freq = 1;
        let norm = 0;
        for (let i = 0; i < octaves; i++) {
            total += this.noise3(x * freq, y * freq, z * freq) * amp;
            norm += amp;
            amp *= persistence;
            freq *= lacunarity;
        }
        return total / norm;
    }

    /**
     * Domain warping: пропустить координаты через дополнительный noise.
     * Даёт «закрученные» узоры вместо однообразных холмов.
     */
    warp2(x, y, strength = 1, scale = 0.1) {
        const wx = x + this.noise2(x * scale, y * scale) * strength;
        const wy = y + this.noise2(x * scale + 100, y * scale + 100) * strength;
        return { x: wx, y: wy };
    }
}
