/**
 * ShaderCanvas — WebGL GLSL renderer with viewport-aware lifecycle.
 *
 * Chromium limits ~16 simultaneous WebGL contexts. When a context is lost
 * (browser evicts it silently), you can't re-create it on the same canvas.
 *
 * Solution: when re-entering viewport, if the context is dead, replace the
 * canvas DOM element with a fresh one. A new canvas gets a new context.
 */

import { useRef, useEffect, useCallback, useState } from 'react';

interface ShaderCanvasProps {
  code: string;
  width?: number;
  height?: number;
  className?: string;
  animate?: boolean;
  onError?: (error: string) => void;
  onCompileSuccess?: () => void;
}

const VERT = `#version 300 es
in vec4 a_position;
void main() { gl_Position = a_position; }`;

function buildFrag(userCode: string): string {
  const pfx = `#version 300 es
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
out vec4 outColor;
`;
  if (userCode.includes('mainImage')) {
    return pfx + userCode + `
void main() { vec4 c; mainImage(c, gl_FragCoord.xy); outColor = c; }`;
  }
  return pfx + userCode.replace(/gl_FragColor/g, 'outColor');
}

export default function ShaderCanvas({
  code,
  width,
  height,
  className = '',
  animate = true,
  onError,
  onCompileSuccess,
}: ShaderCanvasProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const stateRef = useRef({
    canvas: null as HTMLCanvasElement | null,
    gl: null as WebGL2RenderingContext | null,
    prog: null as WebGLProgram | null,
    anim: 0,
    t0: 0,
    visible: false,
    running: false,
    mouse: [0, 0, 0, 0] as [number, number, number, number],
  });
  const codeRef = useRef(code);
  codeRef.current = code;

  // ── Create a fresh canvas element ──────────────────────
  const createCanvas = useCallback(() => {
    const container = containerRef.current;
    if (!container) return null;
    const s = stateRef.current;

    // Remove old canvas
    if (s.canvas && s.canvas.parentNode) {
      if (s.anim) { cancelAnimationFrame(s.anim); s.anim = 0; }
      s.running = false;
      s.prog = null;
      s.gl = null;
      s.canvas.remove();
    }

    const canvas = document.createElement('canvas');
    canvas.className = 'block w-full h-full';
    canvas.width = container.clientWidth || width || 640;
    canvas.height = container.clientHeight || height || 360;
    canvas.addEventListener('mousemove', (e) => {
      const r = canvas.getBoundingClientRect();
      s.mouse = [e.clientX - r.left, r.height - (e.clientY - r.top), 0, 0];
    });
    container.appendChild(canvas);
    s.canvas = canvas;
    return canvas;
  }, [width, height]);

  // ── Compile shader ─────────────────────────────────────
  const compile = useCallback((canvas: HTMLCanvasElement) => {
    const s = stateRef.current;

    let gl = s.gl;
    if (!gl || gl.isContextLost()) {
      gl = canvas.getContext('webgl2', { antialias: false, powerPreference: 'low-power' });
      if (!gl) return false;
      s.gl = gl;
    }

    if (s.prog) { gl.deleteProgram(s.prog); s.prog = null; }

    try {
      const vs = gl.createShader(gl.VERTEX_SHADER)!;
      gl.shaderSource(vs, VERT);
      gl.compileShader(vs);
      if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS))
        throw new Error(gl.getShaderInfoLog(vs) || 'VS error');

      const fs = gl.createShader(gl.FRAGMENT_SHADER)!;
      gl.shaderSource(fs, buildFrag(codeRef.current));
      gl.compileShader(fs);
      if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
        const e = gl.getShaderInfoLog(fs) || 'FS error';
        gl.deleteShader(vs); gl.deleteShader(fs); throw new Error(e);
      }

      const p = gl.createProgram()!;
      gl.attachShader(p, vs); gl.attachShader(p, fs);
      gl.linkProgram(p);
      gl.deleteShader(vs); gl.deleteShader(fs);
      if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
        const e = gl.getProgramInfoLog(p) || 'Link error';
        gl.deleteProgram(p); throw new Error(e);
      }

      s.prog = p;
      gl.useProgram(p);

      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]), gl.STATIC_DRAW);
      const loc = gl.getAttribLocation(p, 'a_position');
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

      s.t0 = performance.now();
      onCompileSuccess?.();
      onError?.('');
      return true;
    } catch (e: any) {
      onError?.(e.message);
      return false;
    }
  }, [onError, onCompileSuccess]);

  // ── Animation loop ─────────────────────────────────────
  const startLoop = useCallback(() => {
    const s = stateRef.current;
    if (s.running || !s.gl || !s.prog || !s.canvas) return;
    s.running = true;

    const gl = s.gl;
    const prog = s.prog;
    const canvas = s.canvas;

    const tick = () => {
      if (!s.visible && animate) { s.running = false; s.anim = 0; return; }
      if (!s.gl || !s.prog || gl.isContextLost()) { s.running = false; s.anim = 0; return; }

      const w = canvas.width, h = canvas.height;
      gl.viewport(0, 0, w, h);
      const t = (performance.now() - s.t0) / 1000;
      const uT = gl.getUniformLocation(prog, 'iTime');
      const uR = gl.getUniformLocation(prog, 'iResolution');
      const uM = gl.getUniformLocation(prog, 'iMouse');
      if (uT) gl.uniform1f(uT, t);
      if (uR) gl.uniform3f(uR, w, h, 1);
      if (uM) gl.uniform4f(uM, ...s.mouse);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

      s.anim = animate ? requestAnimationFrame(tick) : 0;
    };
    tick();
  }, [animate]);

  // ── Full setup: new canvas → compile → loop ────────────
  const fullSetup = useCallback(() => {
    const canvas = createCanvas();
    if (!canvas) return;
    if (compile(canvas)) {
      startLoop();
    }
  }, [createCanvas, compile, startLoop]);

  // ── Visibility observer ────────────────────────────────
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        const s = stateRef.current;
        const was = s.visible;
        s.visible = entry.isIntersecting;

        if (s.visible && !was) {
          // Entering viewport
          if (s.gl && !s.gl.isContextLost() && s.prog) {
            // Context still alive — just restart loop
            startLoop();
          } else {
            // Context lost or never created — fresh canvas
            fullSetup();
          }
        } else if (!s.visible && was) {
          // Leaving viewport — stop loop (context stays on canvas)
          if (s.anim) { cancelAnimationFrame(s.anim); s.anim = 0; }
          s.running = false;
        }
      },
      { threshold: 0.01, rootMargin: '200px' },
    );

    observer.observe(container);

    return () => {
      observer.disconnect();
      const s = stateRef.current;
      if (s.anim) cancelAnimationFrame(s.anim);
      s.running = false;
    };
  }, [fullSetup, startLoop]);

  // ── Code change (editor) ───────────────────────────────
  useEffect(() => {
    const s = stateRef.current;
    if (s.visible && s.canvas) {
      if (s.anim) { cancelAnimationFrame(s.anim); s.anim = 0; }
      s.running = false;
      if (compile(s.canvas)) startLoop();
    }
  }, [code]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Container resize → canvas resize ───────────────────
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const ro = new ResizeObserver(([e]) => {
      const s = stateRef.current;
      if (!s.canvas) return;
      const w = Math.floor(e.contentRect.width);
      const h = Math.floor(e.contentRect.height);
      if (w > 0 && h > 0) { s.canvas.width = w; s.canvas.height = h; }
    });
    ro.observe(container);
    return () => ro.disconnect();
  }, []);

  return (
    <div
      ref={containerRef}
      className={className}
      style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%' }}
    />
  );
}
