import { Terrain } from "./Terrain"
import type { Vec3 } from "../engine/math/Vec3"
import type { Aabb, Level } from "./level"

/** The player as a vertical cylinder. `position` is at the feet; the camera
 *  eye sits EYE_HEIGHT above it. */
export type Player = {
  position: Vec3
  yaw: number
  pitch: number
  velocityY: number
  onGround: boolean
}

export const EYE_HEIGHT = 1.6
const RADIUS = 0.35
const SPEED = 6
/** Speed multiplier while a Run key (Shift) is held. Tweak to taste; set high to
 *  blast across the big terrain -- move+collision is substepped, so walls stay
 *  solid even at big multipliers. */
const RUN_MULTIPLIER = 2
const GRAVITY = 22
const JUMP_SPEED = 14
const NPC_RADIUS = 0.5

export namespace Player {
  /** Advance the player one frame: jump, horizontal move + collision, gravity. */
  export function update(player: Player, keys: Set<string>, dt: number, level: Level): void {
    if (keys.has("Space") && player.onGround) {
      player.velocityY = JUMP_SPEED
      player.onGround = false
    }
    // Move + collide in small substeps: collision is discrete (move, then push
    // out), so a single big running step could otherwise skip clean through a
    // wall. Substepping keeps each advance short enough to always hit it.
    const steps = moveSubsteps(keys, dt)
    for (let i = 0; i < steps; i++) {
      moveHorizontal(player, keys, dt / steps)
      collide(player, level)
    }
    fall(player, dt, level)
  }

  /** Run-speed factor for the frame: RUN_MULTIPLIER while Shift is held, else 1. */
  function runFactor(keys: Set<string>): number {
    return keys.has("ShiftLeft") || keys.has("ShiftRight") ? RUN_MULTIPLIER : 1
  }

  /** Number of move+collide substeps so each advances at most ~RADIUS, keeping
   *  the player from tunneling walls however fast they run. */
  function moveSubsteps(keys: Set<string>, dt: number): number {
    const perFrame = SPEED * runFactor(keys) * dt * Math.SQRT2
    return Math.max(1, Math.ceil(perFrame / RADIUS))
  }

  function moveHorizontal(player: Player, keys: Set<string>, dt: number): void {
    const speed = SPEED * runFactor(keys) * dt
    const fx = Math.sin(player.yaw)
    const fz = -Math.cos(player.yaw)
    const rx = Math.cos(player.yaw)
    const rz = Math.sin(player.yaw)
    const p = player.position
    if (keys.has("KeyW")) {
      p.x += fx * speed
      p.z += fz * speed
    }
    if (keys.has("KeyS")) {
      p.x -= fx * speed
      p.z -= fz * speed
    }
    if (keys.has("KeyD")) {
      p.x += rx * speed
      p.z += rz * speed
    }
    if (keys.has("KeyA")) {
      p.x -= rx * speed
      p.z -= rz * speed
    }
  }

  /** Push the player's circle out of any solid it overlaps: level colliders it
   *  is not standing above, and the NPC. This is what makes walls and the NPC
   *  impassable while still letting you stand on the crate. */
  function collide(player: Player, level: Level): void {
    for (const aabb of level.colliders) {
      if (player.position.y < aabb.top - 0.01) {
        pushFromAabb(player.position, aabb)
      }
    }
    pushFromCircle(player.position, level.npcPosition.x, level.npcPosition.z, NPC_RADIUS)
  }

  /** Apply gravity and land on the highest ground under the player. */
  function fall(player: Player, dt: number, level: Level): void {
    player.velocityY -= GRAVITY * dt
    player.position.y += player.velocityY * dt
    const ground = groundHeight(player.position, level)
    if (player.position.y <= ground) {
      player.position.y = ground
      player.velocityY = 0
      player.onGround = true
    } else {
      player.onGround = false
    }
  }

  function groundHeight(position: Vec3, level: Level): number {
    let ground = Terrain.height(level.terrain, position.x, position.z)
    for (const aabb of level.colliders) {
      if (
        aabb.standable &&
        position.x >= aabb.minX &&
        position.x <= aabb.maxX &&
        position.z >= aabb.minZ &&
        position.z <= aabb.maxZ
      ) {
        ground = Math.max(ground, aabb.top)
      }
    }
    return ground
  }

  function pushFromAabb(position: Vec3, aabb: Aabb): void {
    const cx = Math.max(aabb.minX, Math.min(aabb.maxX, position.x))
    const cz = Math.max(aabb.minZ, Math.min(aabb.maxZ, position.z))
    const dx = position.x - cx
    const dz = position.z - cz
    const d2 = dx * dx + dz * dz
    if (d2 >= RADIUS * RADIUS) {
      return
    }
    if (d2 > 1e-6) {
      const d = Math.sqrt(d2)
      const push = (RADIUS - d) / d
      position.x += dx * push
      position.z += dz * push
      return
    }
    // Center is inside the box: eject through the nearest face.
    const left = position.x - aabb.minX
    const rightSide = aabb.maxX - position.x
    const near = position.z - aabb.minZ
    const far = aabb.maxZ - position.z
    const m = Math.min(left, rightSide, near, far)
    if (m === left) {
      position.x = aabb.minX - RADIUS
    } else if (m === rightSide) {
      position.x = aabb.maxX + RADIUS
    } else if (m === near) {
      position.z = aabb.minZ - RADIUS
    } else {
      position.z = aabb.maxZ + RADIUS
    }
  }

  function pushFromCircle(position: Vec3, cx: number, cz: number, otherRadius: number): void {
    const dx = position.x - cx
    const dz = position.z - cz
    const reach = RADIUS + otherRadius
    const d2 = dx * dx + dz * dz
    if (d2 >= reach * reach || d2 < 1e-6) {
      return
    }
    const d = Math.sqrt(d2)
    const push = (reach - d) / d
    position.x += dx * push
    position.z += dz * push
  }
}
