<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import * as PIXI from 'pixi.js'
import { Live2DModel, MotionPriority } from 'pixi-live2d-display/cubism4'
import { GetBallPosition, SaveBallPosition, GetScreenSize, GetConfig, SaveConfig, GetMousePosition, GetPetSize } from '../../wailsjs/go/main/App'
import { Quit, EventsOn, EventsEmit } from '../../wailsjs/runtime/runtime'
import { debounce } from '../utils/timing.js'
import { usePetState } from '../composables/usePetState.js'
import { useModelPath } from '../composables/useModelPath.js'
import ContextMenu from './ContextMenu.vue'
import { ICON_FACE, ICON_SHIRT, ICON_SETTING, ICON_POWER, ICON_POMODORO, ICON_CPU, ICON_CLAUDE_CODE } from '../utils/icons'

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 pos = ref(null)
const { petState } = usePetState()
const { currentModel, availableModels, modelPath, loadModels } = useModelPath()
const petMenuRef = ref(null)

// Expression names are read dynamically from the loaded model's settings.
// Falls back to an empty list if the model has no registered expressions.
let modelExpressions = []
let exprIdx = 0

/** cycleExpression advances the Live2D model to the next expression in rotation. */
function cycleExpression() {
  if (!live2dModel || modelExpressions.length === 0) return
  exprIdx = (exprIdx + 1) % modelExpressions.length
  live2dModel.expression(modelExpressions[exprIdx])
}

/**
 * namedExpr returns the expression name matching the given keyword (case-insensitive substring).
 * Returns undefined if no match is found, which resets the expression when passed to live2dModel.expression().
 */
function namedExpr(keyword) {
  if (!keyword) return undefined
  return modelExpressions.find(n => n.includes(keyword))
}

/** switchToNextModel cycles availableModels and persists the selection. */
async function switchToNextModel() {
  const models = availableModels.value
  if (models.length <= 1) return
  const idx = models.indexOf(currentModel.value)
  const next = models[(idx + 1) % models.length]
  // Emit immediately for instant visual feedback via the composable's EventsOn listener.
  EventsEmit('config:model:changed', next)
  try {
    const cfg = await GetConfig()
    if (cfg) {
      cfg.Live2DModel = next
      await SaveConfig(cfg)
    }
  } catch (e) {
    console.warn('switchToNextModel: failed to save config', e)
  }
}

const { t } = useI18n()

const petMenuItems = computed(() => [
  { iconSvg: ICON_FACE,    label: t('petMenu.switchExpression'), action: cycleExpression },
  { iconSvg: ICON_SHIRT,   label: t('petMenu.switchModel'), action: switchToNextModel },
  { 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 },
])

/** onContextMenu shows the pet right-click menu. */
function onContextMenu(e) {
  e.preventDefault()
  petMenuRef.value?.show(e.clientX, e.clientY)
}
const canvasRef = ref(null)
const petSize = ref(160)
const sw = ref(0)
const sh = ref(0)

let pixiApp = null
let live2dModel = null
let dragStart = null
let isDragging = false
let mounted = true
let offSizeChange = null
let offPositionReset = null
let offScreenChanged = null
let screenChangedHandler = null

watch(pos, (p) => { if (p) emit('position', { ...p }) })

/** waitForRuntime polls until the Wails Go bridge is available. */
async function waitForRuntime() {
  while (!window.go?.main?.App) {
    await new Promise(r => setTimeout(r, 20))
  }
}

/** attachModel loads a Live2D model from path, scales it, and wires up interactions. */
async function attachModel(path) {
  const newModel = await Live2DModel.from(path, { autoInteract: false })
  if (!mounted || !pixiApp) {
    newModel.destroy()
    return
  }
  // Remove old model if present.
  if (live2dModel) {
    live2dModel.off('hit')
    pixiApp.stage.removeChild(live2dModel)
    live2dModel.destroy()
    live2dModel = null
  }
  live2dModel = newModel
  // Read expression names from the ExpressionManager's definitions (populated from model3.json).
  const exprMgr = live2dModel.internalModel?.motionManager?.expressionManager
  const defs = exprMgr?.definitions ?? live2dModel.internalModel?.settings?.expressions ?? []
  modelExpressions = Array.isArray(defs) ? defs.map(e => e.Name ?? e.name).filter(Boolean) : []
  exprIdx = 0
  pixiApp.stage.addChild(live2dModel)
  const scale = petSize.value / live2dModel.internalModel.originalHeight
  live2dModel.scale.set(scale)
  live2dModel.anchor.set(0.5, 0.5)
  live2dModel.position.set(petSize.value / 2, petSize.value / 2)
  live2dModel.on('hit', (hitAreas) => {
    if (hitAreas.includes('Body')) {
      live2dModel.motion('TapBody', undefined, MotionPriority.NORMAL)
    }
  })
  live2dModel.motion('Idle', undefined, MotionPriority.IDLE)
}

