/**
 * Deterministic scalar fields for Orby.
 *
 * Coordinate convention
 * ---------------------
 * `x` and `y` are continuous field-space coordinates (normally normalized to
 * -1..1 across a centered glyph), and `time` is an arbitrary monotonically
 * increasing value, conventionally seconds. Every named field returns a finite
 * value in 0..1. Options are optional plain objects and are never mutated.
 *
 * The hot sampling path is allocation-free. `FieldKit` owns its permutation
 * table and a few scalar scratch values; pass an output array to `curl2` when
 * calling that vector helper in an animation loop.
 */

export const TAU = Math.PI * 2;
export const BAYER8_SIZE = 8;

/** Clamp `value` to an inclusive range. Non-finite values become `min`. */
export function clamp(value, min = 0, max = 1) {
  if (!Number.isFinite(value)) return Number.isFinite(min) ? min : 0;
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

/** Linear interpolation without implicit clamping. */
export function lerp(a, b, amount) {
  return a + (b - a) * amount;
}

/** Hermite interpolation from 0 to 1 between `edge0` and `edge1`. */
export function smoothstep(edge0, edge1, value) {
  if (!Number.isFinite(value)) return 0;
  if (edge0 === edge1) return value < edge0 ? 0 : 1;
  const t = clamp((value - edge0) / (edge1 - edge0));
  return t * t * (3 - 2 * t);
}

/** Quintic interpolation from 0 to 1 between `edge0` and `edge1`. */
export function smootherstep(edge0, edge1, value) {
  if (!Number.isFinite(value)) return 0;
  if (edge0 === edge1) return value < edge0 ? 0 : 1;
  const t = clamp((value - edge0) / (edge1 - edge0));
  return t * t * t * (t * (t * 6 - 15) + 10);
}

/** Positive fractional part, including for negative inputs. */
export function fract(value) {
  if (!Number.isFinite(value)) return 0;
  return value - Math.floor(value);
}

/** Standard 8x8 Bayer ordered-dither matrix, containing each value 0..63. */
export const BAYER8 = Object.freeze([
  Object.freeze([0, 48, 12, 60, 3, 51, 15, 63]),
  Object.freeze([32, 16, 44, 28, 35, 19, 47, 31]),
  Object.freeze([8, 56, 4, 52, 11, 59, 7, 55]),
  Object.freeze([40, 24, 36, 20, 43, 27, 39, 23]),
  Object.freeze([2, 50, 14, 62, 1, 49, 13, 61]),
  Object.freeze([34, 18, 46, 30, 33, 17, 45, 29]),
  Object.freeze([10, 58, 6, 54, 9, 57, 5, 53]),
  Object.freeze([42, 26, 38, 22, 41, 25, 37, 21]),
]);

/** Return the centered Bayer threshold for an integer pixel coordinate. */
export function bayer8(x, y) {
  const ix = Number.isFinite(x) ? Math.floor(x) & 7 : 0;
  const iy = Number.isFinite(y) ? Math.floor(y) & 7 : 0;
  return (BAYER8[iy][ix] + 0.5) / 64;
}

export const bayerThreshold = bayer8;

/**
 * Quantize a 0..1 value with ordered dithering.
 * `levels=2` produces a binary pixel switch; larger values produce terraces.
 */
export function orderedDither(value, x, y, levels = 2) {
  const count = clamp(Math.floor(levels), 2, 256);
  const scaled = clamp(value) * (count - 1);
  const low = Math.floor(scaled);
  const high = Math.min(count - 1, low + 1);
  const mix = scaled - low;
  return (mix > bayer8(x, y) ? high : low) / (count - 1);
}

/** Stable 32-bit hash for numeric, string, bigint, boolean, or null seeds. */
export function hashSeed(seed = 0) {
  let hash = 0x811c9dc5;

  if (typeof seed === "number" && Number.isFinite(seed)) {
    if (Number.isInteger(seed)) {
      hash ^= seed >>> 0;
      hash = Math.imul(hash, 0x01000193);
    } else {
      const text = String(seed);
      for (let i = 0; i < text.length; i += 1) {
        hash ^= text.charCodeAt(i);
        hash = Math.imul(hash, 0x01000193);
      }
    }
  } else {
    const text = typeof seed === "string" ? seed : String(seed ?? 0);
    for (let i = 0; i < text.length; i += 1) {
      hash ^= text.charCodeAt(i);
      hash = Math.imul(hash, 0x01000193);
    }
  }

  hash ^= hash >>> 16;
  hash = Math.imul(hash, 0x7feb352d);
  hash ^= hash >>> 15;
  hash = Math.imul(hash, 0x846ca68b);
  hash ^= hash >>> 16;
  return hash >>> 0;
}

const EMPTY_OPTIONS = Object.freeze({});
const SQRT2 = Math.SQRT2;
const INV_255 = 1 / 255;

function finite(value, fallback = 0) {
  return Number.isFinite(value) ? value : fallback;
}

function option(options, key, fallback, min = -Infinity, max = Infinity) {
  if (!options) return fallback;
  const value = options[key];
  if (!Number.isFinite(value)) return fallback;
  return clamp(value, min, max);
}

function optionInt(options, key, fallback, min, max) {
  return Math.floor(option(options, key, fallback, min, max));
}

function contrast(value, amount) {
  return clamp((value - 0.5) * amount + 0.5);
}

function fade(value) {
  return value * value * value * (value * (value * 6 - 15) + 10);
}

function grad2(hash, x, y) {
  switch (hash & 7) {
    case 0: return x;
    case 1: return -x;
    case 2: return y;
    case 3: return -y;
    case 4: return (x + y) * Math.SQRT1_2;
    case 5: return (-x + y) * Math.SQRT1_2;
    case 6: return (x - y) * Math.SQRT1_2;
    default: return (-x - y) * Math.SQRT1_2;
  }
}

function grad3(hash, x, y, z) {
  const h = hash & 15;
  const u = h < 8 ? x : y;
  const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
  return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}

function ensureKit(kit) {
  return kit instanceof FieldKit ? kit : DEFAULT_FIELD_KIT;
}

/**
 * Seeded noise and named-field sampler.
 *
 * Construct once and reuse:
 * `const fields = new FieldKit("product-icon");`
 * `const alpha = fields.sample("electric", x, y, seconds);`
 */
export class FieldKit {
  constructor(seed = 0) {
    const initialSeed =
      seed && typeof seed === "object" && "seed" in seed ? seed.seed : seed;

    this.seed = hashSeed(initialSeed);
    this.permutation = new Uint8Array(512);

    // Scalar scratch state keeps cellular/curl samplers allocation-free.
    this._cellF1 = 0;
    this._cellF2 = 0;
    this._cellHash = 0;
    this._curlX = 0;
    this._curlY = 0;
    this._curlMagnitude = 0;

    this._buildPermutation();
  }

  /** Rebuild the lookup table from a new seed and return this instance. */
  reseed(seed = 0) {
    this.seed = hashSeed(seed);
    this._buildPermutation();
    return this;
  }

  setSeed(seed = 0) {
    return this.reseed(seed);
  }

  /** Create an independent sampler with the same seed. */
  clone() {
    const copy = new FieldKit(0);
    copy.seed = this.seed;
    copy.permutation.set(this.permutation);
    return copy;
  }

  /** Frozen list of canonical named scalar fields. */
  list() {
    return FIELD_IDS;
  }

  has(id) {
    return resolveFieldId(id) !== null;
  }

  /**
   * Return the standalone sampler function for an ID, or undefined.
   * Standalone samplers accept `(x, y, time, options, kit)`.
   */
  get(id) {
    const resolved = resolveFieldId(id);
    return resolved === null ? undefined : FIELDS[resolved];
  }

  /** Sample a canonical field or alias. Unknown IDs safely return 0. */
  sample(id, x, y, time = 0, options = EMPTY_OPTIONS) {
    const resolved = resolveFieldId(id);
    if (resolved === null) return 0;
    const value = FIELDS[resolved](
      finite(x),
      finite(y),
      finite(time),
      options || EMPTY_OPTIONS,
      this,
    );
    return clamp(value);
  }

  /**
   * Seeded lattice hash in 0..1. Useful for deterministic sprite decisions.
   * Inputs are treated as integer lattice coordinates.
   */
  hash2(x, y, salt = 0) {
    const p = this.permutation;
    const xi = finite(Math.floor(x)) & 255;
    const yi = finite(Math.floor(y)) & 255;
    const si = finite(Math.floor(salt)) & 255;
    return p[xi + p[yi + p[si]]] * INV_255;
  }

  /** Signed 2D gradient noise in -1..1. */
  signedNoise2(x, y) {
    return this._signedNoise2(finite(x), finite(y));
  }

  /** 2D gradient noise remapped to 0..1. */
  noise2(x, y) {
    return this._signedNoise2(finite(x), finite(y)) * 0.5 + 0.5;
  }

  /** Signed 3D gradient noise in -1..1; use z as animation time. */
  signedNoise3(x, y, z) {
    return this._signedNoise3(finite(x), finite(y), finite(z));
  }

  /** 3D gradient noise remapped to 0..1. */
  noise3(x, y, z) {
    return this._signedNoise3(finite(x), finite(y), finite(z)) * 0.5 + 0.5;
  }

  /** Seeded smooth value noise in 0..1. */
  value2(x, y) {
    x = finite(x);
    y = finite(y);
    const x0 = Math.floor(x);
    const y0 = Math.floor(y);
    const tx = fade(x - x0);
    const ty = fade(y - y0);
    const p = this.permutation;
    const ax = x0 & 255;
    const ay = y0 & 255;
    const bx = (x0 + 1) & 255;
    const by = (y0 + 1) & 255;
    const a = p[ax + p[ay]] * INV_255;
    const b = p[bx + p[ay]] * INV_255;
    const c = p[ax + p[by]] * INV_255;
    const d = p[bx + p[by]] * INV_255;
    return clamp(lerp(lerp(a, b, tx), lerp(c, d, tx), ty));
  }

  /** Fractal gradient noise in 0..1. */
  fbmNoise(x, y, z = 0, options = EMPTY_OPTIONS) {
    const octaves = optionInt(options, "octaves", 5, 1, 9);
    const lacunarity = option(options, "lacunarity", 2, 1.01, 4);
    const gain = option(options, "gain", 0.5, 0.05, 0.95);
    return this._fbmSigned(
      finite(x),
      finite(y),
      finite(z),
      octaves,
      lacunarity,
      gain,
    ) * 0.5 + 0.5;
  }

  /** Multi-octave ridged noise in 0..1. */
  ridgedNoise(x, y, z = 0, options = EMPTY_OPTIONS) {
    return this._ridged(
      finite(x),
      finite(y),
      finite(z),
      optionInt(options, "octaves", 5, 1, 9),
      option(options, "lacunarity", 2.05, 1.01, 4),
      option(options, "gain", 0.52, 0.05, 0.95),
    );
  }

  /**
   * Normalized curl vector of a scalar noise potential.
   * Supply `out` (array or typed array) to avoid the one fallback allocation.
   */
  curl2(x, y, z = 0, epsilon = 0.0125, out) {
    this._setCurl(
      finite(x),
      finite(y),
      finite(z),
      clamp(finite(epsilon, 0.0125), 0.0001, 0.25),
    );
    const target = out || new Float32Array(2);
    target[0] = this._curlX;
    target[1] = this._curlY;
    return target;
  }

  /** Nearest animated Worley feature distance, normalized to 0..1. */
  cellular2(x, y, time = 0, options = EMPTY_OPTIONS) {
    this._setCellular(
      finite(x),
      finite(y),
      finite(time),
      option(options, "jitter", 0.88, 0, 1),
      option(options, "motion", 0.1, 0, 0.45),
    );
    return clamp(Math.sqrt(this._cellF1) / SQRT2);
  }

  /** Difference between smoothly seed-morphed Voronoi features, in 0..1. */
  voronoi2(x, y, time = 0, options = EMPTY_OPTIONS) {
    this._setCellular(
      finite(x),
      finite(y),
      finite(time) + option(options, "phase", 0, -10000, 10000),
      option(options, "jitter", 0.88, 0, 1),
      option(options, "motion", 0.16, 0, 0.45),
      option(options, "seedRate", 0.9, 0, 8),
    );
    return clamp((Math.sqrt(this._cellF2) - Math.sqrt(this._cellF1)) / SQRT2);
  }

  // Named convenience methods mirror `sample` without string lookup.
  fbm(x, y, time = 0, options) {
    return fbm(x, y, time, options, this);
  }

  ridged(x, y, time = 0, options) {
    return ridged(x, y, time, options, this);
  }

  domainWarp(x, y, time = 0, options) {
    return domainWarp(x, y, time, options, this);
  }

  curl(x, y, time = 0, options) {
    return curl(x, y, time, options, this);
  }

  flow(x, y, time = 0, options) {
    return flow(x, y, time, options, this);
  }

  worley(x, y, time = 0, options) {
    return worley(x, y, time, options, this);
  }

  voronoi(x, y, time = 0, options) {
    return voronoi(x, y, time, options, this);
  }

  plasma(x, y, time = 0, options) {
    return plasma(x, y, time, options, this);
  }

  interference(x, y, time = 0, options) {
    return interference(x, y, time, options, this);
  }

  vortex(x, y, time = 0, options) {
    return vortex(x, y, time, options, this);
  }

  metaballs(x, y, time = 0, options) {
    return metaballs(x, y, time, options, this);
  }

  caustics(x, y, time = 0, options) {
    return caustics(x, y, time, options, this);
  }

  strata(x, y, time = 0, options) {
    return strata(x, y, time, options, this);
  }

  radar(x, y, time = 0, options) {
    return radar(x, y, time, options, this);
  }

  constellation(x, y, time = 0, options) {
    return constellation(x, y, time, options, this);
  }

  liquid(x, y, time = 0, options) {
    return liquid(x, y, time, options, this);
  }

  electric(x, y, time = 0, options) {
    return electric(x, y, time, options, this);
  }

  ripple(x, y, time = 0, options) {
    return ripple(x, y, time, options, this);
  }

  kaleidoscope(x, y, time = 0, options) {
    return kaleidoscope(x, y, time, options, this);
  }

  _buildPermutation() {
    const p = this.permutation;
    for (let i = 0; i < 256; i += 1) p[i] = i;

    let state = this.seed || 0x6d2b79f5;
    for (let i = 255; i > 0; i -= 1) {
      state += 0x6d2b79f5;
      let random = state;
      random = Math.imul(random ^ (random >>> 15), random | 1);
      random ^= random + Math.imul(random ^ (random >>> 7), random | 61);
      random = (random ^ (random >>> 14)) >>> 0;
      const j = Math.floor((random / 0x100000000) * (i + 1));
      const swap = p[i];
      p[i] = p[j];
      p[j] = swap;
    }

    for (let i = 0; i < 256; i += 1) p[i + 256] = p[i];
  }

  _signedNoise2(x, y) {
    const x0 = Math.floor(x);
    const y0 = Math.floor(y);
    const xf = x - x0;
    const yf = y - y0;
    const u = fade(xf);
    const v = fade(yf);
    const p = this.permutation;
    const xi = x0 & 255;
    const yi = y0 & 255;

    const aa = p[xi + p[yi]];
    const ba = p[xi + 1 + p[yi]];
    const ab = p[xi + p[yi + 1]];
    const bb = p[xi + 1 + p[yi + 1]];

    const low = lerp(grad2(aa, xf, yf), grad2(ba, xf - 1, yf), u);
    const high = lerp(
      grad2(ab, xf, yf - 1),
      grad2(bb, xf - 1, yf - 1),
      u,
    );
    return clamp(lerp(low, high, v) * 1.55, -1, 1);
  }

  _signedNoise3(x, y, z) {
    const x0 = Math.floor(x);
    const y0 = Math.floor(y);
    const z0 = Math.floor(z);
    const xf = x - x0;
    const yf = y - y0;
    const zf = z - z0;
    const u = fade(xf);
    const v = fade(yf);
    const w = fade(zf);
    const p = this.permutation;
    const xi = x0 & 255;
    const yi = y0 & 255;
    const zi = z0 & 255;

    const a = p[xi] + yi;
    const aa = p[a] + zi;
    const ab = p[a + 1] + zi;
    const b = p[xi + 1] + yi;
    const ba = p[b] + zi;
    const bb = p[b + 1] + zi;

    const zLow = lerp(
      lerp(
        grad3(p[aa], xf, yf, zf),
        grad3(p[ba], xf - 1, yf, zf),
        u,
      ),
      lerp(
        grad3(p[ab], xf, yf - 1, zf),
        grad3(p[bb], xf - 1, yf - 1, zf),
        u,
      ),
      v,
    );
    const zHigh = lerp(
      lerp(
        grad3(p[aa + 1], xf, yf, zf - 1),
        grad3(p[ba + 1], xf - 1, yf, zf - 1),
        u,
      ),
      lerp(
        grad3(p[ab + 1], xf, yf - 1, zf - 1),
        grad3(p[bb + 1], xf - 1, yf - 1, zf - 1),
        u,
      ),
      v,
    );
    return clamp(lerp(zLow, zHigh, w) * 0.94, -1, 1);
  }

  _fbmSigned(x, y, z, octaves, lacunarity, gain) {
    let sum = 0;
    let amplitude = 0.5;
    let normalization = 0;

    for (let octave = 0; octave < octaves; octave += 1) {
      sum += this._signedNoise3(x, y, z) * amplitude;
      normalization += amplitude;

      // Rotate and offset between octaves to suppress axial lattice artifacts.
      const nextX = (x * 0.8 - y * 0.6) * lacunarity + 17.17;
      y = (x * 0.6 + y * 0.8) * lacunarity - 9.23;
      x = nextX;
      z = z * lacunarity + 5.71;
      amplitude *= gain;
    }

    return normalization > 0 ? clamp(sum / normalization, -1, 1) : 0;
  }

  _ridged(x, y, z, octaves, lacunarity, gain) {
    let sum = 0;
    let amplitude = 0.5;
    let normalization = 0;
    let weight = 1;

    for (let octave = 0; octave < octaves; octave += 1) {
      let ridge = 1 - Math.abs(this._signedNoise3(x, y, z));
      ridge *= ridge;
      ridge *= weight;
      weight = clamp(ridge * 2.25);
      sum += ridge * amplitude;
      normalization += amplitude;

      const nextX = (x * 0.764 - y * 0.645) * lacunarity + 11.37;
      y = (x * 0.645 + y * 0.764) * lacunarity + 3.19;
      x = nextX;
      z = z * lacunarity - 4.31;
      amplitude *= gain;
    }

    return normalization > 0 ? clamp(sum / normalization) : 0;
  }

  _setCurl(x, y, z, epsilon) {
    const dY =
      (this._signedNoise3(x, y + epsilon, z) -
        this._signedNoise3(x, y - epsilon, z)) /
      (epsilon * 2);
    const dX =
      (this._signedNoise3(x + epsilon, y, z) -
        this._signedNoise3(x - epsilon, y, z)) /
      (epsilon * 2);
    const vx = dY;
    const vy = -dX;
    const magnitude = Math.hypot(vx, vy);
    this._curlMagnitude = Number.isFinite(magnitude) ? magnitude : 0;

    if (magnitude > 1e-9 && Number.isFinite(magnitude)) {
      this._curlX = vx / magnitude;
      this._curlY = vy / magnitude;
    } else {
      this._curlX = 0;
      this._curlY = 0;
    }
  }

  _setCellular(x, y, time, jitter, motion, seedRate) {
    const baseX = Math.floor(x);
    const baseY = Math.floor(y);
    let f1 = Infinity;
    let f2 = Infinity;
    let nearestHash = 0;
    const p = this.permutation;
    const morphing = seedRate !== undefined && motion > 0;
    const seedPhase = morphing ? time * seedRate : 0;
    const seedEpoch = Math.floor(seedPhase);
    const seedU = morphing ? fract(seedPhase) : 0;
    const seedU2 = seedU * seedU;
    const seedU3 = seedU2 * seedU;
    const seedB0 = ((1 - seedU) * (1 - seedU) * (1 - seedU)) / 6;
    const seedB1 = (4 - 6 * seedU2 + 3 * seedU3) / 6;
    const seedB2 = (1 + 3 * seedU + 3 * seedU2 - 3 * seedU3) / 6;
    const seedB3 = seedU3 / 6;
    const seedSalt0 = morphing ? p[(seedEpoch - 1) & 255] : 0;
    const seedSalt1 = morphing ? p[seedEpoch & 255] : 0;
    const seedSalt2 = morphing ? p[(seedEpoch + 1) & 255] : 0;
    const seedSalt3 = morphing ? p[(seedEpoch + 2) & 255] : 0;
    const anchorScale = morphing ? Math.max(0, 1 - motion * 2) : 1;

    for (let oy = -1; oy <= 1; oy += 1) {
      const cellY = baseY + oy;
      const py = cellY & 255;
      for (let ox = -1; ox <= 1; ox += 1) {
        const cellX = baseX + ox;
        const px = cellX & 255;
        const hashA = p[px + p[py]];
        const hashB = p[px + p[py + 71]];
        const hashC = p[px + p[py + 149]];

        let featureX = 0.5 + (hashA * INV_255 - 0.5) * jitter * anchorScale;
        let featureY = 0.5 + (hashB * INV_255 - 0.5) * jitter * anchorScale;
        if (morphing) {
          const offsetX =
            (p[hashA + seedSalt0] * seedB0 +
              p[hashA + seedSalt1] * seedB1 +
              p[hashA + seedSalt2] * seedB2 +
              p[hashA + seedSalt3] * seedB3) *
              INV_255 *
              2 -
            1;
          const offsetY =
            (p[hashB + seedSalt0] * seedB0 +
              p[hashB + seedSalt1] * seedB1 +
              p[hashB + seedSalt2] * seedB2 +
              p[hashB + seedSalt3] * seedB3) *
              INV_255 *
              2 -
            1;
          featureX += offsetX * motion;
          featureY += offsetY * motion;
        } else {
          const phase = hashC * INV_255 * TAU;
          featureX += Math.sin(time + phase) * motion;
          featureY += Math.cos(time * 0.91 + phase) * motion;
        }
        featureX = clamp(featureX, 0.015, 0.985);
        featureY = clamp(featureY, 0.015, 0.985);

        const dx = cellX + featureX - x;
        const dy = cellY + featureY - y;
        const distance = dx * dx + dy * dy;

        if (distance < f1) {
          f2 = f1;
          f1 = distance;
          nearestHash = hashC;
        } else if (distance < f2) {
          f2 = distance;
        }
      }
    }

    this._cellF1 = Number.isFinite(f1) ? f1 : 0;
    this._cellF2 = Number.isFinite(f2) ? f2 : this._cellF1;
    this._cellHash = nearestHash;
  }
}

/** Soft multi-octave gradient noise. */
export function fbm(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 2.35, 0.001, 256);
  const speed = option(options, "speed", 0.18, -20, 20);
  const value =
    kit._fbmSigned(
      finite(x) * frequency,
      finite(y) * frequency,
      finite(time) * speed + option(options, "phase", 0, -10000, 10000),
      optionInt(options, "octaves", 5, 1, 9),
      option(options, "lacunarity", 2, 1.01, 4),
      option(options, "gain", 0.5, 0.05, 0.95),
    ) *
      0.5 +
    0.5;
  return contrast(value, option(options, "contrast", 1.08, 0, 8));
}

