<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as OV from 'online-3d-viewer'
import * as THREE from 'three'
import { useViewerStore } from '@/stores/viewer'
import { useModelsStore } from '@/stores/models'
import { useThemeStore } from '@/stores/theme'
import { getModel, getModelLodUrls, fetchWithProgress } from '@/api/client'
import { getPartsTreeService } from '@/services/partsTreeService'
import { getClippingService, type Axis } from '@/services/clippingService'
import { getRenderService } from '@/services/renderService'
import { captureViewerScreenshot, uploadThumbnail } from '@/services/screenshotService'
import ContextMenu from './ContextMenu.vue'
import ViewCube from './ViewCube.vue'

const props = defineProps<{
  modelId: string
}>()

const containerRef = ref<HTMLDivElement | null>(null)
const viewerStore = useViewerStore()
const modelsStore = useModelsStore()
const themeStore = useThemeStore()
let viewer: OV.EmbeddedViewer | null = null

// Theme-aware background colors
const LIGHT_BG = { r: 248, g: 249, b: 252 }  // #f8f9fc
const DARK_BG = { r: 22, g: 24, b: 31 }      // #16181f
let loadAbortController: AbortController | null = null
let loadCheckInterval: ReturnType<typeof setInterval> | null = null
let loadTimeoutId: ReturnType<typeof setTimeout> | null = null

// Maximum load timeout: 5 minutes for large files
const MAX_LOAD_TIMEOUT = 5 * 60 * 1000

onMounted(() => {
  if (!containerRef.value) return

  // Patch canvas.getContext to enable stencil buffer for WebGL
  // This is needed for section cap rendering using stencil buffer technique
  const originalGetContext = HTMLCanvasElement.prototype.getContext
  HTMLCanvasElement.prototype.getContext = function(
    this: HTMLCanvasElement,
    contextType: string,
    contextAttributes?: WebGLContextAttributes
  ) {
    if (contextType === 'webgl' || contextType === 'webgl2') {
      const attrs = { ...contextAttributes, stencil: true }
      return originalGetContext.call(this, contextType, attrs)
    }
    return originalGetContext.call(this, contextType, contextAttributes)
  } as typeof HTMLCanvasElement.prototype.getContext

  // Initialize Online3DViewer with theme-aware background
  const bg = themeStore.isDark ? DARK_BG : LIGHT_BG
  viewer = new OV.EmbeddedViewer(containerRef.value, {
    backgroundColor: new OV.RGBAColor(bg.r, bg.g, bg.b, 255),
    defaultColor: new OV.RGBColor(180, 180, 180),
    edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1),
  })

  // Restore original getContext after viewer is created
  HTMLCanvasElement.prototype.getContext = originalGetContext

  viewerStore.setViewer(viewer)

  // Enable free orbit mode for unlimited rotation in all directions
  const threeViewer = viewer.GetViewer()
  if (threeViewer) {
    const navigation = (threeViewer as unknown as { navigation?: { SetNavigationMode: (mode: number) => void } }).navigation
    if (navigation) {
      navigation.SetNavigationMode(OV.NavigationMode.FreeOrbit)
    }
  }

  // Setup WebGL context loss handling after viewer is created
  nextTick(() => {
    setupWebGLErrorHandling()
    setupPlaneDragEvents()
  })

  // Load initial model
  loadModel(props.modelId)
})

onUnmounted(() => {
  cancelLoading()
  cleanupPlaneDragEvents()
  if (viewer) {
    viewer.Destroy()
    viewer = null
  }
  viewerStore.setViewer(null)
  viewerStore.resetFeatures()
})

watch(() => props.modelId, (newId) => {
  if (newId) {
    loadModel(newId)
  }
})

