/**
 * avatar.mjs — AI-Do v5.0 composite avatar component
 *
 * Renders a PNG body image with an animated SVG face overlay.
 * The face uses avatar-animated.svg (eyes, eyebrows, nose, mouth) with
 * lip-sync.mjs driving the 6-part mouth between phoneme shapes.
 *
 * Features: blinking, gaze tracking, lip-sync speaking, breathing, emotions.
 *
 * Exports (same API as v4):
 *   createAvatar(size)   — Create and return an avatar DOM element
 *   startSpeaking()      — Start lip-sync mouth animation
 *   stopSpeaking()       — Stop lip-sync, return to rest
 *   setEmotion(emotion)  — Set eyebrow expression
 *   init()               — Subscribe to store (call once at startup)
 */

import store from '../store.mjs';
import * as lipSync from './lip-sync.mjs';

// ── Size presets (px) ──
const SIZE_MAP = {
  sidebar: 64,
  welcome: 120,
  header: 40,
  'voice-mode': 300
};

// Face position offsets as fraction of container size.
// The PNGs have a blank face area roughly in the upper-center.
// These values position the SVG face over that area.
const FACE_LAYOUT = {
  // Face SVG offset from top-left of container, as fraction of container size.
  // Tuned to align SVG eyes/nose/mouth with the blank face area of avatar PNGs.
  top: 0.22,
  left: 0.15,
  width: 0.70,
  height: 0.55
};

// ── Module state ──
let instances = [];
let animFrameId = null;
let lastTime = 0;
let lipSyncReady = false;

// Shared blink state
let blinkState = 0;
let nextBlinkTime = Date.now() + 2000 + Math.random() * 3000;
let blinkStartTime = 0;
const BLINK_DURATION = 150;

// Shared gaze state
let gazeX = 0;
let gazeY = 0;
let targetGazeX = 0;
let targetGazeY = 0;
let lastMouseMove = Date.now();
let idleGazeTimer = 0;

// Speaking state (controlled via lip-sync.mjs)
let isSpeaking = false;

// Breathing
let breathPhase = 0;

// Emotion
let currentEmotion = 'neutral';

// SVG face template (loaded once from avatar-animated.svg)
let faceSvgTemplate = null;

// ── Helpers ──

function uid() {
  return 'av-' + Math.random().toString(36).substr(2, 9);
}

function getAvatarImage() {
  const state = store.getState();
  const style = state.persona?.avatarStyle || 'avatar1';
  // Map avatarStyle to PNG filename
  const validStyles = ['avatar1', 'avatar2', 'avatar3', 'avatar4'];
  const filename = validStyles.includes(style) ? style : 'avatar1';
  return `/assets/${filename}.png`;
}

// ── Load face SVG ──

async function loadFaceSvg() {
  if (faceSvgTemplate) return faceSvgTemplate;
  try {
    const resp = await fetch('/assets/avatar-animated.svg');
    const text = await resp.text();
    faceSvgTemplate = text;
    return text;
  } catch (err) {
    console.warn('[avatar] Failed to load face SVG:', err.message);
    return null;
  }
}

// ── Mouse tracking (shared) ──

function onMouseMove(e) {
  lastMouseMove = Date.now();
  if (instances.length === 0) return;
  const firstRect = instances[0].container.getBoundingClientRect();
  const centerX = firstRect.left + firstRect.width / 2;
  const centerY = firstRect.top + firstRect.height * 0.3;
  const dx = e.clientX - centerX;
  const dy = e.clientY - centerY;
  // maxOffset in SVG coordinate space (viewBox is 1200x1200)
  // 20px gives noticeable iris movement within the sclera
  const maxOffset = 20;
  const dist = Math.sqrt(dx * dx + dy * dy);
  const angle = Math.atan2(dy, dx);
  const clampedDist = Math.min(maxOffset, dist * 0.04);
  targetGazeX = Math.cos(angle) * clampedDist;
  targetGazeY = Math.sin(angle) * clampedDist;
}

// ── Element caching ──