/** Sharp mountain/filament ridges with octave feedback. */
export function ridged(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 2.7, 0.001, 256);
  const value = kit._ridged(
    finite(x) * frequency,
    finite(y) * frequency,
    finite(time) * option(options, "speed", 0.22, -20, 20),
    optionInt(options, "octaves", 5, 1, 9),
    option(options, "lacunarity", 2.04, 1.01, 4),
    option(options, "gain", 0.53, 0.05, 0.95),
  );
  return contrast(value, option(options, "contrast", 1.32, 0, 8));
}

/** fBm evaluated through a second pair of animated fBm coordinate fields. */
export function domainWarp(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 2.1, 0.001, 256);
  const speed = option(options, "speed", 0.2, -20, 20);
  const strength = option(options, "warp", 0.72, 0, 6);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const pz = finite(time) * speed;
  const qx = kit._fbmSigned(px + 5.2, py + 1.3, pz, 3, 2, 0.5);
  const qy = kit._fbmSigned(px - 2.8, py + 8.1, pz + 3.4, 3, 2, 0.5);
  const value =
    kit._fbmSigned(
      px + qx * strength,
      py + qy * strength,
      pz + (qx - qy) * 0.22,
      optionInt(options, "octaves", 5, 1, 9),
      option(options, "lacunarity", 2.02, 1.01, 4),
      option(options, "gain", 0.5, 0.05, 0.95),
    ) *
      0.5 +
    0.5;
  return contrast(value, option(options, "contrast", 1.18, 0, 8));
}