// Watch for theme changes to update viewer background
watch(() => themeStore.resolvedTheme, (theme) => {
  if (!viewer) return

  const bg = theme === 'dark' ? DARK_BG : LIGHT_BG
  const threeViewer = viewer.GetViewer()
  if (threeViewer) {
    const renderer = (threeViewer as unknown as { renderer?: THREE.WebGLRenderer }).renderer
    if (renderer) {
      const hexColor = (bg.r << 16) | (bg.g << 8) | bg.b
      renderer.setClearColor(hexColor)
      viewerStore.forceRender()
    }
  }
})

/**
 * Setup WebGL context loss error handling
 */
function setupWebGLErrorHandling() {
  const canvas = containerRef.value?.querySelector('canvas')
  if (canvas) {
    canvas.addEventListener('webglcontextlost', (e) => {
      e.preventDefault()
      console.error('WebGL context lost')
      cancelLoading()
      viewerStore.setError('WebGL 内存不足，模型过大无法加载。请尝试加载较小的模型。')
      viewerStore.setLoading(false)
    })

    canvas.addEventListener('webglcontextrestored', () => {
      console.log('WebGL context restored')
    })
  }
}

/**
 * Setup industrial lighting for better model visibility from all angles
 */
function setupIndustrialLighting() {
  const scene = viewerStore.scene
  if (!scene) return

  // Remove existing industrial lights (for model reload)
  scene.children
    .filter(child => child.name.startsWith('__industrial_'))
    .forEach(light => scene.remove(light))

  // 1. Ambient light - base brightness
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
  ambientLight.name = '__industrial_ambient__'
  scene.add(ambientLight)

  // 2. Hemisphere light - sky/ground environment
  const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.4)
  hemiLight.name = '__industrial_hemi__'
  hemiLight.position.set(0, 20, 0)
  scene.add(hemiLight)

  // 3. Multi-directional fill lights - ensure all angles are lit
  const positions = [
    [1, 1, 1], [-1, 1, 1], [1, 1, -1], [-1, 1, -1],  // Top corners
    [1, -1, 1], [-1, -1, 1]  // Bottom fill
  ]
  positions.forEach((pos, i) => {
    const light = new THREE.DirectionalLight(0xffffff, 0.3)
    light.name = `__industrial_dir_${i}__`
    light.position.set(pos[0] * 10, pos[1] * 10, pos[2] * 10)
    scene.add(light)
  })
}

/**
 * Auto-capture thumbnail after model loads
 * Captures the current view and uploads to server
 */
async function autoCaptureThumbnail(modelId: string) {
  // Wait for render to complete (2 frames + delay for all effects)
  await new Promise(resolve => requestAnimationFrame(resolve))
  await new Promise(resolve => requestAnimationFrame(resolve))
  await new Promise(resolve => setTimeout(resolve, 800))

  const renderer = viewerStore.renderer
  const scene = viewerStore.scene
  const camera = viewerStore.camera

  if (!renderer || !scene || !camera) {
    console.warn('Auto-capture: Viewer not ready')
    return
  }

  try {
    // Force a final render before capture
    viewerStore.forceRender()
    await new Promise(resolve => requestAnimationFrame(resolve))

    const blob = await captureViewerScreenshot(renderer, scene, camera, 512)
    const thumbnailUrl = await uploadThumbnail(modelId, blob)

    // Update store to show new thumbnail immediately with cache-busting
    const model = modelsStore.models.find(m => m.id === modelId)
    if (model) {
      // Add timestamp to force browser to fetch fresh image instead of cached old one
      const cacheBustedUrl = `${thumbnailUrl}?t=${Date.now()}`
      modelsStore.updateModelInStore({ ...model, thumbnail_url: cacheBustedUrl })
    }
    console.log('Auto-captured thumbnail for model:', modelId)
  } catch (error) {
    // Log detailed error for debugging
    if (error instanceof Error) {
      console.warn('Failed to auto-capture thumbnail:', error.message, error.stack)
    } else {
      console.warn('Failed to auto-capture thumbnail:', String(error))
    }
  }
}

/**
 * Cancel ongoing loading
 */
