import { CameraC_internal, JoystickC, ThreeC_internal, UpdateController } from "@24tools/playable_template";
import { AnimationAction, AnimationMixer, Object3D, Vector3 } from "three";
import { Body } from "cannon-es";
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";

const ANIM_NAMES: Record<string, string[]> = {
  idle: ["idle", "Idle", "IDLE", "stand", "Stand"],
  walk: ["walk", "Walk", "WALK", "walking", "Walking"],
  run:  ["run",  "Run",  "RUN",  "running", "Running", "sprint", "Sprint"],
};

enum MoveState { Idle = "idle", Walk = "walk", Run = "run" }

const CHAR_RADIUS = 0.35;

const _inputTarget = new Vector3();

export class PlayerC {
  static maxSpeed     = 4;
  static acceleration = 10;
  static rotateSpeed  = 8;

  private static mesh: Object3D;
  private static body: Body;                 // cannon physics body (collision + gravity)
  private static batInHand: Object3D | null = null;  // GLB node "Tool_1" — bat held in the hand
  private static batOnBack: Object3D | null = null;  // GLB node "Tool_2" — bat holstered on the back
  private static mixer: AnimationMixer;
  private static actions       = new Map<string, AnimationAction>();
  private static state         = MoveState.Idle;
  private static currentAction: AnimationAction | null = null;

  // Desired planar velocity (x/z). The body's y is left to gravity.
  static velocity  = new Vector3();
  private static inputDir = new Vector3();

  private static _camForward = new Vector3();
  private static _camRight   = new Vector3();
  private static _worldUp    = new Vector3(0, 1, 0);

  static init(mesh: Object3D) {
    this.mesh = mesh;
    this.createBody();
    this.setupWeapon();
    this.setupAnimations();
    this.setupJoystick();
    UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
  }

  // Toggle the bat between the back (idle/normal) and the hand (attack).
  // Call setBatInHand(true) when the attack animation starts, and
  // setBatInHand(false) when it ends.
  static setBatInHand(inHand: boolean) {
    if (this.batInHand) this.batInHand.visible = inHand;
    if (this.batOnBack) this.batOnBack.visible = !inHand;
  }

  // ── Private ────────────────────────────────────────────────────────────────

  private static setupWeapon() {
    this.batInHand = this.mesh.getObjectByName("Tool_1") ?? null;
    this.batOnBack = this.mesh.getObjectByName("Tool_2") ?? null;
    this.setBatInHand(false); // normal state: bat rests on the back
  }

  private static createBody() {
    // Sphere collider (PhysicsLayer.Player makes PhysicsBody use a Sphere shape).
    const pb = new PhysicsBody(
      this.mesh,
      false,                 // not a trigger
      1,                     // dynamic
      PhysicsLayer.Player,
      PhysicsLayer.Wall,     // collide with floor + boundary walls
      CHAR_RADIUS
    );
    this.body = pb.getPhysicsBody();

    // A character must not topple — keep it upright and let us steer it directly.
    this.body.fixedRotation = true;
    this.body.updateMassProperties();
    this.body.linearDamping = 0; // we set planar velocity explicitly every frame

    // Rest the sphere on the floor at the spawn point.
    this.body.position.set(this.mesh.position.x, CHAR_RADIUS + 0.05, this.mesh.position.z);
    this.body.velocity.set(0, 0, 0);
  }

  private static setupAnimations() {
    const gltf = ThreeC_internal.getMesh("character");
    this.mixer = new AnimationMixer(this.mesh);
    ThreeC_internal.addAnimMixer(this.mixer);
    if (gltf.animations?.length) {
      console.log("[PlayerC] Available animations:", gltf.animations.map(a => a.name));
      gltf.animations.forEach(clip => {
        this.actions.set(clip.name, this.mixer.clipAction(clip));
      });
    }
    // Play idle directly — transitionTo guards same-state calls so it would no-op here
    const idleAction = this.findAction(MoveState.Idle);
    if (idleAction) {
      idleAction.reset().play();
      this.currentAction = idleAction;
    }
  }