/** Curl magnitude of animated gradient noise, useful as turbulent density. */
export function curl(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 2.2, 0.001, 256);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const pz = finite(time) * option(options, "speed", 0.18, -20, 20);
  kit._setCurl(
    px,
    py,
    pz,
    option(options, "epsilon", 0.018, 0.0001, 0.2),
  );
  const density =
    1 -
    Math.exp(
      -kit._curlMagnitude * option(options, "strength", 0.72, 0.001, 20),
    );
  const modulation =
    kit._fbmSigned(px * 0.63, py * 0.63, pz + 4.7, 3, 2, 0.5) * 0.5 + 0.5;
  return contrast(
    density * 0.74 + modulation * 0.26,
    option(options, "contrast", 1.2, 0, 8),
  );
}

/** Animated ribbons advected along a normalized curl-noise direction. */
export function flow(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 2.15, 0.001, 256);
  const speed = option(options, "speed", 0.24, -20, 20);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const pz = finite(time) * speed;
  kit._setCurl(px * 0.72, py * 0.72, pz, 0.02);
  const warp = option(options, "warp", 0.65, 0, 6);
  const ax = px + kit._curlX * warp;
  const ay = py + kit._curlY * warp;
  const base = kit._fbmSigned(ax, ay, pz, 4, 2.03, 0.5);
  const directional =
    ax * kit._curlX * 0.37 + ay * kit._curlY * 0.37 + base * 1.35;
  const ribbon =
    0.5 +
    Math.sin(
      directional * TAU -
        pz * option(options, "travel", 1.7, -20, 20),
    ) *
      0.5;
  const value = ribbon * 0.72 + (base * 0.5 + 0.5) * 0.28;
  return contrast(value, option(options, "contrast", 1.28, 0, 8));
}