function cancelLoading() {
  if (loadAbortController) {
    loadAbortController.abort()
    loadAbortController = null
  }
  if (loadCheckInterval) {
    clearInterval(loadCheckInterval)
    loadCheckInterval = null
  }
  if (loadTimeoutId) {
    clearTimeout(loadTimeoutId)
    loadTimeoutId = null
  }
}

/**
 * Select optimal LOD level based on face count
 * - Small models (<100k faces): LOD0 (full quality)
 * - Medium models (100k-500k): LOD1 (50% quality)
 * - Large models (>500k): LOD2 (25% quality)
 */
function selectOptimalLod(faces: number): number {
  if (faces < 100000) return 0
  if (faces < 500000) return 1
  return 2
}

async function loadModel(modelId: string) {
  if (!viewer) return

  // Cancel any previous loading
  cancelLoading()

  viewerStore.setLoading(true)
  viewerStore.setError(null)
  viewerStore.setLoadingProgress(0, 'downloading')

  // Clear color maps BEFORE resetFeatures to avoid stale mesh references
  const partsService = getPartsTreeService()
  partsService.clearColorMaps()

  // Invalidate cached edges for new model
  getRenderService().invalidateEdges()

  viewerStore.resetFeatures()

  // Create abort controller for this load
  loadAbortController = new AbortController()

  try {
    // Get model info to determine face count for LOD selection
    const modelInfo = await getModel(modelId)
    const metadata = modelInfo.metadata as Record<string, unknown> | null
    const faceCount = (metadata?.faces as number) || 0

    // Get LOD URLs
    let lodUrls: Record<string, string>
    try {
      lodUrls = await getModelLodUrls(modelId)
    } catch {
      // Fallback to legacy model_url if LOD not available
      lodUrls = { lod0: modelInfo.model_url || '' }
    }

    // Select optimal LOD based on face count
    const optimalLod = selectOptimalLod(faceCount)
    const lodKey = `lod${optimalLod}`
    const url = lodUrls[lodKey] || lodUrls.lod0 || modelInfo.model_url

    if (!url) {
      throw new Error('No model URL available')
    }

    console.log(`Loading model with ${faceCount} faces, using LOD${optimalLod}`)
    viewerStore.setCurrentModelUrl(url)

    // Extract filename from URL (before query params)
    const urlPath = new URL(url).pathname
    const filename = urlPath.split('/').pop() || 'model.glb'

    // Download with progress tracking
    const blob = await fetchWithProgress(
      url,
      (progress) => {
        viewerStore.setLoadingProgress(progress, 'downloading')
      },
      loadAbortController.signal
    )

    // Switch to parsing stage
    viewerStore.setLoadingProgress(100, 'parsing')

    // Create File object with proper filename (online-3d-viewer needs extension)
    const file = new File([blob], filename, { type: blob.type })

    // Load model from File object
    viewer.LoadModelFromFileList([file])

    // Wait for model to load (Online3DViewer doesn't have a proper callback)
    // We'll use a polling approach - check both model metadata AND mesh in scene
    loadCheckInterval = setInterval(() => {
      const model = viewer?.GetModel()
      if (model) {
        // Also check if model mesh is actually in the scene
        const threeViewer = viewer?.GetViewer()
        const scene = (threeViewer as unknown as { scene?: { children: Array<{ name: string; type: string }> } })?.scene
        if (scene) {
          // Check scene has non-system objects (actual model mesh)
          const hasModelInScene = scene.children.some(child =>
            !child.name.startsWith('__') &&
            !child.type.includes('Light') &&
            !child.type.includes('Camera') &&
            !child.type.includes('Helper')
          )

          if (hasModelInScene) {
            if (loadCheckInterval) {
              clearInterval(loadCheckInterval)
              loadCheckInterval = null
            }
            if (loadTimeoutId) {
              clearTimeout(loadTimeoutId)
              loadTimeoutId = null
            }
            viewerStore.setModel(model)
            setupIndustrialLighting()

            // Compute BVH for all meshes for fast raycasting (O(log n) instead of O(n))
            // Wrapped in try-catch to prevent errors from blocking loading
            try {
              const threeScene = viewerStore.scene
              if (threeScene) {
                threeScene.traverse((object: THREE.Object3D) => {
                  if (object.type === 'Mesh' || (object as THREE.Mesh).isMesh) {
                    const mesh = object as THREE.Mesh
                    const geom = mesh.geometry as THREE.BufferGeometry & { boundsTree?: unknown; computeBoundsTree?: () => void }
                    if (geom && !geom.boundsTree && typeof geom.computeBoundsTree === 'function') {
                      geom.computeBoundsTree()
                    }
                  }
                })
              }
            } catch (e) {
              console.warn('BVH computation failed, using standard raycasting:', e)
            }

            viewerStore.setLoading(false)
            // Cache mesh data for section cap worker (avoids blocking on drag end)
            getClippingService().updateMeshDataCache()

            // Set scene reference in partsService BEFORE applying auto-coloring
            // This fixes the race condition where applyAutoColors runs before buildTree sets the scene
            const threeSceneForColors = viewerStore.scene
            if (threeSceneForColors) {
              partsService.setScene(threeSceneForColors)
            }

            // Apply auto-coloring if enabled (global setting persists across models)
            if (viewerStore.renderSettings.autoColorEnabled) {
              partsService.applyAutoColors()
            }

            // Apply edges if enabled (global setting persists across models)
            if (viewerStore.renderSettings.edgesEnabled) {
              getRenderService().setEdgesEnabled(true)
            }

            viewerStore.forceRender()

            // Auto-capture thumbnail after model loads with all effects applied
            autoCaptureThumbnail(modelId)
          }
        }
      }
    }, 100)

    // Timeout after MAX_LOAD_TIMEOUT (5 minutes)
    loadTimeoutId = setTimeout(() => {
      if (loadCheckInterval) {
        clearInterval(loadCheckInterval)
        loadCheckInterval = null
      }
      if (viewerStore.isLoading) {
        viewerStore.setLoading(false)
        viewerStore.setError('模型加载超时，文件可能过大或网络较慢。')
      }
    }, MAX_LOAD_TIMEOUT)
  } catch (error) {
    viewerStore.setLoading(false)
    if (error instanceof Error) {
      if (error.name === 'AbortError') {
        // Loading was cancelled, don't show error
        return
      }
      viewerStore.setError(error.message)
    } else {
      viewerStore.setError('加载模型失败')
    }
  }
}

