import React, { useEffect, useRef, useState } from 'react'
import { useThree, useFrame } from 'react-three-fiber'
import {
  Geometry,
  Event,
  Group,
  PerspectiveCamera,
  Vector3,
  Raycaster,
  Mesh,
  CircleGeometry,
} from 'three'
import { relativeAngle } from './lib/math'
import Collisions from './Collisions'

import useStore from '../store'
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial'
import { Quaternion } from 'three/src/math/Quaternion'
import { ActionName } from './models/Human'

const SPEED = 0.8
export const HEIGHT = 1.5
const CIRCLE_RADIUS = 0.5
const CIRCLE_SEGMENTS = 8
const InitialPosition = new Vector3(49.92, 3.15, 34.52)

const keys: Record<string, string> = {
  KeyW: 'left',
  KeyS: 'right',
  KeyA: 'backward',
  KeyD: 'forward',
  ArrowUp: 'left',
  ArrowDown: 'right',
  ArrowLeft: 'backward',
  ArrowRight: 'forward',
  ShiftLeft: 'run',
  Space: 'jump',
}
const moveFieldByKey = (key: string) => keys[key]

export const animations: Record<ActionName, ActionName> = {
  idle: 'idle',
  turn_right: 'turn_right',
  turn_left: 'turn_left',
  walk_backwards: 'walk_backwards',
  walk: 'walk',
  walk_right: 'walk_right',
  walk_left: 'walk_left',
  run: 'run',
  run_right: 'run_right',
  run_left: 'run_left',
  jump: 'jump',
}

function FirstPersonCamera(props: JSX.IntrinsicElements['perspectiveCamera']) {
  const ref = useRef<PerspectiveCamera>()
  const { setDefaultCamera } = useThree()
  // Make the camera known to the system
  useEffect(() => {
    if (ref.current) setDefaultCamera(ref.current)
  }, [ref.current])
  // Update it every frame
  useFrame(() => ref.current?.updateMatrixWorld())
  return <perspectiveCamera ref={ref} {...props} />
}

const usePlayerControls = () => {
  const [movement, setMovement] = useState({
    forward: false,
    backward: false,
    left: false,
    right: false,
    run: false,
    jump: false,
  })
  useEffect(() => {
    const handleKeyDown = (e: Event) => {
      setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: true }))
    }
    const handleKeyUp = (e: Event) => {
      setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: false }))
    }
    document.addEventListener('keydown', handleKeyDown)
    document.addEventListener('keyup', handleKeyUp)
    return () => {
      document.removeEventListener('keydown', handleKeyDown)
      document.removeEventListener('keyup', handleKeyUp)
    }
  }, [])
  return movement
}

// define an axis, usually just up
const upVector = new Vector3(0, 1, 0)