function cacheEls(container, instanceId) {
  const svg = container.querySelector('.avatar-face-svg');
  if (!svg) return null;

  // After ID renaming, IDs become e.g. "left_eye_av-abc123" — use suffix to query
  const $ = (id) => svg.querySelector(`[id="${id}_${instanceId}"]`);
  const leftEye = $('left_eye');
  const rightEye = $('right_eye');

  return {
    svg,
    leftEye,
    rightEye,
    leftIris: leftEye?.querySelector('ellipse:nth-of-type(3)'),     // iris (3rd ellipse)
    rightIris: rightEye?.querySelector('ellipse:nth-of-type(3)'),
    leftPupil: leftEye?.querySelector('ellipse:nth-of-type(4)'),    // pupil (4th ellipse)
    rightPupil: rightEye?.querySelector('ellipse:nth-of-type(4)'),
    leftHighlight1: leftEye?.querySelector('ellipse:nth-of-type(5)'), // specular 1
    rightHighlight1: rightEye?.querySelector('ellipse:nth-of-type(5)'),
    leftHighlight2: leftEye?.querySelector('ellipse:nth-of-type(6)'), // specular 2
    rightHighlight2: rightEye?.querySelector('ellipse:nth-of-type(6)'),
    leftSclera: leftEye?.querySelector('ellipse:nth-of-type(2)'),   // sclera (2nd ellipse)
    rightSclera: rightEye?.querySelector('ellipse:nth-of-type(2)'),
    leftEyelidUpper: leftEye?.querySelector('path:nth-of-type(1)'), // upper eyelid arc
    rightEyelidUpper: rightEye?.querySelector('path:nth-of-type(1)'),
    leftEyebrow: $('left_eyebrow'),
    rightEyebrow: $('right_eyebrow'),
    mouthGroup: $('avatar_mouth'),
    eyesGroup: $('avatar_eyes'),
    noseGroup: $('avatar_nose'),
    eyebrowsGroup: $('avatar_eyebrows')
  };
}

// ── Animation update ──

function updateAll(dt) {
  const now = Date.now();

  // ----- Blink -----
  if (blinkState === 0 && now >= nextBlinkTime) {
    blinkState = 1;
    blinkStartTime = now;
  }

  let blinkAmount = 0;
  if (blinkState === 1) {
    const progress = (now - blinkStartTime) / BLINK_DURATION;
    if (progress >= 1) {
      blinkState = 0;
      nextBlinkTime = now + 2000 + Math.random() * 3000;
      blinkAmount = 0;
    } else {
      blinkAmount = progress < 0.5 ? progress * 2 : (1 - progress) * 2;
    }
  }

  // ----- Gaze -----
  const timeSinceMouseMove = now - lastMouseMove;
  if (timeSinceMouseMove > 2000) {
    idleGazeTimer += dt;
    if (idleGazeTimer > 1500 + Math.random() * 1500) {
      idleGazeTimer = 0;
      targetGazeX = (Math.random() - 0.5) * 10;
      targetGazeY = (Math.random() - 0.5) * 6;
    }
  }
  gazeX += (targetGazeX - gazeX) * 0.12;
  gazeY += (targetGazeY - gazeY) * 0.12;

  // ----- Apply to all instances -----
  for (const inst of instances) {
    const els = inst.els;
    if (!els) continue;

    // Blink — scale the eye sclera vertically
    applyBlink(els, blinkAmount);

    // Gaze — translate iris, pupil, and highlights
    applyGaze(els, gazeX, gazeY);
  }
}

function applyBlink(els, amount) {
  // Scale eyes vertically to simulate blink
  const scaleY = 1 - amount * 0.9;
  if (els.leftEye) {
    els.leftEye.style.transformOrigin = '460px 350px';
    els.leftEye.style.transform = `scaleY(${scaleY})`;
  }
  if (els.rightEye) {
    els.rightEye.style.transformOrigin = '740px 350px';
    els.rightEye.style.transform = `scaleY(${scaleY})`;
  }
}

