/* site-animations.jsx — wires the standalone site up with animation libraries.
   Each library is opt-in via the Tweaks panel; defaults pass a tasteful baseline.

   Libraries loaded by the HTML:
     - gsap + ScrollTrigger   (window.gsap, window.ScrollTrigger)
     - three                  (window.THREE)
     - lottie-web             (window.lottie)
     - animejs                (window.anime)
     - motion one             (window.Motion)
*/

const { useState: useStateSA, useEffect: useEffectSA, useRef: useRefSA } = React;

/* ── tweak defaults ──────────────────────── */
const SITE_TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "animations": true,
  "intensity": "subtle",
  "gsapReveals": true,
  "threeHero": true,
  "lottiePulse": true,
  "numberCounters": true,
  "microInteractions": true,
  "palette": "sport"
}/*EDITMODE-END*/;

const INTENSITY_TO_DURATION = { subtle: 1.4, normal: 0.9, loud: 0.6 };
const INTENSITY_TO_DISTANCE = { subtle: 16, normal: 40, loud: 80 };

/* ── helper: split a node's textContent into spans per char for reveal ── */
function splitChars(el) {
  if (!el || el.dataset.split === "1") return;
  const text = el.textContent;
  el.dataset.split = "1";
  el.dataset.original = text;
  el.innerHTML = "";
  for (const ch of text) {
    const s = document.createElement("span");
    s.className = "char";
    s.textContent = ch === " " ? "\u00A0" : ch;
    el.appendChild(s);
  }
}

/* ── IntersectionObserver-driven reveals (bulletproof, no scroll dep) ── */
function useScrollReveals(enabled) {
  useEffectSA(() => {
    const els = document.querySelectorAll(".reveal-up, .reveal-fade, .reveal-stagger");
    if (!enabled) {
      els.forEach((el) => el.classList.add("in"));
      return;
    }

    const playReveal = (el) => {
      if (el.classList.contains("in")) return;
      el.classList.add("in");
      const isStagger = el.classList.contains("reveal-stagger");
      const isFade = el.classList.contains("reveal-fade");
      const targets = isStagger ? [...el.children] : [el];
      targets.forEach((t, i) => {
        // Use Element.animate() — bypasses CSS transitions entirely so it can't
        // be frozen by stale style overrides.
        const from = isFade
          ? { opacity: 0 }
          : { opacity: 0, transform: "translateY(40px)" };
        const to = isFade
          ? { opacity: 1 }
          : { opacity: 1, transform: "translateY(0px)" };
        const animation = t.animate([from, to], {
          duration: 700,
          delay: isStagger ? i * 70 : 0,
          easing: "cubic-bezier(.2,.7,.2,1)",
          fill: "forwards",
        });
        // Belt + suspenders: when the animation finishes, also set inline styles.
        animation.onfinish = () => {
          t.style.opacity = "1";
          t.style.transform = "none";
        };
      });
    };

    const obs = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            playReveal(entry.target);
            obs.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
    );
    els.forEach((el) => obs.observe(el));
    return () => obs.disconnect();
  }, [enabled]);
}

