<script setup lang="ts">
import * as THREE from 'three';
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';

const vertexShader = `
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = `
precision highp float;

uniform float uTime, uAttenuation, uLineThickness;
uniform float uBaseRadius, uRadiusStep, uScaleRate;
uniform float uOpacity, uNoiseAmount, uRotation, uRingGap;
uniform float uFadeIn, uFadeOut;
uniform float uMouseInfluence, uHoverAmount, uHoverScale, uParallax, uBurst;
uniform vec2 uResolution, uMouse;
uniform vec3 uColor, uColorTwo;
uniform int uRingCount;

const float HP = 1.5707963;
const float CYCLE = 3.45;

float fade(float t) {
  return t < uFadeIn ? smoothstep(0.0, uFadeIn, t) : 1.0 - smoothstep(uFadeOut, CYCLE - 0.2, t);
}

float ring(vec2 p, float ri, float cut, float t0, float px) {
  float t = mod(uTime + t0, CYCLE);
  float r = ri + t / CYCLE * uScaleRate;
  float d = abs(length(p) - r);
  float a = atan(abs(p.y), abs(p.x)) / HP;
  float th = max(1.0 - a, 0.5) * px * uLineThickness;
  float h = (1.0 - smoothstep(th, th * 1.5, d)) + 1.0;
  d += pow(cut * a, 3.0) * r;
  return h * exp(-uAttenuation * d) * fade(t);
}

