<script setup>
import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
import {
  VRMAnimationLoaderPlugin,
  createVRMAnimationClip,
} from "@pixiv/three-vrm-animation";
import {
  AmbientLight,
  AnimationMixer,
  Clock,
  DirectionalLight,
  LoopOnce,
  LoopPingPong,
  PerspectiveCamera,
  Scene,
  SRGBColorSpace,
  WebGLRenderer,
} from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { nextTick, onMounted, onUnmounted, ref, watch, computed } from "vue";
import { useI18n } from 'vue-i18n'
import {
  GetBallPosition,
  GetConfig,
  GetMousePosition,
  GetPetSize,
  GetScreenSize,
  ImportVRMFile,
  SaveBallPosition,
  SaveConfig,
} from "../../wailsjs/go/main/App";
import { EventsEmit, EventsOn, Quit } from "../../wailsjs/runtime/runtime";
import { debounce } from "../utils/timing.js";
import { useBehaviorEvents } from "../composables/useBehaviorEvents.js";
import { usePetState } from "../composables/usePetState.js";
import { useVRMModel } from "../composables/useVRMModel.js";
import { ICON_POWER, ICON_SETTING, ICON_SHIRT, ICON_POMODORO, ICON_CPU, ICON_CLAUDE_CODE } from "../utils/icons";
import ContextMenu from "./ContextMenu.vue";

const emit = defineEmits(["click", "position", "ball-size", "open-settings", "open-pomodoro", "toggle-system-panel", "toggle-claude-panel"]);
const props = defineProps({
  activeScreen: { type: Object, default: () => ({ width: 0, height: 0 }) },
  pomodoroPanelOpen: { type: Boolean, default: false },
  claudePanelOpen: { type: Boolean, default: false },
  systemPanelOpen: { type: Boolean, default: true },
});

const canvasRef = ref(null);
const pos = ref(null);
const petSize = ref(200);
const sw = ref(0);
const sh = ref(0);
const petMenuRef = ref(null);
const { petState } = usePetState();
const { currentVRMModel, availableVRMModels, vrmModelURL, loadVRMModels } =
  useVRMModel();

let scene, camera, renderer, vrm, clock, rafId, idleMixer;
let mounted = true;
let _vrmLoadSeq = 0; // incremented on each loadVRM call; stale loads self-abort
let mouseTrackTimer = null;
let isDragging = false;
let dragStart = null;
let vrmBaseRotationY = 0; // base rotation set at load time; drag tilts relative to this
let mouthPhase = 0;
let blinkTimer = null;
const MOUSE_POLL_MS = 100;

// Head IK targets (-1 to 1 normalized)
let targetHeadX = 0;
let targetHeadY = 0;

// Emotion blendshape targets and current values
const targetEmotionWeights = {};
const currentEmotionWeights = {};

// VRM canvas is square but the model head sits ~35% down from the top.
// Shift the reported Y down so ChatBubble attaches near the actual head, not the canvas top.
const VRM_HEAD_CANVAS_OFFSET = 0.35;

// LLM emotion → VRM blendshape name
const EMOTION_MAP = {
  joy: "happy",
  sad: "sad",
  surprised: "surprised",
  angry: "angry",
};

// LLM emotion → sustained animation (applies to ALL pet states, loop)
const EMOTION_ANIMS = {
  joy: "/vrm/celebrate.vrma",
  sad: "/vrm/sad.vrma",
  surprised: "/vrm/surprised_react.vrma",
  angry: "/vrm/angry.vrma",
};

// LLM action → one-shot animation (plays once, then returns to current animation)
const ACTION_ANIMS = {
  wave: "/vrm/wave_big.vrma",
  nod: "/vrm/nod.vrma",
  celebrate: "/vrm/celebrate.vrma",
  surprised_react: "/vrm/surprised_react.vrma",
};

watch(pos, (p) => {
  if (p)
    emit("position", { ...p, y: p.y + petSize.value * VRM_HEAD_CANVAS_OFFSET });
});

// ── Imperative API (exposed to App.vue) ─────────────────────────────────────