/** applySize resizes the PixiJS canvas and rescales the Live2D model. */
function applySize(size) {
  if (!pixiApp || size <= 0) return
  petSize.value = size
  emit('ball-size', size)
  pixiApp.renderer.resize(size, size)
  if (live2dModel) {
    const scale = size / live2dModel.internalModel.originalHeight
    live2dModel.scale.set(scale)
    live2dModel.position.set(size / 2, size / 2)
  }
}

/** initPixi creates the PixiJS application and loads the initial Live2D model. */
async function initPixi() {
  Live2DModel.registerTicker(PIXI.Ticker)
  pixiApp = new PIXI.Application({
    view: canvasRef.value,
    width: petSize.value,
    height: petSize.value,
    backgroundAlpha: 0,
    antialias: true,
    autoDensity: true,
    resolution: Math.min(window.devicePixelRatio || 1, 2),
    preserveDrawingBuffer: true,
  })
  await attachModel(modelPath.value)
}

onMounted(async () => {
  // Kick off model list fetch in background; composable handles errors.
  loadModels()

  // Phase 1: load position — isolated so PixiJS errors can't reset a successfully loaded position.
  try {
    await waitForRuntime()
    // Fallback to window dimensions so sw/sh are never 0 if the Go call fails.
    sw.value = window.innerWidth
    sh.value = window.innerHeight
    try {
      const [screenW, screenH] = await GetScreenSize()
      if (screenW > 0 && screenH > 0) {
        sw.value = screenW
        sh.value = screenH
      }
    } catch (e) {
      console.warn('GetScreenSize failed, using window dimensions', e)
    }

    // Use configured size, or auto-calculate from screen height.
    let configuredSize = 0
    try {
      configuredSize = await GetPetSize(sw.value, sh.value)
    } catch (e) {
      console.warn('GetPetSize failed', e)
    }
    petSize.value = configuredSize > 0 ? configuredSize : 350
    emit('ball-size', petSize.value)

    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 (err) {
    console.error('Live2DPet position init failed:', err)
    if (!sw.value) sw.value = window.innerWidth
    if (!sh.value) sh.value = window.innerHeight
    const ps = petSize.value
    if (!pos.value) pos.value = { x: window.innerWidth - ps - 40, y: window.innerHeight - ps - 40 }
  }

  // Phase 2: initialize PixiJS — separate try/catch so errors here don't affect saved position.
  await nextTick()
  try {
    await initPixi()
    startGlobalMouseTracking()
  } catch (err) {
    console.error('Live2DPet PixiJS init failed:', err)
  }

  // Listen for real-time pet size changes from settings.
  offSizeChange = EventsOn('config:pet:size:changed', (size) => {
    applySize(size)
  })

  offPositionReset = EventsOn('ball:position:reset', () => {
    pos.value = { x: sw.value - petSize.value - 40, y: sh.value - petSize.value - 40 }
  })

  screenChangedHandler = debounce(async (info) => {
    const w = info.width
    const h = info.height
    sw.value = w
    sh.value = h

    // Reload per-screen pet size.
    try {
      const size = await GetPetSize(w, h)
      if (size > 0) applySize(size)
    } catch (e) {
      console.warn('screen:active:changed: GetPetSize failed', e)
    }

    // Reload per-screen position.
    try {
      const [bx, by] = await GetBallPosition(w, h)
      if (bx >= 0 && by >= 0) {
        pos.value = { x: bx, y: by }
      } else {
        pos.value = { x: w - petSize.value - 40, y: h - petSize.value - 40 }
      }
    } catch (e) {
      console.warn('screen:active:changed: GetBallPosition failed', e)
    }
  }, 200)
  offScreenChanged = EventsOn('screen:active:changed', screenChangedHandler)
})

/** Watch modelPath and hot-reload the Live2D model when it changes. */
watch(modelPath, async (path) => {
  if (!pixiApp || !mounted) return
  try {
    await attachModel(path)
  } catch (err) {
    console.error('Live2DPet model reload failed:', err)
  }
})

/** watchPetState maps pet states to Live2D motions and expressions. */
watch(petState, (state) => {
  if (!live2dModel) return
  switch (state) {
    case 'thinking':
      live2dModel.motion('Idle', undefined, MotionPriority.NORMAL)
      live2dModel.expression(namedExpr('星星眼'))
      break
    case 'speaking':
      live2dModel.motion('TapBody', undefined, MotionPriority.FORCE)
      live2dModel.expression(namedExpr('爱心'))
      break
    case 'listening':
      live2dModel.motion('Idle', undefined, MotionPriority.NORMAL)
      live2dModel.expression(namedExpr('星星眼'))
      break
    case 'error':
      live2dModel.motion('TapBody', undefined, MotionPriority.FORCE)
      live2dModel.expression(namedExpr('生气'))
      break
    case 'idle':
    default:
      live2dModel.motion('Idle', undefined, MotionPriority.IDLE)
      live2dModel.expression()
      break
  }
})

// Global mouse tracking via polling — works even when the app is not focused.
let mouseTrackTimer = null
const MOUSE_POLL_INTERVAL_MS = 100

/** startGlobalMouseTracking polls the Go backend for the cursor position every 50 ms
 *  and drives the Live2D eye/body focus, enabling tracking even when the window is unfocused. */
function startGlobalMouseTracking() {
  let lastX = -1, lastY = -1
  mouseTrackTimer = setInterval(async () => {
    // The await below yields the event loop; the component may be unmounted
    // by the time we resume, so re-check `mounted` before touching PIXI state.
    if (!mounted || !live2dModel || !canvasRef.value) return
    try {
      const { x, y } = await GetMousePosition()
      if (!mounted || !live2dModel || !canvasRef.value) return
      if (x === lastX && y === lastY) return
      lastX = x; lastY = y
      const rect = canvasRef.value.getBoundingClientRect()
      live2dModel.focus(x - rect.left, y - rect.top)
    } catch (_) { /* ignore transient errors */ }
  }, MOUSE_POLL_INTERVAL_MS)
}

onUnmounted(() => {
  mounted = false
  if (mouseTrackTimer !== null) { clearInterval(mouseTrackTimer); mouseTrackTimer = null }
  offSizeChange?.()
  offPositionReset?.()
  offScreenChanged?.()
  screenChangedHandler?.cancel?.()
  // Clean up any in-progress drag listeners to prevent ghost handlers.
  window.removeEventListener('mousemove', onMouseMove)
  window.removeEventListener('mouseup', onMouseUp)
  window.removeEventListener('blur', onMouseUp)
  if (live2dModel) {
    live2dModel.off('hit')
    live2dModel = null
  }
  if (pixiApp) {
    pixiApp.destroy(true, { children: true, texture: true, baseTexture: true })
    pixiApp = null
  }
})

/** onCanvasMouseMove updates Live2D eye/body tracking using canvas-relative coordinates. */
function onCanvasMouseMove(e) {
  if (!live2dModel || !canvasRef.value || isDragging) return
  const rect = canvasRef.value.getBoundingClientRect()
  live2dModel.focus(e.clientX - rect.left, e.clientY - rect.top)
}

/** onCanvasClick triggers a tap interaction on the Live2D model and emits click. */
function onCanvasClick(e) {
  if (isDragging || !live2dModel || !canvasRef.value) return
  const rect = canvasRef.value.getBoundingClientRect()
  live2dModel.tap(e.clientX - rect.left, e.clientY - rect.top)
  emit('click')
}

/** onMouseDown starts drag tracking on left mouse button press. */
function onMouseDown(e) {
  if (e.button !== 0) 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)
  // If the user drags out of the window, mouseup never fires; treat window
  // blur as an implicit mouseup so we don't leave the listeners attached.
  window.addEventListener('blur', onMouseUp)
}

/** onMouseMove updates the pet position during drag. */
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 }
}

/** onMouseUp finalizes drag or fires click event, then persists position. */
async function onMouseUp(e) {
  window.removeEventListener('mousemove', onMouseMove)
  window.removeEventListener('mouseup', onMouseUp)
  window.removeEventListener('blur', onMouseUp)
  try {
    // A blur event fires this handler with no positional data, so only treat
    // it as a click when we actually have a MouseEvent and the user didn't drag.
    if (!isDragging && e && typeof e.clientX === 'number') {
      onCanvasClick(e)
    } else if (isDragging) {
      await SaveBallPosition(Math.round(pos.value.x), Math.round(pos.value.y), sw.value, sh.value)
    }
  } catch (err) {
    console.error('Failed to save pet position:', err)
  } finally {
    dragStart = null
    isDragging = false
  }
}
</script>

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

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