/** Animated Worley feature islands (bright centers, dark gaps). */
export function worley(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 3.4, 0.001, 256);
  kit._setCellular(
    finite(x) * frequency,
    finite(y) * frequency,
    finite(time) * option(options, "speed", 0.52, -20, 20),
    option(options, "jitter", 0.9, 0, 1),
    option(options, "motion", 0.1, 0, 0.45),
  );
  const distance = Math.sqrt(kit._cellF1) / SQRT2;
  const value =
    1 -
    smoothstep(
      option(options, "inner", 0.035, 0, 1),
      option(options, "outer", 0.62, 0.001, 2),
      distance,
    );
  return contrast(value, option(options, "contrast", 1.1, 0, 8));
}

/** Voronoi borders whose feature sites ease between deterministic seed states. */
export function voronoi(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 3.25, 0.001, 256);
  kit._setCellular(
    finite(x) * frequency,
    finite(y) * frequency,
    finite(time) * option(options, "speed", 0.43, -20, 20) +
      option(options, "phase", 0, -10000, 10000),
    option(options, "jitter", 0.92, 0, 1),
    option(options, "motion", 0.16, 0, 0.45),
    option(options, "seedRate", 0.9, 0, 8),
  );
  const edgeDistance = Math.sqrt(kit._cellF2) - Math.sqrt(kit._cellF1);
  const width = option(options, "width", 0.085, 0.001, 1);
  const value = 1 - smoothstep(width, width * 2.75, edgeDistance);
  return contrast(value, option(options, "contrast", 1.32, 0, 8));
}

