// util.js — seeded RNG, noise, procgen names, small math helpers.

// ---------- RNG ----------
export 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;
  };
}

export function hashStr(str) {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

// Convenience RNG wrapper bound to a seed.
export class RNG {
  constructor(seed) {
    this.r = mulberry32(typeof seed === 'string' ? hashStr(seed) : seed >>> 0);
  }
  next() { return this.r(); }
  range(a, b) { return a + (b - a) * this.r(); }
  int(a, b) { return Math.floor(this.range(a, b + 1)); }
  pick(arr) { return arr[Math.floor(this.r() * arr.length)]; }
  bool(p = 0.5) { return this.r() < p; }
  sign() { return this.r() < 0.5 ? -1 : 1; }
}

// ---------- math ----------
export const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
export const lerp = (a, b, t) => a + (b - a) * t;
export const smoothstep = (e0, e1, x) => {
  const t = clamp((x - e0) / (e1 - e0), 0, 1);
  return t * t * (3 - 2 * t);
};

// ---------- value/simplex noise (2D + 3D) ----------
// Compact simplex noise (Stefan Gustavson style), seeded permutation.
export class Noise {
  constructor(seed) {
    const rng = mulberry32(typeof seed === 'string' ? hashStr(seed) : seed >>> 0);
    const p = new Uint8Array(256);
    for (let i = 0; i < 256; i++) p[i] = i;
    for (let i = 255; i > 0; i--) {
      const j = Math.floor(rng() * (i + 1));
      const t = p[i]; p[i] = p[j]; p[j] = t;
    }
    this.perm = new Uint8Array(512);
    this.permMod12 = new Uint8Array(512);
    for (let i = 0; i < 512; i++) {
      this.perm[i] = p[i & 255];
      this.permMod12[i] = this.perm[i] % 12;
    }
  }

  static grad3 = new Float32Array([
    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,
  ]);

  noise2D(xin, yin) {
    const F2 = 0.5 * (Math.sqrt(3) - 1);
    const G2 = (3 - Math.sqrt(3)) / 6;
    const { perm, permMod12 } = this;
    const grad3 = Noise.grad3;
    let n0, n1, n2;
    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, Y0 = j - t;
    const x0 = xin - X0, y0 = yin - Y0;
    let i1, j1;
    if (x0 > y0) { i1 = 1; j1 = 0; } else { i1 = 0; j1 = 1; }
    const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
    const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2;
    const ii = i & 255, jj = j & 255;
    let t0 = 0.5 - x0 * x0 - y0 * y0;
    if (t0 < 0) n0 = 0; else {
      const gi0 = permMod12[ii + perm[jj]] * 3;
      t0 *= t0; n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0);
    }
    let t1 = 0.5 - x1 * x1 - y1 * y1;
    if (t1 < 0) n1 = 0; else {
      const gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3;
      t1 *= t1; n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
    }
    let t2 = 0.5 - x2 * x2 - y2 * y2;
    if (t2 < 0) n2 = 0; else {
      const gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3;
      t2 *= t2; n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
    }
    return 70 * (n0 + n1 + n2);
  }

  // fractal brownian motion in 2D, returns ~[-1,1]
  fbm2(x, y, oct = 5, lac = 2, gain = 0.5) {
    let amp = 1, freq = 1, sum = 0, norm = 0;
    for (let o = 0; o < oct; o++) {
      sum += amp * this.noise2D(x * freq, y * freq);
      norm += amp; amp *= gain; freq *= lac;
    }
    return sum / norm;
  }

  // ridged variant for mountainous looks
  ridged2(x, y, oct = 5, lac = 2, gain = 0.5) {
    let amp = 1, freq = 1, sum = 0, norm = 0;
    for (let o = 0; o < oct; o++) {
      const n = 1 - Math.abs(this.noise2D(x * freq, y * freq));
      sum += amp * n * n;
      norm += amp; amp *= gain; freq *= lac;
    }
    return (sum / norm) * 2 - 1;
  }
}

