// Curator Bookings — shared UI components

const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ============ CUSTOM CURSOR ============ */
function Cursor() {
  const dotRef = useRef(null);
  const labelRef = useRef(null);
  const stateRef = useRef({ x: 0, y: 0, tx: 0, ty: 0, raf: 0 });

  useEffect(() => {
    const onMove = (e) => {
      stateRef.current.tx = e.clientX;
      stateRef.current.ty = e.clientY;
    };
    const tick = () => {
      const s = stateRef.current;
      s.x += (s.tx - s.x) * 0.28;
      s.y += (s.ty - s.y) * 0.28;
      if (dotRef.current) {
        dotRef.current.style.transform = `translate(${s.x}px, ${s.y}px) translate(-50%, -50%)`;
      }
      if (labelRef.current) {
        labelRef.current.style.transform = `translate(${s.x + 18}px, ${s.y + 14}px)`;
      }
      s.raf = requestAnimationFrame(tick);
    };
    window.addEventListener("mousemove", onMove);
    stateRef.current.raf = requestAnimationFrame(tick);

    // Delegate hover state via data attributes
    const checkHover = (e) => {
      const t = e.target.closest?.("[data-cursor]");
      const dot = dotRef.current, lab = labelRef.current;
      if (!dot || !lab) return;
      if (t) {
        const label = t.getAttribute("data-cursor");
        if (label) { lab.textContent = label; lab.classList.add("show"); }
        else lab.classList.remove("show");
        if (t.getAttribute("data-cursor-grow") !== null) dot.classList.add("grow");
        else dot.classList.remove("grow");
      } else {
        lab.classList.remove("show");
        dot.classList.remove("grow");
      }
    };
    window.addEventListener("mouseover", checkHover);

    return () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseover", checkHover);
      cancelAnimationFrame(stateRef.current.raf);
    };
  }, []);

  return (
    <>
      <div className="cursor-dot" ref={dotRef}></div>
      <div className="cursor-label" ref={labelRef}></div>
    </>
  );
}

