import { useEffect, useMemo, useRef, useState } from 'react'
import * as THREE from 'three'
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
import {
  ACTION_MODEL_ASSETS,
  ACTION_MODEL_CATEGORIES,
  ACTION_WEAPON_ASSETS,
  type ActionModelAsset,
  type ActionModelAssetCategory,
  type ActionWeaponAsset,
} from '../actionModelAssets'
import {
  addBirdMesh,
  addBoarMesh,
  addCyberDragonMesh,
} from '../action3dCombatRender'

type ViewerRuntime = {
  camera: THREE.PerspectiveCamera
  clips: THREE.AnimationClip[]
  controls: {
    dragging: boolean
    lastX: number
    yaw: number
  }
  currentAction: THREE.AnimationAction | null
  grid: THREE.GridHelper
  mixer: THREE.AnimationMixer | null
  renderer: THREE.WebGLRenderer
  root: THREE.Group
  scene: THREE.Scene
}

const loader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder)
const colladaLoader = new ColladaLoader()
const mtlLoader = new MTLLoader()

export function ActionModelViewer() {
  const mountRef = useRef<HTMLDivElement | null>(null)
  const runtimeRef = useRef<ViewerRuntime | null>(null)
  const autoRotateRef = useRef(true)
  const speedRef = useRef(1)
  const [category, setCategory] = useState<ActionModelAssetCategory>('claudecraft-players')
  const filteredAssets = useMemo(
    () => ACTION_MODEL_ASSETS.filter((asset) => asset.category === category),
    [category],
  )
  const [assetId, setAssetId] = useState(() => filteredAssets[0]?.id ?? ACTION_MODEL_ASSETS[0].id)
  const selectedAssetId = filteredAssets.some((asset) => asset.id === assetId)
    ? assetId
    : filteredAssets[0]?.id ?? ACTION_MODEL_ASSETS[0].id
  const [animationName, setAnimationName] = useState('')
  const [animationNames, setAnimationNames] = useState<string[]>([])
  const [autoRotate, setAutoRotate] = useState(true)
  const [speed, setSpeed] = useState(1)
  const [status, setStatus] = useState('Ready')
  const [weaponId, setWeaponId] = useState('none')
  const selectedAsset = ACTION_MODEL_ASSETS.find((asset) => asset.id === selectedAssetId) ?? ACTION_MODEL_ASSETS[0]
  const selectedWeapon = ACTION_WEAPON_ASSETS.find((weapon) => weapon.id === weaponId) ?? ACTION_WEAPON_ASSETS[0]

  useEffect(() => {
    autoRotateRef.current = autoRotate
  }, [autoRotate])

  useEffect(() => {
    speedRef.current = speed
  }, [speed])

  useEffect(() => {
    const mount = mountRef.current
    if (!mount) return

    const scene = new THREE.Scene()
    scene.background = new THREE.Color(0x10151a)
    scene.fog = new THREE.Fog(0x10151a, 12, 28)

    const camera = new THREE.PerspectiveCamera(35, 1, 0.05, 80)
    camera.position.set(0, 2.1, 4.2)

    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' })
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.8))
    renderer.shadowMap.enabled = true
    renderer.shadowMap.type = THREE.PCFSoftShadowMap
    mount.appendChild(renderer.domElement)

    const ambient = new THREE.HemisphereLight(0xf6f0de, 0x202937, 1.6)
    scene.add(ambient)

    const keyLight = new THREE.DirectionalLight(0xffedbd, 3.1)
    keyLight.position.set(-4, 7, 5)
    keyLight.castShadow = true
    scene.add(keyLight)

    const rimLight = new THREE.DirectionalLight(0x8ec7ff, 1.1)
    rimLight.position.set(4, 3, -5)
    scene.add(rimLight)

    const ground = new THREE.Mesh(
      new THREE.CircleGeometry(3.4, 48),
      new THREE.MeshStandardMaterial({ color: 0x243126, roughness: 0.9 }),
    )
    ground.rotation.x = -Math.PI / 2
    ground.receiveShadow = true
    scene.add(ground)

    const grid = new THREE.GridHelper(7, 14, 0x566044, 0x3b4436)
    grid.position.y = 0.012
    scene.add(grid)

    const root = new THREE.Group()
    scene.add(root)

    const runtime: ViewerRuntime = {
      camera,
      clips: [],
      controls: { dragging: false, lastX: 0, yaw: 0 },
      currentAction: null,
      grid,
      mixer: null,
      renderer,
      root,
      scene,
    }
    runtimeRef.current = runtime

    const resize = () => {
      const rect = mount.getBoundingClientRect()
      const width = Math.max(1, rect.width)
      const height = Math.max(1, rect.height)
      renderer.setSize(width, height, false)
      camera.aspect = width / height
      camera.updateProjectionMatrix()
    }
    const observer = new ResizeObserver(resize)
    observer.observe(mount)
    resize()

    const handlePointerDown = (event: PointerEvent) => {
      renderer.domElement.setPointerCapture(event.pointerId)
      runtime.controls.dragging = true
      runtime.controls.lastX = event.clientX
    }
    const handlePointerMove = (event: PointerEvent) => {
      if (!runtime.controls.dragging) return
      runtime.controls.yaw += (event.clientX - runtime.controls.lastX) * 0.012
      runtime.controls.lastX = event.clientX
    }
    const handlePointerUp = (event: PointerEvent) => {
      renderer.domElement.releasePointerCapture(event.pointerId)
      runtime.controls.dragging = false
    }
    renderer.domElement.addEventListener('pointerdown', handlePointerDown)
    renderer.domElement.addEventListener('pointermove', handlePointerMove)
    renderer.domElement.addEventListener('pointerup', handlePointerUp)

    const clock = new THREE.Clock()
    let frameId = 0
    const animate = () => {
      const delta = Math.min(clock.getDelta(), 0.05)
      if (autoRotateRef.current && !runtime.controls.dragging) runtime.controls.yaw += delta * 0.55
      root.rotation.y = runtime.controls.yaw
      if (runtime.mixer) runtime.mixer.update(delta * speedRef.current)
      renderer.render(scene, camera)
      frameId = requestAnimationFrame(animate)
    }
    animate()

    return () => {
      cancelAnimationFrame(frameId)
      observer.disconnect()
      renderer.domElement.removeEventListener('pointerdown', handlePointerDown)
      renderer.domElement.removeEventListener('pointermove', handlePointerMove)
      renderer.domElement.removeEventListener('pointerup', handlePointerUp)
      disposeObject(root)
      scene.traverse((object) => {
        if (!(object instanceof THREE.Mesh)) return
        object.geometry.dispose()
        disposeMaterial(object.material)
      })
      renderer.dispose()
      renderer.domElement.remove()
      runtimeRef.current = null
    }
  }, [])

  useEffect(() => {
    let cancelled = false
    const runtime = runtimeRef.current
    if (!runtime) return
    const activeRuntime = runtime

    setStatus('Loading model')
    setAnimationNames([])
    setAnimationName('')
    activeRuntime.currentAction = null
    activeRuntime.clips = []
    activeRuntime.mixer = null
    clearGroup(activeRuntime.root)

    async function load() {
      const model = await createViewerModel(selectedAsset, selectedWeapon)
      if (cancelled) {
        disposeObject(model.object)
        return
      }

      activeRuntime.root.add(model.object)
      fitObjectToViewer(model.object, activeRuntime.camera)
      activeRuntime.controls.yaw = 0

      if (model.animations.length > 0) {
        activeRuntime.clips = model.animations
        activeRuntime.mixer = new THREE.AnimationMixer(model.object)
        const defaultName = selectedAsset.type === 'file'
          ? selectedAsset.defaultAnimation
          : undefined
        const nextAnimation = pickAnimation(model.animations, defaultName)
        const action = activeRuntime.mixer.clipAction(nextAnimation)
        action.play()
        activeRuntime.currentAction = action
        setAnimationNames(model.animations.map((clip) => clip.name))
        setAnimationName(nextAnimation.name)
      }

      setStatus(model.animations.length > 0 ? `${model.animations.length} animations` : 'Static model')
    }

    load().catch((error) => {
      if (!cancelled) setStatus(error instanceof Error ? error.message : 'Model failed to load')
    })

    return () => {
      cancelled = true
    }
  }, [selectedAsset, selectedWeapon])

  useEffect(() => {
    const runtime = runtimeRef.current
    if (!runtime?.mixer || !animationName) return
    const clip = runtime.clips.find((candidate) => candidate.name === animationName)
    if (!clip) return

    const nextAction = runtime.mixer.clipAction(clip)
    if (runtime.currentAction === nextAction) return
    nextAction.reset().play()
    if (runtime.currentAction) runtime.currentAction.crossFadeTo(nextAction, 0.14, false)
    runtime.currentAction = nextAction
  }, [animationName])

  return (
    <section className="model-viewer-panel">
      <div className="model-viewer-toolbar">
        <label>
          <span>Set</span>
          <select value={category} onChange={(event) => setCategory(event.target.value as ActionModelAssetCategory)}>
            {ACTION_MODEL_CATEGORIES.map((item) => (
              <option key={item.id} value={item.id}>{item.label}</option>
            ))}
          </select>
        </label>
        <label>
          <span>Model</span>
          <select value={selectedAssetId} onChange={(event) => setAssetId(event.target.value)}>
            {filteredAssets.map((asset) => (
              <option key={asset.id} value={asset.id}>{asset.label}</option>
            ))}
          </select>
        </label>
        <label>
          <span>Animation</span>
          <select
            disabled={animationNames.length === 0}
            value={animationName}
            onChange={(event) => setAnimationName(event.target.value)}
          >
            {animationNames.length === 0 && <option value="">None</option>}
            {animationNames.map((name) => (
              <option key={name} value={name}>{name}</option>
            ))}
          </select>
        </label>
        <label>
          <span>Weapon</span>
          <select value={weaponId} onChange={(event) => setWeaponId(event.target.value)}>
            {ACTION_WEAPON_ASSETS.map((weapon) => (
              <option key={weapon.id} value={weapon.id}>{weapon.label}</option>
            ))}
          </select>
        </label>
        <label>
          <span>Speed</span>
          <input
            max="1.8"
            min="0"
            step="0.1"
            type="range"
            value={speed}
            onChange={(event) => setSpeed(Number(event.target.value))}
          />
        </label>
        <button
          aria-pressed={autoRotate}
          className={autoRotate ? 'active' : ''}
          onClick={() => setAutoRotate((current) => !current)}
          type="button"
        >
          Spin
        </button>
      </div>

      <div className="model-viewer-stage" ref={mountRef} />

      <div className="model-viewer-meta">
        <strong>{selectedAsset.label}</strong>
        <span>{selectedAsset.source}</span>
        <span>{selectedAsset.type === 'file' ? selectedAsset.url : selectedAsset.enemyKind}</span>
        <span>{selectedWeapon.id === 'none' ? status : `${status} / ${selectedWeapon.label}`}</span>
      </div>
    </section>
  )
}

