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 { Behaviour } from "./Component.js";

/**
 * SmoothFollow makes the {@link Object3D} (`GameObject`) smoothly follow another target {@link Object3D}.  
 * It can follow the target's position, rotation, or both.   
 * @category Interactivity
 * @group Components
 */
export class SmoothFollow extends Behaviour {

    /**
     * The target to follow. If null, the GameObject will not move.
     */
    @serializable(Object3D)
    target: Object3D | null = null;

    /**
     * The factor to smoothly follow the target's position.  
     * The value is clamped between 0 and 1. If 0, the GameObject will not follow the target's position.  
     */
    @serializable()
    followFactor = .1;
    /**
     * The factor to smoothly follow the target's rotation.   
     * The value is clamped between 0 and 1. If 0, the GameObject will not follow the target's rotation.
     */
    @serializable()
    rotateFactor = .1;

    @serializable()
    positionAxes: Axes = Axes.All;

    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);
    }

    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;
    }
}