/* faulty-terminal.jsx — WebGL "faulty terminal" shader backdrop.
 *
 * Ported from React Bits (reactbits.dev · github.com/DavidHDev/react-bits)
 * for this no-build site: the original uses ES-module imports
 * (`import { Renderer } from 'ogl'`, `import ... from 'react'`), which the
 * Babel-standalone pipeline here can't do. Instead this uses the global
 * `React` hooks and the self-hosted OGL bundle that build.html exposes as
 * `window.OGL`. The effect waits for OGL before booting, so script order
 * never matters.
 *
 * Defines a global `FaultyTerminal` component. Loaded via
 * <script type="text/babel"> BEFORE build-page.jsx so BuildHero can use it.
 */

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

const FT_FRAGMENT = `
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 angle)
{
  float c = cos(angle);
  float s = sin(angle);
  return mat2(c, -s, s, c);
}

float fbm(vec2 p)
{
  p *= 1.1;
  float f = 0.0;
  float amp = 0.5 * uNoiseAmp;

  mat2 modify0 = rotate(time * 0.02);
  f += amp * noise(p);
  p = modify0 * p * 2.0;
  amp *= 0.454545;

  mat2 modify1 = rotate(time * 0.02);
  f += amp * noise(p);
  p = modify1 * p * 2.0;
  amp *= 0.454545;

  mat2 modify2 = rotate(time * 0.08);
  f += amp * noise(p);

  return f;
}

float pattern(vec2 p, out vec2 q, out vec2 r) {
  vec2 offset1 = vec2(1.0);
  vec2 offset0 = vec2(0.0);
  mat2 rot01 = rotate(0.1 * time);
  mat2 rot1 = rotate(0.1);

  q = vec2(fbm(p + offset1), fbm(rot01 * p + offset1));
  r = vec2(fbm(rot1 * q + offset0), fbm(q + offset0));
  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 mouseWorld = uMouse * uScale;
        float distToMouse = distance(s, mouseWorld);
        float mouseInfluence = exp(-distToMouse * 8.0) * uMouseStrength * 10.0;
        intensity += mouseInfluence;

        float ripple = sin(distToMouse * 20.0 - iTime * 5.0) * 0.1 * mouseInfluence;
        intensity += ripple;
    }

    if(uUsePageLoadAnimation > 0.5){
        float cellRandom = fract(sin(dot(s, vec2(12.9898, 78.233))) * 43758.5453);
        float cellDelay = cellRandom * 0.8;
        float cellProgress = clamp((uPageLoadProgress - cellDelay) / 0.2, 0.0, 1.0);

        float fadeAlpha = smoothstep(0.0, 1.0, cellProgress);
        intensity *= fadeAlpha;
    }

    p = fract(p);
    p *= uDigitSize;

    float px5 = p.x * 5.0;
    float py5 = (1.0 - p.y) * 5.0;
    float x = fract(px5);
    float y = fract(py5);

    float i = floor(py5) - 2.0;
    float j = floor(px5) - 2.0;
    float n = i * i + j * j;
    float f = n * 0.0625;

    float isOn = step(0.1, intensity - f);
    float brightness = 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) * brightness;
}

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 window = 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)) * window;
}

vec3 getColor(vec2 p){

    float bar = step(mod(p.y + time * 20.0, 1.0), 0.2) * 0.4 + 1.0;
    bar *= uScanlineIntensity;

    float displacement = displace(p);
    p.x += displacement;

    if (uGlitchAmount != 1.0) {
      float extra = displacement * (uGlitchAmount - 1.0);
      p.x += extra;
    }

    float middle = digit(p);

    const float off = 0.002;
    float sum = digit(p + vec2(-off, -off)) + digit(p + vec2(0.0, -off)) + digit(p + vec2(off, -off)) +
                digit(p + vec2(-off, 0.0)) + digit(p + vec2(0.0, 0.0)) + digit(p + vec2(off, 0.0)) +
                digit(p + vec2(-off, off)) + digit(p + vec2(0.0, off)) + digit(p + vec2(off, off));

    vec3 baseColor = vec3(0.9) * middle + sum * 0.1 * vec3(1.0) * bar;
    return baseColor;
}

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

void main() {
    time = iTime * 0.333333;
    vec2 uv = vUv;

    if(uCurvature != 0.0){
      uv = barrel(uv);
    }

    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;
    col *= uBrightness;

    if(uDither > 0.0){
      float rnd = hash21(gl_FragCoord.xy);
      col += (rnd - 0.5) * (uDither * 0.003922);
    }

    gl_FragColor = vec4(col, 1.0);
}
`;

function ftHexToRgb(hex) {
  let h = hex.replace('#', '').trim();
  if (h.length === 3) h = h.split('').map(c => c + c).join('');
  const num = parseInt(h.slice(0, 6), 16);
  return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255];
}