// ==================== Plane Drag Interaction ====================

// Track if we're in rotation mode (prevents plane detection during rotation drag)
let isRotating = false
// Track pending finalizeDrag callback (to cancel if new interaction starts)
let pendingFinalizeId: number | null = null
// Track ALL axes that need finalizeDrag (persists across interactions until completed)
let pendingFinalizeAxes: Set<Axis> = new Set()
// Cached camera reference for light updates
let cachedCamera: THREE.Camera | null = null
// Cached canvas reference for accurate mouse coordinate calculation
let cachedCanvas: HTMLCanvasElement | null = null
// Track mouse down position for click vs drag detection
let mouseDownPos: { x: number; y: number } | null = null
// Raycaster for click detection - firstHitOnly for better performance
const raycaster = new THREE.Raycaster()
raycaster.firstHitOnly = true

/**
 * Get normalized mouse coordinates (-1 to 1)
 * Uses the canvas bounding rect for accurate coordinate calculation
 */
function getNormalizedMouse(event: MouseEvent): THREE.Vector2 {
  // Use canvas rect for accurate coordinates (canvas may differ from container)
  const element = cachedCanvas || containerRef.value!
  const rect = element.getBoundingClientRect()
  return new THREE.Vector2(
    ((event.clientX - rect.left) / rect.width) * 2 - 1,
    -((event.clientY - rect.top) / rect.height) * 2 + 1
  )
}

