var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { AnimationClip, AnimationMixer, LoopOnce, LoopRepeat } from "three";
import { AnimationUtils } from "../engine/engine_animation.js";
import { Mathf } from "../engine/engine_math.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getParam } from "../engine/engine_utils.js";
import { Behaviour } from "./Component.js";
const debug = getParam("debuganimation");
class Vec2 {
    x;
    y;
}
/**
 * Animation component to play animations on a GameObject.
 * For simpler animation needs compared to {@link Animator}, this component directly
 * plays AnimationClips without state machine logic.
 *
 * **Key features:**
 * - Play animations by index, name, or clip reference
 * - Cross-fade between animations with `fadeDuration`
 * - Loop or play once with optional clamping
 * - Random start time and speed variation
 * - Promise-based completion handling
 *
 *
 * ![](https://cloud.needle.tools/-/media/zXQhLgtxr5ZaxLDTDb3MXA.gif)
 *
 * @example Play animation by name
 * ```ts
 * const anim = this.gameObject.getComponent(Animation);
 * await anim?.play("Walk", { loop: true, fadeDuration: 0.3 });
 * ```
 *
 * @example Play with options
 * ```ts
 * anim?.play(0, {
 *   loop: false,
 *   clampWhenFinished: true,
 *   speed: 2
 * });
 * ```
 *
 * @summary Plays animations from AnimationClips
 * @category Animation and Sequencing
 * @group Components
 * @see {@link Animator} for state machine-based animation
 * @see {@link PlayOptions} for all playback options
 * @link https://engine.needle.tools/samples/?overlay=samples&tag=animation
 * @link https://engine.needle.tools/samples/imunogard/
 *
 * @link https://engine.needle.tools/docs/blender/animation.html
 *
 * ![](https://cloud.needle.tools/-/media/vAYv-kU-eMpICqQZHJktCA.gif)
 *
 */