/* ── GSAP: hero entry + sec-head char reveals ── */
function useGsapReveals(enabled, intensity) {
  useEffectSA(() => {
    if (!enabled || !window.gsap) return;
    const gsap = window.gsap;
    const ST = window.ScrollTrigger;
    if (ST) gsap.registerPlugin(ST);

    const dur = INTENSITY_TO_DURATION[intensity] ?? 0.9;
    const dist = INTENSITY_TO_DISTANCE[intensity] ?? 40;

    const ctx = gsap.context(() => {
      // ── Hero entry: a plain timeline (no scrollTrigger). The hero is always
      // above the fold so we don't need to gate on scroll — and it works even
      // if ScrollTrigger fails to evaluate state correctly on first paint.
      // (Hero kids are NOT given a `reveal-up` class by useTagReveals, so the
      // section-level scrollTrigger pass below won't double-grab them.)
      const heroKicker = document.querySelector(".hero__kicker");
      const heroTitle = document.querySelector(".hero__title");
      const heroLede = document.querySelector(".hero__lede");
      const heroCta = document.querySelector(".hero__cta");
      const heroTabs = document.querySelector(".platform-tabs");
      const heroPrice = document.querySelector(".hero__pricetag");

      // Use gsap.set + gsap.to instead of from() so the start state is
      // unambiguous and the animation always plays.
      if (heroKicker) {
        gsap.set(heroKicker, { y: 16, opacity: 0 });
        gsap.to(heroKicker, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out", delay: 0.12 });
      }
      if (heroTitle) {
        // Don't split the hero title into chars — the .smash span has a complex
        // box-shadow/background treatment that breaks when its inner text is
        // replaced by per-char spans. Animate the whole title as one unit.
        gsap.set(heroTitle, { y: 40, opacity: 0 });
        gsap.to(heroTitle, {
          y: 0,
          opacity: 1,
          duration: dur,
          ease: "power3.out",
          delay: 0.17,
        });
      }
      if (heroLede) {
        gsap.set(heroLede, { y: 20, opacity: 0 });
        gsap.to(heroLede, { y: 0, opacity: 1, duration: 0.55, ease: "power3.out", delay: 0.52 });
      }
      if (heroCta) {
        gsap.set(heroCta, { y: 20, opacity: 0 });
        gsap.to(heroCta, { y: 0, opacity: 1, duration: 0.55, ease: "power3.out", delay: 0.62 });
      }
      if (heroTabs) {
        gsap.set(heroTabs, { y: 20, opacity: 0 });
        gsap.to(heroTabs, { y: 0, opacity: 1, duration: 0.55, ease: "power3.out", delay: 0.72 });
      }
      if (heroPrice) {
        gsap.set(heroPrice, { scale: 0.7, opacity: 0 });
        gsap.to(heroPrice, { scale: 1, opacity: 1, duration: 0.5, ease: "back.out(2)", delay: 0.82 });
      }

      // ── Section-level scroll reveals are handled by useScrollReveals
      // (IntersectionObserver + CSS), NOT by GSAP. We only handle char-by-char
      // section title reveals here, since those need text splitting.

      gsap.utils.toArray(".reveal-chars").forEach((el) => {
        const inlines = el.querySelectorAll("span, strong, em");
        if (inlines.length) {
          inlines.forEach(splitChars);
        } else {
          splitChars(el);
        }
        const chars = el.querySelectorAll(".char");
        gsap.set(chars, { yPercent: 60, rotation: 6, opacity: 0 });

        // Play on first intersection — fire-and-forget, no ScrollTrigger sync needed.
        const io = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) {
                gsap.to(chars, {
                  yPercent: 0,
                  rotation: 0,
                  opacity: 1,
                  duration: dur * 0.6,
                  ease: "back.out(1.4)",
                  stagger: 0.012,
                });
                io.unobserve(entry.target);
              }
            });
          },
          { threshold: 0.15, rootMargin: "0px 0px -10% 0px" }
        );
        io.observe(el);
      });
    });

    return () => {
      ctx.revert();
      ST?.getAll().forEach((t) => t.kill());
    };
  }, [enabled, intensity]);
}