/**
 * Get OrbitControls from viewer
 */
function getControls() {
  const threeViewer = viewer?.GetViewer()
  return (threeViewer as unknown as { navigation?: { camera?: { orbitEnabled: boolean } } })?.navigation?.camera
}

/**
 * Handle mouse down - check for plane intersection
 * Locks interaction mode: either plane drag or rotation (no switching mid-drag)
 */
function handleMouseDown(event: MouseEvent) {
  if (!containerRef.value) return

  // Record mouse down position for click detection
  mouseDownPos = { x: event.clientX, y: event.clientY }

  // Cancel any pending finalizeDrag from previous interaction
  if (pendingFinalizeId !== null) {
    clearTimeout(pendingFinalizeId)
    pendingFinalizeId = null
  }

  const service = getClippingService()
  if (!service.isInitialized()) {
    isRotating = true  // Default to rotation if service not ready
    return
  }

  const mouse = getNormalizedMouse(event)
  const hitAxis = service.checkPlaneIntersection(mouse)

  if (hitAxis) {
    // Plane drag mode
    isRotating = false
    service.startDrag(hitAxis)
    // Disable orbit controls during drag
    const controls = getControls()
    if (controls) {
      controls.orbitEnabled = false
    }
    containerRef.value.style.cursor = 'grabbing'
    event.preventDefault()
    event.stopPropagation()
  } else {
    // Rotation mode - lock this mode for the entire drag
    isRotating = true
  }
}

/**
 * Handle mouse move - update drag or hover state
 * If rotating, skip all plane detection
 */
function handleMouseMove(event: MouseEvent) {
  if (!containerRef.value) return

  // If rotating, update plane render order for correct occlusion
  // and cancel any pending section cap generation (debounce)
  if (isRotating) {
    getClippingService().updatePlaneRenderOrder()
    // Update camera light to follow camera direction
    if (cachedCamera) {
      getRenderService().updateCameraLight(cachedCamera)
    }
    if (pendingFinalizeId !== null) {
      clearTimeout(pendingFinalizeId)
      pendingFinalizeId = null
    }
    return
  }

  const service = getClippingService()
  if (!service.isInitialized()) return

  const mouse = getNormalizedMouse(event)

  if (service.isDragging()) {
    // Calculate new position during drag
    const newPercent = service.drag(mouse)
    if (newPercent !== null) {
      const axis = service.getDragAxis()!
      // Call setPlanePosition directly with isDragging=true for performance
      // This skips expensive cap geometry regeneration during drag
      // Don't update store during drag - this would trigger watcher and regenerate cap
      service.setPlanePosition(axis, newPercent, true)
      // Trigger render to show plane movement and clipping updates
      viewerStore.forceRender()
    }
    containerRef.value.style.cursor = 'grabbing'
  } else {
    // Check for hover state (only when not dragging and not rotating)
    const hoverAxis = service.checkPlaneIntersection(mouse)
    containerRef.value.style.cursor = hoverAxis ? 'grab' : ''
  }
}

/**
 * Handle wheel event - update plane render order and camera light when zooming
 */
function handleWheel() {
  getClippingService().updatePlaneRenderOrder()
  // Update camera light to follow camera position
  if (cachedCamera) {
    getRenderService().updateCameraLight(cachedCamera)
  }
}

/**
 * Handle click for part selection
 * Uses the parts tree to find the correct node for the clicked mesh
 */
