/* global React */
// ASCII flow background — uniform monospace grid where each cell's character
// is sampled from a slowly drifting 2D value-noise field. Inspired by the
// hero on openhands.dev: regular grid, low contrast, gentle motion, radial
// vignette so it reads as ambient texture rather than a feature.

const { useEffect, useRef } = React;

// ramp from sparse to dense glyphs — keeps low-density areas almost empty
const CHARS = " ··..-:;+=*xoco?7c1lzZYn298e0fOSVTUhP4wDFdgqbRpm@QAEHKBNWM";

// ---- value noise ------------------------------------------------------------
function makePerm() {
  const p = new Uint8Array(512);
  for (let i = 0; i < 256; i++) p[i] = i;
  for (let i = 255; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [p[i], p[j]] = [p[j], p[i]];
  }
  for (let i = 0; i < 256; i++) p[i + 256] = p[i];
  return p;
}
function fade(t) { return t * t * (3 - 2 * t); }
function makeNoise2D() {
  const perm = makePerm();
  // hash a grid corner to a pseudo-random 0..1
  const grad = (ix, iy) => {
    const h = perm[(ix + perm[iy & 255]) & 255];
    return h / 255;
  };
  return (x, y) => {
    const ix = Math.floor(x), iy = Math.floor(y);
    const fx = x - ix, fy = y - iy;
    const u = fade(fx), v = fade(fy);
    const a = grad(ix, iy);
    const b = grad(ix + 1, iy);
    const c = grad(ix, iy + 1);
    const d = grad(ix + 1, iy + 1);
    return a + (b - a) * u + (c - a) * v + (d - c - b + a) * u * v;
  };
}

function AsciiBackground({
  cell = 13,           // px per cell (square-ish)
  scale = 0.06,        // noise frequency in cell-units
  drift = 0.04,        // noise drift speed (cell units / sec)
  bias = 0.55,         // re-center the noise field; lower = sparser
  contrast = 1.5,      // exponent on density; >1 = more sparse
  opacity = 0.18,      // canvas-level opacity
}) {
  const canvasRef = useRef(null);
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d", { alpha: true });
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const noise = makeNoise2D();

    let cols = 0, rows = 0, w = 0, h = 0;
    let raf = 0;

    function resize() {
      const rect = canvas.getBoundingClientRect();
      w = rect.width; h = rect.height;
      canvas.width = Math.floor(w * dpr);
      canvas.height = Math.floor(h * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.font = `${cell - 1}px ui-monospace, "Geist Mono", "JetBrains Mono", "SF Mono", monospace`;
      ctx.textBaseline = "top";
      cols = Math.ceil(w / cell) + 1;
      rows = Math.ceil(h / cell) + 1;
    }

    function pickColor() {
      const cs = getComputedStyle(document.documentElement);
      return cs.getPropertyValue("--fg").trim() || "#fff";
    }

    function frame(now) {
      const t = (now || 0) * 0.001;
      ctx.clearRect(0, 0, w, h);
      const color = pickColor();
      ctx.fillStyle = color;
      const N = CHARS.length;

      // soft radial vignette: brighter in center, fading toward edges
      const cx = w / 2, cy = h / 2;
      const maxR = Math.hypot(cx, cy);

      const drx = t * drift;        // drift x
      const dry = t * drift * 0.7;  // drift y (slight asymmetry)

      for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
          const px = x * cell;
          const py = y * cell;
          // base density from noise
          let n = noise(x * scale + drx, y * scale + dry);
          // add a second octave for more organic shapes
          n = 0.7 * n + 0.3 * noise((x + 31.4) * scale * 2 + drx * 2,
                                    (y + 17.7) * scale * 2 + dry * 2);
          n = (n - bias);                // recenter
          n = n < 0 ? 0 : n;            // clip negative
          n = Math.pow(n, contrast);     // contrast curve

          // radial falloff so edges fade out
          const r = Math.hypot(px - cx, py - cy) / maxR;
          const fall = Math.max(0, 1 - r * 1.05);
          n *= fall;

          if (n < 0.02) continue;
          const idx = Math.min(N - 1, Math.floor(n * N * 1.4));
          const ch = CHARS[idx];
          if (ch === " " || ch === undefined) continue;

          ctx.globalAlpha = Math.min(1, n * 1.2);
          ctx.fillText(ch, px, py);
        }
      }
      ctx.globalAlpha = 1;
      raf = requestAnimationFrame(frame);
    }

    resize();
    raf = requestAnimationFrame(frame);

    let rT;
    const onResize = () => {
      clearTimeout(rT);
      rT = setTimeout(() => { resize(); }, 150);
    };
    window.addEventListener("resize", onResize);
    const onVis = () => {
      if (document.hidden) cancelAnimationFrame(raf);
      else raf = requestAnimationFrame(frame);
    };
    document.addEventListener("visibilitychange", onVis);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("resize", onResize);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [cell, scale, drift, bias, contrast]);

  return (
    <canvas
      ref={canvasRef}
      className="ascii-bg"
      aria-hidden="true"
      style={{ opacity }}
    />
  );
}

Object.assign(window, { AsciiBackground });
