/* Clara — Hooks: scroll progress, intersection reveal, spring lerp, beat bus */

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

/* Global scroll progress 0..1 across the document */
function useScrollProgress() {
  const [p, setP] = useState(0);

  useEffect(() => {
    let raf = 0;
    const measure = () => {
      const max = (document.documentElement.scrollHeight - window.innerHeight) || 1;
      const v = Math.min(1, Math.max(0, window.scrollY / max));
      setP(v);
      raf = 0;
    };
    const onScroll = () => {
      if (!raf) raf = requestAnimationFrame(measure)
    };
    measure();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', measure);
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', measure);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);

  return p;
}

/* Scroll-driven observation registry (works even when IntersectionObserver doesn't fire in some iframes) */
const ScrollObs = {
  items: new Set(),
  add(item) { this.items.add(item); this.tick(); },
  remove(item) { this.items.delete(item); },
  raf: 0,
  schedule() {
    if (this.raf) return;
    this.raf = requestAnimationFrame(() => { this.raf = 0; this.tick(); });
  },
  tick() {
    const vh = window.innerHeight;
    // Trigger when element's top crosses 88% of viewport (i.e. ~12% from bottom)
    const triggerY = vh * 0.88;
    for (const item of this.items) {
      const el = item.el;
      if (!el || !el.isConnected) { this.items.delete(item); continue; }
      const r = el.getBoundingClientRect();
      if (r.top < triggerY && r.bottom > 0) {
        item.onEnter();
        if (item.once) this.items.delete(item);
      } else if (!item.once && r.top >= triggerY) {
        item.onLeave && item.onLeave();
      }
    }
  }
};
if (typeof window !== 'undefined') {
  window.addEventListener('scroll', () => ScrollObs.schedule(), { passive: true });
  window.addEventListener('resize', () => ScrollObs.schedule());
}

function useReveal(opts = {}) {
  const ref = useRef(null);
  const { once = true } = opts;
  useEffect(() => {
    const item = {
      get el() { return ref.current; },
      once,
      onEnter: () => { if (ref.current) ref.current.classList.add('in'); },
      onLeave: () => { if (ref.current) ref.current.classList.remove('in'); }
    };
    ScrollObs.add(item);
    return () => ScrollObs.remove(item);
  }, []);
  return ref;
}

/* Spring lerp: returns a current value that follows target with physics */
function useSpring(target, opts = {}) {
  const { stiffness = 80, damping = 18, mass = 1.2 } = opts;
  const [value, setValue] = useState(target);
  const valueRef = useRef(target);
  const velRef = useRef(0);
  const targetRef = useRef(target);

  useEffect(() => { targetRef.current = target; }, [target]);

  useEffect(() => {
    let raf;
    let last = performance.now();
    const tick = (now) => {
      const dt = Math.min(0.064, (now - last) / 1000);
      last = now;
      const k = stiffness, c = damping, m = mass;
      const x = valueRef.current;
      const v = velRef.current;
      const force = -k * (x - targetRef.current);
      const damp = -c * v;
      const acc = (force + damp) / m;
      velRef.current = v + acc * dt;
      valueRef.current = x + velRef.current * dt;
      setValue(valueRef.current);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [stiffness, damping, mass]);

  return value;
}

/* Beat bus — dispatch radar pulses on key anchors */
const BeatBus = {
  listeners: new Set(),
  fire(key) { this.listeners.forEach(fn => fn(key)); },
  on(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
};

function useBeatAnchor(id) {
  const ref = useRef(null);
  useEffect(() => {
    let fired = false;
    const item = {
      get el() { return ref.current; },
      once: true,
      onEnter: () => {
        if (fired) return;
        fired = true;
        BeatBus.fire(id);
      }
    };
    // Wider trigger zone for beats (center of viewport)
    const customItem = {
      ...item,
      onEnter: () => {
        if (fired) return;
        const r = ref.current && ref.current.getBoundingClientRect();
        if (!r) return;
        const vh = window.innerHeight;
        // fire when element top crosses 70% of viewport
        if (r.top < vh * 0.7 && r.bottom > 0) {
          fired = true;
          BeatBus.fire(id);
        }
      }
    };
    ScrollObs.add(customItem);
    return () => ScrollObs.remove(customItem);
  }, [id]);
  return ref;
}

/* Typing animation: returns substring of `text` revealed over `speed` ms/char.
   start: kicks off when truthy. onDone fires when complete. */
function useTyping(text, { speed = 32, start = true, pauseChars = '.,;', pauseMs = 200, onDone } = {}) {
  const [out, setOut] = useState('');
  const [done, setDone] = useState(false);
  useEffect(() => {
    if (!start) return;
    let i = 0;
    let timer;
    const step = () => {
      if (i >= text.length) {
        setDone(true);
        if (onDone) onDone();
        return;
      }
      const next = text.slice(0, i + 1);
      setOut(next);
      const c = text[i];
      i += 1;
      const delay = pauseChars.includes(c) ? pauseMs : speed;
      timer = setTimeout(step, delay);
    };
    step();
    return () => clearTimeout(timer);
  }, [text, start]);
  return [out, done];
}

/* Count from `from` to `to` over `duration`, easing decel */
function useCounter(target, { duration = 1600, start = true, decimals = 0 } = {}) {
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!start) return;
    let raf;
    const t0 = performance.now();
    const ease = (t) => 1 - Math.pow(1 - t, 3); // close enough to [0.16,1,0.3,1]
    const tick = (now) => {
      const t = Math.min(1, (now - t0) / duration);
      setVal(target * ease(t));
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, duration, start]);
  return decimals === 0 ? Math.round(val) : Number(val.toFixed(decimals));
}

window.useScrollProgress = useScrollProgress;
window.useReveal = useReveal;
window.useSpring = useSpring;
window.useBeatAnchor = useBeatAnchor;
window.BeatBus = BeatBus;
window.useTyping = useTyping;
window.useCounter = useCounter;
