/**
 * lip-sync.mjs — Avatar lip-sync animation controller
 *
 * Drives the 6-part mouth SVG between phoneme shapes using
 * requestAnimationFrame for smooth transitions. Supports:
 *   - Instant phoneme set (no animation)
 *   - Animated transition between phonemes
 *   - Sequence playback (array of phoneme + duration)
 *   - Speaking simulation (random phoneme cycling)
 *   - Integration with TTS / audio analysis
 */

import { MOUTH_PARTS, PHONEMES, DEMO_SEQUENCE } from './lip-sync-data.mjs';

// ── State ────────────────────────────────────────────────────

let svgRoot = null;
let mouthElements = {};
let currentPhoneme = 'start';
let animationId = null;
let sequenceTimer = null;
let isSpeaking = false;

// ── Public API ───────────────────────────────────────────────

/**
 * Initialize the lip-sync controller.
 * @param {SVGElement|HTMLElement} svgEl - The SVG element or container with the avatar mouth parts
 */
export function init(svgEl) {
  svgRoot = svgEl;
  mouthElements = {};

  for (const partId of MOUTH_PARTS) {
    const el = svgRoot.querySelector(`#${partId}`);
    if (el) {
      mouthElements[partId] = el;
    }
  }

  const found = Object.keys(mouthElements).length;
  if (found < MOUTH_PARTS.length) {
    console.warn(`[lip-sync] Found ${found}/${MOUTH_PARTS.length} mouth elements`);
  }

  // Ensure we start at rest
  setPhoneme('start');
}

/**
 * Immediately set the mouth to a phoneme shape (no animation).
 * @param {string} name - Phoneme name (start, ow1, ow2, t, aa, m, ax, n)
 */
export function setPhoneme(name) {
  const paths = PHONEMES[name];
  if (!paths) {
    console.warn(`[lip-sync] Unknown phoneme: ${name}`);
    return;
  }

  for (const partId of MOUTH_PARTS) {
    const el = mouthElements[partId];
    if (el && paths[partId]) {
      el.setAttribute('d', paths[partId]);
    }
  }

  currentPhoneme = name;
}

/**
 * Animate smoothly from the current phoneme to a target phoneme.
 * Uses SVG path interpolation via requestAnimationFrame.
 *
 * @param {string} targetName - Target phoneme name
 * @param {number} duration - Transition duration in seconds
 * @returns {Promise<void>} Resolves when animation completes
 */
export function animateToPhoneme(targetName, duration = 0.1) {
  return new Promise((resolve) => {
    const targetPaths = PHONEMES[targetName];
    if (!targetPaths) {
      console.warn(`[lip-sync] Unknown phoneme: ${targetName}`);
      resolve();
      return;
    }

    // Cancel any running animation
    if (animationId) {
      cancelAnimationFrame(animationId);
      animationId = null;
    }

    // Snapshot current path d values
    const startPaths = {};
    for (const partId of MOUTH_PARTS) {
      const el = mouthElements[partId];
      if (el) {
        startPaths[partId] = el.getAttribute('d') || '';
      }
    }

    // Parse paths into numeric arrays for interpolation
    const startNums = {};
    const targetNums = {};
    const templates = {};

    for (const partId of MOUTH_PARTS) {
      if (!startPaths[partId] || !targetPaths[partId]) continue;
      const { numbers: sn, template: st } = parsePath(startPaths[partId]);
      const { numbers: tn } = parsePath(targetPaths[partId]);
      if (sn.length === tn.length) {
        startNums[partId] = sn;
        targetNums[partId] = tn;
        templates[partId] = st;
      }
    }

    const durationMs = duration * 1000;
    const startTime = performance.now();

    function tick(now) {
      const elapsed = now - startTime;
      const t = Math.min(elapsed / durationMs, 1);
      // Ease-in-out cubic
      const eased = t < 0.5
        ? 4 * t * t * t
        : 1 - Math.pow(-2 * t + 2, 3) / 2;

      for (const partId of MOUTH_PARTS) {
        if (!startNums[partId]) continue;
        const sn = startNums[partId];
        const tn = targetNums[partId];
        const interpolated = sn.map((s, i) => s + (tn[i] - s) * eased);
        const d = buildPath(templates[partId], interpolated);
        mouthElements[partId].setAttribute('d', d);
      }

      if (t < 1) {
        animationId = requestAnimationFrame(tick);
      } else {
        animationId = null;
        currentPhoneme = targetName;
        resolve();
      }
    }

    animationId = requestAnimationFrame(tick);
  });
}

