import type { Vec3 } from "../math/Vec3"
import { CollisionWorld, type CollisionWorld as World } from "./CollisionWorld"

export type Character = {
  position: Vec3
  yaw: number
  velocityY: number
  onGround: boolean
}

export type CharacterInput = {
  forward: number
  right: number
  jump: boolean
  run: boolean
}

export type CharacterConfig = {
  radius: number
  speed: number
  runMultiplier: number
  gravity: number
  jumpSpeed: number
  eyeHeight: number
}

export namespace CharacterController {
  export function update(
    character: Character,
    input: CharacterInput,
    dt: number,
    world: World,
    config: CharacterConfig,
  ): void {
    if (input.jump && character.onGround) {
      character.velocityY = config.jumpSpeed
      character.onGround = false
    }
    const steps = moveSubsteps(input, dt, config)
    for (let i = 0; i < steps; i++) {
      moveHorizontal(character, input, dt / steps, config)
      CollisionWorld.pushOut(world, character.position, config.radius)
    }
    character.velocityY -= config.gravity * dt
    character.position.y += character.velocityY * dt
    const ground = CollisionWorld.groundHeight(world, character.position)
    if (character.position.y <= ground) {
      character.position.y = ground
      character.velocityY = 0
      character.onGround = true
    } else {
      character.onGround = false
    }
  }

  function moveSubsteps(
    input: CharacterInput,
    dt: number,
    config: CharacterConfig,
  ): number {
    const distance =
      config.speed *
      runFactor(input, config) *
      dt *
      Math.hypot(input.forward, input.right)
    return Math.max(1, Math.ceil(distance / config.radius))
  }

  function moveHorizontal(
    character: Character,
    input: CharacterInput,
    dt: number,
    config: CharacterConfig,
  ): void {
    const speed = config.speed * runFactor(input, config) * dt
    const forwardX = Math.sin(character.yaw)
    const forwardZ = -Math.cos(character.yaw)
    const rightX = Math.cos(character.yaw)
    const rightZ = Math.sin(character.yaw)
    character.position.x +=
      (forwardX * input.forward + rightX * input.right) * speed
    character.position.z +=
      (forwardZ * input.forward + rightZ * input.right) * speed
  }

  function runFactor(input: CharacterInput, config: CharacterConfig): number {
    return input.run ? config.runMultiplier : 1
  }
}
