import { useEffect, useRef, useCallback } from 'react';

// Config-driven. Loaded from /morphy/headphones.json.
// Clips map to states:
//   idle (hold)      → 'idle'
//   enter (forward)  → 'activating'
//   active (pingpong)→ 'recording'
//   exit (forward)   → 'deactivating'
const HP_CONFIG_URL = '/morphy/headphones.json';
const HP_ASSETS_DIR = '/morphy/';

interface HpConfig {
  spritesheet: string;
  frame: { w: number; h: number };
  grid: { cols: number; rows: number };
  fps: number;
  clips: {
    idle: { from: number; to: number; mode: string; fps?: number };
    enter: { from: number; to: number; mode: string; fps?: number };
    active: { from: number; to: number; mode: string; fps?: number };
    exit: { from: number; to: number; mode: string; fps?: number };
  };
}

let cachedSprite: HTMLImageElement | null = null;
let cachedConfig: HpConfig | null = null;

function loadConfigAndSprite(): Promise<{ img: HTMLImageElement; cfg: HpConfig }> {
  if (cachedSprite && cachedConfig) return Promise.resolve({ img: cachedSprite, cfg: cachedConfig });
  return fetch(HP_CONFIG_URL)
    .then((r) => { if (!r.ok) throw new Error('hp config ' + r.status); return r.json() as Promise<HpConfig>; })
    .then((cfg) => new Promise<{ img: HTMLImageElement; cfg: HpConfig }>((resolve, reject) => {
      const img = new Image();
      img.onload = () => { cachedSprite = img; cachedConfig = cfg; resolve({ img, cfg }); };
      img.onerror = reject;
      img.src = HP_ASSETS_DIR + cfg.spritesheet;
    }));
}

interface Props {
  recording: boolean;
  height?: number;
  onDone?: () => void;
}

type HpState = 'idle' | 'activating' | 'activating_then_deactivate' | 'recording' | 'deactivating';

export default function HeadphonesAnimation({ recording, height = 36, onDone }: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const spriteRef = useRef<HTMLImageElement | null>(cachedSprite);
  const configRef = useRef<HpConfig | null>(cachedConfig);
  const stateRef = useRef<HpState>('idle');
  const frameRef = useRef(0);
  const pingPongRef = useRef(1);
  const lastRef = useRef(0);
  const rafRef = useRef(0);
  const prevRecording = useRef(false);
  const onDoneRef = useRef(onDone);
  onDoneRef.current = onDone;

  // Aspect ratio from config (defaults to 1:1 — matches new sprite).
  const cfg = configRef.current;
  const aspectW = cfg ? cfg.frame.w : 180;
  const aspectH = cfg ? cfg.frame.h : 180;
  const displayH = height;
  const displayW = Math.round(displayH * (aspectW / aspectH));

  useEffect(() => {
    loadConfigAndSprite().then(({ img, cfg }) => {
      spriteRef.current = img;
      configRef.current = cfg;
      if (stateRef.current === 'idle') frameRef.current = cfg.clips.idle.from;
    });
  }, []);

  // React to recording prop
  useEffect(() => {
    const cfg = configRef.current;
    if (!cfg) return;
    if (recording && !prevRecording.current) {
      stateRef.current = 'activating';
      frameRef.current = cfg.clips.enter.from;
      pingPongRef.current = 1;
      lastRef.current = performance.now();
    } else if (!recording && prevRecording.current) {
      if (stateRef.current === 'activating') {
        stateRef.current = 'activating_then_deactivate';
      } else if (stateRef.current === 'recording') {
        stateRef.current = 'deactivating';
        frameRef.current = cfg.clips.exit.from;
        lastRef.current = performance.now();
      }
    }
    prevRecording.current = recording;
  }, [recording]);

  const tick = useCallback((now: number) => {
    const canvas = canvasRef.current;
    const sprite = spriteRef.current;
    const cfg = configRef.current;
    if (!canvas) { rafRef.current = requestAnimationFrame(tick); return; }
    const ctx = canvas.getContext('2d')!;
    const state = stateRef.current;

    if (!sprite || !cfg) {
      ctx.clearRect(0, 0, displayW, displayH);
      rafRef.current = requestAnimationFrame(tick);
      return;
    }

    if (state === 'idle') {
      // Hold the configured idle frame so the chat header shows a real visual at rest.
      const cols = cfg.grid.cols;
      const fw = cfg.frame.w;
      const fh = cfg.frame.h;
      const idleFrame = cfg.clips.idle.from;
      const col = idleFrame % cols;
      const row = Math.floor(idleFrame / cols);
      ctx.clearRect(0, 0, displayW, displayH);
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = 'high';
      ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);
      rafRef.current = requestAnimationFrame(tick);
      return;
    }

    const baseFps = cfg.fps;
    const fps =
      state === 'activating' || state === 'activating_then_deactivate'
        ? (cfg.clips.enter.fps ?? baseFps)
        : state === 'deactivating'
          ? (cfg.clips.exit.fps ?? baseFps)
          : (cfg.clips.active.fps ?? baseFps);
    const frameMs = 1000 / fps;
    const delta = now - lastRef.current;
    if (delta >= frameMs) {
      lastRef.current = now - (delta % frameMs);

      if (state === 'activating' || state === 'activating_then_deactivate') {
        frameRef.current++;
        if (frameRef.current > cfg.clips.enter.to) {
          if (state === 'activating_then_deactivate') {
            stateRef.current = 'deactivating';
            frameRef.current = cfg.clips.exit.from;
          } else {
            stateRef.current = 'recording';
            frameRef.current = cfg.clips.active.from;
            pingPongRef.current = 1;
          }
        }
      } else if (state === 'recording') {
        frameRef.current += pingPongRef.current;
        if (frameRef.current >= cfg.clips.active.to) { frameRef.current = cfg.clips.active.to; pingPongRef.current = -1; }
        else if (frameRef.current <= cfg.clips.active.from) { frameRef.current = cfg.clips.active.from; pingPongRef.current = 1; }
      } else if (state === 'deactivating') {
        frameRef.current++;
        if (frameRef.current > cfg.clips.exit.to) {
          stateRef.current = 'idle';
          frameRef.current = cfg.clips.idle.from;
          ctx.clearRect(0, 0, displayW, displayH);
          onDoneRef.current?.();
          rafRef.current = requestAnimationFrame(tick);
          return;
        }
      }
    }

    const cols = cfg.grid.cols;
    const fw = cfg.frame.w;
    const fh = cfg.frame.h;
    const col = frameRef.current % cols;
    const row = Math.floor(frameRef.current / cols);
    ctx.clearRect(0, 0, displayW, displayH);
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = 'high';
    ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);

    rafRef.current = requestAnimationFrame(tick);
  }, [displayW, displayH]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const dpr = window.devicePixelRatio || 1;
    canvas.width = displayW * dpr;
    canvas.height = displayH * dpr;
    const ctx = canvas.getContext('2d')!;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [tick, displayW, displayH]);

  return <canvas ref={canvasRef} style={{ width: displayW, height: displayH, display: 'block' }} />;
}