/** setState maps pet states to VRM animations and expressions. */
function setState(state) {
  mouthPhase = 0;
  switch (state) {
    case "idle":
      // Clear emotion state — pet returns to neutral waiting.
      _activeEmotion = null;
      Object.keys(targetEmotionWeights).forEach((k) => {
        targetEmotionWeights[k] = 0;
      });
      break;
    case "error":
      // Error always overrides with sad expression.
      _activeEmotion = null;
      Object.keys(targetEmotionWeights).forEach((k) => {
        targetEmotionWeights[k] = 0;
      });
      targetEmotionWeights["sad"] = 0.6;
      break;
    // thinking, speaking, listening: keep active emotion if set.
  }
}

/** focusGlobal transforms global screen coordinates into head IK targets. */
function focusGlobal(mx, my) {
  if (!canvasRef.value) return;
  const rect = canvasRef.value.getBoundingClientRect();
  const cx = rect.left + rect.width / 2;
  const cy = rect.top + rect.height / 2;
  targetHeadX = Math.max(-1, Math.min(1, (mx - cx) / (rect.width * 1.5)));
  targetHeadY = Math.max(-0.5, Math.min(0.5, (my - cy) / (rect.height * 1.5)));
}

/**
 * applyBehavior handles a chat:behavior event.
 * 1. If action is set → play one-shot animation, then return to sustained/fallback.
 * 2. If emotion ≠ neutral → set blendshape + switch sustained animation.
 * 3. If emotion = neutral → clear blendshape, return to petState fallback.
 */
function applyBehavior({ emotion, action }) {
  // Reset blendshape targets.
  Object.keys(targetEmotionWeights).forEach((k) => {
    targetEmotionWeights[k] = 0;
  });

  // 1. One-shot action — plays once, then auto-returns to sustained/fallback.
  if (action && ACTION_ANIMS[action]) {
    playAnimation(ACTION_ANIMS[action], { loop: false, fadeTime: 0.3 }).then(() => {
      // After action completes, apply sustained animation.
      _applySustainedAnimation(emotion);
    });
    return;
  }

  // 2. No action — apply sustained animation directly.
  _applySustainedAnimation(emotion);
}

/** _applySustainedAnimation sets blendshape and sustained animation for the given emotion. */
function _applySustainedAnimation(emotion) {
  if (emotion === "neutral" || !emotion) {
    _activeEmotion = null;
    applyStateAnimation(petState.value);
    return;
  }

  const mapped = EMOTION_MAP[emotion];
  if (mapped) {
    targetEmotionWeights[mapped] = 0.7;
  }

  _activeEmotion = emotion;
  const animFile = EMOTION_ANIMS[emotion];
  if (animFile) {
    playAnimation(animFile, { loop: true, fadeTime: 0.4 });
  } else {
    applyStateAnimation(petState.value);
  }
}

/** setSize resizes the renderer to n×n pixels. */
function setSize(n) {
  if (!renderer || n <= 0) return;
  petSize.value = n;
  renderer.setSize(n, n);
  emit("ball-size", n);
}

defineExpose({ setState, focusGlobal, applyBehavior, setSize });

// ── Render Loop Sub-Systems ──────────────────────────────────────────────────


/** updateHeadIK smoothly rotates head and neck bones toward the cursor. */
function updateHeadIK(dt) {
  if (!vrm) return;
  const head = vrm.humanoid?.getNormalizedBoneNode("head");
  const neck = vrm.humanoid?.getNormalizedBoneNode("neck");
  const speed = dt * 5;
  if (head) {
    head.rotation.y += (targetHeadX * (Math.PI / 5) - head.rotation.y) * speed; // ±36°
    head.rotation.x += (targetHeadY * (Math.PI / 8) - head.rotation.x) * speed; // ±11°
  }
  if (neck) {
    neck.rotation.y += (targetHeadX * (Math.PI / 10) - neck.rotation.y) * speed; // ±18°
    neck.rotation.x += (targetHeadY * (Math.PI / 16) - neck.rotation.x) * speed; // ±5.6°
  }
}

/** updateMouthAnim drives the aa blendshape in a sine wave while speaking. */
function updateMouthAnim(dt) {
  if (!vrm?.expressionManager) return;
  if (petState.value === "speaking") {
    mouthPhase += dt * 8;
    const v = (Math.sin(mouthPhase) + 1) * 0.3;
    try {
      vrm.expressionManager.setValue("aa", v);
    } catch (_) {}
  } else {
    const cur = vrm.expressionManager.getValue("aa") ?? 0;
    if (cur > 0.001) {
      try {
        vrm.expressionManager.setValue("aa", Math.max(0, cur - dt * 2));
      } catch (_) {}
    }
  }
}

