import * as THREE from 'three'
import type { BossInput } from './actionCombatTypes'
import type { SpellSlot } from './actionCombatCore'

export const ACTION_ARENA_SCALE = 48
export const ACTION_PLAYER_KEYS = new Set(['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'])
export const ACTION_RENDER_QUALITY = {
  antialias: false,
  maxPixelRatio: 1.1,
  shadows: false,
}
export const CLAUDECRAFT_CAMERA_FOV = 60
const CLAUDECRAFT_CAMERA_PITCH = 0.32
const CLAUDECRAFT_CAMERA_DISTANCE = 12
const CLAUDECRAFT_CAMERA_EYE_HEIGHT = 2
const CLAUDECRAFT_CAMERA_DRAG_SENSITIVITY = 0.006

export type ActionWorldPoint = { x: number, z: number }
export type ActionCameraTarget = { x: number, y: number }
export type ActionArenaProjection = { height: number, width: number }
export type ActionWorldProjector = (x: number, y: number) => ActionWorldPoint
export type ActionCombatCameraController = {
  bindDrag: (element: HTMLElement, options?: {
    isPaused?: () => boolean,
    onPointerDown?: (event: PointerEvent) => void,
  }) => () => void
  reset: (yaw?: number) => void
  yaw: number
}

export type CombatActionQueue = {
  reset: boolean
}

export function readCombatInput(
  keys: Set<string>,
  cameraYaw: number,
  targetId: string | null,
  castSpell: SpellSlot | null,
  actions: CombatActionQueue,
  options?: { fallbackAim?: { x: number, y: number } },
): BossInput {
  const strafe = Number(keys.has('d') || keys.has('arrowright')) - Number(keys.has('a') || keys.has('arrowleft'))
  const forwardInput = Number(keys.has('w') || keys.has('arrowup')) - Number(keys.has('s') || keys.has('arrowdown'))
  const forwardX = Math.sin(cameraYaw)
  const forwardY = Math.cos(cameraYaw)
  const rightX = -Math.cos(cameraYaw)
  const rightY = Math.sin(cameraYaw)
  const xAxis = rightX * strafe + forwardX * forwardInput
  const yAxis = rightY * strafe + forwardY * forwardInput
  const length = Math.hypot(xAxis, yAxis)
  const fallbackAim = options?.fallbackAim ?? { x: 0, y: -1 }

  return {
    xAxis,
    yAxis,
    reset: actions.reset,
    targetDelta: 0,
    targetId,
    castSpell,
    aimX: length > 0.01 ? xAxis / length : fallbackAim.x,
    aimY: length > 0.01 ? yAxis / length : fallbackAim.y,
  }
}

export function clearCombatActions(actions: CombatActionQueue) {
  actions.reset = false
}

export function setupActionSceneEnvironment(scene: THREE.Scene, options?: { divider?: boolean, worldHeight?: number, worldWidth?: number }) {
  scene.background = new THREE.Color(0x10151a)
  scene.fog = new THREE.Fog(0x10151a, 14, 30)
  const worldWidth = options?.worldWidth ?? 20
  const worldHeight = options?.worldHeight ?? 11.25

  const ambient = new THREE.HemisphereLight(0xf8f0d8, 0x1c2530, 1.7)
  scene.add(ambient)

  const sun = new THREE.DirectionalLight(0xfff1c2, 2.8)
  sun.position.set(-6, 11, 8)
  sun.castShadow = true
  sun.shadow.camera.left = -13
  sun.shadow.camera.right = 13
  sun.shadow.camera.top = 9
  sun.shadow.camera.bottom = -9
  scene.add(sun)

  const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(worldWidth, worldHeight, 18, 10),
    new THREE.MeshStandardMaterial({ color: 0x243126, roughness: 0.88 }),
  )
  ground.rotation.x = -Math.PI / 2
  ground.receiveShadow = true
  scene.add(ground)

  const grid = new THREE.GridHelper(worldWidth, Math.max(20, Math.round(worldWidth)), 0x566044, 0x3b4436)
  grid.position.y = 0.012
  scene.add(grid)

  addActionArenaBounds(scene, { divider: options?.divider, worldHeight, worldWidth })
}

