const stage = document.getElementById("stage");
const bubble = document.getElementById("bubble");
const preview = document.getElementById("preview");
const puppet = document.getElementById("puppet");

let currentCharacter = "leesori";
let currentState = "idle";
let registry = new Map();
let textTimer = 0;
let puppetToken = 0;
let loadedRig = null;
let loadedActive = null;
let gestureTimer = 0;
let gestureFrame = 0;

function post(type, payload = {}) {
  if (window.chrome?.webview) {
    window.chrome.webview.postMessage({ type, ...payload });
  }
}

function activeCharacter() {
  return registry.get(currentCharacter) || registry.get("leesori");
}

function label() {
  const active = activeCharacter();
  const name = active?.name || currentCharacter;
  return `${name} - ${currentState}`;
}

function setAccent(active) {
  if (active?.accent) {
    stage.style.setProperty("--accent", active.accent);
  }
}

function imageForState(active, state) {
  if (!active) return "";
  return active.states?.[state] || active.states?.idle || active.preview || "";
}

function setPreview(active, state) {
  const source = imageForState(active, state);
  if (source && preview.getAttribute("src") !== source) {
    preview.src = source;
  }
  preview.alt = active?.name || "Character preview";
}

function stopGestureTimer() {
  if (gestureTimer) {
    window.clearInterval(gestureTimer);
    gestureTimer = 0;
  }
  gestureFrame = 0;
}

function clearPuppet() {
  stopGestureTimer();
  loadedRig = null;
  loadedActive = null;
  stage.classList.remove("has-puppet");
  delete puppet.dataset.rig;
  delete puppet.dataset.mode;
  delete puppet.dataset.gesture;
  puppet.replaceChildren();
}

function transformOrigin(pivot, canvas) {
  const x = ((pivot?.[0] || 0) / canvas.width) * 100;
  const y = ((pivot?.[1] || 0) / canvas.height) * 100;
  return `${x.toFixed(3)}% ${y.toFixed(3)}%`;
}

function selectGesture(rig, state) {
  const gestures = rig?.gestures || [];
  return gestures.find(item => item.states?.includes(state))
    || gestures.find(item => item.id === state)
    || gestures.find(item => item.default)
    || gestures[0]
    || null;
}

function motionTransform(frame, phase) {
  const motion = frame?.motion || {};
  const target = motion.target || { x: 0, y: 0, rotate: 0, scale: 1 };
  const offset = phase === "target"
    ? { x: 0, y: 0, rotate: 0, scale: 1 }
    : (motion[phase] || { x: 0, y: 0, rotate: 0, scale: 1 });
  const x = (target.x || 0) + (offset.x || 0);
  const y = (target.y || 0) + (offset.y || 0);
  const rotate = (target.rotate || 0) + (offset.rotate || 0);
  const scale = (target.scale || 1) * (offset.scale || 1);
  return `translate(${x}px, ${y}px) rotate(${rotate}deg) scale(${scale})`;
}

function frameForElement(element) {
  const frames = loadedRig?.activeGesture?.frames || [];
  return frames[Number(element.dataset.frameIndex)] || {};
}

function hideInactiveFrame(frame) {
  frame.classList.remove("is-active", "is-entering", "is-exiting");
  frame.style.opacity = "0";
  frame.style.zIndex = "1";
}

function setActiveGestureFrame(index) {
  const frames = [...puppet.querySelectorAll(".puppet-frame")];
  if (!frames.length) return;

  const activeIndex = ((index % frames.length) + frames.length) % frames.length;
  const transitionMs = Math.max(80, Number(loadedRig?.activeGesture?.transitionMs || 170));
  const current = frames.find(frame => frame.classList.contains("is-active"));
  const next = frames[activeIndex];
  if (!next || current === next) return;

  for (const frame of frames) {
    if (frame !== current && frame !== next) {
      hideInactiveFrame(frame);
    }
  }

  if (current) {
    current.classList.remove("is-active", "is-entering");
    current.classList.add("is-exiting");
    current.style.transitionDuration = `${transitionMs}ms`;
    current.style.zIndex = "2";
    current.style.opacity = "0";
    current.style.transform = motionTransform(frameForElement(current), "exit");
    window.setTimeout(() => {
      if (current.classList.contains("is-exiting")) hideInactiveFrame(current);
    }, transitionMs + 40);
  }

  next.classList.remove("is-exiting");
  next.classList.add("is-entering");
  next.style.transitionDuration = `${transitionMs}ms`;
  next.style.zIndex = "3";
  next.style.opacity = "0";
  next.style.transform = motionTransform(frameForElement(next), "enter");
  void next.offsetWidth;
  next.classList.add("is-active");
  window.requestAnimationFrame(() => {
    next.style.opacity = "1";
    next.style.transform = motionTransform(frameForElement(next), "target");
  });
}

function startPoseSequence(gesture) {
  stopGestureTimer();
  loadedRig.activeGesture = gesture;
  const first = puppet.querySelector('.puppet-frame[data-frame-index="0"]');
  if (first) {
    first.style.transitionDuration = "0ms";
    first.style.opacity = "1";
    first.style.zIndex = "3";
    first.style.transform = motionTransform(frameForElement(first), "target");
    first.classList.add("is-active");
  }

  const frameMs = Math.max(120, Number(gesture.frameMs || 340));
  if (gesture.loop !== false && (gesture.frames?.length || 0) > 1) {
    gestureTimer = window.setInterval(() => {
      gestureFrame += 1;
      setActiveGestureFrame(gestureFrame);
    }, frameMs);
  }
}

