// Ambient components — magic particles, potion sparks, eyes, lore runes, glitch
const { useState, useEffect, useRef, useMemo } = React;

// ===== Magic Particles drifting up =====
function ParticleField({ count = 40 }) {
  const particles = useMemo(() => {
    const arr = [];
    for (let i = 0; i < count; i++) {
      const isViolet = Math.random() > 0.5;
      const sizeRoll = Math.random();
      let sizeClass = "";
      if (sizeRoll < 0.35) sizeClass = "tiny";else
      if (sizeRoll > 0.85) sizeClass = "large";
      arr.push({
        id: i,
        color: isViolet ? "violet" : "green",
        size: sizeClass,
        left: Math.random() * 100,
        duration: 8 + Math.random() * 14,
        delay: -Math.random() * 20
      });
    }
    return arr;
  }, [count]);

  return (
    <div className="particles-layer">
      {particles.map((p) =>
      <div
        key={p.id}
        className={`particle ${p.color} ${p.size}`}
        style={{
          left: `${p.left}%`,
          animationDuration: `${p.duration}s`,
          animationDelay: `${p.delay}s`
        }}>
      </div>
      )}
    </div>);

}

// ===== Bubbling potion sparks (concentrated pockets) =====
function SparkBurst({ style, color = "green", count = 6 }) {
  const sparks = useMemo(() => {
    return Array.from({ length: count }, (_, i) => ({
      id: i,
      left: Math.random() * 60 - 30,
      delay: Math.random() * 3,
      duration: 2 + Math.random() * 2
    }));
  }, [count]);

  return (
    <div style={{ position: "absolute", ...style, pointerEvents: "none" }}>
      {sparks.map((s) =>
      <span
        key={s.id}
        className={`spark ${color === "violet" ? "v" : ""}`}
        style={{
          left: `${s.left}px`,
          animationDelay: `${s.delay}s`,
          animationDuration: `${s.duration}s`
        }}>
      </span>
      )}
    </div>);

}

// ===== Single Eye — plays ALL of its animations in a random order, on its own clock.
// `closedFrame` marks the most-shut frame of the blink clip (only eye1 truly closes;
// eyes 2 & 3 "blink" via the iris, so their closedFrame is the last frame).
// `minW`/`maxW` give each eye TYPE a size range — instances pick a random size in it.
const EYE_REGISTRY = {
  1: {
    minW: 96, maxW: 138,
    idle:  { dir: "assets/eyes/eye1/idle",  count: 6,  fps: 8  },
    blink: { dir: "assets/eyes/eye1/blink", count: 16, fps: 18, closedFrame: 7 },
    look:  { dir: "assets/eyes/eye1/look",  count: 5,  fps: 8  },
  },
  2: {
    minW: 72, maxW: 110,
    idle:  { dir: "assets/eyes/eye2/idle",  count: 6, fps: 8  },
    blink: { dir: "assets/eyes/eye2/blink", count: 4, fps: 12, closedFrame: 3 },
    look:  { dir: "assets/eyes/eye2/look",  count: 7, fps: 10 },
  },
  3: {
    minW: 80, maxW: 126,
    idle:  { dir: "assets/eyes/eye3/idle",  count: 4, fps: 6, },
    blink: { dir: "assets/eyes/eye3/blink", count: 4, fps: 12, closedFrame: 3 },
    look:  { dir: "assets/eyes/eye3/look",  count: 4, fps: 8  },
  },
};

const padFrame = (i) => String(i + 1).padStart(4, "0");