/* ============ CLOCK (Cape Town) ============ */
function Clock() {
  const [t, setT] = useState("");
  useEffect(() => {
    const tick = () => {
      const d = new Date();
      const utc = d.getTime() + d.getTimezoneOffset() * 60000;
      const cpt = new Date(utc + 2 * 3600000);
      const hh = String(cpt.getHours()).padStart(2, "0");
      const mm = String(cpt.getMinutes()).padStart(2, "0");
      const ss = String(cpt.getSeconds()).padStart(2, "0");
      setT(`${hh}:${mm}:${ss}`);
    };
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
  return <span className="clock">{t} SAST</span>;
}

/* ============ NAV ============ */
function Nav({ page, go, theme, setTheme }) {
  const items = [
    ["roster", "Roster"],
    ["services", "Services"],
    ["about", "About"],
    ["book", "Book"],
  ];
  return (
    <header className="nav">
      <div className="nav-brand" data-cursor="Home" onClick={() => go("roster")}>
        <img className="mark mark-dark" src={window.LOGO_DARK} alt="" />
        <img className="mark mark-light" src={window.LOGO_LIGHT} alt="" />
        <span>Curator <em>Bookings</em></span>
      </div>
      <nav className="nav-center">
        {items.map(([k, label]) => (
          <button
            key={k}
            className={page === k ? "active" : ""}
            onClick={() => go(k)}
            data-cursor=""
          >
            {label}
          </button>
        ))}
      </nav>
      <div className="nav-right">
        <div>Cape Town · 33.9°S</div>
        <div><Clock /></div>
        <div className="theme-toggle" role="group" aria-label="Theme">
          <button
            className={theme === "light" ? "on" : ""}
            onClick={() => setTheme("light")}
            data-cursor="Light"
          >
            <span className="glyph"></span> Light
          </button>
          <button
            className={theme === "dark" ? "on" : ""}
            onClick={() => setTheme("dark")}
            data-cursor="Dark"
          >
            <span className="glyph moon"></span> Dark
          </button>
        </div>
      </div>
    </header>
  );
}

/* ============ FOOTER ============ */
function Foot({ go }) {
  return (
    <footer>
      <div className="foot">
        <div className="col">
          <div className="lbl">Studio</div>
          <div className="big">107 Buitenkant Street<br/><em>Gardens, Cape Town</em></div>
        </div>
        <div className="col">
          <div className="lbl">Enquiries</div>
          <ul>
            <li>riley@curatoragency.co.za</li>
          </ul>
        </div>
        <div className="col">
          <div className="lbl">Index</div>
          <ul>
            <li><a onClick={() => go("roster")} data-cursor="">Roster</a></li>
            <li><a onClick={() => go("services")} data-cursor="">Services</a></li>
            <li><a onClick={() => go("about")} data-cursor="">About</a></li>
            <li><a onClick={() => go("book")} data-cursor="">Book</a></li>
          </ul>
        </div>
      </div>
      <div className="foot-sub">
        <span>© Curator Bookings · MMXXVI</span>
        <span>Cape Town · Johannesburg · Berlin</span>
      </div>
    </footer>
  );
}

/* ============ CURTAIN (page transition) ============ */
function Curtain({ keyTick, label }) {
  return (
    <>
      <div key={"c" + keyTick} className="curtain in"></div>
      <div key={"l" + keyTick} className="curtain-label in"><img className="curtain-logo" src={window.LOGO_DARK} alt="Curator Bookings" /></div>
    </>
  );
}

/* ============ SCRAMBLE TEXT ============ */
// Reveals text char-by-char with a brief glyph-scramble per character.
// Preserves layout by rendering final text as transparent "ghost" and
// overlaying the live scramble absolutely on top.
const SCRAMBLE_GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/\\|*+=<>#@".split("");

function ScrambleText({ text, delay = 0, perChar = 38, scrambleFor = 260, className = "", onDone }) {
  const [display, setDisplay] = useState(() => text.replace(/\S/g, "·"));
  const doneRef = useRef(false);

  useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) {
      setDisplay(text);
      if (onDone) onDone();
      return;
    }

    const chars = Array.from(text);
    // Each char gets a "reveal at" time (ms from mount) and a scramble window.
    const revealAt = chars.map((_, i) => delay + i * perChar);

    let raf = 0;
    const start = performance.now();
    const isSpace = (c) => c === " " || c === "\u00A0";
    const isPunct = (c) => /[,.—–\-:/'"!?()]/.test(c);

    const tick = (now) => {
      const elapsed = now - start;
      let allDone = true;
      const out = chars.map((c, i) => {
        if (isSpace(c)) return c;
        const t = elapsed - revealAt[i];
        if (t < 0) {
          allDone = false;
          // not started yet — show a faint neutral glyph
          return isPunct(c) ? c : SCRAMBLE_GLYPHS[(i * 7 + Math.floor(now / 60)) % SCRAMBLE_GLYPHS.length];
        }
        if (t < scrambleFor) {
          allDone = false;
          if (isPunct(c)) return c;
          // cycle through glyphs fast
          const g = SCRAMBLE_GLYPHS[(i * 13 + Math.floor(now / 32)) % SCRAMBLE_GLYPHS.length];
          return g;
        }
        return c;
      });
      setDisplay(out.join(""));
      if (!allDone) {
        raf = requestAnimationFrame(tick);
      } else if (!doneRef.current) {
        doneRef.current = true;
        if (onDone) onDone();
      }
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [text, delay, perChar, scrambleFor]);

  return <span className={"scramble " + className}>{display}</span>;
}

/* ============ PARTICLE TEXT (cursor-reactive dispersion) ============ */
function ParticleText({ children, radius = 140, strength = 1 }) {
  const wrapRef = useRef(null);

  useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) return;

    // Split every text node into per-character spans
    const chars = [];
    const walk = (node) => {
      if (node.nodeType === 3) {
        const text = node.nodeValue;
        if (!text || !text.trim()) return;
        const frag = document.createDocumentFragment();
        for (const ch of text) {
          if (ch === " " || ch === "\u00A0") {
            frag.appendChild(document.createTextNode(ch));
          } else {
            const s = document.createElement("span");
            s.className = "pt-ch";
            s.textContent = ch;
            s.style.display = "inline-block";
            s.style.willChange = "transform";
            frag.appendChild(s);
            chars.push(s);
          }
        }
        node.parentNode.replaceChild(frag, node);
      } else if (node.nodeType === 1 && !node.classList?.contains("pt-ch")) {
        Array.from(node.childNodes).forEach(walk);
      }
    };
    Array.from(wrap.childNodes).forEach(walk);

    // Per-char state
    const state = chars.map((el) => ({
      el,
      cx: 0, cy: 0,
      x: 0, y: 0,
      vx: 0, vy: 0,
      tx: 0, ty: 0,
      rot: 0, trot: 0, vrot: 0,
    }));

    const measure = () => {
      const wr = wrap.getBoundingClientRect();
      for (const s of state) {
        const r = s.el.getBoundingClientRect();
        s.cx = r.left + r.width / 2 - wr.left;
        s.cy = r.top + r.height / 2 - wr.top;
      }
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(wrap);
    window.addEventListener("resize", measure);

    let mx = -9999, my = -9999;
    let active = false;
    const onMove = (e) => {
      const wr = wrap.getBoundingClientRect();
      mx = e.clientX - wr.left;
      my = e.clientY - wr.top;
      active = true;
    };
    const onLeave = () => { active = false; mx = -9999; my = -9999; };
    wrap.addEventListener("mousemove", onMove);
    wrap.addEventListener("mouseleave", onLeave);

    const R2 = radius * radius;
    let raf = 0;
    const tick = () => {
      for (const s of state) {
        if (active) {
          const dx = s.cx - mx;
          const dy = s.cy - my;
          const d2 = dx * dx + dy * dy;
          if (d2 < R2) {
            const d = Math.sqrt(d2) || 1;
            const f = (1 - d / radius) * 60 * strength;
            s.tx = (dx / d) * f;
            s.ty = (dy / d) * f - Math.min(24, f * 0.4); // slight upward drift
            s.trot = (dx / d) * 18 * strength;
          } else {
            s.tx = 0; s.ty = 0; s.trot = 0;
          }
        } else {
          s.tx = 0; s.ty = 0; s.trot = 0;
        }
        // spring
        const ax = (s.tx - s.x) * 0.14 - s.vx * 0.28;
        const ay = (s.ty - s.y) * 0.14 - s.vy * 0.28;
        s.vx += ax; s.vy += ay;
        s.x += s.vx; s.y += s.vy;
        const arot = (s.trot - s.rot) * 0.12 - s.vrot * 0.3;
        s.vrot += arot; s.rot += s.vrot;
        s.el.style.transform = `translate3d(${s.x.toFixed(2)}px, ${s.y.toFixed(2)}px, 0) rotate(${s.rot.toFixed(2)}deg)`;
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      window.removeEventListener("resize", measure);
      wrap.removeEventListener("mousemove", onMove);
      wrap.removeEventListener("mouseleave", onLeave);
    };
  }, [radius, strength]);

  return <span ref={wrapRef} className="particle-text">{children}</span>;
}

/* ============ MAGNETIC TEXT (cursor-attracted, restrained) ============ */
// Each character is pulled gently toward the cursor when it's nearby.
// Movement is small (capped), elastic (critically damped spring), and
// rotation-free for a refined, minimal feel.
function MagneticText({ children, radius = 220, pull = 14 }) {
  const wrapRef = useRef(null);

  useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) return;

    // Split text nodes into per-character spans, preserving <br/> and <em>.
    const chars = [];
    const walk = (node) => {
      if (node.nodeType === 3) {
        const text = node.nodeValue;
        if (!text || !text.trim()) return;
        const frag = document.createDocumentFragment();
        for (const ch of text) {
          if (ch === " " || ch === "\u00A0") {
            frag.appendChild(document.createTextNode(ch));
          } else {
            const s = document.createElement("span");
            s.className = "mt-ch";
            s.textContent = ch;
            s.style.display = "inline-block";
            s.style.willChange = "transform";
            frag.appendChild(s);
            chars.push(s);
          }
        }
        node.parentNode.replaceChild(frag, node);
      } else if (node.nodeType === 1 && !node.classList?.contains("mt-ch")) {
        Array.from(node.childNodes).forEach(walk);
      }
    };
    Array.from(wrap.childNodes).forEach(walk);

    // Per-char state
    const state = chars.map((el) => ({
      el,
      cx: 0, cy: 0,
      x: 0, y: 0,
      vx: 0, vy: 0,
      tx: 0, ty: 0,
    }));

    const measure = () => {
      const wr = wrap.getBoundingClientRect();
      for (const s of state) {
        const r = s.el.getBoundingClientRect();
        s.cx = r.left + r.width / 2 - wr.left;
        s.cy = r.top + r.height / 2 - wr.top;
      }
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(wrap);
    window.addEventListener("resize", measure);

    let mx = -9999, my = -9999;
    let active = false;
    // Listen on window so cursor proximity works even when not over a glyph.
    const onMove = (e) => {
      const wr = wrap.getBoundingClientRect();
      mx = e.clientX - wr.left;
      my = e.clientY - wr.top;
      active = true;
    };
    const onLeave = () => { active = false; };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseleave", onLeave);

    let raf = 0;
    let idleFrames = 0;
    const tick = () => {
      let moving = false;
      for (const s of state) {
        if (active) {
          const dx = mx - s.cx;
          const dy = my - s.cy;
          const d = Math.hypot(dx, dy) || 1;
          if (d < radius) {
            // Smooth falloff: 1 at center, 0 at radius, with eased curve.
            const t = 1 - d / radius;
            const f = t * t * pull; // quadratic ease for restraint
            s.tx = (dx / d) * f;
            s.ty = (dy / d) * f;
          } else {
            s.tx = 0; s.ty = 0;
          }
        } else {
          s.tx = 0; s.ty = 0;
        }
        // Critically-damped spring — smooth, slightly elastic, no overshoot wobble.
        const ax = (s.tx - s.x) * 0.10 - s.vx * 0.34;
        const ay = (s.ty - s.y) * 0.10 - s.vy * 0.34;
        s.vx += ax; s.vy += ay;
        s.x += s.vx; s.y += s.vy;
        if (Math.abs(s.vx) > 0.01 || Math.abs(s.vy) > 0.01 || Math.abs(s.x - s.tx) > 0.05 || Math.abs(s.y - s.ty) > 0.05) {
          moving = true;
        }
        s.el.style.transform = `translate3d(${s.x.toFixed(2)}px, ${s.y.toFixed(2)}px, 0)`;
      }
      // Keep the loop alive; bail only when fully settled and inactive (saves cycles).
      if (!moving && !active) {
        idleFrames++;
      } else {
        idleFrames = 0;
      }
      if (idleFrames < 30) {
        raf = requestAnimationFrame(tick);
      } else {
        raf = 0;
      }
    };
    const ensureRunning = () => { if (!raf) raf = requestAnimationFrame(tick); };
    const onMoveStart = () => ensureRunning();
    window.addEventListener("mousemove", onMoveStart, { passive: true });
    raf = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      window.removeEventListener("resize", measure);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseleave", onLeave);
      window.removeEventListener("mousemove", onMoveStart);
    };
  }, [radius, pull]);

  return <span ref={wrapRef} className="magnetic-text">{children}</span>;
}