function applyGaze(els, gx, gy) {
  // The avatar-animated.svg has eyes centered around x=460,740 y=350
  // Move iris, pupil, and highlights by gaze offset
  const moveEyePart = (el, baseCx, baseCy) => {
    if (!el) return;
    el.setAttribute('cx', baseCx + gx);
    el.setAttribute('cy', baseCy + gy);
  };

  // Left eye parts (base positions from avatar-animated.svg)
  moveEyePart(els.leftIris, 460, 355);
  moveEyePart(els.leftPupil, 460, 355);
  moveEyePart(els.leftHighlight1, 448, 340);
  moveEyePart(els.leftHighlight2, 472, 365);

  // Right eye parts
  moveEyePart(els.rightIris, 740, 355);
  moveEyePart(els.rightPupil, 740, 355);
  moveEyePart(els.rightHighlight1, 728, 340);
  moveEyePart(els.rightHighlight2, 752, 365);
}

function applyEmotion(emotion) {
  const expressions = {
    neutral: { leftY: 0, rightY: 0, leftRot: 0, rightRot: 0 },
    happy: { leftY: -8, rightY: -8, leftRot: -5, rightRot: 5 },
    thinking: { leftY: -12, rightY: 4, leftRot: 8, rightRot: -3 }
  };

  const expr = expressions[emotion] || expressions.neutral;

  for (const inst of instances) {
    const els = inst.els;
    if (!els) continue;
    if (els.leftEyebrow) {
      els.leftEyebrow.style.transform = `translateY(${expr.leftY}px) rotate(${expr.leftRot}deg)`;
      els.leftEyebrow.style.transformOrigin = '462px 248px';
      els.leftEyebrow.style.transition = 'transform 0.3s ease';
    }
    if (els.rightEyebrow) {
      els.rightEyebrow.style.transform = `translateY(${expr.rightY}px) rotate(${expr.rightRot}deg)`;
      els.rightEyebrow.style.transformOrigin = '738px 248px';
      els.rightEyebrow.style.transition = 'transform 0.3s ease';
    }
  }
}

// ── Animation loop ──

function animationLoop(time) {
  const dt = time - (lastTime || time);
  lastTime = time;
  updateAll(dt);
  animFrameId = requestAnimationFrame(animationLoop);
}

function ensureAnimLoop() {
  if (animFrameId === null) {
    document.addEventListener('mousemove', onMouseMove);
    animFrameId = requestAnimationFrame(animationLoop);
  }
}

function maybeStopAnimLoop() {
  if (instances.length === 0 && animFrameId !== null) {
    cancelAnimationFrame(animFrameId);
    animFrameId = null;
    lastTime = 0;
    document.removeEventListener('mousemove', onMouseMove);
  }
}

// ── Public API ──

/**
 * Create an avatar DOM element.
 * @param {'sidebar'|'welcome'|'header'|'voice-mode'} size
 * @returns {HTMLElement}
 */
