// Breathing engine — a single hook that owns phase timing, cycle count,
// and session duration. Everything runs off one rAF loop; the previous
// phase change is compared to now to advance. The engine is frame-rate
// independent and pauses cleanly.
//
//   const s = useBreathEngine({ pattern, totalSeconds, playing, speed });
//   s.phase       - 'inhale' | 'holdIn' | 'exhale' | 'holdOut'
//   s.phaseT      - 0..1 progress within current phase
//   s.amplitude   - 0..1 "fullness of lungs" (drives visualizations)
//   s.elapsed     - seconds elapsed
//   s.remaining   - seconds left
//   s.cycles      - completed cycles
//   s.done        - true after totalSeconds AND a full cycle close

function useBreathEngine({ pattern, totalSeconds = 180, playing = false, speed = 1, progression = null }) {
  const [tick, setTick] = React.useState(0);
  const state = React.useRef({
    elapsed: 0, phaseElapsed: 0, phaseIdx: 0, cycles: 0, done: false,
  });
  const lastTs = React.useRef(null);

  // Only enumerate the phase keys that have non-zero base duration. The
  // actual duration of each key is resolved fresh at every phase boundary
  // so progression mode (which grows inhale/exhale with elapsed time) can
  // take effect cycle-to-cycle without the engine having to know when the
  // config changed.
  const phaseKeys = React.useMemo(() => (
    ['inhale', 'holdIn', 'exhale', 'holdOut'].filter(k => pattern[k] > 0)
  ), [pattern]);

  const phaseDurationAt = (idx, elapsed) => {
    const k = phaseKeys[idx];
    if (progression && progression.enabled && typeof window.BFProgression !== 'undefined') {
      return BFProgression.effectivePattern(pattern, progression, elapsed)[k];
    }
    return pattern[k];
  };

  // Engine tick. Two scheduling modes:
  //   - Visible page → requestAnimationFrame (smooth 60fps for the visualizer).
  //   - Hidden page  → setTimeout (rAF pauses when the screen locks; setTimeout
  //     keeps firing as long as MediaSession + an active audio source signal
  //     "media playback" to the OS, which the player sets up at session start).
  // Time accounting always uses performance.now() deltas, so even if a setTimeout
  // is throttled to once every few seconds during screen-off, the engine still
  // advances elapsed time correctly and crosses phase boundaries on schedule.
  // The dt cap is 60s instead of 0.1s — long lock-screen gaps catch up cleanly
  // on resume rather than spending 10 minutes inching forward at the old cap.
  React.useEffect(() => {
    if (!playing) { lastTs.current = null; return; }
    let scheduledRaf = null;
    let scheduledTimer = null;
    let cancelled = false;
    const tick = () => {
      if (cancelled) return;
      const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
      if (lastTs.current == null) lastTs.current = now;
      const dt = Math.min(60, (now - lastTs.current) / 1000) * speed;
      lastTs.current = now;
      const s = state.current;
      if (!s.done) {
        s.elapsed += dt;
        s.phaseElapsed += dt;
        let curD = phaseDurationAt(s.phaseIdx, s.elapsed);
        while (s.phaseElapsed >= curD) {
          s.phaseElapsed -= curD;
          s.phaseIdx = (s.phaseIdx + 1) % phaseKeys.length;
          if (s.phaseIdx === 0) s.cycles += 1;
          curD = phaseDurationAt(s.phaseIdx, s.elapsed);
        }
        // finish: wait for cycle boundary AFTER totalSeconds elapsed
        if (s.elapsed >= totalSeconds && s.phaseIdx === 0 && s.phaseElapsed < 0.05) {
          s.done = true;
        }
      }
      setTick(t => t + 1);
      if (cancelled) return;
      schedule();
    };
    const schedule = () => {
      const hidden = (typeof document !== 'undefined' && document.visibilityState === 'hidden');
      if (hidden) {
        scheduledTimer = setTimeout(tick, 250);
        scheduledRaf = null;
      } else {
        scheduledRaf = requestAnimationFrame(tick);
        scheduledTimer = null;
      }
    };
    // When the tab is already hidden at mount (e.g. session resumed after the
    // user backgrounded the app and brought it forward), rAF would never fire
    // and the engine would freeze. Pick the right scheduler from the start.
    schedule();
    // If the user backgrounds the page mid-session and a rAF callback is still
    // queued from when we were visible, the OS pauses it until visible again.
    // Switch to setTimeout immediately on visibility change to keep ticking.
    const onVis = () => {
      if (cancelled) return;
      if (document.visibilityState === 'hidden') {
        if (scheduledRaf != null) { cancelAnimationFrame(scheduledRaf); scheduledRaf = null; }
        if (scheduledTimer == null) scheduledTimer = setTimeout(tick, 250);
      } else {
        if (scheduledTimer != null) { clearTimeout(scheduledTimer); scheduledTimer = null; }
        if (scheduledRaf == null) scheduledRaf = requestAnimationFrame(tick);
      }
    };
    if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
    return () => {
      cancelled = true;
      if (scheduledRaf != null) cancelAnimationFrame(scheduledRaf);
      if (scheduledTimer != null) clearTimeout(scheduledTimer);
      if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
      lastTs.current = null;
    };
  }, [playing, phaseKeys, totalSeconds, speed, progression && progression.enabled]);

  // Reset engine when pattern changes
  React.useEffect(() => {
    state.current = { elapsed: 0, phaseElapsed: 0, phaseIdx: 0, cycles: 0, done: false };
    setTick(t => t + 1);
  }, [pattern.id]);

  const s = state.current;
  const curK = phaseKeys[s.phaseIdx] || phaseKeys[0];
  const curD = phaseDurationAt(s.phaseIdx, s.elapsed);
  const phaseT = curD > 0 ? Math.min(1, s.phaseElapsed / curD) : 0;
  const amplitude = (() => {
    // inhale: 0→1; holdIn: 1; exhale: 1→0; holdOut: 0
    if (curK === 'inhale')  return easeInOutSine(phaseT);
    if (curK === 'holdIn')  return 1;
    if (curK === 'exhale')  return easeInOutSine(1 - phaseT);
    if (curK === 'holdOut') return 0;
    return 0;
  })();

  return {
    phase: curK,
    // Phase label resolved via BFI18n at read time so language changes in
    // Settings flip the Player label without restarting the engine.
    phaseLabel: ((k) => {
      const key = PHASE_KEYS[k];
      return (window.BFI18n && key) ? window.BFI18n.t(key) : PHASE_LABELS[k];
    })(curK),
    phaseDuration: curD,
    phaseT, amplitude,
    elapsed: s.elapsed,
    remaining: Math.max(0, totalSeconds - s.elapsed),
    cycles: s.cycles,
    done: s.done,
    reset: () => {
      state.current = { elapsed: 0, phaseElapsed: 0, phaseIdx: 0, cycles: 0, done: false };
      setTick(t => t + 1);
    },
  };
}

function easeInOutSine(t) { return -(Math.cos(Math.PI * t) - 1) / 2; }

// English fallback labels (used if BFI18n isn't loaded — tests, early boot).
const PHASE_LABELS = {
  inhale: 'Inhale',
  holdIn: 'Hold',
  exhale: 'Exhale',
  holdOut: 'Hold',
};
// i18n keys for each phase.
const PHASE_KEYS = {
  inhale: 'player.inhale',
  holdIn: 'player.hold',
  exhale: 'player.exhale',
  holdOut: 'player.hold',
};

Object.assign(window, { useBreathEngine });