function handleClick(event: MouseEvent) {
  if (!containerRef.value || !mouseDownPos) return

  // Check if this was a click (not a drag)
  const dx = event.clientX - mouseDownPos.x
  const dy = event.clientY - mouseDownPos.y
  const distance = Math.sqrt(dx * dx + dy * dy)

  // If moved more than 5 pixels, it's a drag, not a click
  if (distance > 5) return

  const scene = viewerStore.scene
  const camera = viewerStore.camera
  if (!scene || !camera) return

  // Update camera matrices to ensure accurate raycasting after rotation
  camera.updateMatrixWorld()

  const mouse = getNormalizedMouse(event)
  raycaster.setFromCamera(mouse, camera)

  // Raycast against all objects, filtering out system objects
  const intersects = raycaster.intersectObjects(scene.children, true)
    .filter(hit => {
      // Skip system objects (prefixed with '__')
      if (hit.object.name.startsWith('__')) return false
      // Skip non-mesh objects
      if (!hit.object.type.includes('Mesh')) return false
      // Skip hidden objects
      if (!hit.object.visible) return false
      return true
    })

  const partsService = getPartsTreeService()

  if (intersects.length > 0) {
    // Select the clicked mesh directly - simple and reliable
    const clickedMesh = intersects[0].object
    partsService.selectPart(clickedMesh)
    viewerStore.setSelectedPart(clickedMesh.uuid)
    viewerStore.forceRender()
  } else {
    // Click on empty space - clear selection
    partsService.selectPart(null)
    viewerStore.setSelectedPart(null)
    viewerStore.forceRender()
  }
}

/**
 * Handle right-click context menu
 * Shows a context menu with options for the clicked part
 */
function handleContextMenu(event: MouseEvent) {
  event.preventDefault()

  if (!containerRef.value) return

  const scene = viewerStore.scene
  const camera = viewerStore.camera
  if (!scene || !camera) return

  // Update camera matrices to ensure accurate raycasting after rotation
  camera.updateMatrixWorld()

  const mouse = getNormalizedMouse(event)
  raycaster.setFromCamera(mouse, camera)

  // Raycast against all objects, filtering out system objects
  const intersects = raycaster.intersectObjects(scene.children, true)
    .filter(hit => {
      // Skip system objects (prefixed with '__')
      if (hit.object.name.startsWith('__')) return false
      // Skip non-mesh objects
      if (!hit.object.type.includes('Mesh')) return false
      // Skip hidden objects
      if (!hit.object.visible) return false
      return true
    })

  if (intersects.length > 0) {
    // Found a part - show context menu
    const clickedMesh = intersects[0].object
    viewerStore.showContextMenu(event.clientX, event.clientY, clickedMesh.uuid)
  } else {
    // Right-click on empty space - hide context menu if visible
    viewerStore.hideContextMenu()
  }
}

/**
 * Handle mouse up - end drag and reset rotation mode
 */
function handleMouseUp(event: MouseEvent) {
  // Reset cursor IMMEDIATELY for responsive UX
  if (containerRef.value) {
    containerRef.value.style.cursor = ''
  }

  // Update plane render order and camera light after rotation (before resetting isRotating)
  if (isRotating) {
    getClippingService().updatePlaneRenderOrder()
    if (cachedCamera) {
      getRenderService().updateCameraLight(cachedCamera)
    }
  }

  // Handle click for part selection (before resetting state)
  const service = getClippingService()
  if (!service.isDragging()) {
    handleClick(event)
  }

  // Reset rotation mode
  isRotating = false
  mouseDownPos = null

  if (service.isDragging()) {
    const dragAxis = service.getDragAxis()
    service.endDrag()

    // Re-enable orbit controls immediately
    const controls = getControls()
    if (controls) {
      controls.orbitEnabled = true
    }

    // Track this axis for finalization (don't sync store yet - let timeout handle it)
    if (dragAxis) {
      pendingFinalizeAxes.add(dragAxis)
    }
  }

  // Schedule finalizeDrag for ALL pending axes (re-schedules after each interaction)
  // This ensures section cap is generated after ALL interactions complete
  if (pendingFinalizeAxes.size > 0) {
    // Cancel any existing timeout first
    if (pendingFinalizeId !== null) {
      clearTimeout(pendingFinalizeId)
    }

    pendingFinalizeId = window.setTimeout(() => {
      // Finalize ALL pending axes
      pendingFinalizeAxes.forEach(axis => {
        // Get current visual position (from THREE.Plane.constant)
        const currentPercent = service.getPlanePositionPercent(axis)
        // Generate section cap asynchronously
        service.finalizeDrag(axis)
        // Sync store with current visual position
        viewerStore.setCrossSectionPosition(axis, currentPercent)
      })
      pendingFinalizeAxes.clear()
      pendingFinalizeId = null
    }, 500) as unknown as number  // 500ms debounce - wait for user to finish all interactions
  }
}