/** updateEmotionBlend lerps emotion blendshape weights toward targets each frame. */
function updateEmotionBlend(dt) {
  if (!vrm?.expressionManager) return;
  const speed = dt * 4;
  const allKeys = new Set([
    ...Object.keys(targetEmotionWeights),
    ...Object.keys(currentEmotionWeights),
  ]);
  for (const name of allKeys) {
    const target = targetEmotionWeights[name] ?? 0;
    const cur = currentEmotionWeights[name] ?? 0;
    const next = cur + (target - cur) * speed;
    currentEmotionWeights[name] = next;
    try {
      vrm.expressionManager.setValue(name, next < 0.001 ? 0 : next);
    } catch (_) {}
  }
}

// petState → default animation
const STATE_ANIMS = {
  idle: "/vrm/waiting.vrma",
  listening: "/vrm/curious.vrma",
  thinking: "/vrm/waiting.vrma",
  speaking: "/vrm/hand_talk.vrma",
  error: "/vrm/embarrassed.vrma",
};

// Idle variety pool — randomly shown every 60–120s then returns to waiting
const IDLE_VARIETY_POOL = [
  "/vrm/sleepy.vrma",
  "/vrm/relaxed.vrma",
];

// Occasional one-shot gestures during idle
const IDLE_GESTURES = [
  "/vrm/nod.vrma",
  "/vrm/wave_big.vrma",
];

let _activeEmotion = null; // current sustained emotion (null when neutral/idle-cleared)
let _idleVarietyTimer = null;
/** _pendingTimers tracks inner setTimeout IDs so they can be cleared on unmount. */
const _pendingTimers = [];

// Shared loader + clip cache (module-level singletons).
const _animLoader = new GLTFLoader();
_animLoader.register((parser) => new VRMAnimationLoaderPlugin(parser));
const _clipCache = {};
let _currentAction = null;

/** loadClip fetches and caches a VRMA AnimationClip for the given VRM. */
async function loadClip(url, v) {
  const key = url + "|" + (v?.scene?.uuid ?? "");
  if (_clipCache[key]) return _clipCache[key];
  const gltf = await _animLoader.loadAsync(url);
  const anim = gltf.userData.vrmAnimations?.[0];
  if (!anim) return null;
  const clip = createVRMAnimationClip(anim, v);
  _clipCache[key] = clip;
  return clip;
}

/**
 * playAnimation crossfades to a new VRMA clip.
 * Uses crossFadeTo() for smooth transitions and LoopPingPong to
 * eliminate the hard jump at loop boundaries.
 */
async function playAnimation(url, { loop = true, fadeTime = 0.5 } = {}) {
  if (!vrm || !idleMixer) return;
  try {
    const clip = await loadClip(url, vrm);
    if (!clip) return;
    const newAction = idleMixer.clipAction(clip);
    newAction.setLoop(loop ? LoopPingPong : LoopOnce, Infinity);
    newAction.clampWhenFinished = !loop;
    if (_currentAction && _currentAction !== newAction) {
      newAction.reset().play();
      _currentAction.crossFadeTo(newAction, fadeTime, true);
    } else if (!_currentAction) {
      newAction.reset().play();
    }
    _currentAction = newAction;
  } catch (e) {
    console.warn("playAnimation failed:", url, e);
  }
}

/** initAnimationSystem sets up the mixer and plays the welcome + idle sequence. */
async function initAnimationSystem(v) {
  if (idleMixer) {
    idleMixer.stopAllAction();
    idleMixer = null;
  }
  _currentAction = null;
  idleMixer = new AnimationMixer(v.scene);
  // Welcome greeting on first load, then settle into idle.
  await playAnimation("/vrm/appearing.vrma", { loop: false, fadeTime: 0.3 });
  _pendingTimers.push(setTimeout(() => {
    if (mounted) playAnimation(STATE_ANIMS.idle, { fadeTime: 0.8 });
  }, 3000));
}

/**
 * applyStateAnimation switches to the fallback animation for the given petState.
 * If an active emotion is set, the emotion animation takes priority over the
 * state fallback (except for error, which always forces its animation).
 */
