"use client";

import { useEffect, useRef } from "react";

/**
 * Ein Farbfeld aus einem Fragment-Shader hinter der Bühne.
 *
 * ── WARUM ROHES WEBGL UND KEIN THREE.JS ─────────────────────────────────────
 *
 * Three.js ist eine 3D-Bibliothek: Szenengraph, Kamera, Licht, Material,
 * Lader. Hier wird nichts davon gebraucht — es gibt genau ein Rechteck, das
 * den Ausschnitt füllt, und einen Shader, der jedem Bildpunkt eine Farbe
 * gibt. Das sind zwei Dreiecke. Three.js dafür zu laden hieße, 600 kB für
 * einen Aufruf mitzuschleppen, den man in achtzig Zeilen selbst schreibt.
 *
 * ── DIE FARBEN KOMMEN AUS DEN TOKENS ────────────────────────────────────────
 *
 * Kein Farbwert steht in diesem Shader. Sie werden zur Laufzeit aus
 * `--a2-accent` und `--a2-bg-page` gelesen und als Uniform übergeben. Damit
 * wechselt das Feld mit dem Schema mit, ohne dass es eine zweite Farbtabelle
 * gäbe — genau die Doppelung, die `tokens.css` vermeiden soll.
 *
 * ── WAS PASSIERT, WENN ES NICHT GEHT ────────────────────────────────────────
 *
 * Nichts Sichtbares. Ohne WebGL, ohne JavaScript oder bei einem verlorenen
 * Kontext bleibt der CSS-Verlauf der Bühne stehen — er liegt darunter und
 * wird nie entfernt. Das Feld ist eine Zugabe, keine Bedingung: Der Inhalt
 * liegt darüber und ist von alldem unberührt.
 *
 * ── UND WANN ES ANHÄLT ──────────────────────────────────────────────────────
 *
 * Außer Sicht, bei verborgenem Reiter und bei `prefers-reduced-motion`. Ein
 * Shader, der unter dem Fußbereich weiterrechnet, während niemand hinsieht,
 * ist ein Akku, der leer wird. Bei Bewegungsreduktion wird **ein** Bild
 * gezeichnet und dann angehalten — nicht abgeschaltet: Das Feld ist Fläche,
 * und eine Fläche darf stehen bleiben.
 */

const SCHEITEL = `#version 300 es
in vec2 lage;
void main() { gl_Position = vec4(lage, 0.0, 1.0); }
`;

/*
 * Wertrauschen mit Domänenverzerrung.
 *
 * Zwei Lagen Rauschen, wobei die zweite die Koordinaten der ersten verbiegt.
 * Das ist der Unterschied zwischen „wabert" und „fließt": Ohne die
 * Verzerrung sieht Rauschen aus wie ein Wattebausch, mit ihr wie eine
 * Strömung. Drei Oktaven genügen — mehr sieht man bei dieser Weichheit nicht,
 * kostet aber Rechenzeit auf jedem Bildpunkt.
 */
const SPLITTER = `#version 300 es
precision highp float;

uniform vec2  u_flaeche;
uniform float u_zeit;
uniform vec3  u_akzent;
uniform vec3  u_zweit;
uniform float u_staerke;

out vec4 farbe;

float hash(vec2 p) {
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}

float rauschen(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
             mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}

float fbm(vec2 p) {
  float summe = 0.0;
  float gewicht = 0.5;
  for (int i = 0; i < 3; i++) {
    summe += gewicht * rauschen(p);
    p *= 2.02;
    gewicht *= 0.5;
  }
  return summe;
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_flaeche;
  vec2 p = uv * vec2(u_flaeche.x / u_flaeche.y, 1.0);

  float t = u_zeit * 0.035;

  // Die Verzerrung: Wohin geschaut wird, entscheidet ein zweites Rauschen.
  vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, 1.3 - t)));
  vec2 r = vec2(fbm(p + 3.0 * q + vec2(1.7, 9.2) + 0.15 * t),
                fbm(p + 3.0 * q + vec2(8.3, 2.8) - 0.12 * t));

  float f = fbm(p + 2.4 * r);

  vec3 c = mix(u_akzent, u_zweit, clamp(length(q) * 1.1, 0.0, 1.0));
  c = mix(c, u_akzent, clamp(r.x * r.x * 1.4, 0.0, 1.0));

  // Nach unten und zu den Rändern ausblenden: Das Feld ist Raum hinter dem
  // Text, keine Fläche mit Kante. Eine sichtbare Kante machte daraus ein
  // Bild, und ein Bild müsste etwas bedeuten.
  float rand = smoothstep(1.05, 0.2, length((uv - vec2(0.5, 0.36)) * vec2(1.1, 1.35)));

  // pow(f, 1.4) statt f * f: Quadrieren drueckt alles unter der Mitte fast
  // auf null — nachgemessen kam das Feld dabei auf 6 % mittlere Deckung, also
  // praktisch unsichtbar, mit ein paar hellen Inseln. Der flachere Exponent
  // hebt die Mitte an und laesst die Flaeche zusammenhaengen, ohne die
  // Spitzen zu ueberzeichnen.
  //
  // Keine Backticks in diesem Kommentar: Er steht in einem
  // Template-Literal, und ein Backtick darin beendet die Zeichenkette.
  float staerke = clamp(pow(f, 1.4) * 3.1, 0.0, 1.0) * rand * u_staerke;

  farbe = vec4(c * staerke, staerke);
}
`;