function Eye({
  eye = 1,
  src, // legacy escape hatch
  top, left, right, bottom,
  width, // optional explicit override; otherwise random within the type's min–max
  rotate = 0,
  blinkDelay = 0,
  fadeDelay = 0,
  blinkPeriod = 5.5,
  fadePeriod = 14,
}) {
  const reg = EYE_REGISTRY[eye];

  // ----- per-instance jitter (rotation + mirror + size), stable across re-renders -----
  const jitter = useMemo(() => {
    const min = (reg && reg.minW) || 70;
    const max = (reg && reg.maxW) || 100;
    return {
      rotJitter: (Math.random() - 0.5) * 8, // ±4°
      mirror: Math.random() < 0.5,
      size: width != null ? width : Math.round(min + Math.random() * (max - min)),
    };
  }, [eye, width]);

  const position = {};
  if (top !== undefined) position.top = top;
  if (left !== undefined) position.left = left;
  if (right !== undefined) position.right = right;
  if (bottom !== undefined) position.bottom = bottom;

  const finalRotate = rotate + jitter.rotJitter;
  const mirrorScale = jitter.mirror ? -1 : 1;
  const transform = `rotate(${finalRotate}deg) scaleX(${mirrorScale})`;
  const finalWidth = jitter.size;

  const hasFrames = reg && reg.idle && reg.blink;

  // Static fallback (only if explicit src override)
  if (!hasFrames || src) {
    const finalSrc = src || (reg && reg.staticSrc) || "assets/eye-3.png";
    return (
      <div className="eye" style={{ ...position, width: `${finalWidth}px`, transform }}>
        <img
          className="eye-png"
          src={finalSrc}
          alt=""
          style={{
            "--fade-dur": `${fadePeriod}s`,
            "--fade-delay": `-${fadeDelay}s`,
            "--blink-dur": `${blinkPeriod}s`,
            "--blink-delay": `-${blinkDelay}s`,
          }} />
      </div>);
  }

  // ----- sprite scheduler -----
  const spriteRef = useRef(null);
  const refs = useRef({ idle: [], blink: [], look: [] });

  useEffect(() => {
    return runEyeSequence({
      reg,
      spriteRef,
      refs,
      blinkPeriod,
      blinkDelay,
      fadePeriod,
      fadeDelay,
    });
  }, [eye, blinkPeriod, blinkDelay, fadePeriod, fadeDelay]);

  return (
    <div className="eye" style={{ ...position, width: `${finalWidth}px`, transform }}>
      <div ref={spriteRef} className="eye-sprite">
        {["idle", "blink", "look"].flatMap((state) =>
          reg[state]
            ? Array.from({ length: reg[state].count }, (_, i) =>
                <img
                  key={`${state}${i}`}
                  ref={(el) => (refs.current[state][i] = el)}
                  src={`${reg[state].dir}/${padFrame(i)}.png`}
                  alt=""
                  draggable={false} />
              )
            : []
        )}
      </div>
    </div>);
}