  private static setupJoystick() {
    JoystickC.onJoysticMove.addDelegate(({ event }) => {
      // @ts-ignore
      this.applyJoystickInput(event.data.vector.x, event.data.vector.y);
    });
    JoystickC.onJoysticEnd.addDelegate(() => {
      this.inputDir.set(0, 0, 0);
    });
  }

  private static applyJoystickInput(jx: number, jy: number) {
    const camera = CameraC_internal.camera;
    if (!camera) return;
    camera.getWorldDirection(this._camForward);
    this._camForward.y = 0;
    this._camForward.normalize();
    this._camRight.crossVectors(this._camForward, this._worldUp).normalize();
    this.inputDir
      .copy(this._camForward).multiplyScalar(jy)
      .addScaledVector(this._camRight, jx);

    // Rescale input magnitude with a curve.
    // exponent < 1 → light touch feels faster (e.g. 0.5 = sqrt)
    // exponent > 1 → more precision at low input, snappy at full push
    const CURVE = 0.5;
    const len = this.inputDir.length();
    if (len > 0.01) {
      this.inputDir.multiplyScalar(Math.pow(len, CURVE - 1));
      // equivalent to: newLen = len^CURVE, then normalize and scale
    }

    if (this.inputDir.lengthSq() > 1) this.inputDir.normalize();
  }

  private static update(delta: number) {
    // Smoothly ramp the desired planar velocity toward the input target.
    _inputTarget.copy(this.inputDir).multiplyScalar(this.maxSpeed);
    this.velocity.lerp(_inputTarget, Math.min(1, this.acceleration * delta));

    // Drive the body on x/z; leave y to gravity so the player rests on the
    // floor and cannon resolves collisions with the boundary walls (sliding
    // along them comes for free).
    this.body.velocity.x = this.velocity.x;
    this.body.velocity.z = this.velocity.z;

    // Sync the mesh to the body. The body origin is the sphere centre, so the
    // mesh (origin at the feet) is dropped by the radius.
    this.mesh.position.set(
      this.body.position.x,
      this.body.position.y - CHAR_RADIUS,
      this.body.position.z,
    );

    const speed = this.velocity.length();

    if (speed > 0.05) {
      const targetAngle = Math.atan2(this.velocity.x, this.velocity.z);
      const diff = ((targetAngle - this.mesh.rotation.y + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
      this.mesh.rotation.y += diff * Math.min(1, this.rotateSpeed * delta);
    }

    // Animation state machine
    if      (speed < 0.1)                  this.transitionTo(MoveState.Idle);
    else if (speed < this.maxSpeed * 0.6)  this.transitionTo(MoveState.Walk);
    else                                   this.transitionTo(MoveState.Run);

    // Proportional animation speed, normalized per-state so each state
    // goes from ~0.5x at entry to 1.0x at the top of its range.
    // This avoids the near-frozen look at low joystick input.
    if (this.currentAction && this.state !== MoveState.Idle) {
      const walkTop = this.maxSpeed * 0.6;
      const t = this.state === MoveState.Walk
        ? speed / walkTop                            // 0 → 1 within walk range
        : (speed - walkTop) / (this.maxSpeed - walkTop); // 0 → 1 within run range
      this.currentAction.setEffectiveTimeScale(0.4 + 0.3 * Math.min(1, t));
    }
  }

  private static findAction(state: MoveState): AnimationAction | null {
    for (const name of ANIM_NAMES[state]) {
      const action = this.actions.get(name);
      if (action) return action;
    }
    for (const [clipName, action] of this.actions) {
      if (clipName.toLowerCase().includes(state)) return action;
    }
    return null;
  }

  private static transitionTo(next: MoveState, fadeDuration = 0.2) {
    if (this.state === next) return;
    const action = this.findAction(next);
    if (!action) return;
    this.currentAction?.fadeOut(fadeDuration);
    action.reset().fadeIn(fadeDuration).play();
    this.currentAction = action;
    this.state = next;
  }
}