async function applyStateAnimation(state) {
  // Error always takes priority.
  if (state === "error") {
    await playAnimation(STATE_ANIMS.error, { fadeTime: 0.5 });
    return;
  }

  // Active emotion overrides state fallback.
  if (_activeEmotion && EMOTION_ANIMS[_activeEmotion]) {
    await playAnimation(EMOTION_ANIMS[_activeEmotion], { loop: true, fadeTime: 0.4 });
    return;
  }

  // Speaking with no emotion: hand_talk + mouth blendshape.
  if (state === "speaking") {
    await playAnimation(STATE_ANIMS.speaking, { fadeTime: 0.4 });
    return;
  }

  const file = STATE_ANIMS[state];
  if (file) await playAnimation(file, { fadeTime: 0.5 });
}

/**
 * scheduleIdleEvent fires a random idle event every 60–120s:
 * 40% chance → variety idle (relaxed/sleepy/idle_loop) for 15–20s
 * 60% chance → one-shot gesture (nod/wave/hello)
 */
function scheduleIdleVariety() {
  _idleVarietyTimer = setTimeout(
    async () => {
      if (!mounted) return;
      if (petState.value === "idle") {
        if (Math.random() < 0.4) {
          const pick =
            IDLE_VARIETY_POOL[
              Math.floor(Math.random() * IDLE_VARIETY_POOL.length)
            ];
          await playAnimation(pick, { fadeTime: 0.8 });
          _pendingTimers.push(setTimeout(
            () => {
              if (mounted && petState.value === "idle")
                playAnimation(STATE_ANIMS.idle, { fadeTime: 1.0 });
            },
            15000 + Math.random() * 5000,
          ));
        } else {
          const pick =
            IDLE_GESTURES[Math.floor(Math.random() * IDLE_GESTURES.length)];
          await playAnimation(pick, { loop: false, fadeTime: 0.5 });
          _pendingTimers.push(setTimeout(() => {
            if (mounted && petState.value === "idle")
              playAnimation(STATE_ANIMS.idle, { fadeTime: 0.8 });
          }, 3500));
        }
      }
      scheduleIdleVariety();
    },
    60000 + Math.random() * 60000,
  );
}

/** tick is the main render loop called every animation frame. */
function tick() {
  if (!mounted) return;
  const dt = Math.min(clock.getDelta(), 0.1); // cap dt to avoid huge jumps
  idleMixer?.update(dt);
  if (!idleMixer) updateHeadIK(dt);
  updateMouthAnim(dt);
  updateEmotionBlend(dt);
  if (vrm) vrm.update(dt);
  renderer.render(scene, camera);
  rafId = requestAnimationFrame(tick);
}

// ── Initialization ───────────────────────────────────────────────────────────

/** initRenderer creates the THREE.js scene, camera, lights, and WebGL renderer. */
async function initRenderer() {
  scene = new Scene();
  camera = new PerspectiveCamera(40, 1, 0.1, 20);
  camera.position.set(0, 1.1, 3.2);
  camera.lookAt(0, 1.1, 0);

  renderer = new WebGLRenderer({
    canvas: canvasRef.value,
    alpha: true,
    antialias: true,
    preserveDrawingBuffer: true,
  });
  renderer.setPixelRatio(window.devicePixelRatio || 1);
  renderer.setSize(petSize.value, petSize.value);
  renderer.outputColorSpace = SRGBColorSpace;

  // Key light: front-top to illuminate face
  const keyLight = new DirectionalLight(0xffffff, 1.8);
  keyLight.position.set(0, 2, 2);
  scene.add(keyLight);
  // Fill light: slight left side to avoid flat look
  const fillLight = new DirectionalLight(0xffffff, 0.8);
  fillLight.position.set(-2, 1, 1);
  scene.add(fillLight);
  // Ambient: base brightness so shadows aren't black
  scene.add(new AmbientLight(0xffffff, 0.8));

  clock = new Clock();
  tick();
}

/** loadVRM loads a .vrm file by URL and replaces the current model in the scene. */
async function loadVRM(url) {
  if (!url) return;
  const seq = ++_vrmLoadSeq;
  const loader = new GLTFLoader();
  loader.register((parser) => new VRMLoaderPlugin(parser));
  const gltf = await loader.loadAsync(url);
  // Abort if a newer load started while we were fetching.
  if (seq !== _vrmLoadSeq || !mounted) return;
  const newVrm = gltf.userData.vrm;
  VRMUtils.removeUnnecessaryVertices(gltf.scene);
  VRMUtils.removeUnnecessaryJoints(gltf.scene);
  // VRM 0.x faces +Z (toward camera); VRM 1.0 follows glTF and faces -Z (away).
  // Rotate 1.0 models 180° so the front always faces the camera.
  // VRM 0.x faces -Z; VRM 1.0 faces +Z (glTF standard). Camera is at +Z.
  const isVRM0 = !!gltf.parser.json.extensions?.VRM;
  vrmBaseRotationY = isVRM0 ? Math.PI : 0;
  newVrm.scene.rotation.y = vrmBaseRotationY;

  if (vrm) {
    scene.remove(vrm.scene);
    VRMUtils.deepDispose(vrm.scene);
  }
  vrm = newVrm;
  scene.add(vrm.scene);
  await initAnimationSystem(vrm);
}

