import { Object3D, Vector3 } from "three";
import { CameraC_internal, UpdateController } from "@hitplay/playable_template";

// How fast the camera closes the gap to its target position, per second.
// Higher = snappier, lower = floatier. Framerate-independent (see the
// exponential smoothing in update()), so the feel is the same at 30fps and
// 60fps instead of drifting with frame time.
const POSITION_DAMPING = 5;

// How far (world units) the camera's target position pans in the direction
// the character is actually TRANSLATING — a fixed-length nudge, not scaled
// by speed, so it's the full offset the instant they're moving at all and
// zero the instant they stop. Deliberately derived from position deltas,
// not facing/quaternion: that was the earlier version, and even damped, a
// vector tied to facing rotates right along with a stationary turn (e.g.
// pivoting to face a crate) and drags the camera along an arc. A vector
// tied to actual movement can't do that — turning in place is zero
// movement, so it's zero look-ahead, however the character is turning.
//
// Bigger than the old facing-based version's 1.2 — that version's visible
// "shift" came from two effects stacked together (the position nudge AND
// camera.lookAt() re-aiming toward the shifted point every frame); with
// rotation now completely fixed, only the position nudge is left, so it
// has to move further on its own to read as a comparable shift from a
// camera sitting ~12.7 units away (the (0,9,-9) rig offset).
const LOOK_AHEAD_DISTANCE = 1.5;

// Smooths transitions in and out of the look-ahead pan (starting/stopping)
// so it doesn't snap. Separate from, and slower than, the position damping
// below — see update(). Slower than before (was 2) per feedback: the ease
// itself needed to read more gradual.
const LOOK_AHEAD_DAMPING = 1.2;

// Below this much squared movement in a single frame, treat the character
// as stationary — guards against floating-point noise flickering a tiny
// direction in and out while genuinely standing still.
const MOVEMENT_EPSILON_SQ = 0.0005 * 0.0005;

export class CameraFollowC {
  private static target: Object3D | null = null;
  private static offset = new Vector3();
  private static previousTargetPosition = new Vector3();
  // The last direction actually walked in, held indefinitely — see update()
  // for why this must never get reset to zero on its own.
  private static desiredLookAhead = new Vector3();
  private static smoothedLookAhead = new Vector3();
  // Injected (avoids importing PlayerC directly) — see setFacingTargetGetter.
  private static facingTargetGetter: (() => Vector3 | null) | null = null;

  static init(target: Object3D, offset: Vector3) {
    this.target = target;
    this.offset = offset.clone();
    this.previousTargetPosition.copy(target.position);
    this.desiredLookAhead.set(0, 0, 0);
    this.smoothedLookAhead.set(0, 0, 0);

    // The camera's angle is set once, here, from the actual offset geometry
    // — not left at whatever camera_rotation_p/_l happens to hold, since
    // that config value was never actually tuned to work standalone (the
    // old per-frame lookAt() always overwrote it immediately). update()
    // below never touches rotation again after this — camera.position is
    // the only thing it ever moves, in a straight line toward a single
    // target point (see chat: no facing-based pivoting, just two points
    // and a line between them).
    const camera = CameraC_internal.getCamera();
    camera.position.copy(target.position).add(this.offset);
    camera.lookAt(target.position);

    UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
  }

  // Lets the look-ahead react to the character locking onto a nearby
  // breakable while stopped (see PlayerC.getFacingTarget) — that rotation
  // carries zero movement, so it would otherwise be invisible to the
  // movement-only look-ahead below. Deliberately fed a fixed world POINT,
  // not the character's facing/quaternion: a point doesn't rotate as the
  // body turns to face it, so easing toward it is a single linear shift,
  // not the arc/orbit a facing-tied vector produced (see chat).
  static setFacingTargetGetter(getter: () => Vector3 | null) {
    this.facingTargetGetter = getter;
  }

  private static update(delta: number) {
    if (!this.target) return;

    const camera = CameraC_internal.getCamera();
    const targetPosition = this.target.position;

    const movement = targetPosition.clone().sub(this.previousTargetPosition);
    this.previousTargetPosition.copy(targetPosition);
    movement.y = 0;

    const facingTarget = this.facingTargetGetter ? this.facingTargetGetter() : null;

    if (facingTarget) {
      // Locked onto a nearby breakable — pan toward that fixed point
      // instead of a movement delta (there isn't one; the character is
      // stopped). See setFacingTargetGetter for why a point is safe here
      // where facing/quaternion wasn't.
      const toTarget = facingTarget.clone().sub(targetPosition);
      toTarget.y = 0;
      if (toTarget.lengthSq() > 1e-6) {
        this.desiredLookAhead.copy(toTarget.normalize().multiplyScalar(LOOK_AHEAD_DISTANCE));
      }
    } else if (movement.lengthSq() > MOVEMENT_EPSILON_SQ) {
      // Only updates the held direction while actually translating — when
      // the character stops (and isn't locked onto a target either), this
      // is simply skipped, so the pan holds at wherever it last settled
      // instead of easing back to center (see chat: "камера після зсуву не
      // має повертатись назад... тільки якщо він змінить напрям"). It only
      // moves again once movement resumes in a different direction.
      this.desiredLookAhead.copy(movement).normalize().multiplyScalar(LOOK_AHEAD_DISTANCE);
    }

    const lookAheadT = 1 - Math.exp(-LOOK_AHEAD_DAMPING * delta);
    this.smoothedLookAhead.lerp(this.desiredLookAhead, lookAheadT);

    const desiredPosition = targetPosition.clone().add(this.offset).add(this.smoothedLookAhead);

    // Exponential (framerate-independent) damping instead of a fixed lerp
    // factor per frame — a fixed factor changes speed with the frame rate
    // and reads as jerky; this keeps the same feel regardless of fps.
    const positionT = 1 - Math.exp(-POSITION_DAMPING * delta);
    camera.position.lerp(desiredPosition, positionT);

    // Rotation is deliberately never touched here (no lookAt) — this is a
    // linear-shift follow camera, not an orbiting one. The angle is fixed
    // once in init() and never changes afterward.
  }
}