export function addActionArenaBounds(scene: THREE.Scene, options?: { divider?: boolean, worldHeight?: number, worldWidth?: number }) {
  const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x32323a, roughness: 0.7 })
  const worldWidth = options?.worldWidth ?? 20
  const worldHeight = options?.worldHeight ?? 11.25
  const halfW = worldWidth / 2
  const halfH = worldHeight / 2
  for (const wall of [
    { x: 0, z: -halfH - 0.15, sx: worldWidth + 0.4, sz: 0.28 },
    { x: 0, z: halfH + 0.15, sx: worldWidth + 0.4, sz: 0.28 },
    { x: -halfW - 0.15, z: 0, sx: 0.28, sz: worldHeight + 0.3 },
    { x: halfW + 0.15, z: 0, sx: 0.28, sz: worldHeight + 0.3 },
  ]) {
    const mesh = new THREE.Mesh(new THREE.BoxGeometry(wall.sx, 0.62, wall.sz), wallMaterial)
    mesh.position.set(wall.x, 0.31, wall.z)
    mesh.castShadow = true
    mesh.receiveShadow = true
    scene.add(mesh)
  }

  if (!options?.divider) return
  const dividerHeight = 3.45
  const divider = new THREE.Mesh(
    new THREE.BoxGeometry(0.22, dividerHeight, worldHeight - 0.85),
    new THREE.MeshStandardMaterial({ color: 0x5d6470, roughness: 0.72, transparent: true, opacity: 0.48 }),
  )
  divider.position.set(0, dividerHeight / 2, 0)
  divider.castShadow = true
  divider.receiveShadow = true
  scene.add(divider)
}

export function createActionCombatCamera(options?: { aspect?: number, far?: number, near?: number }) {
  return new THREE.PerspectiveCamera(
    CLAUDECRAFT_CAMERA_FOV,
    options?.aspect ?? 16 / 9,
    options?.near ?? 0.1,
    options?.far ?? 120,
  )
}

export function createActionCombatCameraController(initialYaw = Math.PI): ActionCombatCameraController {
  let dragX: number | null = null
  const controller: ActionCombatCameraController = {
    yaw: initialYaw,
    reset: (yaw = initialYaw) => {
      controller.yaw = yaw
      dragX = null
    },
    bindDrag: (element, options) => {
      const isPaused = options?.isPaused ?? (() => false)
      const handlePointerDown = (event: PointerEvent) => {
        if (isPaused()) return
        element.setPointerCapture(event.pointerId)
        dragX = event.clientX
        options?.onPointerDown?.(event)
      }
      const handlePointerMove = (event: PointerEvent) => {
        if (isPaused() || dragX === null) return
        controller.yaw -= (event.clientX - dragX) * CLAUDECRAFT_CAMERA_DRAG_SENSITIVITY
        dragX = event.clientX
      }
      const handlePointerUp = (event: PointerEvent) => {
        if (isPaused()) return
        if (element.hasPointerCapture(event.pointerId)) element.releasePointerCapture(event.pointerId)
        dragX = null
      }

      element.addEventListener('pointerdown', handlePointerDown)
      element.addEventListener('pointermove', handlePointerMove)
      element.addEventListener('pointerup', handlePointerUp)
      element.addEventListener('pointercancel', handlePointerUp)
      return () => {
        element.removeEventListener('pointerdown', handlePointerDown)
        element.removeEventListener('pointermove', handlePointerMove)
        element.removeEventListener('pointerup', handlePointerUp)
        element.removeEventListener('pointercancel', handlePointerUp)
      }
    },
  }
  return controller
}

export function resizeActionCombatCamera(camera: THREE.PerspectiveCamera, width: number, height: number) {
  camera.aspect = Math.max(1, width) / Math.max(1, height)
  camera.updateProjectionMatrix()
}

export function updateActionCombatCamera(
  camera: THREE.PerspectiveCamera,
  yaw: number,
  target: ActionCameraTarget,
  project: ActionWorldProjector = toActionWorld,
) {
  const point = project(target.x, target.y)
  applyClaudeCraftCamera(camera, yaw, point)
}

export function updateFollowCamera(camera: THREE.PerspectiveCamera, yaw: number, target: ActionCameraTarget) {
  updateActionCombatCamera(camera, yaw, target)
}