// ── Idle Animations ──────────────────────────────────────────────────────────

/** scheduleBlink randomly blinks the VRM model every 3–6 seconds. */
function scheduleBlink() {
  blinkTimer = setTimeout(
    async () => {
      if (!mounted || !vrm?.expressionManager) {
        scheduleBlink();
        return;
      }
      try {
        vrm.expressionManager.setValue("blink", 1);
        await new Promise((r) => setTimeout(r, 100));
        if (!mounted) return;
        vrm.expressionManager.setValue("blink", 0);
      } catch (_) {}
      scheduleBlink();
    },
    3000 + Math.random() * 3000,
  );
}

// ── Context Menu ─────────────────────────────────────────────────────────────

/** switchToNextVRMModel cycles through available VRM models. */
async function switchToNextVRMModel() {
  const models = availableVRMModels.value;
  if (models.length <= 1) return;
  const idx = models.findIndex((m) => m.name === currentVRMModel.value);
  const next = models[(idx + 1) % models.length];
  EventsEmit("config:vrm:model:changed", next.name);
  try {
    const cfg = await GetConfig();
    if (cfg) {
      cfg.VRMModel = next.name;
      await SaveConfig(cfg);
    }
  } catch (e) {
    console.warn("switchToNextVRMModel:", e);
  }
}

function onContextMenu(e) {
  if (isTransparentAt(e.clientX, e.clientY)) return;
  e.preventDefault();
  petMenuRef.value?.show(e.clientX, e.clientY);
}

const { t } = useI18n()

const petMenuItems = computed(() => [
  { iconSvg: ICON_SHIRT, label: t('petMenu.switchModel'), action: switchToNextVRMModel },
  { iconSvg: ICON_POMODORO, label: props.pomodoroPanelOpen ? t('pomodoro.menuLabelHide') : t('pomodoro.menuLabelShow'), action: () => emit("open-pomodoro") },
  { iconSvg: ICON_CLAUDE_CODE, label: props.claudePanelOpen ? t('claudeStatus.hidePanel') : t('claudeStatus.showPanel'), action: () => emit('toggle-claude-panel') },
  { iconSvg: ICON_CPU,   label: props.systemPanelOpen ? t('system.hidePanel') : t('system.showPanel'), action: () => emit('toggle-system-panel') },
  { divider: true },
  {
    iconSvg: ICON_SETTING,
    label: t('petMenu.openSettings'),
    action: () => emit("open-settings"),
  },
  { divider: true },
  {
    iconSvg: ICON_POWER,
    label: t('petMenu.quitApp'),
    action: () => Quit(),
    danger: true,
  },
]);

// ── Drag Handling ────────────────────────────────────────────────────────────

/**
 * isTransparentAt returns true when the WebGL canvas pixel under the given
 * client coordinates has alpha < 10 (i.e. fully transparent background).
 * Used to implement per-pixel click-through so only the visible character
 * region captures mouse events.
 */
function isTransparentAt(clientX, clientY) {
  if (!canvasRef.value || !renderer) return false;
  const rect = canvasRef.value.getBoundingClientRect();
  const scaleX = canvasRef.value.width / rect.width;
  const scaleY = canvasRef.value.height / rect.height;
  const px = Math.round((clientX - rect.left) * scaleX);
  const py = Math.round((clientY - rect.top) * scaleY);
  const glY = canvasRef.value.height - py - 1; // WebGL Y is bottom-up
  const gl = renderer.getContext();
  const buf = new Uint8Array(4);
  gl.readPixels(px, glY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf);
  return buf[3] < 10;
}