/** Layered sinusoidal plasma with a noise-driven phase field. */
export function plasma(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 1.45, 0.001, 256);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const phase =
    finite(time) * option(options, "speed", 0.62, -20, 20) +
    option(options, "phase", 0, -10000, 10000);
  const noise = kit._fbmSigned(px * 0.7, py * 0.7, phase * 0.18, 3, 2, 0.5);
  const a = Math.sin(px * TAU + phase + noise * 1.6);
  const b = Math.sin(py * TAU * 1.13 - phase * 1.17 - noise * 1.35);
  const c = Math.sin((px + py) * TAU * 0.61 + phase * 0.71 + noise * 2.1);
  const d = Math.sin(Math.hypot(px - 0.5, py - 0.5) * TAU * 2.2 - phase);
  return contrast(
    0.5 + (a + b + c + d) * 0.125,
    option(options, "contrast", 1.12, 0, 8),
  );
}

/** Traveling circular waves from multiple moving emitters. */
export function interference(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 8.5, 0.01, 256);
  const phase = finite(time) * option(options, "speed", 2.1, -50, 50);
  const px = finite(x);
  const py = finite(y);
  const sourceA = phase * 0.23 + kit.seed * 1e-7;
  const sourceB = -phase * 0.19 + kit.seed * 1.7e-7;
  const ax = 0.24 + Math.sin(sourceA) * 0.09;
  const ay = 0.35 + Math.cos(sourceA * 0.87) * 0.11;
  const bx = 0.76 + Math.cos(sourceB) * 0.1;
  const by = 0.64 + Math.sin(sourceB * 1.09) * 0.09;
  const da = Math.hypot(px - ax, py - ay);
  const db = Math.hypot(px - bx, py - by);
  const waveA = Math.sin(da * frequency * TAU - phase);
  const waveB = Math.sin(db * frequency * TAU * 1.037 - phase * 1.11);
  const beat = Math.sin((da - db) * frequency * TAU * 0.53 + phase * 0.31);
  return contrast(
    0.5 + (waveA + waveB) * 0.19 + beat * 0.12,
    option(options, "contrast", 1.2, 0, 8),
  );
}

