import type { AnimationAction, AnimationActionLoopStyles, AnimationMixer } from "three";

import { Mathf } from "../engine/engine_math.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { IAnimationComponent } from "../engine/engine_types.js";
import { getParam } from "../engine/engine_utils.js";
import type { AnimatorControllerModel } from "../engine/extensions/NEEDLE_animator_controller_model.js";
import { getObjectAnimated } from "./AnimationUtils.js";
import { AnimatorController } from "./AnimatorController.js";
import { Behaviour } from "./Component.js";

const debug = getParam("debuganimator");


export declare class MixerEvent {
    type: string;
    action: AnimationAction;
    loopDelta: number;
    target: AnimationMixer;
}

export declare class PlayOptions {
    loop?: boolean | AnimationActionLoopStyles;
    clampWhenFinished?: boolean;
}

/** The Animator component is used to play animations on a GameObject. It is used in combination with an AnimatorController (which is a state machine for animations)   
 * A new AnimatorController can be created from code via `AnimatorController.createFromClips`
 * @category Animation and Sequencing
 * @group Components
*/
export class Animator extends Behaviour implements IAnimationComponent {

    get isAnimationComponent() {
        return true;
    }

    @serializable()
    applyRootMotion: boolean = false;
    @serializable()
    hasRootMotion: boolean = false;
    @serializable()
    keepAnimatorControllerStateOnDisable: boolean = false;

    // set from needle animator extension
    @serializable()
    set runtimeAnimatorController(val: AnimatorControllerModel | AnimatorController | undefined | null) {
        if (this._animatorController && this._animatorController.model === val) {
            return;
        }
        if (val) {
            if (!(val instanceof AnimatorController)) {
                if (debug) console.log("Assign animator controller", val, this);
                this._animatorController = new AnimatorController(val);
                if (this.__didAwake)
                    this._animatorController.bind(this);
            }
            else {
                if (val.animator && val.animator !== this) {
                    console.warn("AnimatorController can not be bound to multiple animators", val.model?.name)
                    if (!val.model) {
                        console.error("AnimatorController has no model");
                    }
                    val = new AnimatorController(val.model);
                }
                this._animatorController = val;
                this._animatorController.bind(this);
            }
        }
        else this._animatorController = null;
    }
    get runtimeAnimatorController(): AnimatorController | undefined | null {
        return this._animatorController;
    }

    /** The current state info of the animator.   
     * If you just want to access the currently playing animation action you can use currentAction
     * @returns {AnimatorStateInfo} The current state info of the animator or null if no state is playing
    */
    getCurrentStateInfo() {
        return this.runtimeAnimatorController?.getCurrentStateInfo();
    }
    /** The current action playing. It can be used to modify the action   
     * @returns {AnimationAction | null} The current action playing or null if no state is playing
    */
    get currentAction() {
        return this.runtimeAnimatorController?.currentAction || null;
    }

    /** @returns {boolean} True if parameters have been changed */
    get parametersAreDirty() { return this._parametersAreDirty; }
    private _parametersAreDirty: boolean = false;

    /** @returns {boolean} True if the animator has been changed */
    get isDirty() { return this._isDirty; }
    private _isDirty: boolean = false;