// ---------- procgen names ----------
const SYL_A = ['ka', 'zo', 'vel', 'tor', 'ya', 'qu', 'nim', 'dra', 'sho', 'lu', 'mar', 'ix', 'ae', 'os', 'ven', 'thal', 'or', 'ki', 'um', 'eb'];
const SYL_B = ['ra', 'tu', 'mi', 'sa', 'no', 'la', 'di', 'ron', 'eth', 'ari', 'oss', 'una', 'yx', 'eon', 'is', 'ova', 'ash', 'ul', 'een', 'ade'];
const SUFFIX = ['Prime', 'Tau', 'IX', 'Minor', 'VII', 'Reach', 'II', 'Major', 'Null', 'XII'];

export function planetName(rng) {
  let n = cap(rng.pick(SYL_A)) + rng.pick(SYL_B);
  if (rng.bool(0.55)) n += rng.pick(SYL_A);
  if (rng.bool(0.5)) n += '-' + rng.pick(SUFFIX);
  return n;
}

const FLORA_ADJ = ['Bladder', 'Crystal', 'Lumen', 'Spore', 'Glass', 'Murk', 'Aether', 'Thorn', 'Velvet', 'Drift', 'Ember', 'Frost', 'Hollow', 'Iron', 'Whisper', 'Plume', 'Gossamer', 'Tidal'];
const FLORA_NOUN = ['wort', 'cap', 'frond', 'helix', 'bloom', 'stalk', 'fan', 'pod', 'tendril', 'shroom', 'reed', 'coil', 'crown', 'spire', 'bell', 'vine'];
const FLORA_TAG = ['Helix', 'Cluster', 'Drupe', 'Whorl', 'Nexus', 'Bloom', 'Array', 'Colony'];

export function floraName(rng) {
  let n = rng.pick(FLORA_ADJ) + rng.pick(FLORA_NOUN);
  if (rng.bool(0.4)) n += ' ' + rng.pick(FLORA_TAG);
  return cap(n);
}

const MIN_PRE = ['Cryo', 'Pyro', 'Lume', 'Ferro', 'Halo', 'Vex', 'Aur', 'Duro', 'Kry'];
const MIN_SUF = ['ite', 'um', 'ex', 'ine', 'ar', 'on'];
export function mineralName(rng) {
  return cap(rng.pick(MIN_PRE) + 'rust' + rng.pick(MIN_SUF)).replace('rust', rng.pick(['st', 'cr', 'th', 'gl', 'dr']) + rng.pick(['a', 'o', 'i', 'y']));
}

const BIOMES = [
  'Verdant Toxic', 'Frozen Waste', 'Scorched Dunes', 'Bioluminescent', 'Crimson Mesa',
  'Tranquil Paradise', 'Irradiated Flats', 'Fungal Expanse', 'Glass Tundra', 'Stormbound',
  'Mineral Rich', 'Drowned Lowlands', 'Ashen Steppe', 'Aurora Fields',
];
export function biomeTag(rng) { return rng.pick(BIOMES); }

const GAME_A = ['STELLAR', 'VOID', 'AURORA', 'PALE', 'OUTER', 'DEEP', 'NOVA', 'ASTRAL'];
const GAME_B = ['DRIFT', 'REACH', 'WAKE', 'EXPANSE', 'VOYAGE', 'HORIZON', 'TIDE', 'PASSAGE'];
export function gameName(rng) { return rng.pick(GAME_A) + ' ' + rng.pick(GAME_B); }

function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }

// ---------- soft round sprite for points (motes, sparks) ----------
export function dotCanvas(size = 64) {
  const cv = document.createElement('canvas');
  cv.width = cv.height = size;
  const x = cv.getContext('2d');
  const g = x.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
  g.addColorStop(0, 'rgba(255,255,255,1)');
  g.addColorStop(0.35, 'rgba(255,255,255,0.85)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  x.fillStyle = g;
  x.fillRect(0, 0, size, size);
  return cv;
}