/* ── Three.js subtle hero background ── */
function useThreeHero(enabled) {
  const ref = useRefSA(null);

  useEffectSA(() => {
    if (!enabled || !window.THREE) return;
    const THREE = window.THREE;
    const host = document.querySelector(".hero__three");
    if (!host) return;
    host.innerHTML = "";

    const w = host.clientWidth;
    const h = host.clientHeight;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 100);
    camera.position.z = 6;

    const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(w, h);
    renderer.setClearColor(0x000000, 0);
    host.appendChild(renderer.domElement);

    // Group: wireframe icosphere + halo torus (echoes the spikeball net + ring)
    const group = new THREE.Group();

    const sphere = new THREE.Mesh(
      new THREE.IcosahedronGeometry(1.6, 1),
      new THREE.MeshBasicMaterial({
        color: 0xe8c84a,
        wireframe: true,
        transparent: true,
        opacity: 0.45,
      })
    );
    group.add(sphere);

    const ring = new THREE.Mesh(
      new THREE.TorusGeometry(2.2, 0.02, 8, 80),
      new THREE.MeshBasicMaterial({
        color: 0xff3d2e,
        transparent: true,
        opacity: 0.35,
      })
    );
    ring.rotation.x = Math.PI / 2.3;
    group.add(ring);

    // points around the sphere for a sense of "spikeball pattern"
    const dotGeom = new THREE.BufferGeometry();
    const N = 240;
    const pts = new Float32Array(N * 3);
    for (let i = 0; i < N; i++) {
      const phi = Math.acos(1 - 2 * (i + 0.5) / N);
      const theta = Math.PI * (1 + Math.sqrt(5)) * i;
      pts[i * 3] = 1.7 * Math.sin(phi) * Math.cos(theta);
      pts[i * 3 + 1] = 1.7 * Math.sin(phi) * Math.sin(theta);
      pts[i * 3 + 2] = 1.7 * Math.cos(phi);
    }
    dotGeom.setAttribute("position", new THREE.BufferAttribute(pts, 3));
    const dots = new THREE.Points(
      dotGeom,
      new THREE.PointsMaterial({
        color: 0xe8c84a,
        size: 0.025,
        transparent: true,
        opacity: 0.6,
      })
    );
    group.add(dots);

    scene.add(group);

    let raf;
    let mouseX = 0, mouseY = 0;
    const onMouse = (e) => {
      mouseX = (e.clientX / window.innerWidth) * 2 - 1;
      mouseY = -(e.clientY / window.innerHeight) * 2 + 1;
    };
    window.addEventListener("mousemove", onMouse, { passive: true });

    const start = performance.now();
    const tick = () => {
      const t = (performance.now() - start) / 1000;
      group.rotation.y = t * 0.18 + mouseX * 0.3;
      group.rotation.x = Math.sin(t * 0.2) * 0.18 + mouseY * 0.2;
      ring.rotation.z = t * 0.4;
      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    };
    tick();

    const onResize = () => {
      const w2 = host.clientWidth;
      const h2 = host.clientHeight;
      renderer.setSize(w2, h2);
      camera.aspect = w2 / h2;
      camera.updateProjectionMatrix();
    };
    window.addEventListener("resize", onResize);

    ref.current = { renderer, scene };

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", onMouse);
      window.removeEventListener("resize", onResize);
      renderer.dispose();
      sphere.geometry.dispose();
      sphere.material.dispose();
      ring.geometry.dispose();
      ring.material.dispose();
      dotGeom.dispose();
      dots.material.dispose();
      host.innerHTML = "";
    };
  }, [enabled]);
}

/* ── Lottie pulse — inline JSON for the live-now dot ── */
const LOTTIE_PULSE = {
  v: "5.7.0", fr: 30, ip: 0, op: 60, w: 64, h: 64, nm: "pulse", ddd: 0, assets: [],
  layers: [
    {
      ddd: 0, ind: 1, ty: 4, nm: "ring", sr: 1,
      ks: {
        o: { a: 1, k: [{ t: 0, s: [80] }, { t: 60, s: [0] }] },
        r: { a: 0, k: 0 },
        p: { a: 0, k: [32, 32, 0] },
        a: { a: 0, k: [0, 0, 0] },
        s: { a: 1, k: [{ t: 0, s: [30, 30, 100] }, { t: 60, s: [180, 180, 100] }] }
      },
      ao: 0,
      shapes: [
        {
          ty: "gr",
          it: [
            { ty: "el", p: { a: 0, k: [0, 0] }, s: { a: 0, k: [40, 40] }, d: 1 },
            { ty: "st", c: { a: 0, k: [0.91, 0.78, 0.29, 1] }, o: { a: 0, k: 100 }, w: { a: 0, k: 4 }, lc: 1, lj: 1, ml: 4, bm: 0 },
            { ty: "tr", p: { a: 0, k: [0, 0] }, a: { a: 0, k: [0, 0] }, s: { a: 0, k: [100, 100] }, r: { a: 0, k: 0 }, o: { a: 0, k: 100 } }
          ]
        }
      ],
      ip: 0, op: 60, st: 0, bm: 0
    },
    {
      ddd: 0, ind: 2, ty: 4, nm: "core", sr: 1,
      ks: {
        o: { a: 0, k: 100 },
        r: { a: 0, k: 0 },
        p: { a: 0, k: [32, 32, 0] },
        a: { a: 0, k: [0, 0, 0] },
        s: { a: 0, k: [100, 100, 100] }
      },
      ao: 0,
      shapes: [
        {
          ty: "gr",
          it: [
            { ty: "el", p: { a: 0, k: [0, 0] }, s: { a: 0, k: [16, 16] }, d: 1 },
            { ty: "fl", c: { a: 0, k: [0.91, 0.78, 0.29, 1] }, o: { a: 0, k: 100 }, r: 1, bm: 0 },
            { ty: "tr", p: { a: 0, k: [0, 0] }, a: { a: 0, k: [0, 0] }, s: { a: 0, k: [100, 100] }, r: { a: 0, k: 0 }, o: { a: 0, k: 100 } }
          ]
        }
      ],
      ip: 0, op: 60, st: 0, bm: 0
    }
  ]
};

