// ============================================================
// Alprovid — Shared components (Typewriter, Reveal, Media/Poster)
// ============================================================

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

/* ---------- Typewriter ---------- */
function Typewriter({ lines, typeSpeed = 55, deleteSpeed = 30, holdMs = 2500, className = "" }) {
  const [idx, setIdx] = useState(0);
  const [text, setText] = useState("");
  const [phase, setPhase] = useState("typing");
  const timer = useRef(null);

  useEffect(() => {
    const current = lines[idx];
    if (phase === "typing") {
      if (text.length < current.length) {
        timer.current = setTimeout(() => setText(current.slice(0, text.length + 1)), typeSpeed);
      } else {
        timer.current = setTimeout(() => setPhase("deleting"), holdMs);
      }
    } else if (phase === "deleting") {
      if (text.length > 0) {
        timer.current = setTimeout(() => setText(current.slice(0, text.length - 1)), deleteSpeed);
      } else {
        setIdx((idx + 1) % lines.length);
        setPhase("typing");
      }
    }
    return () => clearTimeout(timer.current);
  }, [text, phase, idx, lines, typeSpeed, deleteSpeed, holdMs]);

  return <span className={`tw-cursor ${className}`}>{text}</span>;
}

/* ---------- ScrollReveal ---------- */
function Reveal({ children, delay = 0, as = "div", className = "", ...rest }) {
  const ref = useRef(null);
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) { setTimeout(() => setShown(true), delay); io.disconnect(); }
      },
      { threshold: 0.1, rootMargin: "0px 0px -50px 0px" }
    );
    io.observe(el);
    return () => io.disconnect();
  }, [delay]);
  const Tag = as;
  return <Tag ref={ref} className={`reveal ${shown ? "in" : ""} ${className}`} {...rest}>{children}</Tag>;
}

/* ---------- R2 video hosting ----------
   Public base URL of the Cloudflare R2 bucket (alprovid-videos).
   Portfolio videos live here, NOT in the Git repo. A video field that is
   just a filename (e.g. "playcast.mp4") is resolved against this base.
   A full http(s) URL is used as-is.
*/
const R2_BASE = "https://pub-3d51ae303ee247818d8fa6a9228e95e3.r2.dev/";
const mediaUrl = (v) => !v ? "" : (/^https?:\/\//i.test(v) ? v : R2_BASE + v.replace(/^\/+/, ""));

/* ---------- Media ----------
   Renders, in priority order:
   1. a video from R2 when `video` is set (autoplay = muted looping reel,
      otherwise a still first-frame / poster for the click-to-play cards)
   2. a real image/gif when `src` is provided
   3. a refined dark "poster" placeholder
*/
const POSTER_TINTS = ["#231c2e", "#1c2630", "#2e221c", "#1c2e24", "#2e1c26", "#262e1c"];
function Media({ src, video, poster, autoplay = false, alt = "", mark = "", tint = 0, className = "", loading = "lazy" }) {
  if (video) {
    const vurl = mediaUrl(video);
    const purl = mediaUrl(poster);
    if (autoplay) {
      return (
        <video className={className} src={vurl} poster={purl || undefined}
               autoPlay muted loop playsInline preload="metadata" aria-label={alt} />
      );
    }
    // Click-to-play card: show poster image if given, else the video's first frame.
    if (purl) return <img className={className} src={purl} alt={alt} loading={loading} />;
    return <video className={className} src={`${vurl}#t=0.1`} muted playsInline preload="metadata" aria-label={alt} />;
  }
  if (src) {
    return <img className={className} src={src} alt={alt} loading={loading} />;
  }
  return (
    <div className={`poster ${className}`} style={{ "--p1": POSTER_TINTS[tint % POSTER_TINTS.length] }} aria-label={alt}>
      <span className="pcorner tl" />
      <span className="pcorner br" />
      <span className="pmark">{mark}</span>
    </div>
  );
}

Object.assign(window, { Typewriter, Reveal, Media, R2_BASE, mediaUrl });
