/**
 * 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;
let audioDriverEnabled = false;

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

/**
 * Initialize the lip-sync controller.
 * @param {SVGElement|HTMLElement} svgEl - The SVG element or container with the avatar mouth parts
 * @param {string} [idSuffix] - Optional ID suffix (e.g. "_av-abc123") appended to element IDs
 */
export function init(svgEl, idSuffix = '') {
  svgRoot = svgEl;
  mouthElements = {};

  for (const partId of MOUTH_PARTS) {
    const el = svgRoot.querySelector(`[id="${partId}${idSuffix}"]`);
    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;

  // When audio driver is enabled, amplitude calls drive mouth directly
  if (audioDriverEnabled) return;

  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;
}

/**
 * Enable or disable audio-driven lip-sync mode.
 * When enabled, startSpeaking() skips random cycling and relies
 * on setAmplitude() calls from real-time audio analysis.
 * @param {boolean} enabled
 */
export function enableAudioDriver(enabled) {
  audioDriverEnabled = enabled;
  if (enabled) {
    stopAnimation();
    isSpeaking = true;
  }
}

/**
 * Set mouth shape based on audio amplitude (RMS 0.0–1.0).
 * Maps amplitude to phoneme shapes by mouth openness.
 * @param {number} rms - Root mean square amplitude
 */
export function setAmplitude(rms) {
  let target;
  if (rms < 0.02)      target = 'start';
  else if (rms < 0.06) target = 'm';
  else if (rms < 0.12) target = 'n';
  else if (rms < 0.20) target = 'ax';
  else if (rms < 0.30) target = 't';
  else if (rms < 0.45) target = 'ow1';
  else if (rms < 0.65) target = 'ow2';
  else                  target = 'aa';

  if (target !== currentPhoneme) {
    animateToPhoneme(target, 0.06);
  }
}

// ── 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);
  });
}