function useLottiePulse(enabled) {
  useEffectSA(() => {
    if (!enabled || !window.lottie) return;
    const lottie = window.lottie;
    const slots = document.querySelectorAll('[data-lottie="pulse"]');
    const players = [];
    slots.forEach((slot) => {
      slot.innerHTML = "";
      const p = lottie.loadAnimation({
        container: slot,
        renderer: "svg",
        loop: true,
        autoplay: true,
        animationData: LOTTIE_PULSE,
      });
      players.push(p);
    });
    return () => players.forEach((p) => p.destroy());
  }, [enabled]);
}

/* ── Anime.js number counters ── */
function useAnimeCounters(enabled) {
  useEffectSA(() => {
    if (!enabled || !window.anime) return;
    const anime = window.anime;
    const els = document.querySelectorAll("[data-counter-target], .community__stat__v, .lobby__score [data-counter-target]");
    const animations = [];
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          const el = entry.target;
          if (el.dataset.counted) return;
          el.dataset.counted = "1";
          const raw = (el.dataset.counterTarget || el.textContent || "0").replace(/[^0-9.]/g, "");
          const target = parseFloat(raw) || 0;
          const suffix = (el.textContent || "").replace(/[0-9,.\s]+/g, "").trim();
          const obj = { v: 0 };
          const a = anime({
            targets: obj,
            v: target,
            duration: 1600,
            easing: "easeOutCubic",
            round: target > 100 ? 1 : 1,
            update: () => {
              const n = Math.round(obj.v);
              el.textContent = n.toLocaleString() + (suffix ? suffix : "");
            },
          });
          animations.push(a);
          observer.unobserve(el);
        });
      },
      { threshold: 0.4 }
    );
    els.forEach((el) => {
      // capture the original target before zeroing
      if (!el.dataset.counterTarget) {
        const raw = (el.textContent || "").replace(/[^0-9.]/g, "");
        if (raw) el.dataset.counterTarget = raw;
      }
      // freeze original suffix in the displayed text so we can re-apply it during anim
      observer.observe(el);
    });
    return () => {
      animations.forEach((a) => a.pause());
      observer.disconnect();
    };
  }, [enabled]);
}

/* ── Motion One button hover springs ── */
function useMotionHovers(enabled) {
  useEffectSA(() => {
    if (!enabled || !window.Motion) return;
    const { animate } = window.Motion;
    const btns = document.querySelectorAll(".btn, .platform-tab, .modes__item, .feat__cell, .stickybuy__cta");
    const cleanups = [];
    btns.forEach((btn) => {
      const onEnter = () => animate(btn, { scale: 1.04 }, { duration: 0.25, easing: [0.2, 0.7, 0.2, 1] });
      const onLeave = () => animate(btn, { scale: 1 }, { duration: 0.4, easing: [0.2, 0.7, 0.2, 1] });
      const onDown = () => animate(btn, { scale: 0.97 }, { duration: 0.1 });
      const onUp = () => animate(btn, { scale: 1.04 }, { duration: 0.18 });
      btn.addEventListener("mouseenter", onEnter);
      btn.addEventListener("mouseleave", onLeave);
      btn.addEventListener("mousedown", onDown);
      btn.addEventListener("mouseup", onUp);
      cleanups.push(() => {
        btn.removeEventListener("mouseenter", onEnter);
        btn.removeEventListener("mouseleave", onLeave);
        btn.removeEventListener("mousedown", onDown);
        btn.removeEventListener("mouseup", onUp);
      });
    });
    return () => cleanups.forEach((fn) => fn());
  }, [enabled]);
}

/* ── library status pill ──────────────────── */
function LibraryStatusPill({ libs }) {
  return (
    <div
      style={{
        position: "fixed",
        bottom: 14,
        left: 14,
        zIndex: 70,
        background: "rgba(7,7,10,0.85)",
        color: "var(--c-yellow)",
        padding: "8px 10px 7px",
        border: "1px solid rgba(232,200,74,0.35)",
        fontFamily: "var(--font-mono)",
        fontSize: 9,
        letterSpacing: "0.16em",
        textTransform: "uppercase",
        display: "flex",
        gap: 10,
        backdropFilter: "blur(8px)",
        pointerEvents: "none",
      }}
    >
      {libs.map(([name, on]) => (
        <span key={name} style={{ display: "inline-flex", alignItems: "center", gap: 4, opacity: on ? 1 : 0.3 }}>
          <span style={{ width: 5, height: 5, background: on ? "var(--c-yellow)" : "#555", borderRadius: "50%", display: "inline-block" }}></span>
          {name}
        </span>
      ))}
    </div>
  );
}

