/* global React */

const { useEffect, useRef } = React;

function FaultyTerminal({
  scale               = 2.2,
  gridMul             = [2, 1],
  digitSize           = 2.7,
  timeScale           = 0.5,
  pause               = false,
  scanlineIntensity   = 0.5,
  glitchAmount        = 1,
  flickerAmount       = 1,
  noiseAmp            = 1,
  chromaticAberration = 0,
  dither              = 0,
  curvature           = 0,
  tint                = "#383838",
  mouseReact          = true,
  mouseStrength       = 0.5,
  pageLoadAnimation   = false,
  brightness          = 0.6,
  style,
  className           = "",
}) {
  const containerRef = useRef(null);
  const glRef        = useRef(null); // { gl, U, prog }

  // ── prop-change effect: push uniforms without rebuilding GL context ──────────
  useEffect(() => {
    if (!glRef.current) return;
    const { gl, U, prog } = glRef.current;
    gl.useProgram(prog);
    const [tr, tg, tb] = hexRgb(tint);
    gl.uniform1f(U.uScale,               scale);
    gl.uniform2f(U.uGridMul,             gridMul[0], gridMul[1]);
    gl.uniform1f(U.uDigitSize,           digitSize);
    gl.uniform1f(U.uScanlineIntensity,   scanlineIntensity);
    gl.uniform1f(U.uGlitchAmount,        glitchAmount);
    gl.uniform1f(U.uFlickerAmount,       flickerAmount);
    gl.uniform1f(U.uNoiseAmp,            noiseAmp);
    gl.uniform1f(U.uChromaticAberration, chromaticAberration);
    gl.uniform1f(U.uDither,              ditherVal(dither));
    gl.uniform1f(U.uCurvature,           curvature);
    gl.uniform3f(U.uTint,                tr, tg, tb);
    gl.uniform1f(U.uMouseStrength,       mouseStrength);
    gl.uniform1f(U.uUseMouse,            mouseReact ? 1 : 0);
    gl.uniform1f(U.uBrightness,          brightness);
  }, [scale, gridMul, digitSize, scanlineIntensity, glitchAmount, flickerAmount,
      noiseAmp, chromaticAberration, dither, curvature, tint, mouseStrength,
      mouseReact, brightness]);

  // ── GL setup (runs once) ─────────────────────────────────────────────────────
  useEffect(() => {
    const ctn = containerRef.current;
    if (!ctn) return;

    const canvas = document.createElement("canvas");
    canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
    ctn.appendChild(canvas);

    const gl = canvas.getContext("webgl", { desynchronized: true, antialias: false })
            || canvas.getContext("experimental-webgl");
    if (!gl) { ctn.removeChild(canvas); return; }

    // ── shaders ─────────────────────────────────────────────────────────────
    const VS = `
      attribute vec2 position;
      attribute vec2 uv;
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = vec4(position, 0.0, 1.0);
      }`;

    const FS = `
      precision mediump float;
      varying vec2 vUv;

      uniform float iTime;
      uniform vec3  iResolution;
      uniform float uScale;
      uniform vec2  uGridMul;
      uniform float uDigitSize;
      uniform float uScanlineIntensity;
      uniform float uGlitchAmount;
      uniform float uFlickerAmount;
      uniform float uNoiseAmp;
      uniform float uChromaticAberration;
      uniform float uDither;
      uniform float uCurvature;
      uniform vec3  uTint;
      uniform vec2  uMouse;
      uniform float uMouseStrength;
      uniform float uUseMouse;
      uniform float uPageLoadProgress;
      uniform float uUsePageLoadAnimation;
      uniform float uBrightness;

      float time;

      float hash21(vec2 p){
        p = fract(p * 234.56);
        p += dot(p, p + 34.56);
        return fract(p.x * p.y);
      }
      float noise(vec2 p){
        return sin(p.x * 10.0) * sin(p.y * (3.0 + sin(time * 0.090909))) + 0.2;
      }
      mat2 rotate(float a){ float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }

      float fbm(vec2 p){
        p *= 1.1;
        float f=0.0, amp=0.5*uNoiseAmp;
        f += amp*noise(p); p = rotate(time*0.02)*p*2.0; amp*=0.454545;
        f += amp*noise(p); p = rotate(time*0.02)*p*2.0; amp*=0.454545;
        f += amp*noise(p);
        return f;
      }
      float pattern(vec2 p, out vec2 q, out vec2 r){
        q = vec2(fbm(p+vec2(1.0)),          fbm(rotate(0.1*time)*p+vec2(1.0)));
        r = vec2(fbm(rotate(0.1)*q+vec2(0.0)), fbm(q+vec2(0.0)));
        return fbm(p+r);
      }

      float digit(vec2 p){
        vec2 grid = uGridMul * 15.0;
        vec2 s = floor(p*grid)/grid;
        p = p*grid;
        vec2 q,r;
        float intensity = pattern(s*0.1,q,r)*1.3 - 0.03;
        if(uUseMouse > 0.5){
          vec2 mw = uMouse*uScale;
          float d  = distance(s,mw);
          float mi = exp(-d*8.0)*uMouseStrength*10.0;
          intensity += mi + sin(d*20.0 - iTime*5.0)*0.1*mi;
        }
        if(uUsePageLoadAnimation > 0.5){
          float cr  = fract(sin(dot(s,vec2(12.9898,78.233)))*43758.5453);
          float cp  = clamp((uPageLoadProgress - cr*0.8)/0.2, 0.0, 1.0);
          intensity *= smoothstep(0.0,1.0,cp);
        }
        p = fract(p)*uDigitSize;
        float px5=p.x*5.0, py5=(1.0-p.y)*5.0;
        float x=fract(px5), y=fract(py5);
        float i=floor(py5)-2.0, j=floor(px5)-2.0;
        float isOn = step(0.1, intensity-(i*i+j*j)*0.0625);
        float bri  = isOn*(0.2+y*0.8)*(0.75+x*0.25);
        return step(0.0,p.x)*step(p.x,1.0)*step(0.0,p.y)*step(p.y,1.0)*bri;
      }

      float onOff(float a,float b,float c){
        return step(c,sin(iTime+a*cos(iTime*b)))*uFlickerAmount;
      }
      float displace(vec2 look){
        float y=look.y-mod(iTime*0.25,1.0);
        float w=1.0/(1.0+50.0*y*y);
        return sin(look.y*20.0+iTime)*0.0125*onOff(4.0,2.0,0.8)*(1.0+cos(iTime*60.0))*w;
      }

      vec3 getColor(vec2 p){
        float bar=(step(mod(p.y+time*20.0,1.0),0.2)*0.4+1.0)*uScanlineIntensity;
        float disp=displace(p);
        p.x += disp + (uGlitchAmount!=1.0 ? disp*(uGlitchAmount-1.0) : 0.0);
        float mid=digit(p);
        const float o=0.002;
        float sum=
          digit(p+vec2(-o,-o))+digit(p+vec2(0.,-o))+digit(p+vec2(o,-o))+
          digit(p+vec2(-o, 0.))+digit(p+vec2(0., 0.))+digit(p+vec2(o, 0.))+
          digit(p+vec2(-o, o))+digit(p+vec2(0., o))+digit(p+vec2(o, o));
        return vec3(0.9)*mid + sum*0.1*vec3(1.0)*bar;
      }

      vec2 barrel(vec2 uv){
        vec2 c=uv*2.0-1.0;
        c*=1.0+uCurvature*dot(c,c);
        return c*0.5+0.5;
      }

      void main(){
        time = iTime*0.333333;
        vec2 uv = (uCurvature!=0.0) ? barrel(vUv) : vUv;
        vec2 p  = uv*uScale;
        vec3 col = getColor(p);
        if(uChromaticAberration!=0.0){
          vec2 ca=vec2(uChromaticAberration)/iResolution.xy;
          col.r=getColor(p+ca).r;
          col.b=getColor(p-ca).b;
        }
        col *= uTint*uBrightness;
        if(uDither>0.0) col+=(hash21(gl_FragCoord.xy)-0.5)*(uDither*0.003922);
        gl_FragColor = vec4(col,1.0);
      }`;

    function compileShader(type, src) {
      const s = gl.createShader(type);
      gl.shaderSource(s, src);
      gl.compileShader(s);
      return s;
    }
    const prog = gl.createProgram();
    gl.attachShader(prog, compileShader(gl.VERTEX_SHADER,   VS));
    gl.attachShader(prog, compileShader(gl.FRAGMENT_SHADER, FS));
    gl.linkProgram(prog);
    gl.useProgram(prog);

    // Fullscreen triangle — position + uv matching OGL's Triangle primitive
    const posBuf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
    const posLoc = gl.getAttribLocation(prog, "position");
    gl.enableVertexAttribArray(posLoc);
    gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

    const uvBuf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 2,0, 0,2]), gl.STATIC_DRAW);
    const uvLoc = gl.getAttribLocation(prog, "uv");
    gl.enableVertexAttribArray(uvLoc);
    gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 0, 0);

    // Uniform map
    const U = {};
    ["iTime","iResolution","uScale","uGridMul","uDigitSize","uScanlineIntensity",
     "uGlitchAmount","uFlickerAmount","uNoiseAmp","uChromaticAberration","uDither",
     "uCurvature","uTint","uMouse","uMouseStrength","uUseMouse",
     "uPageLoadProgress","uUsePageLoadAnimation","uBrightness",
    ].forEach(n => { U[n] = gl.getUniformLocation(prog, n); });

    glRef.current = { gl, U, prog };

    // Static uniforms (pageLoad doesn't change after mount)
    gl.uniform1f(U.uUsePageLoadAnimation, pageLoadAnimation ? 1 : 0);
    gl.uniform1f(U.uPageLoadProgress,     pageLoadAnimation ? 0 : 1);

    // Initial prop uniforms
    const [tr, tg, tb] = hexRgb(tint);
    gl.uniform1f(U.uScale,               scale);
    gl.uniform2f(U.uGridMul,             gridMul[0], gridMul[1]);
    gl.uniform1f(U.uDigitSize,           digitSize);
    gl.uniform1f(U.uScanlineIntensity,   scanlineIntensity);
    gl.uniform1f(U.uGlitchAmount,        glitchAmount);
    gl.uniform1f(U.uFlickerAmount,       flickerAmount);
    gl.uniform1f(U.uNoiseAmp,            noiseAmp);
    gl.uniform1f(U.uChromaticAberration, chromaticAberration);
    gl.uniform1f(U.uDither,              ditherVal(dither));
    gl.uniform1f(U.uCurvature,           curvature);
    gl.uniform3f(U.uTint,                tr, tg, tb);
    gl.uniform1f(U.uMouseStrength,       mouseStrength);
    gl.uniform1f(U.uUseMouse,            mouseReact ? 1 : 0);
    gl.uniform1f(U.uBrightness,          brightness);

    // ── mouse — read fresh rect per event (user-triggered, not per-frame) ──────
    // Start off-screen so ripple doesn't fire before the user moves
    const mouse  = { x: -1, y: -1 };
    const smooth = { x: -1, y: -1 };
    const onMove = (e) => {
      const r = ctn.getBoundingClientRect();
      mouse.x = (e.clientX - r.left) / r.width;
      mouse.y = 1 - (e.clientY - r.top) / r.height;
    };
    const onLeave = () => { /* keep mouse at last known position — ripple fades naturally */ };
    if (mouseReact) {
      ctn.addEventListener("mousemove", onMove,  { passive: true });
      ctn.addEventListener("mouseleave", onLeave, { passive: true });
    }

    // ── resize — cap DPR at 1.5 for fill-rate budget ─────────────────────────
    const DPR = Math.min(devicePixelRatio || 1, 1.5);
    const resize = () => {
      canvas.width  = ctn.offsetWidth  * DPR;
      canvas.height = ctn.offsetHeight * DPR;
      gl.viewport(0, 0, canvas.width, canvas.height);
      gl.uniform3f(U.iResolution, canvas.width, canvas.height, canvas.width / canvas.height);
    };
    const ro = new ResizeObserver(resize);
    ro.observe(ctn);
    resize();

    // ── visibility & intersection: pause RAF when hidden ─────────────────────
    let visible  = true;  // in viewport
    let tabVisible = !document.hidden;

    const io = new IntersectionObserver(
      ([e]) => { visible = e.isIntersecting; },
      { threshold: 0 }
    );
    io.observe(canvas);

    const onVisibility = () => { tabVisible = !document.hidden; };
    document.addEventListener("visibilitychange", onVisibility);

    // ── render loop ───────────────────────────────────────────────────────────
    const timeOffset = Math.random() * 100;
    let raf, frozen = 0, loadStart = 0;

    const tick = (now) => {
      raf = requestAnimationFrame(tick);

      // Skip draw when tab hidden or scrolled off-screen
      if (!tabVisible || !visible) return;

      if (!pause) {
        const t = (now * 0.001 + timeOffset) * timeScale;
        gl.uniform1f(U.iTime, t);
        frozen = t;
      } else {
        gl.uniform1f(U.iTime, frozen);
        // Skip redraw when paused — frame is static
        return;
      }

      if (pageLoadAnimation) {
        if (loadStart === 0) loadStart = now;
        gl.uniform1f(U.uPageLoadProgress, Math.min((now - loadStart) / 2000, 1));
      }

      if (mouseReact) {
        smooth.x += (mouse.x - smooth.x) * 0.08;
        smooth.y += (mouse.y - smooth.y) * 0.08;
        gl.uniform2f(U.uMouse, smooth.x, smooth.y);
      }

      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };
    raf = requestAnimationFrame(tick);

    return () => {
      glRef.current = null;
      cancelAnimationFrame(raf);
      ro.disconnect();
      io.disconnect();
      document.removeEventListener("visibilitychange", onVisibility);
      if (mouseReact) {
        ctn.removeEventListener("mousemove",  onMove);
        ctn.removeEventListener("mouseleave", onLeave);
      }
      gl.getExtension("WEBGL_lose_context")?.loseContext();
      if (canvas.parentElement === ctn) ctn.removeChild(canvas);
    };
  }, []);

  return (
    <div
      ref={containerRef}
      className={`faulty-terminal-container ${className}`}
      style={{ position: "relative", overflow: "hidden", ...style }}
    />
  );
}

// ── helpers ────────────────────────────────────────────────────────────────────
function hexRgb(hex) {
  let h = hex.replace("#","").slice(0, 6);
  if (h.length === 3) h = h.split("").map(c => c+c).join("");
  const n = parseInt(h, 16);
  return [(n>>16&255)/255, (n>>8&255)/255, (n&255)/255];
}
function ditherVal(d) { return typeof d === "boolean" ? (d ? 1 : 0) : d; }

Object.assign(window, { FaultyTerminal });
