import { Component, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, type ReactNode } from 'react'
import { ThreeEvent, useFrame } from '@react-three/fiber'
import { PositionalAudio } from '@react-three/drei'
import { CuboidCollider, RigidBody, type RapierRigidBody } from '@react-three/rapier'
import * as THREE from 'three'
import { ObjectRenderMode, type WorldObjectAsset, type WorldObjectPhysics } from '../../types/world'
import { useAudioStore } from '../../store/audio'
import { useSceneObjectVisual } from './useSceneObjectVisual'

export const OBJECT_SCALE = 0.5
const OBJECT_AUTO_ROTATE_Y_SPEED = 0.35

const COLLIDER_WIREFRAME_COLOR = 0x00aaff

type PointerHandler = (event: ThreeEvent<PointerEvent>) => boolean
type HoverHandler = (event: ThreeEvent<PointerEvent>, objectId: string, hovering: boolean) => void
type ClickHandler = (worldPos: THREE.Vector3) => void

const _rotation = new THREE.Quaternion()
export const SCENE_OBJECT_INSTANCE_ID_KEY = 'sceneObjectInstanceId'

export interface SceneObjectHandle {
  id: string
  rigidBody: RapierRigidBody | null
  initialPosition: THREE.Vector3
  initialRotation: THREE.Quaternion
  bounds: THREE.Box3
  getFocusPoint: (target: THREE.Vector3) => THREE.Vector3
  playInteractionSfx: () => void
}

interface Props {
  object: WorldObjectAsset
  position: [number, number, number]
  rotation?: [number, number, number]
  scale?: [number, number, number]
  physics?: WorldObjectPhysics
  renderMode: ObjectRenderMode
  autoRotateY?: boolean
  onHover: HoverHandler
  onClick?: ClickHandler
  onPointerDown?: PointerHandler
  onPointerMove?: PointerHandler
  onPointerUp?: PointerHandler
  onPointerCancel?: PointerHandler
}

interface SfxLoadErrorBoundaryProps {
  url: string
  children: ReactNode
}

interface SfxLoadErrorBoundaryState {
  hasError: boolean
}

class SfxLoadErrorBoundary extends Component<SfxLoadErrorBoundaryProps, SfxLoadErrorBoundaryState> {
  state: SfxLoadErrorBoundaryState = { hasError: false }

  static getDerivedStateFromError(): SfxLoadErrorBoundaryState {
    return { hasError: true }
  }

  componentDidCatch(error: unknown) {
    console.warn(`Skipping object SFX "${this.props.url}" because it failed to load.`, error)
  }

  componentDidUpdate(prevProps: SfxLoadErrorBoundaryProps) {
    if (prevProps.url !== this.props.url && this.state.hasError) {
      this.setState({ hasError: false })
    }
  }

  render() {
    if (this.state.hasError) return null
    return this.props.children
  }
}

