/**
 * A square scalar field over the slice's UV space. Everything the player does to
 * the toast — browning, spread, damage — is one of these, and everything the judge
 * measures is a statistic over one of these.
 */
export class Field {
  readonly n: number;
  readonly data: Float32Array;

  constructor(n: number, fill = 0) {
    this.n = n;
    this.data = new Float32Array(n * n);
    if (fill !== 0) this.data.fill(fill);
  }

  idx(x: number, y: number): number {
    return y * this.n + x;
  }

  /** Bilinear sample in UV space (0..1). */
  sample(u: number, v: number): number {
    const n = this.n;
    const fx = Math.min(Math.max(u * (n - 1), 0), n - 1);
    const fy = Math.min(Math.max(v * (n - 1), 0), n - 1);
    const x0 = Math.floor(fx);
    const y0 = Math.floor(fy);
    const x1 = Math.min(x0 + 1, n - 1);
    const y1 = Math.min(y0 + 1, n - 1);
    const tx = fx - x0;
    const ty = fy - y0;
    const d = this.data;
    const a = d[y0 * n + x0] * (1 - tx) + d[y0 * n + x1] * tx;
    const b = d[y1 * n + x0] * (1 - tx) + d[y1 * n + x1] * tx;
    return a * (1 - ty) + b * ty;
  }

  /**
   * Apply `fn` to every texel within `radius` (UV units) of (u,v), weighted by a
   * smooth falloff. Returns the total weight touched — callers use it to conserve
   * mass (a knife can only deposit what it's carrying).
   */
  brush(u: number, v: number, radius: number, fn: (i: number, w: number) => void): number {
    const n = this.n;
    const cx = u * (n - 1);
    const cy = v * (n - 1);
    const r = radius * (n - 1);
    const r2 = r * r;
    const x0 = Math.max(0, Math.floor(cx - r));
    const x1 = Math.min(n - 1, Math.ceil(cx + r));
    const y0 = Math.max(0, Math.floor(cy - r));
    const y1 = Math.min(n - 1, Math.ceil(cy + r));
    let total = 0;
    for (let y = y0; y <= y1; y++) {
      for (let x = x0; x <= x1; x++) {
        const dx = x - cx;
        const dy = y - cy;
        const d2 = dx * dx + dy * dy;
        if (d2 > r2) continue;
        const t = 1 - Math.sqrt(d2) / r;
        const w = t * t * (3 - 2 * t); // smoothstep falloff
        fn(y * n + x, w);
        total += w;
      }
    }
    return total;
  }

  /** Blur toward the neighbour average — how a spread self-levels. */
  relax(amount: number, mask?: Field): void {
    if (amount <= 0) return;
    const n = this.n;
    const d = this.data;
    const src = d.slice();
    for (let y = 0; y < n; y++) {
      for (let x = 0; x < n; x++) {
        const i = y * n + x;
        if (mask && mask.data[i] < 0.5) continue;
        const xm = x > 0 ? x - 1 : x;
        const xp = x < n - 1 ? x + 1 : x;
        const ym = y > 0 ? y - 1 : y;
        const yp = y < n - 1 ? y + 1 : y;
        const avg =
          (src[y * n + xm] + src[y * n + xp] + src[ym * n + x] + src[yp * n + x]) * 0.25;
        d[i] = src[i] + (avg - src[i]) * amount;
      }
    }
  }

  /** mean / stdev / max over the masked region. */
  stats(mask: Field): { mean: number; stdev: number; max: number; count: number } {
    const d = this.data;
    const m = mask.data;
    let sum = 0;
    let count = 0;
    let max = 0;
    for (let i = 0; i < d.length; i++) {
      if (m[i] < 0.5) continue;
      sum += d[i];
      if (d[i] > max) max = d[i];
      count++;
    }
    if (count === 0) return { mean: 0, stdev: 0, max: 0, count: 0 };
    const mean = sum / count;
    let acc = 0;
    for (let i = 0; i < d.length; i++) {
      if (m[i] < 0.5) continue;
      const dv = d[i] - mean;
      acc += dv * dv;
    }
    return { mean, stdev: Math.sqrt(acc / count), max, count };
  }

  /** Fraction of the masked region for which `pred` holds. */
  fraction(mask: Field, pred: (v: number) => boolean): number {
    const d = this.data;
    const m = mask.data;
    let hit = 0;
    let count = 0;
    for (let i = 0; i < d.length; i++) {
      if (m[i] < 0.5) continue;
      if (pred(d[i])) hit++;
      count++;
    }
    return count === 0 ? 0 : hit / count;
  }

  clear(v = 0): void {
    this.data.fill(v);
  }
}
