/**
 * Dependency-free seeded 2D Perlin noise + fractal Brownian motion (fBm).
 *
 * This is the same procedural-terrain technique rpgtextmier used in 2D
 * (octaves / persistence / lacunarity), now feeding a 3D heightfield. Pure and
 * deterministic from a seed — the sim is authoritative, so the same seed must
 * always yield the same world (the client can regenerate it identically).
 */
'use strict';

/** mulberry32 — small fast seeded PRNG. */
function mulberry32(seed) {
  let a = seed >>> 0;
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10);
}
function lerp(a, b, t) {
  return a + t * (b - a);
}
function grad(hash, x, y) {
  // 8 gradient directions
  switch (hash & 7) {
    case 0: return x + y;
    case 1: return x - y;
    case 2: return -x + y;
    case 3: return -x - y;
    case 4: return x;
    case 5: return -x;
    case 6: return y;
    default: return -y;
  }
}

/** Returns a noise2(x, y) ∈ [-1, 1], deterministic from `seed`. */
function makeNoise2D(seed) {
  const rng = mulberry32(seed);
  const p = new Uint8Array(512);
  const perm = new Uint8Array(256);
  for (let i = 0; i < 256; i++) perm[i] = i;
  for (let i = 255; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    const tmp = perm[i];
    perm[i] = perm[j];
    perm[j] = tmp;
  }
  for (let i = 0; i < 512; i++) p[i] = perm[i & 255];

  return function noise2(x, y) {
    const X = Math.floor(x) & 255;
    const Y = Math.floor(y) & 255;
    const xf = x - Math.floor(x);
    const yf = y - Math.floor(y);
    const u = fade(xf);
    const v = fade(yf);
    const aa = p[p[X] + Y];
    const ab = p[p[X] + Y + 1];
    const ba = p[p[X + 1] + Y];
    const bb = p[p[X + 1] + Y + 1];
    const x1 = lerp(grad(aa, xf, yf), grad(ba, xf - 1, yf), u);
    const x2 = lerp(grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1), u);
    return lerp(x1, x2, v); // ~[-1, 1]
  };
}

/**
 * Fractal Brownian motion over a noise2 function.
 * @param {(x:number,y:number)=>number} noise2
 * @param {{octaves?:number, persistence?:number, lacunarity?:number, scale?:number}} opts
 * @returns {(x:number,y:number)=>number} value ∈ ~[-1, 1]
 */
function makeFbm(noise2, opts = {}) {
  const octaves = opts.octaves ?? 5;
  const persistence = opts.persistence ?? 0.5;
  const lacunarity = opts.lacunarity ?? 2.0;
  const scale = opts.scale ?? 0.012;
  return function fbm(x, y) {
    let amp = 1;
    let freq = scale;
    let sum = 0;
    let norm = 0;
    for (let o = 0; o < octaves; o++) {
      sum += amp * noise2(x * freq, y * freq);
      norm += amp;
      amp *= persistence;
      freq *= lacunarity;
    }
    return sum / norm;
  };
}

module.exports = { mulberry32, makeNoise2D, makeFbm };