export class Animation extends Behaviour {
    get isAnimationComponent() { return true; }
    addClip(clip) {
        if (!this.animations)
            this.animations = [];
        if (!this.animations.includes(clip))
            this.animations.push(clip);
    }
    /**
     * If true, the animation will start playing when the component is enabled
     */
    playAutomatically = true;
    /**
     * If true, the animation will start at a random time. This is used when the animation component is enabled
     * @default false
     */
    randomStartTime = false;
    /**
     * The animation min-max speed range
     * @default undefined
     */
    minMaxSpeed;
    /**
     * The normalized offset to start the animation at. This will override startTime
     * @default undefined
     */
    minMaxOffsetNormalized;
    /**
     * Set to true to loop the animation
     * @default true
     */
    loop = true;
    /**
     * If true, the animation will clamp when finished
     */
    clampWhenFinished = false;
    /**
     * The time in seconds of the first running animation action
     * @default 0
     */
    get time() {
        if (this.actions) {
            for (const action of this.actions) {
                if (action.isRunning())
                    return action.time;
            }
        }
        return 0;
    }
    set time(val) {
        if (this.actions) {
            for (const act of this.actions) {
                act.time = val;
            }
        }
    }
    get duration() {
        if (this.actions) {
            for (const action of this.actions) {
                if (action.isRunning())
                    return action.getClip().duration;
            }
        }
        return 0;
    }
    _tempAnimationClipBeforeGameObjectExisted = null;
    /**
     * Get the first animation clip in the animations array
     */
    get clip() {
        return this.animations?.length ? this.animations[0] : null;
    }
    /**
     * Set the first animation clip in the animations array
     */
    set clip(val) {
        if (!this.__didAwake) {
            if (debug)
                console.warn("Assign clip during serialization", val);
            this._tempAnimationClipBeforeGameObjectExisted = val;
            return;
        }
        if (!val)
            return;
        // if (debug) console.log("Assign clip", val, Boolean(this.gameObject));
        if (!this.gameObject.animations)
            this.gameObject.animations = [];
        if (this.animations.includes(val))
            return;
        if (this.animations.length > 0) {
            this.animations.splice(0, 0, val);
        }
        else
            this.animations.push(val);
    }
    set clips(animations) {
        this.animations = animations;
    }
    _tempAnimationsArray;
    set animations(animations) {
        if (animations === null || animations === undefined || !Array.isArray(animations))
            return;
        if (this.gameObject)
            this.gameObject.animations = animations;
        else {
            this._tempAnimationsArray = animations;
        }
    }
    get animations() {
        return this.gameObject?.animations || this._tempAnimationsArray || [];
    }
    mixer = undefined;
    /**
     * The animation actions
     */
    get actions() {
        return this._actions;
    }
    set actions(val) {
        this._actions = val;
    }
    _actions;
    _handles;
    /** @internal */
    awake() {
        this.mixer = undefined;
        if (debug)
            console.log("Animation Awake", this.name, this);
        if (this._tempAnimationsArray) {
            this.animations = this._tempAnimationsArray;
            this._tempAnimationsArray = undefined;
        }
        if (this._tempAnimationClipBeforeGameObjectExisted) {
            this.clip = this._tempAnimationClipBeforeGameObjectExisted;
            this._tempAnimationClipBeforeGameObjectExisted = null;
        }
        // actions need to reset (e.g. if the animation component was duplicated this array must not contain previous content)
        this.actions = [];
        this._handles = [];
    }
    /** @internal */
    onEnable() {
        if (this.playAutomatically && this.animations?.length > 0) {
            const index = Math.floor(Math.random() * this.animations.length);
            const animation = this.animations[index];
            this.play(index, {
                exclusive: true,
                fadeDuration: 0,
                startTime: this.randomStartTime ? Math.random() * animation.duration : 0,
                loop: this.loop,
                clampWhenFinished: this.clampWhenFinished
            });
        }
    }
    /** @internal */
    update() {
        if (!this.mixer)
            return;
        this.mixer.update(this.context.time.deltaTime);
        this._handles.forEach(h => h.update());
    }
    /** @internal */
    onDisable() {
        if (this.mixer) {
            this.mixer.stopAllAction();
        }
    }
    /** @internal */
    onDestroy() {
        this.context.animations.unregisterAnimationMixer(this.mixer);
    }
    /** Get an animation action by the animation clip name */
    getAction(name) {
        return this.actions?.find(a => a.getClip().name === name) || null;
    }
    /** Is any animation playing? */
    get isPlaying() {
        if (this.actions) {
            for (let i = 0; i < this.actions.length; i++) {
                if (this.actions[i].isRunning())
                    return true;
            }
        }
        return false;
    }
    /** Stops all currently playing animations */
    stopAll(opts) {
        if (this.actions) {
            for (const act of this.actions) {
                if (opts?.fadeDuration) {
                    act.fadeOut(opts.fadeDuration);
                }
                else {
                    act.stop();
                }
            }
        }
    }
    /**
     * Stops a specific animation clip or index. If clip is undefined then all animations will be stopped
     */
    stop(clip, opts) {
        if (clip === undefined) {
            this.stopAll();
            return;
        }
        else if (typeof clip === "number") {
            if (clip >= this.animations.length) {
                if (debug)
                    console.log("No animation at index", clip);
                return;
            }
            clip = this.animations[clip];
        }
        else if (typeof clip === "string") {
            clip = this.animations.find(a => a.name === clip);
        }
        if (!clip) {
            console.error("Could not find clip", clip);
            return;
        }
        const act = this.actions.find(a => a.getClip() === clip);
        if (!act) {
            console.error("Could not find action", clip);
            return;
        }
        if (opts?.fadeDuration) {
            act.fadeOut(opts.fadeDuration);
        }
        else {
            act.stop();
        }
    }
    /**
     * Pause all animations or a specific animation clip or index
     * @param clip optional animation clip, index or name, if undefined all animations will be paused
     * @param unpause if true, the animation will be resumed
     */
    pause(clip, unpause = false) {
        if (clip === undefined) {
            for (const act of this.actions) {
                act.paused = !unpause;
            }
            return;
        }
        else if (typeof clip === "number") {
            if (clip >= this.animations.length) {
                if (debug)
                    console.log("No animation at index", clip);
                return;
            }
            clip = this.animations[clip];
        }
        else if (typeof clip === "string") {
            clip = this.animations.find(a => a.name === clip);
        }
        if (!clip) {
            console.error("Could not find clip", clip);
            return;
        }
        const act = this.actions.find(a => a.getClip() === clip);
        if (!act) {
            console.error("Could not find action", clip);
            return;
        }
        act.paused = !unpause;
    }
    /**
     * Resume all paused animations.
     * Note that this will not fade animations in or out and just unpause previous animations. If an animation was faded out which means it's not running anymore, it will not be resumed.
     */
    resume() {
        for (const act of this.actions) {
            act.paused = false;
        }
    }
    /**
     * Play an animation clip or an clip at the specified index.
     * @param clipOrNumber the animation clip, index or name to play. If undefined, the first animation in the animations array will be played
     * @param options the play options. Use to set the fade duration, loop, speed, start time, end time, clampWhenFinished
     * @returns a promise that resolves when the animation is finished (note that it will not resolve if the animation is looping)
     */
    play(clipOrNumber = 0, options) {
        if (debug)
            console.log("PLAY", clipOrNumber);
        this.ensureMixer();
        if (!this.mixer) {
            if (debug)
                console.warn("Missing mixer", this);
            return;
        }
        if (clipOrNumber === undefined)
            clipOrNumber = 0;
        let clip = clipOrNumber;
        if (typeof clipOrNumber === 'number') {
            if (clipOrNumber >= this.animations.length) {
                if (debug)
                    console.log("No animation at index", clipOrNumber);
                return;
            }
            clip = this.animations[clipOrNumber];
        }
        else if (typeof clipOrNumber === "string") {
            clip = this.animations.find(a => a.name === clipOrNumber);
        }
        if (!clip) {
            console.error("Could not find clip", clipOrNumber);
            return;
        }
        if (!options)
            options = {};
        for (const act of this.actions) {
            if (act.getClip() === clip) {
                return this.internalOnPlay(act, options);
            }
        }
        if (!clip.tracks) {
            console.warn("Clip is no AnimationClip", clip);
            return;
        }
        const act = this.mixer.clipAction(clip);
        this.actions.push(act);
        return this.internalOnPlay(act, options);
    }
    internalOnPlay(action, options) {
        var existing = this.actions.find(a => a === action);
        if (existing === action && existing.isRunning() && existing.time < existing.getClip().duration) {
            const handle = this.tryFindHandle(action);
            if (existing.paused) {
                existing.paused = false;
            }
            if (handle)
                return handle.waitForFinish();
        }
        // Assign defaults
        if (options.loop === undefined)
            options.loop = this.loop;
        if (options.clampWhenFinished === undefined)
            options.clampWhenFinished = this.clampWhenFinished;
        if (options.minMaxOffsetNormalized === undefined && this.randomStartTime)
            options.minMaxOffsetNormalized = this.minMaxOffsetNormalized;
        if (options.minMaxSpeed === undefined)
            options.minMaxSpeed = this.minMaxSpeed;
        // Reset currently running animations
        const stopOther = options?.exclusive ?? true;
        if (stopOther) {
            for (const act of this.actions) {
                if (act != existing) {
                    if (options.fadeDuration) {
                        act.fadeOut(options.fadeDuration);
                    }
                    else {
                        act.stop();
                    }
                }
            }
        }
        if (options?.fadeDuration) {
            action.fadeIn(options.fadeDuration);
        }
        action.enabled = true;
        // Apply start time
        if (options?.startTime != undefined) {
            action.time = options.startTime;
        }
        // Only apply random start offset if it's not 0:0 (default). Otherwise `play` will not resume paused animations but instead restart them
        else if (options?.minMaxOffsetNormalized && options.minMaxOffsetNormalized.x != 0 && options.minMaxOffsetNormalized.y != 0) {
            const clip = action.getClip();
            action.time = Mathf.lerp(options.minMaxOffsetNormalized.x, options.minMaxOffsetNormalized.y, Math.random()) * clip.duration;
        }
        // If the animation is at the end, reset the time
        else if (action.time >= action.getClip().duration) {
            action.time = 0;
        }
        // Apply speed
        if (options?.minMaxSpeed) {
            action.timeScale = Mathf.lerp(options.minMaxSpeed.x, options.minMaxSpeed.y, Math.random());
        }
        else {
            action.timeScale = options?.speed ?? 1;
        }
        // Apply looping
        if (options?.loop != undefined) {
            action.loop = options.loop ? LoopRepeat : LoopOnce;
        }
        else
            action.loop = LoopOnce;
        if (options?.clampWhenFinished) {
            action.clampWhenFinished = true;
        }
        action.paused = false;
        action.play();
        window.requestAnimationFrame(() => AnimationUtils.testIfRootCanAnimate(action));
        if (debug)
            console.log("PLAY", action.getClip().name, action);
        const handle = new AnimationHandle(action, this.mixer, options, _ => {
            this._handles.splice(this._handles.indexOf(handle), 1);
        });
        this._handles.push(handle);
        return handle.waitForFinish();
    }
    tryFindHandle(action) {
        for (const handle of this._handles) {
            if (handle.action === action)
                return handle;
        }
        return undefined;
    }
    ensureMixer() {
        if (!this.mixer) {
            // try getting the animation mixer from the assigned gameobject
            const key = "animationMixer";
            if (this.gameObject[key]) {
                this.mixer = this.gameObject[key];
            }
            if (!this.mixer || !this.mixer.clipAction) {
                this.mixer = new AnimationMixer(this.gameObject);
                this.gameObject[key] = this.mixer;
            }
        }
        this.context.animations.registerAnimationMixer(this.mixer);
    }
}
__decorate([
    serializable()
], Animation.prototype, "playAutomatically", void 0);
__decorate([
    serializable()
], Animation.prototype, "randomStartTime", void 0);
__decorate([
    serializable(Vec2)
], Animation.prototype, "minMaxSpeed", void 0);
__decorate([
    serializable(Vec2)
], Animation.prototype, "minMaxOffsetNormalized", void 0);
__decorate([
    serializable()
], Animation.prototype, "loop", void 0);
__decorate([
    serializable()
], Animation.prototype, "clampWhenFinished", void 0);
__decorate([
    serializable(AnimationClip)
], Animation.prototype, "clips", null);
class AnimationHandle {
    mixer;
    action;
    promise = null;
    _options;
    _resolveCallback = null;
    _resolvedOrRejectedCallback;
    constructor(action, mixer, opts, onDone) {
        this.action = action;
        this.mixer = mixer;
        this._resolvedOrRejectedCallback = onDone;
        this._options = opts;
    }
    waitForFinish() {
        if (this.promise)
            return this.promise;
        this.promise = new Promise((res) => {
            this._resolveCallback = res;
        });
        // this.mixer.addEventListener('loop', this.onLoop);
        this.mixer.addEventListener('finished', this.onFinished);
        return this.promise;
    }
    update() {
        if (!this._options)
            return;
        if (this._options.endTime !== undefined && this.action.time > this._options.endTime) {
            if (this._options.loop === true) {
                this.action.time = this._options.startTime ?? 0;
            }
            else {
                this.action.time = this._options.endTime;
                this.action.timeScale = 0;
                this.onResolve();
            }
        }
    }
    onResolve() {
        this.dispose();
        this._resolvedOrRejectedCallback?.call(this, this);
        this._resolveCallback?.call(this, this.action);
    }
    // private onLoop = (_evt: MixerEvent) => {
    // }
    onFinished = (evt) => {
        if (evt.action === this.action) {
            this.onResolve();
        }
    };
    dispose() {
        // this.mixer.removeEventListener('loop', this.onLoop);
        this.mixer.removeEventListener('finished', this.onFinished);
    }
}
//# sourceMappingURL=Animation.js.map