export const SceneObject = forwardRef<SceneObjectHandle, Props>(function SceneObject(
  {
    object,
    position,
    rotation = [0, 0, 0],
    scale = [1, 1, 1],
    physics = 'rigidbody',
    renderMode,
    autoRotateY = false,
    onHover,
    onClick,
    onPointerDown,
    onPointerMove,
    onPointerUp,
    onPointerCancel,
  },
  ref,
) {
  const rigidBodyRef = useRef<RapierRigidBody>(null)
  const visualGroupRef = useRef<THREE.Group>(null)
  const colliderProxyRef = useRef<THREE.Mesh>(null)
  const sfxRefs = useRef<Array<THREE.PositionalAudio | null>>([])
  const lastSfxIndexRef = useRef<number | null>(null)
  const muted = useAudioStore((s) => s.muted)
  const isStatic = physics === 'static' || physics === 'ghost'
  const usesBoxCollider = physics === 'rigidbody' || physics === 'static'
  const initialPosition = useMemo(() => new THREE.Vector3(...position), [position])
  const initialRotation = useMemo(() => new THREE.Quaternion().setFromEuler(new THREE.Euler(...rotation)), [rotation])
  const { scene, wireframeOverlayScene, offset, size, bounds } = useSceneObjectVisual({
    asset: object,
    renderMode,
  })
  const colliderCenter = useMemo(
    () => new THREE.Vector3(0, (size.y * OBJECT_SCALE) / 2, 0),
    [size],
  )
  const colliderUserData = useMemo(() => ({
    [SCENE_OBJECT_INSTANCE_ID_KEY]: object.id,
  }), [object.id])
  const colliderHalfExtents = useMemo(
    () => new THREE.Vector3(
      Math.max((size.x * OBJECT_SCALE) / 2, 0.01),
      Math.max((size.y * OBJECT_SCALE) / 2, 0.01),
      Math.max((size.z * OBJECT_SCALE) / 2, 0.01),
    ),
    [size],
  )
  const colliderWireframeMaterial = useMemo(() => new THREE.MeshBasicMaterial({
    color: COLLIDER_WIREFRAME_COLOR,
    wireframe: true,
    transparent: true,
    depthTest: false,
    depthWrite: false,
    toneMapped: false,
    fog: false,
  }), [])

  useFrame((_, delta) => {
    if (!autoRotateY || !visualGroupRef.current) return
    visualGroupRef.current.rotation.y += delta * OBJECT_AUTO_ROTATE_Y_SPEED
  })

  useEffect(() => {
    colliderWireframeMaterial.opacity = 0
    colliderWireframeMaterial.transparent = true
    colliderWireframeMaterial.depthTest = false
    colliderWireframeMaterial.depthWrite = false
    colliderWireframeMaterial.needsUpdate = true
  }, [colliderWireframeMaterial])

  useEffect(() => {
    sfxRefs.current.length = object.sfxUrls.length
  }, [object.sfxUrls.length])

  useEffect(() => {
    if (!muted) return
    sfxRefs.current.forEach((sound) => {
      if (!sound) return
      sound.setVolume(0)
      if (sound.isPlaying) sound.stop()
    })
  }, [muted])

  const playRandomSfx = useCallback(() => {
    if (muted || object.sfxUrls.length === 0) return

    const lastIndex = lastSfxIndexRef.current
    let nextIndex = 0
    if (object.sfxUrls.length > 1) {
      nextIndex = Math.floor(Math.random() * (object.sfxUrls.length - 1))
      if (lastIndex !== null && nextIndex >= lastIndex) nextIndex += 1
    }

    const sound = sfxRefs.current[nextIndex]
    if (!sound) return

    lastSfxIndexRef.current = nextIndex
    sound.setVolume(1)
    if (sound.isPlaying) sound.stop()

    const play = () => sound.play()
    if (sound.context.state === 'suspended') {
      sound.context.resume().then(play).catch(() => {})
      return
    }
    play()
  }, [muted, object.sfxUrls.length])

  useEffect(() => {
    const body = rigidBodyRef.current
    if (!body) return
    body.setTranslation({ x: position[0], y: position[1], z: position[2] }, true)
    _rotation.setFromEuler(new THREE.Euler(...rotation))
    body.setRotation({ x: _rotation.x, y: _rotation.y, z: _rotation.z, w: _rotation.w }, true)
    body.setLinvel({ x: 0, y: 0, z: 0 }, true)
    body.setAngvel({ x: 0, y: 0, z: 0 }, true)
    body.wakeUp()
  }, [position, rotation, scale])

  useEffect(() => {
    return () => {
      colliderWireframeMaterial.dispose()
    }
  }, [colliderWireframeMaterial])

  useImperativeHandle(
    ref,
    () => ({
      id: object.id,
      get rigidBody() {
        return rigidBodyRef.current
      },
      initialPosition,
      initialRotation,
      bounds,
      getFocusPoint: (target) => {
        if (colliderProxyRef.current) return colliderProxyRef.current.getWorldPosition(target)
        return target.copy(initialPosition).add(colliderCenter)
      },
      playInteractionSfx: playRandomSfx,
    }),
    [bounds, colliderCenter, initialPosition, initialRotation, object.id, playRandomSfx],
  )

  const visualContent = (
    <group ref={visualGroupRef} scale={[OBJECT_SCALE * scale[0], OBJECT_SCALE * scale[1], OBJECT_SCALE * scale[2]]}>
      <primitive object={scene} position={offset} dispose={null} />
      {renderMode === ObjectRenderMode.ShadedWireframe && (
        <primitive object={wireframeOverlayScene} position={offset} dispose={null} />
      )}
      {object.sfxUrls.map((url, index) => (
        <SfxLoadErrorBoundary key={url} url={url}>
          <PositionalAudio
            ref={(audio) => {
              sfxRefs.current[index] = audio
            }}
            url={url}
            distance={2}
            loop={false}
          />
        </SfxLoadErrorBoundary>
      ))}
    </group>
  )

  return (
    <RigidBody
      ref={rigidBodyRef}
      type={isStatic ? 'fixed' : 'dynamic'}
      colliders={false}
      position={position}
      rotation={rotation}
      linearDamping={0.45}
      angularDamping={0.35}
      additionalSolverIterations={4}
      ccd
      canSleep
    >
      {usesBoxCollider && (
        <CuboidCollider
          args={[
            colliderHalfExtents.x * scale[0],
            colliderHalfExtents.y * scale[1],
            colliderHalfExtents.z * scale[2],
          ]}
          position={[colliderCenter.x * scale[0], colliderCenter.y * scale[1], colliderCenter.z * scale[2]]}
        />
      )}
      <mesh
        ref={colliderProxyRef}
        position={[colliderCenter.x * scale[0], colliderCenter.y * scale[1], colliderCenter.z * scale[2]]}
        material={colliderWireframeMaterial}
        renderOrder={10000}
        userData={colliderUserData}
        onPointerOver={(event) => {
          event.stopPropagation()
          onHover(event, object.id, true)
        }}
        onPointerOut={(event) => {
          event.stopPropagation()
          onHover(event, object.id, false)
        }}
        onClick={(event) => {
          event.stopPropagation()
          onClick?.(event.point.clone())
        }}
        onPointerDown={(event) => {
          if (event.button !== 0) return
          event.stopPropagation()
          if (!onPointerDown?.(event)) playRandomSfx()
        }}
        onPointerMove={(event) => {
          onHover(event, object.id, true)
          onPointerMove?.(event)
        }}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerCancel}
      >
        <boxGeometry args={[
          colliderHalfExtents.x * scale[0] * 2,
          colliderHalfExtents.y * scale[1] * 2,
          colliderHalfExtents.z * scale[2] * 2,
        ]} />
      </mesh>
      {visualContent}
    </RigidBody>
  )
})