/** Rotating noisy spiral arms around a configurable center. */
export function vortex(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const centerX = option(options, "centerX", 0, -1000, 1000);
  const centerY = option(options, "centerY", 0, -1000, 1000);
  const frequency = option(options, "frequency", 1, 0.001, 256);
  const dx = (finite(x) - centerX) * frequency;
  const dy = (finite(y) - centerY) * frequency;
  const radius = Math.hypot(dx, dy);
  const angle = Math.atan2(dy, dx);
  const phase = finite(time) * option(options, "speed", 1.4, -50, 50);
  const turbulence = kit._fbmSigned(
    dx * 2.1 + 4.2,
    dy * 2.1 - 3.7,
    phase * 0.11,
    4,
    2,
    0.5,
  );
  const arms = optionInt(options, "arms", 4, 1, 16);
  const turns = option(options, "turns", 3.8, -30, 30);
  const spiral =
    angle * arms + radius * turns * TAU + turbulence * 2.2 - phase;
  const value = 0.5 + Math.sin(spiral) * 0.5;
  const limit = option(options, "radius", 1.15, 0.01, 20);
  const envelope = 1 - smoothstep(limit * 0.72, limit, radius);
  return contrast(
    value * (0.58 + envelope * 0.42),
    option(options, "contrast", 1.25, 0, 8),
  );
}

/** Smooth union of deterministic, independently moving metaballs. */
export function metaballs(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 1, 0.001, 256);
  const px = fract(finite(x) * frequency);
  const py = fract(finite(y) * frequency);
  const phase = finite(time) * option(options, "speed", 0.7, -20, 20);
  const count = optionInt(options, "count", 6, 2, 16);
  const radius = option(options, "radius", 0.115, 0.005, 0.5);
  const radiusSquared = radius * radius;
  let influence = 0;

  for (let i = 0; i < count; i += 1) {
    const seedX = kit.hash2(i, 17, 41);
    const seedY = kit.hash2(i, 79, 113);
    const seedP = kit.hash2(i, 151, 197) * TAU;
    const cx = fract(seedX + Math.sin(phase * (0.52 + seedY) + seedP) * 0.17);
    const cy = fract(seedY + Math.cos(phase * (0.48 + seedX) + seedP) * 0.17);
    let dx = Math.abs(px - cx);
    let dy = Math.abs(py - cy);
    dx = Math.min(dx, 1 - dx);
    dy = Math.min(dy, 1 - dy);
    const distanceSquared = dx * dx + dy * dy;
    influence += radiusSquared / (distanceSquared + radiusSquared);
  }

  const threshold = option(options, "threshold", 0.72, 0.05, 8);
  return contrast(
    smoothstep(threshold * 0.55, threshold * 1.35, influence),
    option(options, "contrast", 1.15, 0, 8),
  );
}

/** Refractive, sharp cellular light bands resembling water caustics. */
export function caustics(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 4, 0.001, 256);
  const phase = finite(time) * option(options, "speed", 0.56, -20, 20);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const warpX = kit._fbmSigned(px * 0.41, py * 0.41, phase * 0.2, 3, 2, 0.5);
  const warpY = kit._fbmSigned(
    px * 0.41 + 8.7,
    py * 0.41 - 2.4,
    phase * 0.2,
    3,
    2,
    0.5,
  );
  kit._setCellular(
    px + warpX * 0.42,
    py + warpY * 0.42,
    phase,
    0.96,
    0.12,
  );
  const delta = Math.sqrt(kit._cellF2) - Math.sqrt(kit._cellF1);
  const width = option(options, "width", 0.075, 0.001, 0.5);
  const cellularLine = 1 - smoothstep(width, width * 3.1, delta);
  const noiseLine =
    1 -
    smoothstep(
      0.04,
      0.3,
      Math.abs(
        kit._signedNoise3(px * 0.72 + warpX, py * 0.72 + warpY, phase * 0.24),
      ),
    );
  return contrast(
    Math.max(cellularLine, noiseLine * 0.68),
    option(options, "contrast", 1.45, 0, 8),
  );
}