/* ============ SPLIT WORDS (hover split-layer) ============ */
// Each word renders 3 stacked layers: base (transparent on hover) and two
// ghost layers via ::before / ::after that offset horizontally on hover.
// data-text is the source string the pseudo-elements display.
function SplitWords({ children }) {
  // Accept string or array of strings; flatten and split into words while
  // preserving spaces between them.
  const collect = (node) => {
    if (node == null || typeof node === "boolean") return "";
    if (typeof node === "string" || typeof node === "number") return String(node);
    if (Array.isArray(node)) return node.map(collect).join("");
    if (typeof node === "object" && node.props) return collect(node.props.children);
    return "";
  };
  const text = collect(children);
  const tokens = [];
  // tokens alternate words and whitespace
  let m;
  const re = /(\s+)/g;
  let last = 0;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) tokens.push({ word: true, t: text.slice(last, m.index) });
    tokens.push({ word: false, t: m[0] });
    last = re.lastIndex;
  }
  if (last < text.length) tokens.push({ word: true, t: text.slice(last) });

  return (
    <>
      {tokens.map((tk, i) =>
        tk.word
          ? <span key={i} className="sw-word" data-text={tk.t}>{tk.t}</span>
          : <React.Fragment key={i}>{tk.t}</React.Fragment>
      )}
    </>
  );
}