    /**@deprecated use play() */
    Play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) { this.play(name, layer, normalizedTime, transitionDurationInSec); }
    /** Plays an animation on the animator
     * @param {string | number} name The name of the animation to play. Can also be the hash of the animation
     * @param {number} layer The layer to play the animation on. Default is -1
     * @param {number} normalizedTime The normalized time to start the animation at. Default is Number.NEGATIVE_INFINITY
     * @param {number} transitionDurationInSec The duration of the transition to the new animation. Default is 0
     * @returns {void}
     * */
    play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) {
        this.runtimeAnimatorController?.play(name, layer, normalizedTime, transitionDurationInSec);
        this._isDirty = true;
    }

    /**@deprecated use reset */
    Reset() { this.reset(); }
    /** Resets the animatorcontroller */
    reset() {
        this._animatorController?.reset();
        this._isDirty = true;
    }

    /**@deprecated use setBool */
    SetBool(name: string | number, val: boolean) { this.setBool(name, val); }
    setBool(name: string | number, value: boolean) {
        if (debug) console.log("setBool", name, value);
        if (this.runtimeAnimatorController?.getBool(name) !== value)
            this._parametersAreDirty = true;
        this.runtimeAnimatorController?.setBool(name, value);
    }

    /**@deprecated use getBool */
    GetBool(name: string | number) { return this.getBool(name); }
    getBool(name: string | number): boolean {
        const res = this.runtimeAnimatorController?.getBool(name) ?? false;
        if (debug) console.log("getBool", name, res);
        return res;
    }

    toggleBool(name: string | number) {
        this.setBool(name, !this.getBool(name));
    }

    /**@deprecated use setFloat */
    SetFloat(name: string | number, val: number) { this.setFloat(name, val); }
    setFloat(name: string | number, val: number) {
        if (this.runtimeAnimatorController?.getFloat(name) !== val)
            this._parametersAreDirty = true;
        if (debug) console.log("setFloat", name, val);
        this.runtimeAnimatorController?.setFloat(name, val);
    }

    /**@deprecated use getFloat */
    GetFloat(name: string | number) { return this.getFloat(name); }
    getFloat(name: string | number): number {
        const res = this.runtimeAnimatorController?.getFloat(name) ?? -1;
        if (debug) console.log("getFloat", name, res);
        return res;
    }

    /**@deprecated use setInteger */
    SetInteger(name: string | number, val: number) { this.setInteger(name, val); }
    setInteger(name: string | number, val: number) {
        if (this.runtimeAnimatorController?.getInteger(name) !== val)
            this._parametersAreDirty = true;
        if (debug) console.log("setInteger", name, val);
        this.runtimeAnimatorController?.setInteger(name, val);
    }

    /**@deprecated use getInteger */
    GetInteger(name: string | number) { return this.getInteger(name); }
    getInteger(name: string | number): number {
        const res = this.runtimeAnimatorController?.getInteger(name) ?? -1;
        if (debug) console.log("getInteger", name, res);
        return res;
    }

    /**@deprecated use setTrigger */
    SetTrigger(name: string | number) { this.setTrigger(name); }
    setTrigger(name: string | number) {
        this._parametersAreDirty = true;
        if (debug) console.log("setTrigger", name);
        this.runtimeAnimatorController?.setTrigger(name);
    }

    /**@deprecated use resetTrigger */
    ResetTrigger(name: string | number) { this.resetTrigger(name); }
    resetTrigger(name: string | number) {
        this._parametersAreDirty = true;
        if (debug) console.log("resetTrigger", name);
        this.runtimeAnimatorController?.resetTrigger(name);
    }

    /**@deprecated use getTrigger */
    GetTrigger(name: string | number) { this.getTrigger(name); }
    getTrigger(name: string | number) {
        const res = this.runtimeAnimatorController?.getTrigger(name);
        if (debug) console.log("getTrigger", name, res);
        return res;
    }

    /**@deprecated use isInTransition */
    IsInTransition() { return this.isInTransition(); }
    /** @returns `true` if the animator is currently in a transition */
    isInTransition(): boolean {
        return this.runtimeAnimatorController?.isInTransition() ?? false;
    }

    /**@deprecated use setSpeed */
    SetSpeed(speed: number) { return this.setSpeed(speed); }
    setSpeed(speed: number) {
        if (speed === this._speed) return;
        if (debug) console.log("setSpeed", speed);
        this._speed = speed;
        if (this._animatorController?.animator == this)
            this._animatorController.setSpeed(speed);
    }

    /** Will generate a random speed between the min and max values and set it to the animatorcontroller */
    set minMaxSpeed(minMax: { x: number, y: number }) {
        this._speed = Mathf.lerp(minMax.x, minMax.y, Math.random());
        if (this._animatorController?.animator == this)
            this._animatorController.setSpeed(this._speed);
    }

    set minMaxOffsetNormalized(minMax: { x: number, y: number }) {
        this._normalizedStartOffset = Mathf.lerp(minMax.x, minMax.y, Math.random());
        if (this.runtimeAnimatorController?.animator == this)
            this.runtimeAnimatorController.normalizedStartOffset = this._normalizedStartOffset;
    }

    private _speed: number = 1;
    private _normalizedStartOffset: number = 0;
    private _animatorController?: AnimatorController | null = null;

    awake() {
        if (debug)
            console.log("ANIMATOR", this.name, this);
        if (!this.gameObject) return;
        this.initializeRuntimeAnimatorController();
    }

    // Why do we jump through hoops like this? It's because of the PlayableDirector and animation tracks
    // they NEED to use the same mixer when binding/creating the animation clips
    // so when the playable director runs it takes over updating the mixer for blending and then calls the runtimeAnimatorController.update
    // so they effectively share the same mixer. There might be cases still where not the same mixer is being used but then the animation track prints an error in dev
    private _initializeWithRuntimeAnimatorController?: AnimatorController | null;
    initializeRuntimeAnimatorController(force: boolean = false) {
        const shouldRun = (force || this.runtimeAnimatorController !== this._initializeWithRuntimeAnimatorController);
        if (this.runtimeAnimatorController && shouldRun) {
            const clone = this.runtimeAnimatorController.clone();
            this._initializeWithRuntimeAnimatorController = clone;
            if (clone) {
                console.assert(this.runtimeAnimatorController !== clone);
                this.runtimeAnimatorController = clone;
                console.assert(this.runtimeAnimatorController === clone);
                this.runtimeAnimatorController.bind(this);
                this.runtimeAnimatorController.setSpeed(this._speed);
                this.runtimeAnimatorController.normalizedStartOffset = this._normalizedStartOffset;
            }
            else console.warn("Could not clone animator controller", this.runtimeAnimatorController);
        }
    }

    onDisable() {
        if (!this.keepAnimatorControllerStateOnDisable)
            this._animatorController?.reset();
    }

    onBeforeRender() {
        this._isDirty = false;
        this._parametersAreDirty = false;

        const isAnimatedExternally = getObjectAnimated(this.gameObject);
        if (isAnimatedExternally) return;

        if (this._animatorController) {
            this._animatorController.update(1);
        }
    }
}