/** Noise-warped sedimentary bands with controllable slope and sharpness. */
export function strata(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 1, 0.001, 256);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const phase = finite(time) * option(options, "speed", 0.12, -20, 20);
  const warp = kit._fbmSigned(px * 1.7, py * 1.1, phase, 5, 2, 0.5);
  const grain = kit._signedNoise3(px * 6.3, py * 2.2, phase * 0.7);
  const bands = option(options, "bands", 8, 0.1, 128);
  const slope = option(options, "slope", 0.12, -20, 20);
  const position =
    (py +
      px * slope +
      warp * option(options, "warp", 0.16, 0, 5) -
      phase * 0.03) *
    bands;
  let value = 0.5 + Math.sin(position * TAU + grain * 0.35) * 0.5;
  value = Math.pow(value, option(options, "sharpness", 1.5, 0.1, 12));
  value = value * 0.86 + (grain * 0.5 + 0.5) * 0.14;
  return contrast(value, option(options, "contrast", 1.18, 0, 8));
}

/** Rotating radar beam, concentric rings, and deterministic target blips. */
export function radar(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const dx = finite(x) - option(options, "centerX", 0, -1000, 1000);
  const dy = finite(y) - option(options, "centerY", 0, -1000, 1000);
  const radius = Math.hypot(dx, dy);
  const angle = Math.atan2(dy, dx);
  const sweep =
    finite(time) * option(options, "speed", 1.15, -50, 50) +
    option(options, "phase", 0, -10000, 10000);
  let angleDelta = angle - sweep;
  angleDelta = Math.atan2(Math.sin(angleDelta), Math.cos(angleDelta));
  const beamWidth = option(options, "beamWidth", 0.18, 0.005, Math.PI);
  const beam = Math.exp(-Math.abs(angleDelta) / beamWidth);
  const limit = option(options, "radius", 1.05, 0.01, 20);
  const envelope = 1 - smoothstep(limit * 0.86, limit, radius);
  const rings = option(options, "rings", 5, 1, 64);
  const ringPhase = fract((radius / limit) * rings);
  const ringDistance = Math.min(ringPhase, 1 - ringPhase);
  const ringLine =
    (1 -
      smoothstep(
        option(options, "ringWidth", 0.025, 0.001, 0.49),
        0.12,
        ringDistance,
      )) *
    envelope *
    0.34;
  let blip = 0;
  const targets = optionInt(options, "targets", 7, 0, 20);

  for (let i = 0; i < targets; i += 1) {
    const tx = kit.hash2(i, 31, 97) * 1.72 - 0.86;
    const ty = kit.hash2(i, 83, 181) * 1.72 - 0.86;
    const bx = finite(x) - tx;
    const by = finite(y) - ty;
    const spot = Math.exp(
      -(bx * bx + by * by) /
        option(options, "blipSize", 0.00085, 0.00001, 0.1),
    );
    if (spot > blip) blip = spot;
  }

  return clamp(
    Math.max(beam * envelope * 0.78, ringLine, blip * (0.28 + beam * 0.72)),
  );
}

/** Twinkling cellular stars connected by faint Voronoi filaments. */
export function constellation(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 4.2, 0.001, 256);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const phase = finite(time) * option(options, "speed", 1.7, -50, 50);
  kit._setCellular(px, py, 0, option(options, "jitter", 0.96, 0, 1), 0);
  const nearest = Math.sqrt(kit._cellF1);
  const edge = Math.sqrt(kit._cellF2) - nearest;
  const starSize = option(options, "starSize", 0.12, 0.005, 1);
  const star = 1 - smoothstep(starSize * 0.16, starSize, nearest);
  const twinkle =
    0.68 +
    0.32 *
      Math.sin(phase + kit._cellHash * INV_255 * TAU + nearest * 8.0);
  const lineWidth = option(options, "lineWidth", 0.035, 0.001, 0.5);
  const network =
    (1 - smoothstep(lineWidth, lineWidth * 3.8, edge)) *
    option(options, "lineOpacity", 0.28, 0, 1);
  return contrast(
    Math.max(star * twinkle, network),
    option(options, "contrast", 1.3, 0, 8),
  );
}

/** Layered, domain-warped water surface with traveling highlights. */
export function liquid(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 2.3, 0.001, 256);
  const phase = finite(time) * option(options, "speed", 0.35, -20, 20);
  const px = finite(x) * frequency;
  const py = finite(y) * frequency;
  const qx = kit._fbmSigned(px * 0.72, py * 0.72, phase, 4, 2, 0.52);
  const qy = kit._fbmSigned(
    px * 0.72 + 6.2,
    py * 0.72 - 1.7,
    phase + 2.1,
    4,
    2,
    0.52,
  );
  const warp = option(options, "warp", 0.58, 0, 6);
  const surface = kit._fbmSigned(
    px + qx * warp,
    py + qy * warp,
    phase * 0.72,
    optionInt(options, "octaves", 5, 1, 9),
    2.03,
    0.51,
  );
  const wave =
    0.5 +
    Math.sin((py + qx * 0.42) * TAU * 1.35 - phase * 2.4) * 0.5;
  const highlight = Math.pow(
    1 - Math.abs(kit._signedNoise3(px * 1.8 + qy, py * 1.8, phase) || 0),
    5,
  );
  return contrast(
    (surface * 0.5 + 0.5) * 0.52 + wave * 0.31 + highlight * 0.17,
    option(options, "contrast", 1.18, 0, 8),
  );
}