async function createViewerModel(
  asset: ActionModelAsset,
  weapon: ActionWeaponAsset,
): Promise<{ animations: THREE.AnimationClip[]; object: THREE.Object3D }> {
  if (asset.type === 'procedural') {
    const group = new THREE.Group()
    if (asset.enemyKind === 'bulldrome') addBoarMesh(group, { kind: 'bulldrome' })
    else if (asset.enemyKind === 'bullfango') addBoarMesh(group, { kind: 'bullfango' })
    else if (asset.enemyKind === 'yian-kut-ku') addBirdMesh(group, 'yian-kut-ku')
    else if (asset.enemyKind === 'cyber-dragon') addCyberDragonMesh(group)
    else addBirdMesh(group, 'bird')
    if (weapon.id !== 'none') await addPreviewWeapon(group, weapon, false)
    return { animations: [], object: group }
  }

  const loadedModel = await loadFileModel(asset)
  const object = new THREE.Group()
  object.add(loadedModel.scene)
  if (asset.materialVariant) applyMaterialVariant(loadedModel.scene, asset.materialVariant)
  object.animations = loadedModel.animations
  object.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.castShadow = true
    child.frustumCulled = false
    child.receiveShadow = true
  })
  if (weapon.id !== 'none') await addPreviewWeapon(object, weapon, true)
  return { animations: loadedModel.animations, object }
}

