import { Object3D, Quaternion, Vector3 } from "three";

import { Mathf } from "../engine/engine_math.js";
import { Axes } from "../engine/engine_physics.types.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getWorldPosition, getWorldQuaternion } from "../engine/engine_three_utils.js";
import type { LookAt } from "./api.js";
import { Behaviour } from "./Component.js";

/**
 * [SmoothFollow](https://engine.needle.tools/docs/api/SmoothFollow) makes this GameObject smoothly follow another target object's position and/or rotation.  
 *
 * **Position Following:**  
 * When enabled (`followFactor > 0`), this object will move towards the target's world position.  
 * The object interpolates from its current position to the target's position each frame.  
 * Use `positionAxes` to restrict following to specific axes (e.g., only horizontal movement).  
 *
 * **Rotation Following:**  
 * When enabled (`rotateFactor > 0`), this object will rotate to match the target's world rotation.  
 * The object smoothly interpolates from its current rotation to the target's rotation each frame.  
 * This makes the object face the same direction as the target, not look at it (use {@link LookAt} for that).  
 *
 * **Smoothing:**  
 * Both position and rotation use time-based interpolation (lerp/slerp).  
 * Higher factor values = faster following (less lag), lower values = slower following (more lag).  
 * Set a factor to 0 to disable that type of following entirely.  
 *
 * **Common Use Cases:**  
 * - Camera following a player character  
 * - UI elements tracking world objects  
 * - Delayed motion effects (ghost trails, spring arms)  
 * - Smoothed object attachment  
 *
 * @example Follow a target with smooth position
 * ```ts
 * const follower = myObject.addComponent(SmoothFollow);
 * follower.target = playerObject;
 * follower.followFactor = 5;  // Higher = faster following
 * follower.rotateFactor = 0;  // Don't follow rotation
 * ```
 *
 * @example Follow only on horizontal plane
 * ```ts
 * follower.positionAxes = Axes.X | Axes.Z; // Follow X and Z only (no vertical)
 * ```
 *
 * @example Follow both position and rotation
 * ```ts
 * follower.target = targetObject;
 * follower.followFactor = 3;  // Smooth position following
 * follower.rotateFactor = 2;  // Smooth rotation following
 * ```
 *
 * @summary Smoothly follows a target object's position and/or rotation
 * @category Interactivity
 * @group Components
 * @see {@link Mathf} for the interpolation used
 */
export class SmoothFollow extends Behaviour {

    /**
     * The target to follow. If null, the GameObject will not move.
     */
    @serializable(Object3D)
    target: Object3D | null = null;

    /**
     * Speed factor for position following.
     * Controls how quickly this object moves to match the target's position.
     * Higher values = faster/tighter following (less lag), lower = slower/looser (more lag).
     * Set to 0 to disable position following entirely.
     * @default 0.1
     */
    @serializable()
    followFactor: number = .1;

    /**
     * Speed factor for rotation following.
     * Controls how quickly this object rotates to match the target's rotation.
     * Higher values = faster/tighter following (less lag), lower = slower/looser (more lag).
     * Set to 0 to disable rotation following entirely.
     * @default 0.1
     */
    @serializable()
    rotateFactor: number = .1;

    /**
     * Which position axes to follow. Use bitwise OR to combine:
     * `Axes.X | Axes.Y` follows only X and Y axes.
     * @default Axes.All
     */
    @serializable()
    positionAxes: Axes = Axes.All;

    /** When true, rotates 180° around Y axis (useful for mirrored setups) */
    flipForward: boolean = false;

    private static _invertForward: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
    private _firstUpdate = true;

    /**
     * Update the position and rotation of the GameObject to follow the target.
     */
    onBeforeRender(): void {
        this.updateNow(false);
    }

    /**
     * Manually update the position/rotation to follow the target.
     * @param hard If true, snaps instantly to target without smoothing
     */
    updateNow(hard: boolean) {
        if (!this.target || this.target === this.gameObject) return;
        if (this.followFactor > 0) {
            const wp = getWorldPosition(this.target);
            const fpos = this._firstUpdate || hard ? 1 : Mathf.clamp01(this.context.time.deltaTime * this.followFactor);
            const currentPosition = this.worldPosition;

            if (this.positionAxes & Axes.X) currentPosition.x = Mathf.lerp(currentPosition.x, wp.x, fpos);
            if (this.positionAxes & Axes.Y) currentPosition.y = Mathf.lerp(currentPosition.y, wp.y, fpos);
            if (this.positionAxes & Axes.Z) currentPosition.z = Mathf.lerp(currentPosition.z, wp.z, fpos);

            // TODO lerp distance from target as well

            this.worldPosition = currentPosition;
        }
        if (this.rotateFactor > 0) {
            const wr = getWorldQuaternion(this.target);
            if (this.flipForward) {
                wr.premultiply(SmoothFollow._invertForward);
            }
            const frot = this._firstUpdate || hard ? 1 : Mathf.clamp01(this.context.time.deltaTime * this.rotateFactor);

            this.worldQuaternion = this.worldQuaternion.slerp(wr, frot);
        }
        this._firstUpdate = false;
    }
}