// ===== Eye sequencer ==========================================================
// Each cycle: blink-IN → a shuffled playlist of EVERY animation the eye owns
// (idle / look / blink, with little idle "breaths" between) → blink-OUT → hidden.
// • Fade IN is bound to a blink opening: opacity 0→1 as the lid lifts off the
//   closed frame (eye1) or across the iris blink (eyes 2 & 3).
// • Fade OUT is bound to a blink closing: opacity 1→0 reaching 0 exactly on the
//   closed frame, so the eye vanishes the instant it shuts.
// onPhaseChange(phase) is optional (used by the Eyes.html workshop panel).
// Returns a cleanup function that cancels the raf.
function runEyeSequence({
  reg, spriteRef, refs,
  blinkPeriod, blinkDelay, fadePeriod, fadeDelay,
  onPhaseChange = null,
}) {
  const sprite = spriteRef.current;
  if (!sprite) return () => {};

  const show = (state, idx) => {
    const allStates = ["idle", "blink", "look"];
    for (const s of allStates) {
      const arr = refs.current[s];
      for (let i = 0; i < arr.length; i++) {
        if (!arr[i]) continue;
        const want = s === state && i === idx;
        if (want !== arr[i].classList.contains("on")) {
          arr[i].classList.toggle("on", want);
        }
      }
    }
  };
  const setOpacity = (o) => { sprite.style.opacity = o; };
  const smooth = (t) => { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); };
  const rand = (a, b) => a + Math.random() * (b - a);
  const shuffle = (arr) => {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr;
  };

  const idleMs   = 1000 / reg.idle.fps;
  const blinkMs  = 1000 / reg.blink.fps;
  const lookMs   = reg.look ? 1000 / reg.look.fps : idleMs;
  const closedF  = reg.blink.closedFrame != null ? reg.blink.closedFrame : reg.blink.count - 1;
  const lastF    = reg.blink.count - 1;
  const hasClose = closedF < lastF; // true blink that shuts mid-clip (eye1)

  // Hidden window scales with fadePeriod (the desync knob from each call site).
  const hiddenDur = () => Math.max(1600, fadePeriod * 1000 * (0.3 + Math.random() * 0.3));

  // Build one cycle: blink-in, every animation once (shuffled, idle breaths
  // between), blink-out, hidden.
  function buildCycle() {
    const beats = [{ type: "blinkIn" }];
    const clips = ["idle", "look", "blink"].filter((c) => reg[c]);
    shuffle(clips);
    clips.forEach((c) => {
      if (c === "idle") beats.push({ type: "idle", loops: 1 + Math.floor(Math.random() * 2) });
      else beats.push({ type: c });
      if (reg.idle && Math.random() < 0.55) beats.push({ type: "idle", loops: 1 });
    });
    beats.push({ type: "blinkOut" });
    beats.push({ type: "hidden", ms: hiddenDur() });
    return beats;
  }

  const PHASE = {
    blinkIn: "appearing", idle: "idle", look: "look",
    blink: "blink", blinkOut: "blinkOut", hidden: "hidden",
  };

  // Start hidden, staggered, then the first cycle.
  const initialStagger = Math.max(250, fadeDelay * 600 + blinkDelay * 250 + Math.random() * 1500);
  let beats = [{ type: "hidden", ms: initialStagger }].concat(buildCycle());
  let bi = 0;
  let beat = null;
  let acc = 0, phaseElapsed = 0, frame = 0, loopsDone = 0, idleFrame = 0;
  let last = performance.now();
  let raf;

  function startBeat() {
    beat = beats[bi];
    acc = 0; phaseElapsed = 0; loopsDone = 0;
    if (onPhaseChange) onPhaseChange(PHASE[beat.type]);
    if (beat.type === "blinkIn") {
      frame = hasClose ? closedF : 0; // open from the closed pose where possible
      setOpacity(0);
      show("blink", frame);
    } else if (beat.type === "blinkOut" || beat.type === "blink") {
      frame = 0; setOpacity(1); show("blink", 0);
    } else if (beat.type === "look") {
      frame = 0; beat.sub = "fwd"; setOpacity(1); show("look", 0);
    } else if (beat.type === "idle") {
      setOpacity(1); show("idle", idleFrame);
    } else if (beat.type === "hidden") {
      setOpacity(0); show("idle", -1);
    }
  }

  function nextBeat() {
    bi++;
    if (bi >= beats.length) { beats = buildCycle(); bi = 0; }
    startBeat();
  }

  startBeat();

  const tick = (now) => {
    const dt = now - last;
    last = now;
    phaseElapsed += dt;
    const t = beat.type;

    if (t === "hidden") {
      if (phaseElapsed >= beat.ms) { nextBeat(); }

    } else if (t === "blinkIn") {
      const startF = hasClose ? closedF : 0;
      const totalMs = (lastF - startF + 1) * blinkMs;
      setOpacity(smooth(Math.min(1, phaseElapsed / totalMs)));
      acc += dt;
      while (acc >= blinkMs) {
        acc -= blinkMs;
        frame++;
        if (frame > lastF) { setOpacity(1); idleFrame = 0; nextBeat(); return; }
        show("blink", frame);
      }

    } else if (t === "blink") {
      // a normal, in-place blink (stays visible)
      acc += dt;
      while (acc >= blinkMs) {
        acc -= blinkMs;
        frame++;
        if (frame > lastF) { nextBeat(); return; }
        show("blink", frame);
      }

    } else if (t === "blinkOut") {
      const endF = hasClose ? closedF : lastF;
      // Fade is bound to the lid SHUTTING with a gentle ease-in: opacity leaves
      // 1 the instant the close begins (no hold/hitch), stays high while the lid
      // is visibly closing, and reaches exactly 0 as the closed frame lands.
      const closeMs = Math.max(1, endF) * blinkMs;
      const ct = Math.min(1, phaseElapsed / closeMs);
      setOpacity(1 - ct * ct);
      acc += dt;
      while (acc >= blinkMs) {
        acc -= blinkMs;
        frame++;
        if (frame >= endF) { show("blink", endF); setOpacity(0); nextBeat(); return; }
        show("blink", frame);
      }

    } else if (t === "idle") {
      acc += dt;
      while (acc >= idleMs) {
        acc -= idleMs;
        idleFrame = (idleFrame + 1) % reg.idle.count;
        show("idle", idleFrame);
        if (idleFrame === 0) { loopsDone++; if (loopsDone >= beat.loops) { nextBeat(); return; } }
      }

    } else if (t === "look") {
      if (beat.sub === "fwd") {
        acc += dt;
        while (acc >= lookMs) {
          acc -= lookMs;
          frame++;
          if (frame >= reg.look.count - 1) {
            frame = reg.look.count - 1;
            show("look", frame);
            beat.sub = "hold"; beat.holdMs = rand(170, 360); beat.holdEl = 0;
            break;
          }
          show("look", frame);
        }
      } else if (beat.sub === "hold") {
        beat.holdEl += dt;
        // step back the instant the dwell ends (acc primed) so the eye never
        // sits an extra frame on the open-most pose
        if (beat.holdEl >= beat.holdMs) { beat.sub = "rev"; acc = lookMs; }
      } else { // rev
        acc += dt;
        while (acc >= lookMs) {
          acc -= lookMs;
          frame--;
          if (frame <= 0) { show("look", 0); nextBeat(); return; }
          show("look", frame);
        }
      }
    }
  };
  const loop = (now) => { tick(now); raf = requestAnimationFrame(loop); };
  raf = requestAnimationFrame((now) => { last = now; loop(now); });
  return () => cancelAnimationFrame(raf);
}