/** `#0a0a0a` oder `rgb(10 10 10)` → `[0.04, 0.04, 0.04]`. */
function alsRgb(wert: string): [number, number, number] {
  const s = wert.trim();
  if (s.startsWith("#")) {
    const h = s.length === 4 ? [...s.slice(1)].map((z) => z + z).join("") : s.slice(1, 7);
    const n = parseInt(h, 16);
    return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
  }
  const zahlen = s.match(/[\d.]+/g)?.map(Number) ?? [0, 0, 0];
  return [(zahlen[0] ?? 0) / 255, (zahlen[1] ?? 0) / 255, (zahlen[2] ?? 0) / 255];
}

function baue(gl: WebGL2RenderingContext) {
  const uebersetze = (art: number, quelle: string) => {
    const s = gl.createShader(art)!;
    gl.shaderSource(s, quelle);
    gl.compileShader(s);
    return s;
  };

  const programm = gl.createProgram()!;
  gl.attachShader(programm, uebersetze(gl.VERTEX_SHADER, SCHEITEL));
  gl.attachShader(programm, uebersetze(gl.FRAGMENT_SHADER, SPLITTER));
  gl.linkProgram(programm);
  if (!gl.getProgramParameter(programm, gl.LINK_STATUS)) return null;

  // Zwei Dreiecke, die den ganzen Ausschnitt decken. Mehr Geometrie gibt es
  // nicht — alles Weitere entscheidet der Shader je Bildpunkt.
  const puffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, puffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);

  const lage = gl.getAttribLocation(programm, "lage");
  gl.enableVertexAttribArray(lage);
  gl.vertexAttribPointer(lage, 2, gl.FLOAT, false, 0, 0);

  return programm;
}

