"use client";

import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { motion } from "framer-motion";
import {
  frameSrc,
  loadFrameSequence,
  type FrameSet,
  type LoadHandle,
} from "@/lib/frameSequence";

export type FrameCaption = {
  id: string;
  /** Scroll progress window, 0–1, in which the caption is on screen. */
  show: number;
  hide: number;
  kicker: string;
  title: string;
  body: string;
};

/**
 * A sticky canvas that scrubs a frame sequence as the section scrolls past.
 *
 * Same engine as the landing hero, generalised over the frame set: one queued
 * RAF at a time, the canvas written directly rather than through React state,
 * a fractional position cross-faded between adjacent frames so the sequence
 * reads as continuous motion, and a backing store that tracks
 * devicePixelRatio.
 *
 * React state is used for exactly one thing — which captions are visible — and
 * only when that set actually changes.
 */
export default function ScrollFrameSection({
  frames,
  captions = [],
  heightClass = "h-[400vh]",
  loadingLabel = "Loading sequence",
  showLoader = true,
  tone = "dark",
  children,
  id,
}: {
  frames: FrameSet;
  captions?: readonly FrameCaption[];
  /** Total scroll length. Longer means the animation unfolds more slowly. */
  heightClass?: string;
  loadingLabel?: string;
  /**
   * Show the preload bar. Turning it off does not skip preloading — the first
   * frame is painted as soon as it arrives, so the section shows the subject
   * rather than sitting blank while the rest streams in behind it.
   */
  showLoader?: boolean;
  /**
   * Brightness of the footage, not of the copy. Light sequences need dark
   * captions, a light preload screen, and the navbar left in its normal
   * treatment rather than inverted.
   */
  tone?: "dark" | "light";
  /** Static overlay content, pinned above the canvas. */
  children?: ReactNode;
  id?: string;
}) {
  const dark = tone === "dark";
  const sectionRef = useRef<HTMLElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const framesRef = useRef<HTMLImageElement[]>([]);
  const tickingRef = useRef(false);
  const currentPosRef = useRef(-1);
  const prevVisibleRef = useRef("");

  const [progress, setProgress] = useState(0);
  const [loaded, setLoaded] = useState(false);
  const [visible, setVisible] = useState<Set<string>>(new Set());

  useEffect(() => {
    const handle: LoadHandle = { cancelled: false };
    loadFrameSequence(frames, (p) => setProgress(p), handle)
      .then((imgs) => {
        if (handle.cancelled) return;
        framesRef.current = imgs;
        setLoaded(true);
      })
      .catch(() => {
        // Without frames there is nothing to scrub; drop the overlay rather
        // than trapping the visitor behind a bar that will never fill.
        if (!handle.cancelled) setLoaded(true);
      });
    return () => {
      handle.cancelled = true;
    };
  }, [frames]);

  const drawAt = useCallback((exact: number) => {
    const canvas = canvasRef.current;
    const imgs = framesRef.current;
    if (!canvas || !imgs.length) return;
    const ctx = canvas.getContext("2d", { alpha: false });
    if (!ctx) return;

    const maxIndex = imgs.length - 1;
    const clamped = Math.max(0, Math.min(maxIndex, exact));
    const base = Math.floor(clamped);
    const blend = clamped - base;

    // Device pixels throughout: the backing store is already sized to DPR, so
    // drawing 1:1 avoids resampling the source twice.
    const cw = canvas.width;
    const ch = canvas.height;
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = "high";

    const paint = (img: HTMLImageElement | undefined, alpha: number) => {
      if (!img || !img.complete || !img.naturalWidth) return false;
      const imgRatio = img.naturalWidth / img.naturalHeight;
      const canvasRatio = cw / ch;
      let drawW: number;
      let drawH: number;
      if (canvasRatio > imgRatio) {
        drawW = cw;
        drawH = cw / imgRatio;
      } else {
        drawH = ch;
        drawW = ch * imgRatio;
      }
      if (window.innerWidth <= 768) {
        drawW *= 1.3;
        drawH *= 1.3;
      }
      ctx.globalAlpha = alpha;
      ctx.drawImage(img, (cw - drawW) / 2, (ch - drawH) / 2, drawW, drawH);
      ctx.globalAlpha = 1;
      return true;
    };

    // The base frame is opaque and cover-fits, so it wipes the previous paint.
    if (!paint(imgs[base], 1)) return;
    if (blend > 0.01) paint(imgs[Math.min(maxIndex, base + 1)], blend);
  }, []);

  /* ---- First frame, ahead of the full preload ---- */

  useEffect(() => {
    if (loaded) return;
    const img = new Image();
    img.onload = () => {
      // Only stand in while the real sequence is still streaming; once it
      // lands it replaces framesRef wholesale.
      if (!framesRef.current.length) {
        framesRef.current = [img];
        currentPosRef.current = 0;
        drawAt(0);
      }
    };
    img.src = frameSrc(frames, 0);
  }, [frames, drawAt, loaded]);

  /* ---- DPR-aware sizing ---- */

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    let mq: MediaQueryList | null = null;

    const resize = () => {
      const dpr = window.devicePixelRatio || 1;
      const w = window.innerWidth;
      const h = window.innerHeight;
      canvas.width = Math.round(w * dpr);
      canvas.height = Math.round(h * dpr);
      canvas.style.width = `${w}px`;
      canvas.style.height = `${h}px`;
      if (currentPosRef.current >= 0) drawAt(currentPosRef.current);
    };

    // devicePixelRatio changes on a move between displays or a zoom, and
    // `resize` does not reliably fire for either.
    const onDprChange = () => {
      resize();
      watchDpr();
    };
    const watchDpr = () => {
      mq?.removeEventListener("change", onDprChange);
      mq = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
      mq.addEventListener("change", onDprChange);
    };

    resize();
    watchDpr();
    window.addEventListener("resize", resize, { passive: true });
    return () => {
      window.removeEventListener("resize", resize);
      mq?.removeEventListener("change", onDprChange);
    };
  }, [drawAt]);

  /* ---- Scroll handler ---- */

  useEffect(() => {
    if (!loaded) return;
    const section = sectionRef.current;
    if (!section) return;

    const update = () => {
      const rect = section.getBoundingClientRect();
      const scrollable = section.offsetHeight - window.innerHeight;
      const p =
        scrollable > 0 ? Math.min(1, Math.max(0, -rect.top / scrollable)) : 0;

      const exact = p * (frames.count - 1);
      if (Math.abs(exact - currentPosRef.current) > 0.008) {
        currentPosRef.current = exact;
        drawAt(exact);
      }

      const next = new Set<string>();
      for (const c of captions) if (p >= c.show && p < c.hide) next.add(c.id);
      const key = [...next].sort().join(",");
      if (key !== prevVisibleRef.current) {
        prevVisibleRef.current = key;
        setVisible(next);
      }

      tickingRef.current = false;
    };

    const onScroll = () => {
      if (tickingRef.current) return;
      tickingRef.current = true;
      requestAnimationFrame(update);
    };

    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, [loaded, drawAt, captions, frames.count]);

  return (
    <section
      ref={sectionRef}
      id={id}
      data-nav-dark={dark ? "" : undefined}
      className={`relative ${heightClass}`}
    >
      <div className="sticky top-0 h-screen w-full overflow-hidden">
        <canvas ref={canvasRef} className="block h-full w-full" />

        {/* Preload overlay — real progress, not a timer. */}
        {showLoader && (
        <div
          className={`absolute inset-0 z-30 flex flex-col items-center justify-center bg-cover bg-center transition-opacity duration-700 ${
            loaded ? "pointer-events-none opacity-0" : "opacity-100"
          }`}
          style={{ backgroundImage: `url('/images/loading-bg.jpg')` }}
        >
          {/* Subtle dark vignette overlay for contrast */}
          <div className="absolute inset-0 bg-black/40 backdrop-blur-[2px]" />

          <div className="relative z-10 flex flex-col items-center">
            {/* Animated Solar Energy Orbit Ring */}
            <div className="relative mb-6 flex items-center justify-center p-6">
              <motion.div
                className="absolute inset-0 rounded-full border-2 border-dashed border-cyan-400/50"
                animate={{ rotate: 360 }}
                transition={{ duration: 18, repeat: Infinity, ease: "linear" }}
              />
              <motion.div
                className="absolute -inset-2.5 rounded-full border border-amber-400/40"
                animate={{ rotate: -360 }}
                transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
              />
              <motion.div
                className="absolute inset-0 rounded-full shadow-[0_0_40px_rgba(34,211,238,0.4)]"
                animate={{ scale: [1, 1.08, 1], opacity: [0.5, 0.9, 0.5] }}
                transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
              />
            </div>

            <span className="mb-4 font-mono text-xs font-bold tracking-[0.25em] text-cyan-300 uppercase drop-shadow-[0_0_10px_rgba(34,211,238,0.9)]">
              {loadingLabel}
            </span>
            <div className="relative h-2.5 w-72 overflow-hidden rounded-full border border-white/20 bg-black/60 shadow-[0_0_20px_rgba(0,0,0,0.8)] backdrop-blur-md">
              <div
                className="h-full rounded-full bg-gradient-to-r from-cyan-400 via-emerald-300 to-amber-400 transition-[width] duration-200 ease-out shadow-[0_0_16px_rgba(34,211,238,0.9)]"
                style={{ width: `${Math.round(progress * 100)}%` }}
              />
            </div>
            <span className="mt-4 font-mono text-3xl font-extrabold text-white tracking-wider tabular-nums drop-shadow-[0_0_25px_rgba(255,255,255,1)]">
              {Math.round(progress * 100)}%
            </span>
          </div>
        </div>
        )}

        {children}

        {/* Captions sit over moving footage, so contrast cannot be assumed —
            here the backdrop is pale but the panel itself is near-black. A
            scrim fading up from the bottom keeps the copy readable whatever
            passes behind it. */}
        {captions.length > 0 && (
          <div
            className={`pointer-events-none absolute inset-x-0 bottom-0 z-10 h-[45%] ${
              dark
                ? "bg-gradient-to-t from-zinc-950 via-zinc-950/70 to-transparent"
                : "bg-gradient-to-t from-[#f0f0f0] via-[#f0f0f0]/88 to-transparent"
            }`}
          />
        )}

        {captions.map((c) => {
          const on = visible.has(c.id);
          return (
            <div
              key={c.id}
              className={`absolute inset-x-0 bottom-[12%] z-20 mx-auto max-w-[620px] px-6 text-center transition-all duration-500 ease-out ${
                on ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-5 opacity-0"
              }`}
            >
              <span
                className={`font-mono text-[10px] font-semibold tracking-[0.25em] uppercase ${
                  dark ? "text-amber-400" : "text-orange-600"
                }`}
              >
                {c.kicker}
              </span>
              <h3
                className={`mt-3 text-3xl font-semibold tracking-tighter md:text-4xl ${
                  dark ? "text-white" : "text-zinc-950"
                }`}
              >
                {c.title}
              </h3>
              <p
                className={`mx-auto mt-3 max-w-[46ch] text-sm leading-relaxed md:text-base ${
                  dark ? "text-zinc-300" : "text-zinc-600"
                }`}
              >
                {c.body}
              </p>
            </div>
          );
        })}
      </div>
    </section>
  );
}