/* ── apply reveal class hooks to existing markup after first paint ── */
function useTagReveals() {
  useEffectSA(() => {
    // Section title char-by-char reveal — but NOT the hero, which animates via
    // the dedicated hero entry timeline.
    document.querySelectorAll(".sec-head__title").forEach((el) => el.classList.add("reveal-chars"));
    // Below-the-fold containers — hero kids deliberately excluded; they get a
    // dedicated entry timeline so they don't depend on scrollTrigger eval state.
    document.querySelectorAll(".feat").forEach((el) => el.classList.add("reveal-stagger"));
    document.querySelectorAll(".gallery").forEach((el) => el.classList.add("reveal-stagger"));
    document.querySelectorAll(".chars").forEach((el) => el.classList.add("reveal-stagger"));
    document.querySelectorAll(".press").forEach((el) => el.classList.add("reveal-stagger"));
    document.querySelectorAll(".roadmap").forEach((el) => el.classList.add("reveal-stagger"));
    document.querySelectorAll(".community__card, .community__stats, .lobby, .xplay2__copy").forEach((el) => el.classList.add("reveal-up"));
    document.querySelectorAll(".email__inner > *").forEach((el) => el.classList.add("reveal-up"));
    document.querySelectorAll(".trailer").forEach((el) => el.classList.add("reveal-fade"));
  }, []);
}

/* ── the standalone site host ─────────────── */
function SmashLeagueSite() {
  const [t, setTweak] = useTweaks(SITE_TWEAK_DEFAULTS);
  const [platform, setPlatform] = useStateSA("steam");
  const [showSticky, setShowSticky] = useStateSA(false);
  const [lbox, setLbox] = useStateSA({ open: false, idx: 0 });

  useEffectSA(() => {
    const onScroll = () => setShowSticky(window.scrollY > 600);
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // Hook up animations after the DOM renders. The order matters:
  // tag reveals FIRST so the elements have the classes before observers grab them.
  useTagReveals();
  useScrollReveals(t.animations);
  useGsapReveals(t.animations && t.gsapReveals, t.intensity);
  useThreeHero(t.animations && t.threeHero);
  useLottiePulse(t.animations && t.lottiePulse);
  useAnimeCounters(t.animations && t.numberCounters);
  useMotionHovers(t.animations && t.microInteractions);

  return (
    <>
      <div className="lp is-site" data-palette={t.palette} data-animations={t.animations ? "on" : "off"}>
        <Nav onBuy={() => setShowSticky(true)} />
        <Hero platform={platform} setPlatform={setPlatform} annotate={false} />
        <Features annotate={false} />
        <Modes annotate={false} />
        <Gallery onOpen={(i) => setLbox({ open: true, idx: i })} />
        <Characters />
        <CrossPlay />
        <Community annotate={false} />
        <Email annotate={false} />
        <Press />
        <Roadmap annotate={false} />
        <FAQ />
        <Foot />
        <StickyBuy show={showSticky} platform={platform} setPlatform={setPlatform} />
        <Lightbox
          open={lbox.open}
          idx={lbox.idx}
          onClose={() => setLbox({ open: false, idx: 0 })}
          onPrev={() => setLbox((s) => ({ ...s, idx: (s.idx - 1 + GALLERY_TILES.length) % GALLERY_TILES.length }))}
          onNext={() => setLbox((s) => ({ ...s, idx: (s.idx + 1) % GALLERY_TILES.length }))}
        />
      </div>

      <TweaksPanel>
        <TweakSection label="Animation master" />
        <TweakToggle
          label="Enable animations"
          value={t.animations}
          onChange={(v) => setTweak("animations", v)}
        />

        <TweakSection label="Reload" />
        <TweakButton
          label="↺ Hard refresh page"
          onClick={() => window.location.reload()}
        />
      </TweaksPanel>
    </>
  );
}

const __siteRoot = ReactDOM.createRoot(document.getElementById("root"));
__siteRoot.render(<SmashLeagueSite />);