export function Schimmer({
  className,
  staerke,
}: {
  className?: string;
  /**
   * Überschreibt die aus der Grundhelligkeit abgeleitete Stärke.
   *
   * Für Seiten, auf denen das Feld leiser sein soll als auf der Bühne —
   * Registerdaten brauchen keinen Auftritt. Ohne Angabe entscheidet die
   * Helligkeit des Grundes.
   */
  staerke?: number;
}) {
  const tuch = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const leinwand = tuch.current;
    if (!leinwand) return;

    const gl = leinwand.getContext("webgl2", {
      alpha: true,
      antialias: false,
      // Kein Tiefenpuffer und kein Erhalt: Wir zeichnen eine Fläche, kein
      // Gebäude, und lesen nie zurück. Beides spart Speicher auf dem Gerät.
      depth: false,
      stencil: false,
      preserveDrawingBuffer: false,
      powerPreference: "low-power",
    });
    if (!gl) return;

    const programm = baue(gl);
    if (!programm) return;
    gl.useProgram(programm);

    const wo = (name: string) => gl.getUniformLocation(programm, name);
    const uFlaeche = wo("u_flaeche");
    const uZeit = wo("u_zeit");
    const uAkzent = wo("u_akzent");
    const uZweit = wo("u_zweit");
    const uStaerke = wo("u_staerke");

    /*
     * Farben und Stärke aus den Tokens — und zwar bei **jedem** Wechsel neu.
     *
     * Einmal beim Montieren zu lesen sah richtig aus und war es nicht: Die
     * Seite hat einen Umschalter für die Darstellung, und nach einem Wechsel
     * lief das Feld in den Farben des alten Schemas weiter.
     *
     * Die Stärke hängt an der Helligkeit des Grundes. Auf Schwarz trägt ein
     * Farbfeld viel; auf Weiß wird dasselbe Feld zur Trübung, die unter dem
     * Text liegt — nachgemessen 19 % mittlere Deckung in Hell gegenüber 33 %
     * in Dunkel, und in Hell sah es nach Schmutz aus statt nach Licht.
     */
    const liesFarben = () => {
      const stil = getComputedStyle(document.documentElement);
      const hole = (name: string) => stil.getPropertyValue(name).trim();

      gl.uniform3fv(uAkzent, alsRgb(hole("--a2-accent") || "#0070f3"));
      gl.uniform3fv(uZweit, alsRgb(hole("--a2-agent") || hole("--a2-accent") || "#7c5cff"));

      const grund = alsRgb(hole("--a2-bg-page") || hole("--a2-bg") || "#000");
      const helligkeit = 0.2126 * grund[0] + 0.7152 * grund[1] + 0.0722 * grund[2];
      gl.uniform1f(uStaerke, staerke ?? (helligkeit > 0.5 ? 0.3 : 0.85));
    };

    liesFarben();

    const ruhig = window.matchMedia("(prefers-reduced-motion: reduce)");

    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    let laufend = 0;
    let sichtbar = true;

    const messen = () => {
      // Auf zwei begrenzt: Ein Retina-Schirm mit drei wäre die neunfache
      // Zahl an Bildpunkten für ein weiches Farbfeld, dem man den
      // Unterschied nicht ansieht.
      const dichte = Math.min(window.devicePixelRatio || 1, 2);
      const b = Math.floor(leinwand.clientWidth * dichte);
      const h = Math.floor(leinwand.clientHeight * dichte);
      if (leinwand.width !== b || leinwand.height !== h) {
        leinwand.width = b;
        leinwand.height = h;
        gl.viewport(0, 0, b, h);
      }
      gl.uniform2f(uFlaeche, leinwand.width, leinwand.height);
    };

    const zeichne = (zeit: number) => {
      messen();
      gl.uniform1f(uZeit, zeit / 1000);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };

    const schleife = (zeit: number) => {
      zeichne(zeit);
      laufend = requestAnimationFrame(schleife);
    };

    const starte = () => {
      if (laufend || !sichtbar || ruhig.matches) return;
      laufend = requestAnimationFrame(schleife);
    };
    const halte = () => {
      if (!laufend) return;
      cancelAnimationFrame(laufend);
      laufend = 0;
    };

    // Ein Bild in jedem Fall — auch bei Bewegungsreduktion und auch, bevor
    // die Schleife anläuft. Sonst bliebe die Fläche einen Wimpernschlag leer.
    zeichne(0);

    const beobachter = new IntersectionObserver(([eintrag]) => {
      sichtbar = eintrag.isIntersecting;
      if (sichtbar) starte();
      else halte();
    });
    beobachter.observe(leinwand);

    const beiReiter = () => (document.hidden ? halte() : starte());
    document.addEventListener("visibilitychange", beiReiter);

    // Der Umschalter setzt `data-theme` an der Wurzel. Ohne diesen Beobachter
    // liefe das Feld nach einem Wechsel in den alten Farben weiter.
    const schemaWaechter = new MutationObserver(() => {
      liesFarben();
      // Ein Bild sofort, damit der Wechsel auch dann greift, wenn die
      // Schleife gerade angehalten ist — außer Sicht oder bei
      // Bewegungsreduktion.
      zeichne(performance.now());
    });
    schemaWaechter.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["data-theme"],
    });
    ruhig.addEventListener("change", () => (ruhig.matches ? halte() : starte()));

    // Ein verlorener Kontext — Grafiktreiber schlafen, Reiter im Hintergrund
    // zu lange — würde sonst als schwarze Fläche stehen bleiben.
    const beiVerlust = (e: Event) => {
      e.preventDefault();
      halte();
      leinwand.style.opacity = "0";
    };
    leinwand.addEventListener("webglcontextlost", beiVerlust);

    starte();

    return () => {
      halte();
      beobachter.disconnect();
      schemaWaechter.disconnect();
      document.removeEventListener("visibilitychange", beiReiter);
      leinwand.removeEventListener("webglcontextlost", beiVerlust);
      /*
       * **Hier wird der Kontext bewusst nicht weggeworfen.**
       *
       * `loseContext()` sah nach sauberem Aufräumen aus und war der Grund,
       * warum in der Entwicklung nie etwas zu sehen war: React ruft Effekte
       * im strengen Modus doppelt auf — montieren, aufräumen, montieren. Nach
       * dem Aufräumen liefert `getContext` **denselben**, nun toten Kontext
       * zurück, und der zweite Durchgang zeichnet ins Nichts. Genau so
       * verhält es sich auch bei jedem schnellen Seitenwechsel.
       *
       * Nötig ist es ohnehin nicht: Die Leinwand verlässt mit dem Bauteil den
       * Baum, und der Browser gibt den Kontext mit ihr frei. `loseContext`
       * ist für Leinwände gedacht, die stehen bleiben.
       */
    };
  }, [staerke]);

  return (
    <canvas
      ref={tuch}
      aria-hidden
      className={className}
      /* Der Inhalt liegt darüber und muss anklickbar bleiben. */
      style={{ pointerEvents: "none" }}
    />
  );
}