export function applyClaudeCraftCamera(camera: THREE.PerspectiveCamera, yaw: number, point: ActionWorldPoint) {
  const eyeY = CLAUDECRAFT_CAMERA_EYE_HEIGHT
  camera.position.x = point.x - Math.sin(yaw) * Math.cos(CLAUDECRAFT_CAMERA_PITCH) * CLAUDECRAFT_CAMERA_DISTANCE
  camera.position.y = eyeY + Math.sin(CLAUDECRAFT_CAMERA_PITCH) * CLAUDECRAFT_CAMERA_DISTANCE
  camera.position.z = point.z - Math.cos(yaw) * Math.cos(CLAUDECRAFT_CAMERA_PITCH) * CLAUDECRAFT_CAMERA_DISTANCE
  if (camera.fov !== CLAUDECRAFT_CAMERA_FOV) {
    camera.fov = CLAUDECRAFT_CAMERA_FOV
    camera.updateProjectionMatrix()
  }
  camera.lookAt(point.x, eyeY, point.z)
}

export function toActionWorld(x: number, y: number, arena: ActionArenaProjection = { height: 540, width: 960 }) {
  return {
    x: (x - arena.width / 2) / ACTION_ARENA_SCALE,
    z: (y - arena.height / 2) / ACTION_ARENA_SCALE,
  }
}

export function getLineTelegraph(scene: THREE.Scene, map: Map<string, THREE.Mesh>, id: string) {
  let mesh = map.get(id)
  if (!mesh) {
    mesh = new THREE.Mesh(
      new THREE.BufferGeometry(),
      new THREE.MeshBasicMaterial({ color: 0xff5d43, transparent: true, opacity: 0.28, side: THREE.DoubleSide }),
    )
    scene.add(mesh)
    map.set(id, mesh)
  }
  return mesh
}

export function syncLineTelegraphGeometry(
  mesh: THREE.Mesh,
  startPoint: { x: number; y: number },
  endPoint: { x: number; y: number },
  width: number,
  project: (x: number, y: number) => { x: number, z: number } = toActionWorld,
) {
  const start = project(startPoint.x, startPoint.y)
  const end = project(endPoint.x, endPoint.y)
  const dx = end.x - start.x
  const dz = end.z - start.z
  const length = Math.max(0.001, Math.hypot(dx, dz))
  const normalX = -dz / length
  const normalZ = dx / length
  const halfWidth = width / ACTION_ARENA_SCALE / 2
  const y = 0.035
  const positions = new Float32Array([
    start.x + normalX * halfWidth, y, start.z + normalZ * halfWidth,
    start.x - normalX * halfWidth, y, start.z - normalZ * halfWidth,
    end.x + normalX * halfWidth, y, end.z + normalZ * halfWidth,
    end.x - normalX * halfWidth, y, end.z - normalZ * halfWidth,
  ])
  const geometry = new THREE.BufferGeometry()
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
  geometry.setIndex([0, 2, 1, 2, 3, 1])
  geometry.computeVertexNormals()
  mesh.geometry.dispose()
  mesh.geometry = geometry
}

export function getCircleTelegraph(scene: THREE.Scene, map: Map<string, THREE.Mesh>, id: string) {
  let mesh = map.get(id)
  if (!mesh) {
    mesh = new THREE.Mesh(
      new THREE.CircleGeometry(1, 42),
      new THREE.MeshBasicMaterial({ color: 0xffd36d, transparent: true, opacity: 0.24, side: THREE.DoubleSide }),
    )
    mesh.rotation.x = -Math.PI / 2
    scene.add(mesh)
    map.set(id, mesh)
  }
  return mesh
}

export function getArcTelegraph(scene: THREE.Scene, map: Map<string, THREE.Mesh>, id: string) {
  let mesh = map.get(id)
  if (!mesh) {
    mesh = new THREE.Mesh(
      new THREE.BufferGeometry(),
      new THREE.MeshBasicMaterial({ color: 0xff8a43, transparent: true, opacity: 0.3, side: THREE.DoubleSide }),
    )
    scene.add(mesh)
    map.set(id, mesh)
  }
  return mesh
}