function FaultyTerminal({
  scale = 1,
  gridMul = [2, 1],
  digitSize = 1.5,
  timeScale = 0.3,
  pause = false,
  scanlineIntensity = 0.3,
  glitchAmount = 1,
  flickerAmount = 1,
  noiseAmp = 0,
  chromaticAberration = 0,
  dither = 0,
  curvature = 0.2,
  tint = '#ffffff',
  mouseReact = true,
  mouseStrength = 0.2,
  dpr = Math.min((typeof window !== 'undefined' && window.devicePixelRatio) || 1, 2),
  pageLoadAnimation = true,
  brightness = 1,
  className = '',
  style = {}
}) {
  const { useEffect, useRef, useMemo, useCallback } = React;

  const containerRef = useRef(null);
  const programRef = useRef(null);
  const rendererRef = useRef(null);
  const mouseRef = useRef({ x: 0.5, y: 0.5 });
  const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });
  const frozenTimeRef = useRef(0);
  const rafRef = useRef(0);
  const loadAnimationStartRef = useRef(0);
  const timeOffsetRef = useRef(Math.random() * 100);

  const tintVec = useMemo(() => ftHexToRgb(tint), [tint]);
  const ditherValue = useMemo(() => (typeof dither === 'boolean' ? (dither ? 1 : 0) : dither), [dither]);

  const handleMouseMove = useCallback(e => {
    const ctn = containerRef.current;
    if (!ctn) return;
    const rect = ctn.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = 1 - (e.clientY - rect.top) / rect.height;
    mouseRef.current = { x, y };
  }, []);

  useEffect(() => {
    const ctn = containerRef.current;
    if (!ctn) return;

    let cancelled = false;
    let resizeObserver = null;
    let onMove = null;

    const boot = () => {
      if (cancelled) return;
      const OGL = window.OGL;
      // OGL is loaded as an ES module in build.html; if it hasn't landed
      // yet, retry rather than silently rendering nothing.
      if (!OGL) { setTimeout(boot, 60); return; }
      const { Renderer, Program, Mesh, Color, Triangle } = OGL;

      const renderer = new Renderer({ dpr });
      rendererRef.current = renderer;
      const gl = renderer.gl;
      gl.clearColor(0, 0, 0, 1);

      const geometry = new Triangle(gl);
      const program = new Program(gl, {
        vertex: FT_VERTEX,
        fragment: FT_FRAGMENT,
        uniforms: {
          iTime: { value: 0 },
          iResolution: { value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height) },
          uScale: { value: scale },
          uGridMul: { value: new Float32Array(gridMul) },
          uDigitSize: { value: digitSize },
          uScanlineIntensity: { value: scanlineIntensity },
          uGlitchAmount: { value: glitchAmount },
          uFlickerAmount: { value: flickerAmount },
          uNoiseAmp: { value: noiseAmp },
          uChromaticAberration: { value: chromaticAberration },
          uDither: { value: ditherValue },
          uCurvature: { value: curvature },
          uTint: { value: new Color(tintVec[0], tintVec[1], tintVec[2]) },
          uMouse: { value: new Float32Array([smoothMouseRef.current.x, smoothMouseRef.current.y]) },
          uMouseStrength: { value: mouseStrength },
          uUseMouse: { value: mouseReact ? 1 : 0 },
          uPageLoadProgress: { value: pageLoadAnimation ? 0 : 1 },
          uUsePageLoadAnimation: { value: pageLoadAnimation ? 1 : 0 },
          uBrightness: { value: brightness }
        }
      });
      programRef.current = program;
      const mesh = new Mesh(gl, { geometry, program });

      const resize = () => {
        if (!ctn) return;
        renderer.setSize(ctn.offsetWidth, ctn.offsetHeight);
        program.uniforms.iResolution.value = new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height);
      };
      resizeObserver = new ResizeObserver(() => resize());
      resizeObserver.observe(ctn);
      resize();

      const update = t => {
        rafRef.current = requestAnimationFrame(update);

        if (pageLoadAnimation && loadAnimationStartRef.current === 0) loadAnimationStartRef.current = t;

        if (!pause) {
          const elapsed = (t * 0.001 + timeOffsetRef.current) * timeScale;
          program.uniforms.iTime.value = elapsed;
          frozenTimeRef.current = elapsed;
        } else {
          program.uniforms.iTime.value = frozenTimeRef.current;
        }

        if (pageLoadAnimation && loadAnimationStartRef.current > 0) {
          const progress = Math.min((t - loadAnimationStartRef.current) / 2000, 1);
          program.uniforms.uPageLoadProgress.value = progress;
        }

        if (mouseReact) {
          const damping = 0.08;
          const sm = smoothMouseRef.current;
          const m = mouseRef.current;
          sm.x += (m.x - sm.x) * damping;
          sm.y += (m.y - sm.y) * damping;
          const mu = program.uniforms.uMouse.value;
          mu[0] = sm.x;
          mu[1] = sm.y;
        }

        renderer.render({ scene: mesh });
      };
      rafRef.current = requestAnimationFrame(update);
      ctn.appendChild(gl.canvas);

      if (mouseReact) {
        onMove = handleMouseMove;
        ctn.addEventListener('mousemove', onMove);
      }
    };

    boot();

    return () => {
      cancelled = true;
      cancelAnimationFrame(rafRef.current);
      if (resizeObserver) resizeObserver.disconnect();
      if (onMove && ctn) ctn.removeEventListener('mousemove', onMove);
      const gl = rendererRef.current && rendererRef.current.gl;
      if (gl && gl.canvas.parentElement === ctn) ctn.removeChild(gl.canvas);
      if (gl) { const ext = gl.getExtension('WEBGL_lose_context'); if (ext) ext.loseContext(); }
      loadAnimationStartRef.current = 0;
      timeOffsetRef.current = Math.random() * 100;
    };
  }, [dpr, pause, timeScale, scale, gridMul, digitSize, scanlineIntensity, glitchAmount,
    flickerAmount, noiseAmp, chromaticAberration, ditherValue, curvature, tintVec,
    mouseReact, mouseStrength, pageLoadAnimation, brightness, handleMouseMove]);

  return (
    <div
      ref={containerRef}
      className={"faulty-terminal-container " + className}
      style={{ width: "100%", height: "100%", position: "relative", overflow: "hidden", ...style }}
    />
  );
}