/* ============ BANDCAMP PLAYER ============ */
// Renders the official Bandcamp iframe when albumId is provided.
// Falls back to a styled placeholder player that matches the site palette.
function BandcampPlayer({ artist }) {
  const [theme, setTheme] = React.useState(() => document.documentElement.getAttribute("data-theme") || "light");
  React.useEffect(() => {
    const obs = new MutationObserver(() => setTheme(document.documentElement.getAttribute("data-theme") || "light"));
    obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
    return () => obs.disconnect();
  }, []);
  const isDark = theme === "dark";
  const bgcol = isDark ? "0b0b0b" : "f6f4ef";
  const linkcol = isDark ? "f2efe8" : "0a0a0a";
  // SoundCloud takes precedence when provided — used per-artist (e.g. Klankers).
  if (artist.soundcloud) {
    const sc = artist.soundcloud;
    const url = typeof sc === "string" ? sc : sc.url;
    const src = `https://w.soundcloud.com/player/?url=${encodeURIComponent(url)}&color=%23111111&inverse=false&auto_play=false&show_user=true&visual=false&hide_related=true&show_comments=false&show_reposts=false&show_teaser=false`;
    return (
      <div className="bcp">
        <div className="bcp-cap">Listen — via SoundCloud</div>
        <div className="bcp-release">
          <iframe
            className="bcp-frame bcp-frame--sc"
            src={src}
            scrolling="no"
            frameBorder="no"
            allow="autoplay"
            title={`${artist.first} ${artist.last} — SoundCloud`}
            loading="lazy"
          />
          <a className="bcp-foot" href={url} target="_blank" rel="noopener">
            <span>{(typeof sc === "object" && sc.title) || "Listen on SoundCloud"}</span>
            <span className="bcp-foot-arrow">↗ soundcloud.com</span>
          </a>
        </div>
      </div>
    );
  }

  const bc = artist.bandcamp || {};
  const releases = bc.releases || (bc.albumId ? [{ albumId: bc.albumId, url: bc.url, title: bc.title, year: bc.year }] : []);

  if (releases.length) {
    return (
      <div className="bcp">
        <div className="bcp-cap">Listen — via Bandcamp</div>
        {releases.map((r, i) => {
          const src = `https://bandcamp.com/EmbeddedPlayer/album=${r.albumId}/size=large/bgcol=${bgcol}/linkcol=${linkcol}/tracklist=true/artwork=small/`;
          return (
            <div className="bcp-release" key={r.albumId || i}>
              <iframe
                className="bcp-frame"
                src={src}
                seamless
                title={`${artist.first} ${artist.last} — ${r.title || "Bandcamp player"}`}
                loading="lazy"
              />
              {r.url && (
                <a className="bcp-foot" href={r.url} target="_blank" rel="noopener">
                  <span>{r.title || "Discography"} {r.year ? `· ${r.year}` : ""}</span>
                  <span className="bcp-foot-arrow">↗ bandcamp.com</span>
                </a>
              )}
            </div>
          );
        })}
      </div>
    );
  }

  // Fallback static player — clearly a placeholder via subtle striping.
  const tracks = bc.tracks || [
    { n: "01", t: bc.title ? `${bc.title} — A1` : "Untitled A1", d: "5:42" },
    { n: "02", t: bc.title ? `${bc.title} — A2` : "Untitled A2", d: "6:14" },
    { n: "03", t: bc.title ? `${bc.title} — B1` : "Untitled B1", d: "4:38" },
    { n: "04", t: bc.title ? `${bc.title} — B2` : "Untitled B2", d: "7:02" },
  ];
  return (
    <div className="bcp bcp--placeholder">
      <div className="bcp-cap">Listen — via Bandcamp</div>
      <div className="bcp-fallback">
        <div className="bcp-art" aria-hidden="true">
          <div className="bcp-art-stripe" />
          <div className="bcp-art-label">{(bc.title || "Discography").toUpperCase()}</div>
        </div>
        <div className="bcp-body">
          <div className="bcp-meta">
            <div className="bcp-title">{bc.title || "Selected Works"}</div>
            <div className="bcp-sub">{artist.first} {artist.last} · {bc.year || artist.years.replace("Est. ","")}</div>
          </div>
          <div className="bcp-controls">
            <button className="bcp-play" aria-label="Play preview">▶</button>
            <div className="bcp-scrub"><div className="bcp-scrub-fill" /></div>
            <div className="bcp-time">0:00 / —:—</div>
          </div>
          <ul className="bcp-tracks">
            {tracks.map((t) => (
              <li key={t.n}>
                <span className="bcp-tn">{t.n}</span>
                <span className="bcp-tt">{t.t}</span>
                <span className="bcp-td">{t.d}</span>
              </li>
            ))}
          </ul>
        </div>
      </div>
      {bc.url && (
        <a className="bcp-foot" href={bc.url} target="_blank" rel="noopener">
          <span>Full discography</span>
          <span className="bcp-foot-arrow">↗ bandcamp.com</span>
        </a>
      )}
    </div>
  );
}

Object.assign(window, { Cursor, Nav, Foot, Curtain, Clock, ScrambleText, ParticleText, MagneticText, SplitWords, BandcampPlayer });