export function syncArcTelegraphGeometry(
  mesh: THREE.Mesh,
  centerPoint: { x: number; y: number },
  radius: number,
  startAngle: number,
  endAngle: number,
  width: number,
  project: (x: number, y: number) => { x: number, z: number } = toActionWorld,
) {
  const center = project(centerPoint.x, centerPoint.y)
  const innerRadius = Math.max(0, radius - width / 2) / ACTION_ARENA_SCALE
  const outerRadius = Math.max(width, radius + width / 2) / ACTION_ARENA_SCALE
  const segments = 32
  const positions: number[] = []
  const indices: number[] = []
  const y = 0.045
  for (let i = 0; i <= segments; i += 1) {
    const t = i / segments
    const angle = startAngle + (endAngle - startAngle) * t
    positions.push(
      center.x + Math.cos(angle) * innerRadius, y, center.z + Math.sin(angle) * innerRadius,
      center.x + Math.cos(angle) * outerRadius, y, center.z + Math.sin(angle) * outerRadius,
    )
    if (i < segments) {
      const base = i * 2
      indices.push(base, base + 1, base + 2, base + 1, base + 3, base + 2)
    }
  }
  const geometry = new THREE.BufferGeometry()
  geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3))
  geometry.setIndex(indices)
  geometry.computeVertexNormals()
  mesh.geometry.dispose()
  mesh.geometry = geometry
}

export function createHealBurstEffect(options?: { simple?: boolean, color?: number }) {
  const color = options?.color ?? 0x7dff9d
  const group = new THREE.Group()
  const ring = new THREE.Mesh(
    new THREE.TorusGeometry(options?.simple ? 0.34 : 0.38, options?.simple ? 0.026 : 0.028, 8, options?.simple ? 32 : 36),
    new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }),
  )
  ring.rotation.x = Math.PI / 2
  group.add(ring)

  if (options?.simple) return group

  const pillar = new THREE.Mesh(
    new THREE.CylinderGeometry(0.18, 0.34, 0.9, 18, 1, true),
    new THREE.MeshBasicMaterial({ color: 0xb8ffd0, transparent: true, opacity: 0.34, side: THREE.DoubleSide }),
  )
  pillar.position.y = 0.45
  group.add(pillar)

  for (let i = 0; i < 5; i += 1) {
    const mote = new THREE.Mesh(
      new THREE.SphereGeometry(0.045, 8, 6),
      new THREE.MeshBasicMaterial({ color: 0xf4fff2, transparent: true, opacity: 0.9 }),
    )
    const angle = (Math.PI * 2 * i) / 5
    mote.position.set(Math.cos(angle) * 0.34, 0.25 + i * 0.12, Math.sin(angle) * 0.34)
    group.add(mote)
  }

  return group
}

export function getOrCreateStunEffect(scene: THREE.Scene, effects: Map<string, THREE.Group>, id: string) {
  let group = effects.get(id)
  if (!group) {
    group = new THREE.Group()
    const starMaterial = new THREE.MeshBasicMaterial({ color: 0xffd36d, transparent: true, opacity: 0.95 })
    for (let i = 0; i < 3; i += 1) {
      const star = new THREE.Mesh(new THREE.OctahedronGeometry(0.09), starMaterial.clone())
      const angle = (Math.PI * 2 * i) / 3
      star.position.set(Math.cos(angle) * 0.28, Math.sin(i * 1.7) * 0.05, Math.sin(angle) * 0.28)
      star.rotation.set(i * 0.7, i * 0.4, i * 0.9)
      group.add(star)
    }
    const ring = new THREE.Mesh(
      new THREE.TorusGeometry(0.28, 0.012, 6, 24),
      new THREE.MeshBasicMaterial({ color: 0xfff0a8, transparent: true, opacity: 0.62 }),
    )
    ring.rotation.x = Math.PI / 2
    group.add(ring)
    scene.add(group)
    effects.set(id, group)
  }
  return group
}

export function setObjectOpacity(group: THREE.Object3D, opacity: number) {
  group.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    const materials = Array.isArray(child.material) ? child.material : [child.material]
    for (const material of materials) {
      material.transparent = true
      material.opacity = opacity
    }
  })
}

export function pruneObjectMap<T extends THREE.Object3D>(map: Map<string, T>, active: Set<string>) {
  for (const [id, object] of map) {
    if (active.has(id)) continue
    disposeObject3d(object)
    map.delete(id)
  }
}

export function disposeObject3d(object: THREE.Object3D) {
  object.removeFromParent()
  object.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.geometry.dispose()
    const material = child.material
    if (Array.isArray(material)) material.forEach((item) => item.dispose())
    else material.dispose()
  })
}