// TODO Improve physics in player
const Player = () => {
  const socket = useStore((state) => state.socket)
  const initialTransform = useStore((state) => state.initialTransform)
  const pointerLocked = useStore((state) => state.pointerLocked)
  const { jump, forward, backward, left, right, run } = usePlayerControls()
  const { camera } = useThree()

  const groupRef = useRef<Group>()
  const collisionsRef = useRef<Mesh<CircleGeometry, MeshBasicMaterial>>(null)
  const velocity = useRef<Vector3>(new Vector3())
  const direction = useRef<Vector3>(new Vector3())
  const currentPositionClone = useRef<Vector3>(new Vector3())
  const pCameraQuaternion = useRef<Quaternion>(new Quaternion())
  const worldOrigin = useRef<Vector3>(new Vector3())
  const speed = useRef<number>(SPEED)

  const bottomRaycaster = useRef(
    new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5),
  )

  const collisionCircle = useRef<Mesh>()
  const collisionCircleGeometry = new CircleGeometry(CIRCLE_RADIUS, CIRCLE_SEGMENTS)
  collisionCircleGeometry.rotateX(Math.PI / 2)
  collisionCircle
  const collisionCircleMaterial = new MeshBasicMaterial({
    color: 0xff0000,
    wireframe: true,
  })

  const wallRaycasters = useRef<Raycaster[]>(
    Array(CIRCLE_SEGMENTS)
      .fill(undefined)
      .map((_, i) => {
        const vert = collisionCircleGeometry.vertices[i + 1].clone()
        const direction = vert.sub(collisionCircleGeometry.vertices[0]).normalize()
        return new Raycaster(new Vector3(), direction, 0, CIRCLE_RADIUS)
      }),
  )

  useEffect(() => {
    if (initialTransform && groupRef.current) {
      groupRef.current.position.fromArray(initialTransform.position)
      camera.quaternion.fromArray(initialTransform.rotation)
    }
  }, [initialTransform, groupRef.current])

  useEffect(() => {
    const socketEmitTransformInterval = setInterval(() => {
      if (socket && groupRef.current && camera) {
        const cameraRotation = camera.quaternion.toArray()
        const [x, y, z] = direction.current.toArray()

        let anim = animations.idle

        if (velocity.current.y !== 0) {
          anim = animations.jump
        } else if (x === 0 && z === 0) {
          const rotationAngle = relativeAngle(
            upVector,
            camera.quaternion,
            pCameraQuaternion.current,
          )

          if (Math.abs(rotationAngle) > 0.5) {
            anim = rotationAngle > 1 ? animations.turn_right : animations.turn_left
          } else {
            anim = animations.idle
          }
        } else if (x < 0) {
          anim = animations.walk_backwards
        } else if (x > 0 && z === 0) {
          anim = speed.current > SPEED ? animations.run : animations.walk
        } else if (x >= 0 && z > 0) {
          anim = speed.current > SPEED ? animations.run_right : animations.walk_right
        } else if (x >= 0 && z < 0) {
          anim = speed.current > SPEED ? animations.run_left : animations.walk_left
        }

        // get the signed difference in these angles
        socket.emit('transform', {
          position: [
            groupRef.current?.position.x,
            groupRef.current?.position.y,
            groupRef.current?.position.z,
          ],
          rotation: [...cameraRotation],
          animation: anim,
        })

        pCameraQuaternion.current.copy(camera.quaternion)
      }
    }, 33)

    return () => {
      clearInterval(socketEmitTransformInterval)
    }
  }, [socket, groupRef.current, camera])

  const moveForward = (distance: number) => {
    if (groupRef.current) {
      worldOrigin.current.setFromMatrixColumn(camera.matrix, 0)
      worldOrigin.current.crossVectors(camera.up, worldOrigin.current)
      groupRef.current.position.addScaledVector(worldOrigin.current, distance)
    }
  }

  const moveRight = (distance: number) => {
    if (groupRef.current) {
      worldOrigin.current.setFromMatrixColumn(camera.matrix, 0)
      groupRef.current.position.addScaledVector(worldOrigin.current, distance)
    }
  }

  useFrame((_, delta) => {
    if (!groupRef.current || !collisionsRef.current || !collisionCircle.current) return

    if (pointerLocked) {
      // Slowdown
      velocity.current.x -= velocity.current.x * 9.0 * delta
      velocity.current.z -= velocity.current.z * 9.0 * delta

      // Fall
      bottomRaycaster.current.ray.origin.copy(groupRef.current.position)

      let intersections = []
      intersections = bottomRaycaster.current.intersectObject(collisionsRef.current, true)

      if (jump && velocity.current.y === 0) {
        velocity.current.y = 5
      }

      if (intersections.length < 1) {
        velocity.current.y -= 9.8 * 8 * delta
      } else {
        if (velocity.current.y <= 0 && intersections[0].distance < HEIGHT) {
          velocity.current.y = 0
          groupRef.current.position.y = intersections[0].point.y + HEIGHT
        }
      }

      // Reset
      if (groupRef.current.position.y < -50) {
        groupRef.current.position.copy(InitialPosition)
        velocity.current.y = 0
      }

      // Direction
      direction.current.z = Number(forward) - Number(backward)
      direction.current.x = Number(left) - Number(right)
      direction.current.normalize()

      // Running
      speed.current = run && direction.current.x >= 0 ? SPEED * 1.5 : SPEED

      // Move
      if (forward || backward)
        velocity.current.z -= direction.current.z * SPEED * delta * speed.current
      if (left || right)
        velocity.current.x -= direction.current.x * SPEED * delta * speed.current

      // Wall collisions
      const collisionCircleGeometry = collisionCircle.current.geometry as Geometry

      if (collisionCircleGeometry.vertices) {
        for (let i = 0; i < CIRCLE_SEGMENTS; i++) {
          wallRaycasters.current[i].ray.origin.copy(groupRef.current.position)
          wallRaycasters.current[i].ray.origin.y -= HEIGHT * 0.6

          let wallIntersections = []
          wallIntersections = wallRaycasters.current[i].intersectObject(
            collisionsRef.current,
            true,
          )

          if (
            wallIntersections.length > 0 &&
            wallIntersections[0].distance < CIRCLE_RADIUS
          ) {
            const distance = CIRCLE_RADIUS - wallIntersections[0].distance

            currentPositionClone.current.copy(wallRaycasters.current[i].ray.origin)
            const direction = wallIntersections[0].point
              .sub(currentPositionClone.current)
              .normalize()
              .multiplyScalar(distance)

            groupRef.current.position.sub(direction)
          }
        }
      }

      // Apply speed
      moveForward(-velocity.current.x * speed.current)
      moveRight(-velocity.current.z * speed.current)
      groupRef.current.position.y += velocity.current.y * delta
    }
  })
  return (
    <>
      <group ref={groupRef} position={InitialPosition}>
        <FirstPersonCamera position={[0, -0.1, 0]} />
        <mesh
          ref={collisionCircle}
          position={[0, -HEIGHT * 0.1, 0]}
          geometry={collisionCircleGeometry}
          material={collisionCircleMaterial}
          visible={false}
        />
      </group>
      <Collisions ref={collisionsRef} visible={false} />
    </>
  )
}

export default Player