// ===== Lore Rune (clickable easter egg) =====
const RUNE_SHAPES = [
<svg key="r1" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
    <circle cx="12" cy="12" r="9" />
    <path d="M12 3 L12 21 M3 12 L21 12" />
    <circle cx="12" cy="12" r="3" fill="currentColor" />
  </svg>,
<svg key="r2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
    <path d="M12 2 L22 20 L2 20 Z" />
    <circle cx="12" cy="14" r="2.5" fill="currentColor" />
  </svg>,
<svg key="r3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
    <path d="M3 12 L12 3 L21 12 L12 21 Z" />
    <path d="M8 12 L16 12 M12 8 L12 16" />
  </svg>,
<svg key="r4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
    <circle cx="12" cy="12" r="9" />
    <path d="M6 14 Q12 22 18 14" />
    <circle cx="9" cy="10" r="1" fill="currentColor" />
    <circle cx="15" cy="10" r="1" fill="currentColor" />
  </svg>,
<svg key="r5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
    <path d="M4 4 L20 20 M20 4 L4 20" />
    <circle cx="12" cy="12" r="4" />
  </svg>];


function LoreRune({ shape = 0, text, style, color = "violet" }) {
  // Disabled per request — the floating "Fragment Recovered" tooltip and its
  // trigger glyphs are removed. Return null to render nothing; the markup below
  // is kept so the easter egg can be restored later if wanted.
  return null;

  const [hover, setHover] = useState(false);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const ref = useRef(null);

  const handleMove = () => {
    if (!ref.current) return;
    const r = ref.current.getBoundingClientRect();
    setPos({ x: r.left + 32, y: r.top - 20 });
  };

  return (
    <>
      <span
        ref={ref}
        className={`lore-rune ${color === "green" ? "green" : ""}`}
        style={style}
        onMouseEnter={() => {setHover(true);handleMove();}}
        onMouseLeave={() => setHover(false)}>
        
        {RUNE_SHAPES[shape % RUNE_SHAPES.length]}
      </span>
      {hover &&
      <div className="lore-tooltip" style={{ left: pos.x, top: pos.y }}>
          {text}
        </div>
      }
    </>);

}

// ===== Glitchable text =====
function Glitch({ children, className = "", trigger = "hover" }) {
  const [active, setActive] = useState(false);

  useEffect(() => {
    if (trigger !== "auto") return;
    const t = setInterval(() => {
      setActive(true);
      setTimeout(() => setActive(false), 300);
    }, 5000 + Math.random() * 4000);
    return () => clearInterval(t);
  }, [trigger]);

  const events = trigger === "hover" ?
  { onMouseEnter: () => setActive(true), onMouseLeave: () => setActive(false) } :
  {};

  return (
    <span className={`glitchable ${active ? "active" : ""} ${className}`} {...events}>
      <span className="ghost r" aria-hidden>{children}</span>
      <span className="ghost c" aria-hidden>{children}</span>
      <span className="main" style={{ fontFamily: "monospace" }}>{children}</span>
    </span>);

}

Object.assign(window, { ParticleField, SparkBurst, Eye, LoreRune, Glitch });