function renderPoseSequence(rig, active, gesture) {
  const canvas = rig.canvas || { width: 1024, height: 1536 };
  const poseBase = active.puppet?.poseBase || rig.poseBase || active.puppet?.imageBase || rig.imageBase || "";
  const assetVersion = encodeURIComponent(rig.status || gesture.id || active.id || "v1");
  puppet.style.setProperty("--rig-width", canvas.width);
  puppet.style.setProperty("--rig-height", canvas.height);
  puppet.style.aspectRatio = `${canvas.width} / ${canvas.height}`;
  puppet.dataset.rig = rig.name || active.id || "puppet";
  puppet.dataset.mode = "pose-sequence";
  puppet.dataset.gesture = gesture.id || "gesture";

  const fragment = document.createDocumentFragment();
  for (const [index, frame] of (gesture.frames || []).entries()) {
    if (!frame.image) continue;
    const img = document.createElement("img");
    img.className = "puppet-frame";
    img.dataset.frameIndex = String(index);
    img.src = `${poseBase}${frame.image}?v=${assetVersion}`;
    img.alt = "";
    img.decoding = "async";
    img.style.transform = motionTransform(frame, "enter");
    fragment.appendChild(img);
  }
  puppet.replaceChildren(fragment);
  stage.classList.add("has-puppet");
  startPoseSequence(gesture);
}

function renderBoneRig(rig, active) {
  const canvas = rig.canvas || { width: 520, height: 900 };
  const imageBase = active.puppet?.imageBase || rig.imageBase || "";
  const assetVersion = encodeURIComponent(rig.status || active.id || "v1");
  puppet.style.setProperty("--rig-width", canvas.width);
  puppet.style.setProperty("--rig-height", canvas.height);
  puppet.style.aspectRatio = `${canvas.width} / ${canvas.height}`;
  puppet.dataset.rig = rig.name || active.id || "puppet";
  puppet.dataset.mode = "bones";

  const fragment = document.createDocumentFragment();
  const bones = [...(rig.bones || [])].sort((a, b) => (a.z || 0) - (b.z || 0));
  for (const bone of bones) {
    if (!bone.image) continue;
    const img = document.createElement("img");
    img.className = "puppet-part";
    img.dataset.bone = bone.name;
    img.src = `${imageBase}${bone.image}?v=${assetVersion}`;
    img.alt = "";
    img.decoding = "async";
    img.style.zIndex = String(bone.z || 0);
    img.style.transformOrigin = transformOrigin(bone.pivot, canvas);
    fragment.appendChild(img);
  }

  puppet.replaceChildren(fragment);
  stage.classList.add("has-puppet");
}

function renderLoadedPuppet() {
  if (!loadedRig || !loadedActive) return;
  if (loadedRig.renderMode === "partsRig" && loadedRig.partsRig) {
    stopGestureTimer();
    renderBoneRig({ ...loadedRig.partsRig, name: loadedRig.name, status: loadedRig.status, imageBase: loadedRig.imageBase || loadedRig.partsRig.imageBase }, loadedActive);
    return;
  }
  if (loadedRig.renderMode === "poseSequence") {
    const gesture = selectGesture(loadedRig, currentState);
    if (gesture) {
      renderPoseSequence(loadedRig, loadedActive, gesture);
      return;
    }
  }
  stopGestureTimer();
  renderBoneRig(loadedRig, loadedActive);
}

async function loadPuppet(active) {
  const token = ++puppetToken;
  clearPuppet();

  if (!active?.puppet?.rig) return;

  try {
    const response = await fetch(active.puppet.rig, { cache: "no-store" });
    const rig = await response.json();
    if (token !== puppetToken) return;

    loadedRig = rig;
    loadedActive = active;
    renderLoadedPuppet();
  } catch (error) {
    clearPuppet();
    console.warn("Puppet load failed", error);
  }
}

function setState(state) {
  const nextState = state || "idle";
  const changed = currentState !== nextState;
  currentState = nextState;
  stage.dataset.state = currentState;
  setPreview(activeCharacter(), currentState);
  if (changed && loadedRig?.renderMode === "poseSequence") {
    renderLoadedPuppet();
  }
  bubble.textContent = label();
}

async function setCharacter(characterId) {
  currentCharacter = characterId || "leesori";
  const active = activeCharacter();
  if (active) {
    currentCharacter = active.id;
    setAccent(active);
    setPreview(active, currentState);
    await loadPuppet(active);
  }
  bubble.textContent = label();
}

function showText(message) {
  window.clearTimeout(textTimer);
  bubble.textContent = message || label();
  if (message) {
    textTimer = window.setTimeout(() => {
      bubble.textContent = label();
    }, 3600);
  }
}

async function loadRegistry() {
  try {
    const response = await fetch("./characters.json", { cache: "no-store" });
    const data = await response.json();
    registry = new Map((data.characters || []).map(item => [item.id, item]));
  } catch (error) {
    registry = new Map([["leesori", {
      id: "leesori",
      name: "LeeSori",
      preview: "../Characters/Live2DPreview/leesori.png",
      accent: "#38E0C4"
    }]]);
  }
}

if (window.chrome?.webview) {
  window.chrome.webview.addEventListener("message", event => {
    const msg = event.data || {};
    if (msg.type === "setState") setState(msg.state);
    if (msg.type === "setCharacter") void setCharacter(msg.characterId);
    if (msg.type === "showText") showText(msg.message);
  });
}

window.addEventListener("DOMContentLoaded", async () => {
  await loadRegistry();
  await setCharacter("leesori");
  setState("idle");
  post("hostReady");
});



