import { Vector3 } from "three";
import { angleDelta } from "@psx/shared";
import type { CharacterMover } from "./CharacterMover.js";
import type { InputManager } from "./InputManager.js";

/** GDD §6.2 — classic tank controls, or modern camera-relative movement. */
export type ControlScheme = "tank" | "cameraRelative";

export interface PlayerControllerConfig {
  walkSpeed: number;
  runSpeed: number;
  /** Tank turn rate, radians/second. */
  turnSpeed: number;
  /** How fast the body swings to face the stick in camera-relative mode. */
  facingSmoothing: number;
  quickTurnDuration: number;
  /** Backing up is deliberately slow — it is a horror game. */
  backpedalFactor: number;
}

export const DEFAULT_PLAYER_CONFIG: PlayerControllerConfig = {
  walkSpeed: 1.9,
  runSpeed: 3.6,
  turnSpeed: 2.6,
  facingSmoothing: 0.0001,
  quickTurnDuration: 0.22,
  backpedalFactor: 0.55,
};

const UP = new Vector3(0, 1, 0);

export class PlayerController {
  scheme: ControlScheme = "tank";
  yaw = 0;

  private quickTurnRemaining = 0;
  private quickTurnFrom = 0;
  private quickTurnTo = 0;

  /**
   * Camera basis frozen at the moment of a camera cut.
   *
   * GDD §6.1 asks that "up" still feels correct after a cut. If the basis
   * swapped mid-hold the player would veer off the moment a new zone triggered,
   * so we keep using the pre-cut basis until the stick returns to neutral —
   * the behaviour the RE remakes settled on.
   */
  private lockedBasis: { forward: Vector3; right: Vector3 } | null = null;
  private inputWasNeutral = true;

  private readonly desiredVelocity = new Vector3();
  private readonly moveDirection = new Vector3();
  private readonly cameraForward = new Vector3(0, 0, 1);
  private readonly cameraRight = new Vector3(1, 0, 0);

  constructor(
    private readonly mover: CharacterMover,
    private readonly input: InputManager,
    readonly config: PlayerControllerConfig = DEFAULT_PLAYER_CONFIG,
  ) {}

  /** Called by CameraManager whenever the active camera changes. */
  onCameraCut(): void {
    if (!this.inputWasNeutral) {
      this.lockedBasis = {
        forward: this.cameraForward.clone(),
        right: this.cameraRight.clone(),
      };
    }
  }

  get isQuickTurning(): boolean {
    return this.quickTurnRemaining > 0;
  }

  get speed(): number {
    return this.mover.state.lastMoveDistance;
  }

  /** `cameraLook` is the world-space direction the active camera faces. */
  update(dt: number, cameraLook: Vector3): void {
    this.cameraForward.copy(cameraLook).setY(0);
    if (this.cameraForward.lengthSq() < 1e-8) this.cameraForward.set(0, 0, 1);
    this.cameraForward.normalize();
    this.cameraRight.copy(this.cameraForward).cross(UP).normalize();

    const strafe = this.input.axis("left", "right");
    const advance = this.input.axis("back", "forward");
    const neutral = strafe === 0 && advance === 0;

    if (neutral) {
      this.lockedBasis = null;
    }
    this.inputWasNeutral = neutral;

    if (this.input.wasPressed("quickTurn") && !this.isQuickTurning) {
      this.beginQuickTurn();
    }

    if (this.isQuickTurning) {
      this.advanceQuickTurn(dt);
      this.mover.update(this.desiredVelocity.set(0, 0, 0), dt);
      return;
    }

    if (this.scheme === "tank") {
      this.updateTank(dt, strafe, advance);
    } else {
      this.updateCameraRelative(dt, strafe, advance);
    }

    this.mover.update(this.desiredVelocity, dt);
  }

  private updateTank(dt: number, strafe: number, advance: number): void {
    // Facing +Z at yaw 0; turning right sweeps toward -X, so yaw decreases.
    this.yaw -= strafe * this.config.turnSpeed * dt;

    const running = this.input.isHeld("run") && advance > 0;
    const speed =
      (running ? this.config.runSpeed : this.config.walkSpeed) *
      (advance < 0 ? this.config.backpedalFactor : 1);

    this.moveDirection.set(Math.sin(this.yaw), 0, Math.cos(this.yaw));
    this.desiredVelocity.copy(this.moveDirection).multiplyScalar(advance * speed);
  }

  private updateCameraRelative(dt: number, strafe: number, advance: number): void {
    const basis = this.lockedBasis ?? { forward: this.cameraForward, right: this.cameraRight };

    this.moveDirection
      .copy(basis.right)
      .multiplyScalar(strafe)
      .addScaledVector(basis.forward, advance);

    if (this.moveDirection.lengthSq() < 1e-8) {
      this.desiredVelocity.set(0, 0, 0);
      return;
    }
    this.moveDirection.normalize();

    const targetYaw = Math.atan2(this.moveDirection.x, this.moveDirection.z);
    const delta = angleDelta(this.yaw, targetYaw);
    this.yaw += delta * (1 - Math.pow(this.config.facingSmoothing, dt));

    const running = this.input.isHeld("run");
    const speed = running ? this.config.runSpeed : this.config.walkSpeed;
    this.desiredVelocity.copy(this.moveDirection).multiplyScalar(speed);
  }

  private beginQuickTurn(): void {
    this.quickTurnFrom = this.yaw;
    this.quickTurnTo = this.yaw + Math.PI;
    this.quickTurnRemaining = this.config.quickTurnDuration;
  }

  private advanceQuickTurn(dt: number): void {
    this.quickTurnRemaining = Math.max(0, this.quickTurnRemaining - dt);
    const t = 1 - this.quickTurnRemaining / this.config.quickTurnDuration;
    // Ease-out so the spin lands with weight instead of stopping dead.
    const eased = 1 - Math.pow(1 - t, 3);
    this.yaw = this.quickTurnFrom + (this.quickTurnTo - this.quickTurnFrom) * eased;
    if (this.quickTurnRemaining === 0) this.yaw = this.quickTurnTo;
  }

  toggleScheme(): void {
    this.scheme = this.scheme === "tank" ? "cameraRelative" : "tank";
    this.lockedBasis = null;
  }
}