function onMouseDown(e) {
  if (e.button !== 0) return;
  if (isTransparentAt(e.clientX, e.clientY)) return;
  dragStart = {
    x: e.clientX - pos.value.x,
    y: e.clientY - pos.value.y,
    startX: e.clientX,
    startY: e.clientY,
  };
  isDragging = false;
  window.addEventListener("mousemove", onMouseMove);
  window.addEventListener("mouseup", onMouseUp);
  window.addEventListener("blur", onMouseUp);
}

function onMouseMove(e) {
  if (!dragStart || !pos.value) return;
  const dx = e.clientX - dragStart.startX;
  const dy = e.clientY - dragStart.startY;
  if (!isDragging && Math.sqrt(dx * dx + dy * dy) < 5) return;
  isDragging = true;
  pos.value = { x: e.clientX - dragStart.x, y: e.clientY - dragStart.y };
  // Tilt VRM body toward drag direction.
  if (vrm)
    vrm.scene.rotation.y =
      vrmBaseRotationY + Math.max(-0.35, Math.min(0.35, dx / 200));
}

async function onMouseUp(e) {
  window.removeEventListener("mousemove", onMouseMove);
  window.removeEventListener("mouseup", onMouseUp);
  window.removeEventListener("blur", onMouseUp);
  if (isDragging) {
    if (vrm) vrm.scene.rotation.y = vrmBaseRotationY;
    try {
      await SaveBallPosition(
        Math.round(pos.value.x),
        Math.round(pos.value.y),
        sw.value,
        sh.value,
      );
    } catch (err) {
      console.error("SaveBallPosition:", err);
    }
  } else if (e && typeof e.clientX === "number") {
    emit("click");
  }
  dragStart = null;
  isDragging = false;
}

// ── VRM Drag-Drop Import ─────────────────────────────────────────────────────

/** onDrop handles a .vrm file dragged onto the pet widget. */
async function onDrop(e) {
  const file = e.dataTransfer?.files?.[0];
  if (!file || !file.name.endsWith(".vrm")) return;
  try {
    const buf = await file.arrayBuffer();
    // Convert ArrayBuffer to base64 in chunks to avoid call stack overflow on large files.
    const bytes = new Uint8Array(buf);
    let binary = "";
    const chunkSize = 8192;
    for (let i = 0; i < bytes.length; i += chunkSize) {
      binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
    }
    const b64 = btoa(binary);
    await ImportVRMFile(file.name, b64);
    await loadVRMModels();
    EventsEmit("config:vrm:model:changed", file.name);
    const cfg = await GetConfig();
    if (cfg) {
      cfg.VRMModel = file.name;
      await SaveConfig(cfg);
    }
  } catch (err) {
    console.error("ImportVRMFile failed:", err);
  }
}

// ── Mouse Tracking ───────────────────────────────────────────────────────────

/** startGlobalMouseTracking polls Go for cursor position every 50ms for head IK. */
function startGlobalMouseTracking() {
  let lastX = -1,
    lastY = -1;
  mouseTrackTimer = setInterval(async () => {
    if (!mounted || !vrm) return;
    try {
      const { x, y } = await GetMousePosition();
      if (!mounted) return;
      if (x === lastX && y === lastY) return;
      lastX = x;
      lastY = y;
      focusGlobal(x, y);
    } catch (_) {}
  }, MOUSE_POLL_MS);
}

// ── Lifecycle ────────────────────────────────────────────────────────────────

// Subscribe to behavior events — forwarded to applyBehavior.
useBehaviorEvents(({ emotion, action }) =>
  applyBehavior({ emotion, action }),
);

// Hot-reload VRM when model URL changes.
watch(vrmModelURL, async (url) => {
  if (!renderer || !mounted || !url) return;
  try {
    await loadVRM(url);
  } catch (err) {
    console.error("VRM reload failed:", err);
  }
});

// Drive pet state: expressions + animation.
watch(petState, (state) => {
  setState(state);
  applyStateAnimation(state);
});

let offSizeChange, offPositionReset, offScreenChanged, offPreviewAnim, screenChangedHandler;