export function createAvatar(size = 'sidebar') {
  const id = uid();
  const px = SIZE_MAP[size] || SIZE_MAP.sidebar;

  const wrapper = document.createElement('div');
  wrapper.className = `avatar-component avatar-component--${size}`;
  wrapper.style.cssText = `
    position: relative;
    width: ${px}px;
    height: ${px}px;
    overflow: hidden;
    border-radius: 50%;
    flex-shrink: 0;
  `;

  // Body image layer
  const bodyImg = document.createElement('img');
  bodyImg.className = 'avatar-body';
  bodyImg.src = getAvatarImage();
  bodyImg.alt = '';
  bodyImg.draggable = false;
  bodyImg.style.cssText = `
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    pointer-events: none;
  `;
  wrapper.appendChild(bodyImg);

  // Face SVG overlay container
  const faceContainer = document.createElement('div');
  faceContainer.className = 'avatar-face-container';
  faceContainer.style.cssText = `
    position: absolute;
    top: ${FACE_LAYOUT.top * 100}%;
    left: ${FACE_LAYOUT.left * 100}%;
    width: ${FACE_LAYOUT.width * 100}%;
    height: ${FACE_LAYOUT.height * 100}%;
    pointer-events: none;
  `;
  wrapper.appendChild(faceContainer);

  const inst = { container: wrapper, els: null, size, faceContainer, bodyImg, id };
  instances.push(inst);

  // Load and inject the face SVG asynchronously
  loadFaceSvg().then(svgText => {
    if (!svgText || !document.body.contains(wrapper)) return;

    // Parse and inject the SVG
    const parser = new DOMParser();
    const doc = parser.parseFromString(svgText, 'image/svg+xml');
    const svgEl = doc.documentElement;
    svgEl.classList.add('avatar-face-svg');
    svgEl.style.cssText = `
      width: 100%; height: 100%;
      pointer-events: none;
    `;
    // Make IDs unique per instance to avoid collisions
    svgEl.querySelectorAll('[id]').forEach(el => {
      const oldId = el.id;
      const newId = `${oldId}_${id}`;
      el.id = newId;
      // Update gradient references
      svgEl.querySelectorAll(`[fill="url(#${oldId})"]`).forEach(ref => {
        ref.setAttribute('fill', `url(#${newId})`);
      });
      svgEl.querySelectorAll(`[stroke="url(#${oldId})"]`).forEach(ref => {
        ref.setAttribute('stroke', `url(#${newId})`);
      });
    });

    faceContainer.appendChild(svgEl);

    // Cache element references (pass instance ID for renamed-ID lookups)
    inst.els = cacheEls(wrapper, id);

    // Init lip-sync on this SVG's mouth group.
    // lip-sync.mjs is a singleton — always prefer the voice-mode avatar
    // since that's the primary use case for real-time audio-driven lip-sync.
    if (inst.els?.mouthGroup) {
      if (size === 'voice-mode' || !lipSyncReady) {
        lipSync.init(svgEl, `_${id}`);
        lipSyncReady = true;
      }
    }

    // Apply current emotion
    if (currentEmotion !== 'neutral') {
      applyEmotion(currentEmotion);
    }

    // If already speaking, start lip-sync
    if (isSpeaking && lipSyncReady) {
      lipSync.startSpeaking();
    }
  });

  ensureAnimLoop();

  // Update body image when persona changes
  const unsubPersona = store.subscribe(
    (s) => s.persona?.avatarStyle,
    (style) => {
      const validStyles = ['avatar1', 'avatar2', 'avatar3', 'avatar4'];
      const filename = validStyles.includes(style) ? style : 'avatar1';
      bodyImg.src = `/assets/${filename}.png`;
    }
  );

  // Cleanup when removed from DOM
  const observer = new MutationObserver(() => {
    if (!document.body.contains(wrapper)) {
      observer.disconnect();
      if (unsubPersona) unsubPersona();
      const idx = instances.indexOf(inst);
      if (idx !== -1) instances.splice(idx, 1);
      if (instances.length === 0) {
        lipSyncReady = false;
      } else if (size === 'voice-mode') {
        // Voice-mode avatar was the lip-sync target — re-init with remaining avatar
        const remaining = instances.find(i => i.els?.svg);
        if (remaining) {
          lipSync.init(remaining.els.svg, `_${remaining.id}`);
        }
      }
      maybeStopAnimLoop();
    }
  });
  requestAnimationFrame(() => {
    if (wrapper.parentNode) {
      observer.observe(wrapper.parentNode, { childList: true });
    }
  });

  return wrapper;
}

/**
 * Start lip-sync mouth animation.
 */
export function startSpeaking() {
  if (isSpeaking) return;
  isSpeaking = true;
  if (lipSyncReady) {
    lipSync.startSpeaking();
  }
}

/**
 * Stop lip-sync mouth animation.
 */
export function stopSpeaking() {
  isSpeaking = false;
  if (lipSyncReady) {
    lipSync.stopSpeaking();
  }
}

/**
 * Set eyebrow emotion.
 * @param {'neutral'|'happy'|'thinking'} emotion
 */
export function setEmotion(emotion) {
  currentEmotion = emotion;
  applyEmotion(emotion);
}

/**
 * Initialize — subscribe to store for voice status and chat streaming.
 * Call once during app bootstrap.
 */
export function init() {
  store.subscribe(
    (s) => s.voice.status,
    (status) => {
      if (status === 'speaking') {
        startSpeaking();
      } else if (isSpeaking) {
        stopSpeaking();
      }
    }
  );

  store.subscribe(
    (s) => s.chat.streaming,
    (streaming) => {
      if (streaming) {
        startSpeaking();
      } else {
        stopSpeaking();
      }
    }
  );
}