void main() {
  float px = 1.0 / min(uResolution.x, uResolution.y);
  vec2 p = (gl_FragCoord.xy - 0.5 * uResolution.xy) * px;
  float cr = cos(uRotation), sr = sin(uRotation);
  p = mat2(cr, -sr, sr, cr) * p;
  p -= uMouse * uMouseInfluence;
  float sc = mix(1.0, uHoverScale, uHoverAmount) + uBurst * 0.3;
  p /= sc;
  vec3 c = vec3(0.0);
  float rcf = max(float(uRingCount) - 1.0, 1.0);
  for (int i = 0; i < 10; i++) {
    if (i >= uRingCount) break;
    float fi = float(i);
    vec2 pr = p - fi * uParallax * uMouse;
    vec3 rc = mix(uColor, uColorTwo, fi / rcf);
    c = mix(c, rc, vec3(ring(pr, uBaseRadius + fi * uRadiusStep, pow(uRingGap, fi), i == 0 ? 0.0 : 2.95 * fi, px)));
  }
  c *= 1.0 + uBurst * 2.0;
  float n = fract(sin(dot(gl_FragCoord.xy + uTime * 100.0, vec2(12.9898, 78.233))) * 43758.5453);
  c += (n - 0.5) * uNoiseAmount;
  gl_FragColor = vec4(c, max(c.r, max(c.g, c.b)) * uOpacity);
}
`;

interface MagicRingsProps {
  color?: string;
  colorTwo?: string;
  speed?: number;
  ringCount?: number;
  attenuation?: number;
  lineThickness?: number;
  baseRadius?: number;
  radiusStep?: number;
  scaleRate?: number;
  opacity?: number;
  blur?: number;
  noiseAmount?: number;
  rotation?: number;
  ringGap?: number;
  fadeIn?: number;
  fadeOut?: number;
  followMouse?: boolean;
  mouseInfluence?: number;
  hoverScale?: number;
  parallax?: number;
  clickBurst?: boolean;
}

const props = withDefaults(defineProps<MagicRingsProps>(), {
  color: '#7cff67',
  colorTwo: '#42fcff',
  speed: 1,
  ringCount: 6,
  attenuation: 10,
  lineThickness: 2,
  baseRadius: 0.35,
  radiusStep: 0.1,
  scaleRate: 0.1,
  opacity: 1,
  blur: 0,
  noiseAmount: 0.1,
  rotation: 0,
  ringGap: 1.5,
  fadeIn: 0.7,
  fadeOut: 0.5,
  followMouse: false,
  mouseInfluence: 0.2,
  hoverScale: 1.2,
  parallax: 0.05,
  clickBurst: false
});

const mountRef = useTemplateRef('mountRef');

const mouseRef = ref<[number, number]>([0, 0]);
const smoothMouseRef = ref<[number, number]>([0, 0]);
const hoverAmountRef = ref(0);
const isHoveredRef = ref(false);
const burstRef = ref(0);

const propsRef = computed(() => ({
  color: props.color,
  colorTwo: props.colorTwo,
  speed: props.speed,
  ringCount: props.ringCount,
  attenuation: props.attenuation,
  lineThickness: props.lineThickness,
  baseRadius: props.baseRadius,
  radiusStep: props.radiusStep,
  scaleRate: props.scaleRate,
  opacity: props.opacity,
  blur: props.blur,
  noiseAmount: props.noiseAmount,
  rotation: props.rotation,
  ringGap: props.ringGap,
  fadeIn: props.fadeIn,
  fadeOut: props.fadeOut,
  followMouse: props.followMouse,
  mouseInfluence: props.mouseInfluence,
  hoverScale: props.hoverScale,
  parallax: props.parallax,
  clickBurst: props.clickBurst
}));

let renderer: THREE.WebGLRenderer | null = null;
let frameId = 0;
let ro: ResizeObserver | null = null;

const cleanupFns: (() => void)[] = [];
onMounted(() => {
  const mount = mountRef.value;
  if (!mount) return;

  try {
    renderer = new THREE.WebGLRenderer({ alpha: true });
  } catch {
    return;
  }

  if (!renderer.capabilities.isWebGL2) {
    renderer.dispose();
    return;
  }

  renderer.setClearColor(0x000000, 0);
  mount.appendChild(renderer.domElement);

  const scene = new THREE.Scene();

  const camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.1, 10);
  camera.position.z = 1;

  const uniforms = {
    uTime: { value: 0 },
    uAttenuation: { value: 0 },
    uResolution: { value: new THREE.Vector2() },
    uColor: { value: new THREE.Color() },
    uColorTwo: { value: new THREE.Color() },
    uLineThickness: { value: 0 },
    uBaseRadius: { value: 0 },
    uRadiusStep: { value: 0 },
    uScaleRate: { value: 0 },
    uRingCount: { value: 0 },
    uOpacity: { value: 1 },
    uNoiseAmount: { value: 0 },
    uRotation: { value: 0 },
    uRingGap: { value: 1.6 },
    uFadeIn: { value: 0.5 },
    uFadeOut: { value: 0.75 },
    uMouse: { value: new THREE.Vector2() },
    uMouseInfluence: { value: 0 },
    uHoverAmount: { value: 0 },
    uHoverScale: { value: 1 },
    uParallax: { value: 0 },
    uBurst: { value: 0 }
  };

  const material = new THREE.ShaderMaterial({
    vertexShader,
    fragmentShader,
    uniforms,
    transparent: true
  });

  const quad = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
  scene.add(quad);

  const resize = () => {
    const w = mount.clientWidth;
    const h = mount.clientHeight;
    const dpr = Math.min(window.devicePixelRatio, 2);

    renderer!.setSize(w, h);
    renderer!.setPixelRatio(dpr);

    uniforms.uResolution.value.set(w * dpr, h * dpr);
  };

  resize();

  window.addEventListener('resize', resize);

  ro = new ResizeObserver(resize);
  ro.observe(mount);

  const onMouseMove = (e: MouseEvent) => {
    const rect = mount.getBoundingClientRect();

    mouseRef.value[0] = (e.clientX - rect.left) / rect.width - 0.5;
    mouseRef.value[1] = -((e.clientY - rect.top) / rect.height - 0.5);
  };

  const onMouseEnter = () => {
    isHoveredRef.value = true;
  };

  const onMouseLeave = () => {
    isHoveredRef.value = false;
    mouseRef.value = [0, 0];
  };

  const onClick = () => {
    burstRef.value = 1;
  };

  mount.addEventListener('mousemove', onMouseMove);
  mount.addEventListener('mouseenter', onMouseEnter);
  mount.addEventListener('mouseleave', onMouseLeave);
  mount.addEventListener('click', onClick);

  const animate = (t: number) => {
    frameId = requestAnimationFrame(animate);

    const p = propsRef.value;

    smoothMouseRef.value[0] += (mouseRef.value[0] - smoothMouseRef.value[0]) * 0.08;
    smoothMouseRef.value[1] += (mouseRef.value[1] - smoothMouseRef.value[1]) * 0.08;

    hoverAmountRef.value += ((isHoveredRef.value ? 1 : 0) - hoverAmountRef.value) * 0.08;

    burstRef.value *= 0.95;
    if (burstRef.value < 0.001) burstRef.value = 0;

    uniforms.uTime.value = t * 0.001 * p.speed;
    uniforms.uAttenuation.value = p.attenuation;

    uniforms.uColor.value.set(p.color);
    uniforms.uColorTwo.value.set(p.colorTwo);

    uniforms.uLineThickness.value = p.lineThickness;
    uniforms.uBaseRadius.value = p.baseRadius;
    uniforms.uRadiusStep.value = p.radiusStep;
    uniforms.uScaleRate.value = p.scaleRate;

    uniforms.uRingCount.value = p.ringCount;

    uniforms.uOpacity.value = p.opacity;
    uniforms.uNoiseAmount.value = p.noiseAmount;

    uniforms.uRotation.value = (p.rotation * Math.PI) / 180;

    uniforms.uRingGap.value = p.ringGap;
    uniforms.uFadeIn.value = p.fadeIn;
    uniforms.uFadeOut.value = p.fadeOut;

    uniforms.uMouse.value.set(smoothMouseRef.value[0], smoothMouseRef.value[1]);

    uniforms.uMouseInfluence.value = p.followMouse ? p.mouseInfluence : 0;

    uniforms.uHoverAmount.value = hoverAmountRef.value;
    uniforms.uHoverScale.value = p.hoverScale;

    uniforms.uParallax.value = p.parallax;
    uniforms.uBurst.value = p.clickBurst ? burstRef.value : 0;

    renderer!.render(scene, camera);
  };

  frameId = requestAnimationFrame(animate);

  cleanupFns.push(() => {
    cancelAnimationFrame(frameId);

    window.removeEventListener('resize', resize);

    ro?.disconnect();

    mount.removeEventListener('mousemove', onMouseMove);
    mount.removeEventListener('mouseenter', onMouseEnter);
    mount.removeEventListener('mouseleave', onMouseLeave);
    mount.removeEventListener('click', onClick);

    mount.removeChild(renderer!.domElement);

    renderer?.dispose();
    material.dispose();
  });
});

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

<template>
  <div ref="mountRef" class="w-full h-full" :style="props.blur > 0 ? { filter: `blur(${props.blur}px)` } : undefined" />
</template>
