<script lang="ts">
  import { cn } from "$lib/utils";
  import type { ClassValue } from "clsx";
  import { onMount, onDestroy } from "svelte";

  let {
    class: className = "",
    style = "",
    scale = 1000,
    length = 10,
    spacing = 20,
    timeScale = 10.25 / 1000,
    angleTimeScale = 2.0,
    lengthTimeScale = 1.5,
    opacity = 0.9,
    radius = 3.5,
    angleOpacityAmplitude = 0.8,
    angleOpacityFloor = 0.1,
    randomOpacityMin = 0.5,
    randomOpacityMax = 1.0,
    dotColor = [200 / 255, 200 / 255, 200 / 255] as [number, number, number],
  }: {
    class?: ClassValue;
    style?: string;
    scale?: number;
    length?: number;
    spacing?: number;
    timeScale?: number;
    angleTimeScale?: number;
    lengthTimeScale?: number;
    opacity?: number;
    radius?: number;
    angleOpacityAmplitude?: number;
    angleOpacityFloor?: number;
    randomOpacityMin?: number;
    randomOpacityMax?: number;
    dotColor?: [number, number, number];
  } = $props();

  let canvas: HTMLCanvasElement;
  let cleanupFns: (() => void)[] = [];
  let ready = $state(false);

  function addCleanup(fn: () => void) {
    cleanupFns.push(fn);
  }

  class UniformManager {
    private gl: WebGLRenderingContext;
    private locations = new Map<string, WebGLUniformLocation>();

    constructor(
      gl: WebGLRenderingContext,
      program: WebGLProgram,
      uniforms: string[],
    ) {
      this.gl = gl;
      uniforms.forEach((name) => {
        const loc = gl.getUniformLocation(program, name);
        if (loc) this.locations.set(name, loc);
      });
    }

    set(
      name: string,
      value: number | [number, number] | [number, number, number],
    ) {
      const loc = this.locations.get(name);
      if (!loc) return;

      if (Array.isArray(value)) {
        if (value.length === 2) {
          this.gl.uniform2f(loc, value[0], value[1]);
        } else if (value.length === 3) {
          this.gl.uniform3f(loc, value[0], value[1], value[2]);
        }
      } else {
        this.gl.uniform1f(loc, value);
      }
    }

    setStatic(values: Record<string, number>) {
      Object.entries(values).forEach(([name, value]) => this.set(name, value));
    }
  }

  const vertexShader = `
    attribute vec2 a_position;
    void main() {
      gl_Position = vec4(a_position, 0.0, 1.0);
    }
  `;

  const fragmentShader = `
    precision mediump float;

    uniform vec2 u_resolution;
    uniform float u_time;
    uniform float u_seed;
    uniform float u_dpr;
    uniform float u_scale;
    uniform float u_length;
    uniform float u_spacing;
    uniform float u_opacity;
    uniform float u_radius;
    uniform float u_angleTimeScale;
    uniform float u_lengthTimeScale;
    uniform float u_angleOpacityAmp;
    uniform float u_angleOpacityFloor;
    uniform float u_randomOpacityMin;
    uniform float u_randomOpacityMax;
    uniform vec3 u_dotColor;

    const float PI = 3.14159265359;

    // Simplex 3D noise
    vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
    vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
    vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
    vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }

    float snoise(vec3 v) {
      const vec2 C = vec2(1.0/6.0, 1.0/3.0);
      const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);

      vec3 i  = floor(v + dot(v, C.yyy));
      vec3 x0 = v - i + dot(i, C.xxx);

      vec3 g = step(x0.yzx, x0.xyz);
      vec3 l = 1.0 - g;
      vec3 i1 = min(g.xyz, l.zxy);
      vec3 i2 = max(g.xyz, l.zxy);

      vec3 x1 = x0 - i1 + C.xxx;
      vec3 x2 = x0 - i2 + C.yyy;
      vec3 x3 = x0 - D.yyy;

      i = mod289(i + u_seed);
      vec4 p = permute(permute(permute(
                i.z + vec4(0.0, i1.z, i2.z, 1.0))
              + i.y + vec4(0.0, i1.y, i2.y, 1.0))
              + i.x + vec4(0.0, i1.x, i2.x, 1.0));

      float n_ = 0.142857142857;
      vec3 ns = n_ * D.wyz - D.xzx;

      vec4 j = p - 49.0 * floor(p * ns.z * ns.z);

      vec4 x_ = floor(j * ns.z);
      vec4 y_ = floor(j - 7.0 * x_);

      vec4 x = x_ *ns.x + ns.yyyy;
      vec4 y = y_ *ns.x + ns.yyyy;
      vec4 h = 1.0 - abs(x) - abs(y);

      vec4 b0 = vec4(x.xy, y.xy);
      vec4 b1 = vec4(x.zw, y.zw);

      vec4 s0 = floor(b0)*2.0 + 1.0;
      vec4 s1 = floor(b1)*2.0 + 1.0;
      vec4 sh = -step(h, vec4(0.0));

      vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
      vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;

      vec3 p0 = vec3(a0.xy, h.x);
      vec3 p1 = vec3(a0.zw, h.y);
      vec3 p2 = vec3(a1.xy, h.z);
      vec3 p3 = vec3(a1.zw, h.w);

      vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
      p0 *= norm.x;
      p1 *= norm.y;
      p2 *= norm.z;
      p3 *= norm.w;

      vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
      m = m * m;
      return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
    }

    float hash(vec2 p) {
      return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
    }

    float noise01(vec3 v) {
      return (snoise(v) + 1.0) * 0.5;
    }

    void main() {
      vec2 pixelCoord = gl_FragCoord.xy;

      float spacing = u_spacing * u_dpr;
      float scaleDpr = u_scale * u_dpr;
      vec2 gridCoord = floor(pixelCoord / spacing) * spacing;

      float minDist = 1000000.0;
      float closestRad = 0.0;
      float pointOpacity = 0.0;

      for (float dx = -1.0; dx <= 1.0; dx += 1.0) {
        for (float dy = -1.0; dy <= 1.0; dy += 1.0) {
          vec2 testGrid = gridCoord + vec2(dx * spacing, dy * spacing);

          float rad = (noise01(vec3(testGrid / scaleDpr, u_time * u_angleTimeScale)) - 0.5) * 4.0 * PI;
          float len = (noise01(vec3(testGrid / scaleDpr, u_time * u_lengthTimeScale)) + 0.5) * u_length * u_dpr;

          vec2 displacedPoint = testGrid + vec2(cos(rad), sin(rad)) * len;
          float dist = distance(pixelCoord, displacedPoint);

          if (dist < minDist) {
            minDist = dist;
            closestRad = rad;
            pointOpacity = hash(testGrid) * (u_randomOpacityMax - u_randomOpacityMin) + u_randomOpacityMin;
          }
        }
      }

      float circle = 1.0 - smoothstep(0.0, u_radius * u_dpr, minDist);
      float angleOpacity = (abs(cos(closestRad)) * u_angleOpacityAmp + u_angleOpacityFloor) * pointOpacity * u_opacity;

      gl_FragColor = vec4(u_dotColor, circle * angleOpacity);
    }
  `;

  function createShader(
    gl: WebGLRenderingContext,
    type: number,
    source: string,
  ): WebGLShader | null {
    const shader = gl.createShader(type);
    if (!shader) return null;

    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error("Shader compile error:", gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
      return null;
    }

    return shader;
  }

  function createProgram(
    gl: WebGLRenderingContext,
    vertexShader: WebGLShader,
    fragmentShader: WebGLShader,
  ): WebGLProgram | null {
    const program = gl.createProgram();
    if (!program) return null;

    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error("Program link error:", gl.getProgramInfoLog(program));
      gl.deleteProgram(program);
      return null;
    }

    return program;
  }

  function initWebGL(canvas: HTMLCanvasElement) {
    const gl = canvas.getContext("webgl", {
      alpha: true,
      premultipliedAlpha: false,
    });

    if (!gl) {
      console.error("WebGL not supported");
      return null;
    }

    const vShader = createShader(gl, gl.VERTEX_SHADER, vertexShader);
    const fShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShader);

    if (!vShader || !fShader) {
      console.error("Shader compilation failed");
      return null;
    }

    const program = createProgram(gl, vShader, fShader);
    if (!program) {
      console.error("Program linking failed");
      return null;
    }

    return { gl, program };
  }

  onMount(() => {
    const context = initWebGL(canvas);
    if (!context) return;

    const { gl, program } = context;

    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
      gl.STATIC_DRAW,
    );

    const positionLocation = gl.getAttribLocation(program, "a_position");
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    const uniforms = new UniformManager(gl, program, [
      "u_resolution",
      "u_time",
      "u_seed",
      "u_dpr",
      "u_scale",
      "u_length",
      "u_spacing",
      "u_opacity",
      "u_radius",
      "u_angleTimeScale",
      "u_lengthTimeScale",
      "u_angleOpacityAmp",
      "u_angleOpacityFloor",
      "u_randomOpacityMin",
      "u_randomOpacityMax",
      "u_dotColor",
    ]);

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

    const dpr = window.devicePixelRatio || 1;

    uniforms.setStatic({
      u_seed: Math.random() * 1000,
      u_dpr: dpr,
      u_scale: scale,
      u_length: length,
      u_spacing: spacing,
      u_opacity: opacity,
      u_radius: radius,
      u_angleTimeScale: angleTimeScale,
      u_lengthTimeScale: lengthTimeScale,
      u_angleOpacityAmp: angleOpacityAmplitude,
      u_angleOpacityFloor: angleOpacityFloor,
      u_randomOpacityMin: randomOpacityMin,
      u_randomOpacityMax: randomOpacityMax,
    });
    uniforms.set("u_dotColor", dotColor);

    const resizeCanvas = () => {
      canvas.width = window.innerWidth * dpr;
      canvas.height = window.innerHeight * dpr;
      canvas.style.width = `${window.innerWidth}px`;
      canvas.style.height = `${window.innerHeight}px`;
      gl.viewport(0, 0, canvas.width, canvas.height);
    };

    resizeCanvas();
    window.addEventListener("resize", resizeCanvas);
    addCleanup(() => window.removeEventListener("resize", resizeCanvas));

    const startTime = Date.now();
    let animationId: number;
    let firstFrameRendered = false;

    function render() {
      if (document.hidden) {
        animationId = requestAnimationFrame(render);
        return;
      }

      const time = ((Date.now() - startTime) / 1000) * timeScale;

      uniforms.set("u_resolution", [canvas.width, canvas.height]);
      uniforms.set("u_time", time);

      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.TRIANGLES, 0, 6);

      if (!firstFrameRendered) {
        firstFrameRendered = true;
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            ready = true;
          });
        });
      }

      animationId = requestAnimationFrame(render);
    }

    render();

    addCleanup(() => cancelAnimationFrame(animationId));
    addCleanup(() => gl.getExtension("WEBGL_lose_context")?.loseContext());
  });

  onDestroy(() => {
    cleanupFns.forEach((fn) => fn());
  });
</script>

<!-- Wrapper for background + dots canvas - single persistent unit -->
<div class="pointer-events-none fixed inset-0 -z-20" {style}>
  <!-- Background overlay -->
  <div
    class="absolute inset-0 bg-white dark:bg-black transition-colors duration-300"
  ></div>

  <!-- Dots canvas -->
  <canvas
    bind:this={canvas}
    class={cn(
      "absolute inset-0 z-10 transition-opacity duration-1300 ease-out",
      ready ? "opacity-100" : "opacity-0",
      className,
    )}
  ></canvas>
</div>