onMounted(async () => {
  loadVRMModels();

  // Phase 1: load screen size + position.
  try {
    sw.value = window.innerWidth;
    sh.value = window.innerHeight;
    try {
      const [screenW, screenH] = await GetScreenSize();
      if (screenW > 0) {
        sw.value = screenW;
        sh.value = screenH;
      }
    } catch (_) {}

    try {
      const size = await GetPetSize(sw.value, sh.value);
      if (size > 0) {
        petSize.value = size;
        emit("ball-size", size);
      }
    } catch (_) {}

    const [bx, by] = await GetBallPosition(sw.value, sh.value);
    pos.value =
      bx >= 0 && by >= 0
        ? { x: bx, y: by }
        : {
            x: sw.value - petSize.value - 40,
            y: sh.value - petSize.value - 40,
          };
  } catch (_) {
    pos.value = {
      x: window.innerWidth - petSize.value - 40,
      y: window.innerHeight - petSize.value - 40,
    };
  }

  // Phase 2: init renderer + load VRM.
  await nextTick();
  try {
    await initRenderer();
    if (vrmModelURL.value) await loadVRM(vrmModelURL.value);
    startGlobalMouseTracking();
    scheduleBlink();
    scheduleIdleVariety();
  } catch (err) {
    console.error("VRMPet init failed:", err);
  }

  offPreviewAnim = EventsOn("vrm:preview:anim", async (url) => {
    if (!idleMixer || !vrm) return;
    const returnAnim = STATE_ANIMS[petState.value] ?? STATE_ANIMS.idle;
    await playAnimation(url, { loop: false, fadeTime: 0.3 });
    // Return to current state animation after one-shot finishes or after 15s max.
    let returned = false;
    const doReturn = () => {
      if (returned) return;
      returned = true;
      idleMixer?.removeEventListener("finished", onFinished);
      if (mounted) playAnimation(returnAnim, { fadeTime: 0.8 });
    };
    const onFinished = (e) => { if (e.action === _currentAction) doReturn(); };
    idleMixer.addEventListener("finished", onFinished);
    setTimeout(doReturn, 15000);
  });
  offSizeChange = EventsOn("config:pet:size:changed", (size) => setSize(size));
  offPositionReset = EventsOn("ball:position:reset", () => {
    pos.value = {
      x: sw.value - petSize.value - 40,
      y: sh.value - petSize.value - 40,
    };
  });
  screenChangedHandler = debounce(async (info) => {
    sw.value = info.width;
    sh.value = info.height;
    try {
      const size = await GetPetSize(info.width, info.height);
      if (size > 0) setSize(size);
    } catch (_) {}
    try {
      const [bx, by] = await GetBallPosition(info.width, info.height);
      pos.value =
        bx >= 0 && by >= 0
          ? { x: bx, y: by }
          : {
              x: info.width - petSize.value - 40,
              y: info.height - petSize.value - 40,
            };
    } catch (_) {}
  }, 200);
  offScreenChanged = EventsOn("screen:active:changed", screenChangedHandler);
});

onUnmounted(() => {
  mounted = false;
  if (mouseTrackTimer) {
    clearInterval(mouseTrackTimer);
    mouseTrackTimer = null;
  }
  clearTimeout(blinkTimer);
  clearTimeout(_idleVarietyTimer);
  _pendingTimers.forEach(clearTimeout);
  _pendingTimers.length = 0;
  offPreviewAnim?.();
  offSizeChange?.();
  offPositionReset?.();
  offScreenChanged?.();
  screenChangedHandler?.cancel?.();
  window.removeEventListener("mousemove", onMouseMove);
  window.removeEventListener("mouseup", onMouseUp);
  window.removeEventListener("blur", onMouseUp);
  if (rafId) {
    cancelAnimationFrame(rafId);
    rafId = null;
  }
  if (idleMixer) {
    idleMixer.stopAllAction();
    idleMixer = null;
  }
  if (vrm) {
    VRMUtils.deepDispose(vrm.scene);
    vrm = null;
  }
  if (renderer) {
    renderer.dispose();
    renderer = null;
  }
  scene = null;
});
</script>

<template>
  <div
    v-if="pos"
    class="vrm-pet"
    :style="{
      left: pos.x + 'px',
      top: pos.y + 'px',
      width: petSize + 'px',
      height: petSize + 'px',
    }"
    @mousedown="onMouseDown"
    @contextmenu="onContextMenu"
    @dragover.prevent
    @drop.prevent="onDrop"
  >
    <canvas ref="canvasRef" class="pet-canvas" />
    <ContextMenu ref="petMenuRef" :items="petMenuItems" />
  </div>
</template>

<style scoped>
.vrm-pet {
  position: fixed;
  z-index: 1000;
  cursor: pointer;
  user-select: none;
}
.pet-canvas {
  width: 100%;
  height: 100%;
  display: block;
}
</style>
