
import { Object3D, Vector3 } from "three"

import { Mathf } from "../../engine/engine_math.js";
import { serializeable } from "../../engine/engine_serialization_decorator.js";
import { Behaviour } from "../Component.js";
import { SplineContainer } from "./Spline.js";

/**
 * [SplineWalker](https://engine.needle.tools/docs/api/SplineWalker) Moves an object along a {@link SplineContainer}.   
 * Use this with a SplineContainer component.  
 * 
 * ![](https://cloud.needle.tools/-/media/XIHaiNFsA1IbMZVJepp1aQ.gif)
 * 
 * - Example http://samples.needle.tools/splines
 * 
 * @summary Moves an object along a spline
 * @category Splines
 * @group Components
 */
export class SplineWalker extends Behaviour {

    /**
     * The spline to use/walk along. Add a SplineContainer component to an object and assign it here.
     */
    @serializeable(SplineContainer)
    spline: SplineContainer | null = null;

    /** 
     * The object to move along the spline.   
     * If object is undefined then the spline walker will use it's own object (gameObject).  
     * If object is null the spline walker will not move any object.
     * @default undefined
    */
    @serializeable(Object3D)
    object?: Object3D | null = undefined;



    /**
     * If true the object will rotate to look in the direction of the spline while moving along it.
     * @default true
     */
    @serializeable()
    useLookAt = true;

    /** 
     * The object to look at while moving along the spline.   
     * If null the object will look in the direction of the spline.   
     * This can be disabled by setting useLookAt to false.
     * @default null
    */
    @serializeable(Object3D)
    lookAt: Object3D | null = null;



    /**
     * When clamp is set to true, the position01 value will be clamped between 0 and 1 and the object will not loop the spline.
     * @default false
     */
    @serializeable()
    clamp: boolean = false;

    /**   
     * The current position on the spline. The value ranges from 0 (start of the spline curve) to 1 (end of the spline curve)  
     * 
     * When setting this value, the position will be updated in the next frame.
     * @default 0
     */
    // @type float
    @serializeable()
    get position01(): number {
        return this._position01;
    }
    set position01(v: number) {
        this._position01 = v;
        this._needsUpdate = true;
    }

    /** Resets the position to 0 */
    reset() {
        this._position01 = 0;
    }

    /**
     * If true the SplineWalker will automatically move along the spline
     * @default true
     */
    @serializeable()
    autoRun: boolean = true;

    /**
     * The duration in seconds it takes to complete the whole spline when autoWalk is enabled.
     * @default 10
     */
    @serializeable()
    duration: number = 10;

    /**
     * The strength with which the object is pulled to the spline.  
     * This can be used to create a "rubber band" effect when the object is moved away from the spline by other forces.  
     * A value of 0 means no pull, a value of 1 means the object is always on the spline.  
     * @default 1
     */
    pullStrength: number = 1;


    // #region internal

    private _position01: number = 0;
    private _needsUpdate = false;

    /** @internal */
    start() {
        if (this.object === undefined) this.object = this.gameObject;
        this.updateFromPosition();
    }

    /** @internal */
    onEnable(): void {
        window.addEventListener("pointerdown", this.onUserInput, { passive: true });
        // TODO: wheel event is also triggered for touch and it interrupts spline pull if it's an actual site scroll
        this.context.domElement.addEventListener("wheel", this.onUserInput, { passive: true });
    }
    /** @internal */
    onDisable(): void {
        window.removeEventListener("pointerdown", this.onUserInput);
        this.context.domElement.removeEventListener("wheel", this.onUserInput);
    }
    private onUserInput = () => {
        if (this.object?.contains(this.context.mainCamera)) {
            this._needsUpdate = false;
            this._performedUpdates += 999;
        }
    }

    /** @internal */
    update() {
        if (this.autoRun) {
            this._needsUpdate = true;
            this._position01 += this.context.time.deltaTime / this.duration;
        }

        if (this._needsUpdate) {
            this._needsUpdate = false;
            this.updateFromPosition();
        }
    }

    /**
     * Updates the position of the object based on the current position01 value.  
     * @internal
     */
    private updateFromPosition() {
        if (!this.spline || !this.spline.curve) return;
        if (!this.object) return;

        if (this.clamp) this._position01 = Mathf.clamp01(this._position01);
        else this._position01 = this._position01 % 1;

        const t = this._position01 >= 1 ? 1 : this._position01 % 1;
        const pt = this.spline.getPointAt(t);

        if (this.pullStrength >= 1) {
            this.object.worldPosition = pt;
        }
        else {
            if (this._position01 !== this._lastPosition01) {
                this._performedUpdates = 0;
            }
            this._requiredUpdates = Math.round(100 / this.pullStrength);
            if (this._performedUpdates < this._requiredUpdates) {
                const wp = this.object.worldPosition;
                this._performedUpdates++;
                const pull = Mathf.clamp01(this.pullStrength);
                const newPosition = this.object.worldPosition = wp.lerp(pt, pull * (this.context.time.deltaTime / .3));
                this._lastPositionVector.copy(newPosition);
                this._needsUpdate = true;
            }
        }

        if (this.useLookAt) {
            if (!this.lookAt) {
                const tan = this.spline.getTangentAt(t);
                this.object.lookAt(pt.add(tan));
            }
            else this.object.lookAt(this.lookAt.worldPosition);
        }

        this._lastPosition01 = this._position01;
    }

    private _lastPosition01 = 0;
    private _requiredUpdates: number = 0;
    private _performedUpdates: number = 0;
    private _lastPositionVector = new Vector3();
}