/** Repeating branching lightning channels with traveling spark intensity. */
export function electric(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const frequency = option(options, "frequency", 1.6, 0.001, 256);
  const px = fract(finite(x) * frequency) - 0.5;
  const py = finite(y) * frequency;
  const phase = finite(time) * option(options, "speed", 1.8, -50, 50);
  const path =
    kit._fbmSigned(py * 0.46, phase * 0.17, phase * 0.11, 6, 2.12, 0.54) *
    option(options, "wander", 0.26, 0, 0.48);
  const forkNoise = kit._fbmSigned(
    py * 0.93 + 8.4,
    phase * 0.23,
    phase * 0.19,
    4,
    2,
    0.5,
  );
  const width = option(options, "width", 0.025, 0.001, 0.3);
  const main = Math.exp(-Math.abs(px - path) / width);
  const forkOffset = 0.1 + Math.abs(forkNoise) * 0.17;
  const forkGate = smoothstep(-0.2, 0.65, forkNoise);
  const forkA =
    Math.exp(-Math.abs(px - path - forkOffset) / (width * 1.8)) * forkGate;
  const forkB =
    Math.exp(-Math.abs(px - path + forkOffset) / (width * 1.8)) *
    (1 - forkGate);
  const pulse =
    0.67 +
    0.33 *
      Math.sin(
        py * TAU * option(options, "travel", 2.4, -30, 30) - phase * 4.1,
      );
  const sparks = kit._ridged(px * 14, py * 8, phase * 0.5, 3, 2.2, 0.46);
  return contrast(
    Math.max(main, forkA * 0.7, forkB * 0.7) * pulse * (0.72 + sparks * 0.28),
    option(options, "contrast", 1.65, 0, 8),
  );
}

/** Noise-distorted radial rings radiating from a configurable origin. */
export function ripple(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const centerX = option(options, "centerX", 0, -1000, 1000);
  const centerY = option(options, "centerY", 0, -1000, 1000);
  const dx = finite(x) - centerX;
  const dy = finite(y) - centerY;
  const phase = finite(time) * option(options, "speed", 1.5, -50, 50);
  const distortion = kit._fbmSigned(dx * 4, dy * 4, phase * 0.12, 4, 2, 0.5);
  const radius =
    Math.hypot(dx, dy) +
    distortion * option(options, "warp", 0.035, 0, 1);
  const rings = option(options, "frequency", 11, 0.01, 256);
  const wave = 0.5 + Math.sin(radius * rings * TAU - phase * TAU) * 0.5;
  const envelope =
    1 -
    smoothstep(
      option(options, "radius", 1.15, 0.01, 20) * 0.82,
      option(options, "radius", 1.15, 0.01, 20),
      radius,
    );
  return contrast(
    Math.pow(wave, option(options, "sharpness", 2.1, 0.1, 16)) * envelope,
    option(options, "contrast", 1.2, 0, 8),
  );
}

/** Folded polar noise producing rotating mandala-like symmetry. */
export function kaleidoscope(x, y, time = 0, options = EMPTY_OPTIONS, kit) {
  kit = ensureKit(kit);
  options = options || EMPTY_OPTIONS;
  const dx = finite(x) - option(options, "centerX", 0, -1000, 1000);
  const dy = finite(y) - option(options, "centerY", 0, -1000, 1000);
  const radius = Math.hypot(dx, dy);
  const segments = optionInt(options, "segments", 8, 2, 32);
  const sector = TAU / segments;
  let angle =
    Math.atan2(dy, dx) +
    finite(time) * option(options, "speed", 0.32, -20, 20);
  angle = fract(angle / sector) * sector;
  angle = Math.abs(angle - sector * 0.5);
  const px = Math.cos(angle) * radius;
  const py = Math.sin(angle) * radius;
  const frequency = option(options, "frequency", 7, 0.001, 256);
  const noise = kit._fbmSigned(
    px * frequency,
    py * frequency,
    finite(time) * 0.16,
    optionInt(options, "octaves", 5, 1, 9),
    2.03,
    0.5,
  );
  const rings =
    0.5 +
    Math.sin(
      radius * TAU * option(options, "rings", 5, 0, 64) + noise * 2.3,
    ) *
      0.5;
  return contrast(
    (noise * 0.5 + 0.5) * 0.58 + rings * 0.42,
    option(options, "contrast", 1.24, 0, 8),
  );
}

/**
 * Canonical named samplers. Each function accepts
 * `(x, y, time = 0, options = {}, kit = DEFAULT_FIELD_KIT)` and returns 0..1.
 */
export const FIELDS = Object.freeze({
  fbm,
  ridged,
  "domain-warp": domainWarp,
  curl,
  flow,
  worley,
  voronoi,
  plasma,
  interference,
  vortex,
  metaballs,
  caustics,
  strata,
  radar,
  constellation,
  liquid,
  electric,
  ripple,
  kaleidoscope,
});

export const FIELD_IDS = Object.freeze(Object.keys(FIELDS));

/** Compatibility aliases accepted by `FieldKit.sample`, `get`, and `has`. */
export const FIELD_ALIASES = Object.freeze({
  noise: "fbm",
  turbulence: "fbm",
  ridge: "ridged",
  domainWarp: "domain-warp",
  domain_warp: "domain-warp",
  warp: "domain-warp",
  "curl-flow": "flow",
  curlFlow: "flow",
  cellular: "worley",
  cells: "voronoi",
  cell: "voronoi",
  water: "liquid",
  lightning: "electric",
  waves: "ripple",
  mandala: "kaleidoscope",
});

function resolveFieldId(id) {
  if (typeof id !== "string") return null;
  if (Object.prototype.hasOwnProperty.call(FIELDS, id)) return id;
  if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, id)) {
    return FIELD_ALIASES[id];
  }

  // Normalize only the uncommon path so canonical per-pixel sampling allocates
  // no temporary strings.
  const normalized = id.trim().toLowerCase().replace(/[\s_]+/g, "-");
  if (Object.prototype.hasOwnProperty.call(FIELDS, normalized)) {
    return normalized;
  }
  if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, normalized)) {
    return FIELD_ALIASES[normalized];
  }
  return null;
}

/** Shared seed-0 kit for the standalone sampler functions. */
export const DEFAULT_FIELD_KIT = new FieldKit(0);

/** Factory form for consumers that prefer functions over constructors. */
export function createFieldKit(seed = 0) {
  return new FieldKit(seed);
}

export default FieldKit;