/**
 * Setup plane drag event listeners
 * Uses capture mode to intercept events before OrbitControls
 */
function setupPlaneDragEvents() {
  const canvas = containerRef.value?.querySelector('canvas')
  if (!canvas) return

  // Cache canvas reference for accurate mouse coordinate calculation
  cachedCanvas = canvas

  // Set camera reference for clipping service and cache for light updates
  const threeViewer = viewer?.GetViewer()
  const camera = (threeViewer as unknown as { camera?: THREE.Camera })?.camera
  if (camera) {
    getClippingService().setCamera(camera)
    cachedCamera = camera
    // Initialize camera light position
    getRenderService().updateCameraLight(camera)
  }

  // Add event listeners with capture mode to intercept before OrbitControls
  canvas.addEventListener('mousedown', handleMouseDown, { capture: true })
  canvas.addEventListener('mousemove', handleMouseMove, { capture: true })
  canvas.addEventListener('mouseup', handleMouseUp, { capture: true })
  canvas.addEventListener('mouseleave', handleMouseUp, { capture: true })
  // Wheel listener for zoom - update plane render order
  canvas.addEventListener('wheel', handleWheel, { passive: true })
  // Context menu listener for right-click (capture mode to intercept before OrbitControls)
  canvas.addEventListener('contextmenu', handleContextMenu, { capture: true })
}

/**
 * Cleanup plane drag event listeners
 */
function cleanupPlaneDragEvents() {
  const canvas = containerRef.value?.querySelector('canvas')
  if (!canvas) return

  canvas.removeEventListener('mousedown', handleMouseDown, { capture: true })
  canvas.removeEventListener('mousemove', handleMouseMove, { capture: true })
  canvas.removeEventListener('mouseup', handleMouseUp, { capture: true })
  canvas.removeEventListener('mouseleave', handleMouseUp, { capture: true })
  canvas.removeEventListener('wheel', handleWheel)
  canvas.removeEventListener('contextmenu', handleContextMenu, { capture: true })

  // Clear cached references
  cachedCanvas = null
}
</script>

<template>
  <div ref="containerRef" class="model-viewer">
    <ContextMenu />
    <ViewCube v-if="viewerStore.model" />
    <div v-if="viewerStore.isLoading" class="loading-indicator">
      <div class="spinner"></div>
      <span class="loading-text">
        {{ viewerStore.loadingStage === 'downloading' ? '下载中' : '解析中' }}...
      </span>
    </div>
    <div v-if="viewerStore.error" class="viewer-overlay error">
      <p>{{ viewerStore.error }}</p>
    </div>
  </div>
</template>

<style scoped>
.model-viewer {
  width: 100%;
  height: 100%;
  position: relative;
}

/* Loading indicator - bottom left corner */
.loading-indicator {
  position: absolute;
  bottom: 20px;
  left: 20px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(8px);
  border-radius: 20px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
  z-index: 10;
}

.loading-indicator .spinner {
  width: 18px;
  height: 18px;
  border-width: 2px;
}

.loading-indicator .loading-text {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0;
  white-space: nowrap;
}

/* Error overlay */
.viewer-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 10;
}

.viewer-overlay p {
  margin-top: 12px;
  color: var(--text-primary);
}

.viewer-overlay.error {
  background: rgba(239, 68, 68, 0.2);
}

.viewer-overlay.error p {
  color: var(--danger-color);
  max-width: 400px;
  text-align: center;
  padding: 20px;
  background: var(--bg-secondary);
  border-radius: 8px;
}
</style>