async function loadFileModel(asset: Extract<ActionModelAsset, { type: 'file' }>) {
  if (asset.format === 'dae') {
    const collada = await colladaLoader.loadAsync(asset.url)
    if (!collada) throw new Error('Collada model failed to load')
    return { animations: collada.scene.animations, scene: collada.scene }
  }

  if (asset.format === 'obj') {
    const activeObjLoader = new OBJLoader()
    if (asset.mtlUrl) {
      const materials = await mtlLoader.loadAsync(asset.mtlUrl)
      materials.preload()
      activeObjLoader.setMaterials(materials)
    }
    const scene = await activeObjLoader.loadAsync(asset.url)
    return { animations: [], scene }
  }

  const gltf = await loader.loadAsync(asset.url)
  return { animations: gltf.animations, scene: gltf.scene }
}

function applyMaterialVariant(model: THREE.Object3D, variant: NonNullable<Extract<ActionModelAsset, { type: 'file' }>['materialVariant']>) {
  const variantColor = {
    black: 0x221a1c,
    emerald: 0x2f8f5f,
    frost: 0x9ed8ff,
    red: 0xa73524,
  }[variant]
  const emissive = {
    black: 0x090202,
    emerald: 0x06391e,
    frost: 0x17445f,
    red: 0x3f0b05,
  }[variant]
  model.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    const sourceMaterial = Array.isArray(child.material) ? child.material[0] : child.material
    const sourceMap = sourceMaterial instanceof THREE.MeshStandardMaterial ? sourceMaterial.map : null
    child.material = new THREE.MeshStandardMaterial({
      color: variantColor,
      emissive,
      emissiveIntensity: variant === 'frost' ? 0.12 : 0.07,
      map: sourceMap,
      metalness: 0,
      roughness: variant === 'frost' ? 0.52 : 0.68,
      side: THREE.DoubleSide,
    })
  })
}