/**
 * Play a sequence of phoneme transitions.
 * @param {Array<{phoneme: string, duration: number}>} sequence
 * @returns {Promise<void>} Resolves when sequence completes
 */
export async function playSequence(sequence) {
  stopAnimation();

  for (const step of sequence) {
    if (!isSpeaking && sequence !== DEMO_SEQUENCE) break;
    await animateToPhoneme(step.phoneme, step.duration);
  }
}

/**
 * Play the built-in demo sequence (from the original SMIL animation).
 * @returns {Promise<void>}
 */
export function playDemo() {
  return playSequence(DEMO_SEQUENCE);
}

/**
 * Start simulating speech with random phoneme cycling.
 * Useful when TTS is playing but no phoneme data is available.
 * Cycles through phonemes semi-randomly to create a talking effect.
 */
export function startSpeaking() {
  if (isSpeaking) return;
  isSpeaking = true;

  const speakPhonemes = ['start', 'aa', 'ow1', 'ax', 't', 'm', 'n', 'ow2'];

  function nextPhoneme() {
    if (!isSpeaking) return;

    // Pick a random phoneme, avoiding repeats
    let next;
    do {
      next = speakPhonemes[Math.floor(Math.random() * speakPhonemes.length)];
    } while (next === currentPhoneme && speakPhonemes.length > 1);

    // Random duration between 50ms and 200ms
    const dur = 0.05 + Math.random() * 0.15;

    animateToPhoneme(next, dur).then(() => {
      if (isSpeaking) {
        // Small pause between phonemes (30-80ms)
        const pause = 30 + Math.random() * 50;
        sequenceTimer = setTimeout(nextPhoneme, pause);
      }
    });
  }

  nextPhoneme();
}

/**
 * Stop all lip-sync animation and return to rest position.
 */
export function stopSpeaking() {
  isSpeaking = false;
  stopAnimation();
  animateToPhoneme('start', 0.15);
}

/**
 * Stop any running animation immediately.
 */
export function stopAnimation() {
  isSpeaking = false;

  if (animationId) {
    cancelAnimationFrame(animationId);
    animationId = null;
  }

  if (sequenceTimer) {
    clearTimeout(sequenceTimer);
    sequenceTimer = null;
  }
}

/**
 * Get the current phoneme name.
 * @returns {string}
 */
export function getCurrentPhoneme() {
  return currentPhoneme;
}

/**
 * Check if currently animating.
 * @returns {boolean}
 */
export function isAnimating() {
  return animationId !== null || isSpeaking;
}

// ── Path interpolation helpers ───────────────────────────────

/**
 * Parse an SVG path `d` attribute into a template string and
 * an array of numbers. The template has placeholders ({0}, {1}, ...)
 * where numbers appeared.
 */
function parsePath(d) {
  const numbers = [];
  const template = d.replace(/-?\d+\.?\d*/g, (match) => {
    numbers.push(parseFloat(match));
    return `{${numbers.length - 1}}`;
  });
  return { numbers, template };
}

/**
 * Rebuild an SVG path from a template and interpolated numbers.
 */
function buildPath(template, numbers) {
  return template.replace(/\{(\d+)\}/g, (_, i) => {
    const n = numbers[parseInt(i)];
    return Number.isInteger(n) ? n.toString() : n.toFixed(2);
  });
}
