/**
 * 셰이더 플레인 부트스트랩.
 *
 * 스킬의 references/three.md 가 권하는 그 구조다.
 *   - three 는 초기 번들에 없다. 화면에 가까워질 때만 받는다
 *   - WebGL 이 없거나 모션을 줄이는 설정이면 아예 받지 않는다
 *   - DPR 를 클램프한다. 3 을 그대로 쓰면 픽셀이 네 배가 된다
 *   - 화면 밖이거나 탭이 숨으면 그리지 않는다
 *   - 실패하면 조용히 물러난다. CSS 폴백이 그대로 결과물이 된다
 *
 * 무거운 씬을 임포트하지 않고 평면 하나에 프래그먼트 셰이더만 얹는다.
 */

export interface ShaderPlaneOptions {
  canvas: HTMLCanvasElement;
  /** GLSL 프래그먼트 셰이더. vUv, uTime, uAspect, uPointer 를 받는다 */
  fragment: string;
  /** CSS 커스텀 프로퍼티에서 읽어 vec3 로 넘길 색 */
  colors?: Record<string, string>;
  /** 포인터를 따라가게 할지. 히어로처럼 큰 면적에만 쓴다 */
  pointer?: boolean;
  /** 시간 진행 속도 배수 */
  speed?: number;
  /** 미리 받아둘 거리 */
  rootMargin?: string;
}

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

/** 두 셰이더가 공유하는 값 노이즈. 라이브러리를 끌어오지 않는다 */
export const GLSL_NOISE = `
  float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
  float noise(vec2 p) {
    vec2 i = floor(p), f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x),
               mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);
  }
  float fbm(vec2 p) {
    float v = 0.0, a = 0.5;
    for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.03; a *= 0.5; }
    return v;
  }
`;

export function mountShaderPlane(opts: ShaderPlaneOptions): void {
  const { canvas } = opts;
  if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;

  const io = new IntersectionObserver(
    (entries) => {
      if (!entries.some((e) => e.isIntersecting)) return;
      io.disconnect();
      void boot(opts);
    },
    { rootMargin: opts.rootMargin ?? "300px 0px" },
  );
  io.observe(canvas);
}

async function boot(opts: ShaderPlaneOptions): Promise<void> {
  const { canvas, fragment, colors = {}, pointer = false, speed = 1 } = opts;

  let THREE: typeof import("three");
  try {
    THREE = await import("three");
  } catch {
    return; // CSS 폴백이 그대로 결과물이 된다
  }

  let renderer: import("three").WebGLRenderer;
  try {
    renderer = new THREE.WebGLRenderer({ canvas, antialias: false, alpha: true });
  } catch {
    return; // WebGL 이 없는 환경
  }

  const css = getComputedStyle(document.documentElement);
  const uniforms: Record<string, { value: unknown }> = {
    uTime: { value: 0 },
    uAspect: { value: 1 },
    uPointer: { value: new THREE.Vector2(0.5, 0.5) },
  };
  for (const [name, cssVar] of Object.entries(colors)) {
    const raw = css.getPropertyValue(cssVar).trim();
    uniforms[name] = { value: new THREE.Color(raw || "#4FD6C4") };
  }

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

  const scene = new THREE.Scene();
  scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
  const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

  const fit = () => {
    const r = canvas.getBoundingClientRect();
    if (!r.width || !r.height) return;
    renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
    renderer.setSize(r.width, r.height, false);
    (uniforms["uAspect"] as { value: number }).value = r.width / r.height;
  };
  fit();
  addEventListener("resize", fit, { passive: true });

  if (pointer) {
    const target = new THREE.Vector2(0.5, 0.5);
    addEventListener(
      "pointermove",
      (e) => {
        const r = canvas.getBoundingClientRect();
        target.set((e.clientX - r.left) / r.width, 1 - (e.clientY - r.top) / r.height);
      },
      { passive: true },
    );
    // 매 프레임 목표값으로 조금씩 따라간다. 즉시 반응하면 값싸 보인다
    material.userData["follow"] = () => {
      const p = uniforms["uPointer"] as { value: import("three").Vector2 };
      p.value.lerp(target, 0.045);
    };
  }

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

  let onScreen = true;
  new IntersectionObserver((es) => {
    onScreen = es.some((e) => e.isIntersecting);
  }).observe(canvas);

  canvas.classList.add("is-live");
  renderer.setAnimationLoop((ms) => {
    if (!tabVisible || !onScreen) return;
    (material.userData["follow"] as (() => void) | undefined)?.();
    (uniforms["uTime"] as { value: number }).value = (ms * 0.001) * speed;
    renderer.render(scene, camera);
  });
}
