/*
 * Puckstone reactive orb — WebGL2 orb + background shader, reused from
 * harveywhiting.com and extended with two audio-reactive channels:
 *   uUserLevel  (0..1) -> ORANGE ripples while the user speaks
 *   uAgentLevel (0..1) -> BLUE   ripples while the agent speaks
 * driven hard by volume. One full-screen shader draws the background and the orb.
 *
 * Framework-agnostic: attaches `window.OrbRenderer`. Used by the web voice client
 * and by the macOS app (loaded in a WKWebView). No imports, no build step.
 *
 *   const orb = new OrbRenderer(canvas, { theme: 'dark', orbCenter: [0.5, 0.42], scale: 0.24 });
 *   orb.start();
 *   orb.setLevels(userLevel, agentLevel);   // 0..1 each, call every audio frame
 *   orb.setTheme('light' | 'dark');
 *   orb.setActivity(0..1);                   // optional "thinking" morph
 *   orb.destroy();
 */
(function () {
  "use strict";

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

  const FRAG = `#version 300 es
precision highp float;
out vec4 outColor;

uniform vec3  iResolution;
uniform float iTime;
uniform float uTheme;       // 0.0 = light, 1.0 = dark
uniform vec2  uOrbCenter;   // pixel coords (origin bottom-left)
uniform float uOrbScale;    // pixel radius
uniform float uActivity;    // 0..1 "thinking" hook
uniform float uUserLevel;   // 0..1 mic volume  -> orange ripples
uniform float uAgentLevel;  // 0..1 tts volume  -> blue ripples

#define filmGrainIntensity 0.22

// Speaking tints.
const vec3 ORANGE = vec3(1.00, 0.42, 0.10);
const vec3 BLUE   = vec3(0.16, 0.55, 1.00);

mat2 Rot(float a) { float s = sin(a), c = cos(a); return mat2(c, -s, s, c); }

vec2 bgHash(vec2 p) {
    p = vec2(dot(p, vec2(2127.1, 81.17)), dot(p, vec2(1269.5, 283.37)));
    return fract(sin(p) * 43758.5453);
}
float bgNoise(in vec2 p) {
    vec2 i = floor(p); vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    float n = mix(mix(dot(-1.0 + 2.0 * bgHash(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),
                      dot(-1.0 + 2.0 * bgHash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),
                  mix(dot(-1.0 + 2.0 * bgHash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
                      dot(-1.0 + 2.0 * bgHash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), u.y);
    return 0.5 + 0.5 * n;
}
float filmGrainNoise(in vec2 uv) { return length(bgHash(vec2(uv.x, uv.y))); }

vec3 backgroundLight(in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    float aspectRatio = iResolution.x / iResolution.y;
    vec2 tuv = uv - 0.5;
    float degree = bgNoise(vec2(iTime * 0.05, tuv.x * tuv.y));
    tuv.y *= 1.0 / aspectRatio;
    tuv *= Rot(radians((degree - 0.5) * 720.0 + 180.0));
    tuv.y *= aspectRatio;
    float frequency = 5.0, amplitude = 30.0, speed = iTime * 2.0;
    tuv.x += sin(tuv.y * frequency + speed) / amplitude;
    tuv.y += sin(tuv.x * frequency * 1.5 + speed) / (amplitude * 0.5);
    vec3 amberYellow = vec3(255, 220, 200) / 255.0;
    vec3 deepBlue    = vec3(150, 200, 245) / 255.0;
    vec3 pink        = vec3(250, 218, 243) / 255.0;
    vec3 blue        = vec3(180, 220, 250) / 255.0;
    vec3 layer1 = mix(pink, deepBlue, smoothstep(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
    vec3 layer2 = mix(blue, amberYellow, smoothstep(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
    vec3 color  = mix(layer1, layer2, smoothstep(0.5, -0.3, tuv.y));
    float boundary = 0.5 + 0.18 * (uv.x - 0.5);
    float whiteAmount = 1.0 - smoothstep(boundary - 0.35, boundary, uv.y);
    color = mix(color, vec3(1.0), whiteAmount);
    color = color - filmGrainNoise(uv) * filmGrainIntensity * 0.5;
    return color;
}

vec3 backgroundDark(in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    float aspectRatio = iResolution.x / iResolution.y;
    vec2 tuv = uv - 0.5;
    float degree = bgNoise(vec2(iTime * 0.05, tuv.x * tuv.y));
    tuv.y *= 1.0 / aspectRatio;
    tuv *= Rot(radians((degree - 0.5) * 720.0 + 180.0));
    tuv.y *= aspectRatio;
    float frequency = 5.0, amplitude = 30.0, speed = iTime * 2.0;
    tuv.x += sin(tuv.y * frequency + speed) / amplitude;
    tuv.y += sin(tuv.x * frequency * 1.5 + speed) / (amplitude * 0.5);
    vec3 nearBlack = vec3(2, 4, 10) / 255.0;
    vec3 deepNavy  = vec3(8, 18, 44) / 255.0;
    vec3 darkBlue  = vec3(14, 30, 70) / 255.0;
    vec3 midnight  = vec3(4, 8, 22) / 255.0;
    vec3 layer1 = mix(deepNavy, midnight, smoothstep(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
    vec3 layer2 = mix(darkBlue, nearBlack, smoothstep(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
    vec3 color  = mix(layer1, layer2, smoothstep(0.5, -0.3, tuv.y));
    vec2 centerUV = (fragCoord - uOrbCenter) / iResolution.y;
    float r = length(centerUV);
    float orbGlow = exp(-r * 2.2) * 0.18;
    color += vec3(0.05, 0.12, 0.30) * orbGlow;
    color = color - filmGrainNoise(uv) * filmGrainIntensity * 0.4;
    return color;
}

vec2 rot(vec2 v, float a) { return cos(a) * v + sin(a) * vec2(-v.y, v.x); }
float sfract(float x, float k) { float f = fract(x); return f * smoothstep(1.0, k, f); }
vec3 hue_shift(vec3 col, float hue) {
    return mix(vec3(dot(vec3(0.333), col)), col, cos(hue)) + cross(vec3(0.577), col) * sin(hue);
}

vec3 orb(vec3 p, float t) {
    vec3 v = vec3(0); float x = 0.0, y = 0.0; const float it = 11.0;
    float halo = smoothstep(0.5, 0.0, p.z); vec3 c = vec3(0);
    for (float i = 1.0; i < it + 1.0; ++i) {
        p.xy = rot(p.xy, p.z + t / i * 0.4);
        v = v * 0.5 + 0.5;
        v.xz = rot(v.xz, v.y - x + t / i + p.y);
        p.xy = rot(p.xy, length(v.xy) - x);
        x += sfract(v.z, 0.9 - sin(y * 1.5) * 0.2 + p.z * 0.1) / it / (1.0 + x + x * x);
        y += sfract(-v.z, 0.9 + sin(x) * 0.1) / it;
        c += exp(vec3(0.7, 1.9, 4) * log(x));
    }
    float xy = (x - y) * (x - y);
    c += xy * sqrt(c);
    c = clamp(c, 0.0, 1.0);
    c = hue_shift(c, 0.5 * (1.0 - c.y) * (1.0 - y));
    c = mix(vec3(dot(c, vec3(0.2, 0.7, 0.1))), c, 1.0 + y);
    c = max(c, 0.0);
    c = mix(c, sqrt(c) * 0.7 + 0.3, halo);
    c = mix(c, sqrt(c) * 0.5 + 0.5, sqrt(halo));
    return c;
}

vec3 colorOrb(vec3 n, float theme) {
    vec3 base = sqrt(n);
    float lum    = clamp(dot(base, vec3(0.299, 0.587, 0.114)), 0.0, 1.0);
    float warmth = base.r - base.b;
    float greenness = base.g - 0.5 * (base.r + base.b);
    vec3 dBlack = vec3(0.0), dInk = vec3(0.00, 0.01, 0.04), dNavy = vec3(0.02, 0.06, 0.22);
    vec3 dRoyal = vec3(0.08, 0.25, 0.75), dElectric = vec3(0.20, 0.55, 1.00);
    vec3 dCyanRim = vec3(0.55, 0.88, 1.00), dRimWhite = vec3(0.90, 0.96, 1.00);
    vec3 dark = mix(dBlack, dInk, smoothstep(0.50, 0.60, lum));
    dark = mix(dark, dNavy,     smoothstep(0.60, 0.72, lum));
    dark = mix(dark, dRoyal,    smoothstep(0.72, 0.84, lum));
    dark = mix(dark, dElectric, smoothstep(0.84, 0.92, lum));
    dark = mix(dark, dCyanRim,  smoothstep(0.92, 0.97, lum));
    dark = mix(dark, dRimWhite, smoothstep(0.97, 1.00, lum));
    dark = mix(dark, dRoyal, smoothstep(0.00, -0.20, warmth) * smoothstep(0.65, 0.88, lum) * 0.25);
    vec3 lBlue = vec3(0.25, 0.50, 0.95), lTeal = vec3(0.20, 0.65, 0.85), lCyan = vec3(0.35, 0.72, 0.98);
    vec3 lPurple = vec3(0.85, 0.78, 1.00), lViolet = vec3(0.78, 0.72, 0.98), lIndigo = vec3(0.30, 0.45, 0.92);
    vec3 lWhite = vec3(0.96, 0.97, 1.00);
    vec3 cool = mix(lBlue, lPurple, smoothstep(-0.30, 0.10, warmth));
    vec3 teal = mix(lCyan, lTeal, smoothstep(-0.05, 0.25, greenness));
    vec3 light = mix(cool, teal, smoothstep(0.00, 0.20, greenness));
    light = mix(light, lViolet, smoothstep(0.00, -0.20, warmth) * smoothstep(0.15, 0.55, lum) * 0.55);
    light = mix(light, lIndigo, smoothstep(0.10, -0.15, warmth) * smoothstep(0.25, 0.65, lum) * 0.35);
    light = mix(light, lWhite, smoothstep(0.60, 1.00, lum));
    vec3 lShadow = vec3(0.70, 0.66, 0.92);
    light = mix(lShadow, light, smoothstep(0.00, 0.25, lum));
    return mix(light, dark, theme);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    float uLvl = clamp(uUserLevel, 0.0, 1.0);
    float aLvl = clamp(uAgentLevel, 0.0, 1.0);
    float lvl  = max(uLvl, aLvl);
    vec3 speakTint = ORANGE * uLvl + BLUE * aLvl;

    vec3 bg = mix(backgroundLight(fragCoord), backgroundDark(fragCoord), uTheme);

    // --- Background ripples: colored rings emanating from the orb, scaled by volume ---
    if (lvl > 0.001) {
        vec2 d = (fragCoord - uOrbCenter) / uOrbScale;
        float rr = length(d);
        float rings = sin(rr * 9.0 - iTime * 7.0);
        rings = smoothstep(0.2, 1.0, rings);
        // fade in a shell outside the orb body (avoid washing the center) and with distance
        float shell = smoothstep(0.7, 1.4, rr) * smoothstep(4.5, 1.2, rr);
        bg += speakTint * rings * shell * lvl * 0.7;
    }
    fragColor = vec4(bg, 1.0);

    vec2 centered = fragCoord.xy - uOrbCenter;
    vec2 uv = centered / uOrbScale;
    float t = iTime * (0.35 + uActivity * 0.10);
    float morphSpeed = 1.0 + uActivity * 0.15;
    float phase = mod(iTime * morphSpeed, 30.0);
    float twistPulse = exp(-pow((phase - 15.0) / 4.0, 2.0));

    // Radial breath: gentle idle whisper, but pulses HARD with volume.
    float breath = 1.0 + uActivity * 0.008 * sin(iTime * 1.8)
                 + lvl * 0.07 * sin(iTime * 12.0);

    vec2 uvR = uv;
    float r = length(uvR);
    float theta = atan(uvR.y, uvR.x);

    float c = twistPulse * 0.98;
    float c2 = c * c, c4 = c2 * c2, cos2t = cos(2.0 * theta);
    float disc = c4 * cos2t * cos2t + 1.0 - c4;
    float maxR2 = c2 * cos2t + sqrt(max(disc, 0.0));
    float maxR = sqrt(max(maxR2, 0.0));

    // Perimeter wobble — soft when idle, agitated + rippling when loud.
    float wobble = 0.009 * sin(theta * 3.0 + iTime * 0.45)
                 + 0.005 * sin(theta * 5.0 - iTime * 0.30);
    wobble *= (1.0 + 5.0 * lvl);
    wobble += 0.045 * lvl * sin(theta * 6.0 - iTime * 9.0);
    maxR = max(0.0, maxR * breath + wobble);

    float boundary = r - maxR;
    if (boundary > 0.10) return;

    float rNorm = clamp(r / max(maxR, 0.02), 0.0, 1.05);
    vec2 uvSample = vec2(rNorm * cos(theta), rNorm * sin(theta));
    float l2 = dot(uvSample, uvSample);
    vec3 sn = vec3(uvSample, sqrt(max(0.0, 1.0 - l2)));
    vec3 n = orb(sn, t);

    vec3 oc = colorOrb(n, uTheme);

    // --- Orb surface ripples: traveling orange/blue bands, stronger toward the rim ---
    if (lvl > 0.001) {
        float surf = 0.5 + 0.5 * sin(rNorm * 16.0 - iTime * 8.0);
        surf = pow(surf, 2.0);
        oc += speakTint * surf * lvl * 1.25 * smoothstep(0.05, 1.0, rNorm);
        // brighten the whole orb a touch with loudness so it "lights up"
        oc += speakTint * lvl * 0.20;
    }

    float f = length(vec2(dFdx(boundary), dFdy(boundary)));
    float e = smoothstep(f, -f * 2.0, boundary);
    fragColor = mix(fragColor, vec4(oc, 1.0), e);
}

void main() { mainImage(outColor, gl_FragCoord.xy); }`;

  function compile(gl, type, src) {
    const s = gl.createShader(type);
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      const log = gl.getShaderInfoLog(s);
      console.error("[OrbRenderer] shader compile failed:", log);
      throw new Error("Orb shader compile failed: " + log);
    }
    return s;
  }

  class OrbRenderer {
    constructor(canvas, opts = {}) {
      this.canvas = canvas;
      this.orbCenter = opts.orbCenter || [0.5, 0.42]; // fractions of canvas (x, y-from-top)
      this.scale = opts.scale || 0.24;                // orb radius as fraction of min dimension
      this.maxDpr = opts.maxDpr || 2;

      this._userTarget = 0; this._userValue = 0;
      this._agentTarget = 0; this._agentValue = 0;
      this._themeTarget = opts.theme === "light" ? 0 : 1;
      this._themeValue = this._themeTarget;
      this._activityTarget = 0; this._activityValue = 0;
      this._raf = 0; this._running = false;

      const gl = canvas.getContext("webgl2", { antialias: true, premultipliedAlpha: false });
      if (!gl) throw new Error("WebGL2 not supported");
      this.gl = gl;

      const program = gl.createProgram();
      gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
      gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAG));
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw new Error("Orb program link failed: " + gl.getProgramInfoLog(program));
      }
      this.program = program;

      const vao = gl.createVertexArray();
      gl.bindVertexArray(vao);
      const buf = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buf);
      gl.bufferData(gl.ARRAY_BUFFER,
        new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
      const aPos = gl.getAttribLocation(program, "a_pos");
      gl.enableVertexAttribArray(aPos);
      gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
      this.vao = vao;

      this.u = {
        res: gl.getUniformLocation(program, "iResolution"),
        time: gl.getUniformLocation(program, "iTime"),
        theme: gl.getUniformLocation(program, "uTheme"),
        center: gl.getUniformLocation(program, "uOrbCenter"),
        oScale: gl.getUniformLocation(program, "uOrbScale"),
        activity: gl.getUniformLocation(program, "uActivity"),
        user: gl.getUniformLocation(program, "uUserLevel"),
        agent: gl.getUniformLocation(program, "uAgentLevel"),
      };

      this._onResize = () => this.resize();
      window.addEventListener("resize", this._onResize);
      this.resize();
      this._t0 = (typeof performance !== "undefined" ? performance.now() : Date.now());
      this._lastT = this._t0;
    }

    setLevels(user, agent) {
      this._userTarget = Math.max(0, Math.min(1, user || 0));
      this._agentTarget = Math.max(0, Math.min(1, agent || 0));
    }
    setTheme(theme) { this._themeTarget = theme === "light" ? 0 : 1; }
    setActivity(a) { this._activityTarget = Math.max(0, Math.min(1, a || 0)); }

    resize() {
      const dpr = Math.min(window.devicePixelRatio || 1, this.maxDpr);
      const w = this.canvas.clientWidth || window.innerWidth;
      const h = this.canvas.clientHeight || window.innerHeight;
      this.canvas.width = Math.max(1, Math.floor(w * dpr));
      this.canvas.height = Math.max(1, Math.floor(h * dpr));
    }

    start() {
      if (this._running) return;
      this._running = true;
      const loop = (now) => {
        if (!this._running) return;
        this._frame(now);
        this._raf = requestAnimationFrame(loop);
      };
      this._raf = requestAnimationFrame(loop);
    }
    stop() { this._running = false; if (this._raf) cancelAnimationFrame(this._raf); this._raf = 0; }

    destroy() {
      this.stop();
      window.removeEventListener("resize", this._onResize);
    }

    _frame(now) {
      const gl = this.gl;
      const dt = Math.min(0.05, (now - this._lastT) / 1000);
      this._lastT = now;

      // Fast attack, slower release — lively but not jittery.
      const ramp = (val, target) => {
        const k = target > val ? 14.0 : 5.0;
        return val + (target - val) * Math.min(1, dt * k);
      };
      this._userValue = ramp(this._userValue, this._userTarget);
      this._agentValue = ramp(this._agentValue, this._agentTarget);
      this._themeValue += (this._themeTarget - this._themeValue) * Math.min(1, dt * 6);
      this._activityValue += (this._activityTarget - this._activityValue) * Math.min(1, dt * 4);

      const W = this.canvas.width, H = this.canvas.height;
      const orbScale = Math.min(W, H) * this.scale;
      const time = (now - this._t0) / 1000;
      // Gentle Lissajous drift.
      const drift = orbScale * 0.05;
      const cx = W * this.orbCenter[0] + (Math.sin(time * 0.27) + 0.55 * Math.cos(time * 0.41)) * drift;
      const cy = H * (1 - this.orbCenter[1]) + (Math.cos(time * 0.31) + 0.5 * Math.sin(time * 0.23)) * drift;

      gl.viewport(0, 0, W, H);
      gl.useProgram(this.program);
      gl.bindVertexArray(this.vao);
      gl.uniform3f(this.u.res, W, H, 1.0);
      gl.uniform1f(this.u.time, time);
      gl.uniform1f(this.u.theme, this._themeValue);
      gl.uniform2f(this.u.center, cx, cy);
      gl.uniform1f(this.u.oScale, orbScale);
      gl.uniform1f(this.u.activity, this._activityValue);
      gl.uniform1f(this.u.user, this._userValue);
      gl.uniform1f(this.u.agent, this._agentValue);
      gl.drawArrays(gl.TRIANGLES, 0, 6);
    }
  }

  if (typeof window !== "undefined") window.OrbRenderer = OrbRenderer;
})();