async function addPreviewWeapon(model: THREE.Object3D, weapon: ActionWeaponAsset, allowHandAttach: boolean) {
  const gltf = await loader.loadAsync(weapon.url)
  const weaponModel = gltf.scene
  weaponModel.name = 'previewWeapon'
  weaponModel.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.castShadow = true
    child.frustumCulled = false
    child.receiveShadow = true
  })

  const handSlot = allowHandAttach ? findPreferredHandSlot(model) : null
  if (handSlot) {
    handSlot.add(weaponModel)
    return
  }

  const modelBox = new THREE.Box3().setFromObject(model)
  const modelSize = new THREE.Vector3()
  modelBox.getSize(modelSize)
  const modelCenter = new THREE.Vector3()
  modelBox.getCenter(modelCenter)

  normalizeLooseWeapon(weaponModel, Math.max(0.8, Math.min(2.1, modelSize.y * 0.45)))
  weaponModel.rotation.z = -0.55

  const weaponBox = new THREE.Box3().setFromObject(weaponModel)
  weaponModel.position.x -= weaponBox.min.x
  weaponModel.position.y -= weaponBox.min.y
  weaponModel.position.z -= (weaponBox.min.z + weaponBox.max.z) * 0.5

  const holder = new THREE.Group()
  holder.add(weaponModel)
  holder.position.set(
    modelBox.max.x + Math.max(1.4, modelSize.x * 0.55),
    modelBox.min.y,
    modelBox.max.z + Math.max(0.7, modelSize.z * 0.1),
  )
  model.add(holder)
}

function findPreferredHandSlot(model: THREE.Object3D) {
  const candidates: THREE.Object3D[] = []
  model.traverse((object) => {
    const name = normalizeNodeName(object.name)
    if (
      name === 'handslotr'
      || name === 'handr'
      || name === 'righthand'
      || name.endsWith('handslotr')
      || name.endsWith('handr')
    ) {
      candidates.unshift(object)
      return
    }
    if (
      name === 'handslotl'
      || name === 'handl'
      || name === 'lefthand'
      || name.endsWith('handslotl')
      || name.endsWith('handl')
    ) {
      candidates.push(object)
    }
  })
  return candidates[0] ?? null
}

function normalizeNodeName(name: string) {
  return name.toLowerCase().replace(/[^a-z0-9]/g, '')
}

function normalizeLooseWeapon(weapon: THREE.Object3D, targetLength: number) {
  const box = new THREE.Box3().setFromObject(weapon)
  const size = new THREE.Vector3()
  const center = new THREE.Vector3()
  box.getSize(size)
  box.getCenter(center)
  const maxDimension = Math.max(size.x, size.y, size.z, 0.001)
  weapon.scale.multiplyScalar(targetLength / maxDimension)
  weapon.position.sub(center.multiplyScalar(targetLength / maxDimension))
}

function pickAnimation(clips: THREE.AnimationClip[], preferred?: string) {
  return clips.find((clip) => clip.name === preferred)
    ?? clips.find((clip) => clip.name.toLowerCase().includes('idle'))
    ?? clips[0]
}

function fitObjectToViewer(object: THREE.Object3D, camera: THREE.PerspectiveCamera) {
  const box = new THREE.Box3().setFromObject(object)
  const size = new THREE.Vector3()
  const center = new THREE.Vector3()
  box.getSize(size)
  box.getCenter(center)
  const footprint = Math.max(size.x, size.z, 0.001)
  const scale = Math.min(2.05 / Math.max(size.y, 0.001), 3.85 / footprint)
  object.scale.multiplyScalar(scale)
  object.position.sub(center.multiplyScalar(scale))

  const scaledBox = new THREE.Box3().setFromObject(object)
  const scaledCenter = new THREE.Vector3()
  scaledBox.getCenter(scaledCenter)
  const scaledSize = new THREE.Vector3()
  scaledBox.getSize(scaledSize)
  object.position.y -= scaledBox.min.y
  camera.position.set(
    0,
    Math.max(1.35, scaledSize.y * 0.58),
    Math.max(4.8, scaledSize.z * 1.25 + 3.2, scaledSize.x * 0.7 + 3.2),
  )
  camera.lookAt(scaledCenter.x, Math.max(0.8, scaledSize.y * 0.5), scaledCenter.z)
}

function clearGroup(group: THREE.Group) {
  for (const child of [...group.children]) {
    group.remove(child)
    disposeObject(child)
  }
}

function disposeObject(object: THREE.Object3D) {
  object.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.geometry.dispose()
    disposeMaterial(child.material)
  })
}

function disposeMaterial(material: THREE.Material | THREE.Material[]) {
  const materials = Array.isArray(material) ? material : [material]
  for (const item of materials) item.dispose()
}
