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");

/** 
 * Represents an event emitted by an animation mixer  
 * @category Animation and Sequencing
 * @group Components
 */
export declare class MixerEvent {
    /** The type of event that occurred */
    type: string;
    /** The animation action that triggered this event */
    action: AnimationAction;
    /** Number of loops completed in this cycle */
    loopDelta: number;
    /** The animation mixer that emitted this event */
    target: AnimationMixer;
}

/**
 * Configuration options for playing animations  
 * @category Animation and Sequencing
 */
export declare class PlayOptions {
    /** Whether the animation should loop, and if so, which loop style to use */
    loop?: boolean | AnimationActionLoopStyles;
    /** Whether the final animation state should be maintained after playback completes */
    clampWhenFinished?: boolean;
}

/**
 * Animator plays and manages state-machine based animations on a GameObject.  
 * Uses an {@link AnimatorController} for state transitions, blending, and parameters.  
 *
 * **State machine animations:**  
 * Define animation states and transitions in Unity's Animator window or in [Blender's Animator Controller editor](https://engine.needle.tools/docs/blender/animation.html)
 * Control transitions via parameters (bool, int, float, trigger).  
 * 
 * ![](https://cloud.needle.tools/-/media/zXQhLgtxr5ZaxLDTDb3MXA.gif)  
 *
 * **Creating at runtime:**  
 * Use `AnimatorController.createFromClips()` to create controllers from code.  
 *
 * **Parameters:**  
 * - `setTrigger(name)` - Trigger a one-shot transition
 * - `setBool(name, value)` - Set boolean parameter
 * - `setFloat(name, value)` - Set float parameter
 * - `setInteger(name, value)` - Set integer parameter
 *
 * @example Trigger animation state
 * ```ts
 * const animator = myCharacter.getComponent(Animator);
 * animator.setTrigger("Jump");
 * animator.setFloat("Speed", 5);
 * animator.setBool("IsRunning", true);
 * ```
 *
 * @example Listen to animation events
 * ```ts
 * animator.onLoop(evt => console.log("Animation looped"));
 * animator.onFinished(evt => console.log("Animation finished"));
 * ```
 *
 * @summary Plays and manages animations on a GameObject based on an AnimatorController
 * @category Animation and Sequencing
 * @group Components
 * @see {@link AnimatorController} for state machine configuration
 * @see {@link Animation} for simple clip playback
 * @see {@link PlayableDirector} for timeline-based animation
 * 
 * @link https://engine.needle.tools/docs/blender/animation.html
 */
export class Animator extends Behaviour implements IAnimationComponent {

    /** 
     * Identifies this component as an animation component in the engine
     */
    get isAnimationComponent() {
        return true;
    }

    /**
     * When enabled, animation will affect the root transform position and rotation
     */
    @serializable()
    applyRootMotion: boolean = false;
    
    /**
     * Indicates whether this animator contains root motion data
     */
    @serializable()
    hasRootMotion: boolean = false;
    
    /**
     * When enabled, the animator will maintain its state when the component is disabled
     */
    @serializable()
    keepAnimatorControllerStateOnDisable: boolean = false;

    // set from needle animator extension
    /**
     * Sets or replaces the animator controller for this component.
     * Handles binding the controller to this animator instance and ensures
     * proper initialization when the controller changes.
     * @param val The animator controller model or instance to use
     */
    @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;
    }

    /**
     * Gets the current animator controller instance
     * @returns The current animator controller or null if none is assigned
     */
    get runtimeAnimatorController(): AnimatorController | undefined | null {
        return this._animatorController;
    }

    /** 
     * Retrieves information about the current animation state
     * @returns The current state information, or undefined if no state is playing
     */
    getCurrentStateInfo() {
        return this.runtimeAnimatorController?.getCurrentStateInfo();
    }
    /** 
     * The currently playing animation action that can be used to modify animation properties
     * @returns The current animation action, or null if no animation is playing
     */
    get currentAction() {
        return this.runtimeAnimatorController?.currentAction || null;
    }

    /** 
     * Indicates whether animation parameters have been modified since the last update
     * @returns True if parameters have been changed
     */
    get parametersAreDirty() { return this._parametersAreDirty; }
    private _parametersAreDirty: boolean = false;

    /** 
     * Indicates whether the animator state has changed since the last update
     * @returns 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 name The name or hash of the animation to play
     * @param layer The layer to play the animation on (-1 for default layer)
     * @param normalizedTime The time position to start playing (0-1 range, NEGATIVE_INFINITY for current position)
     * @param transitionDurationInSec The duration of the blend transition in seconds
     */
    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 animator controller to its initial state
     */
    reset() {
        this._animatorController?.reset();
        this._isDirty = true;
    }

    /**@deprecated use setBool */
    SetBool(name: string | number, val: boolean) { this.setBool(name, val); }

    /**
     * Sets a boolean parameter in the animator
     * @param name The name or hash of the parameter
     * @param value The boolean value to set
     */
    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); }

    /**
     * Gets a boolean parameter from the animator
     * @param name The name or hash of the parameter
     * @returns The value of the boolean parameter, or false if not found
     */
    getBool(name: string | number): boolean {
        const res = this.runtimeAnimatorController?.getBool(name) ?? false;
        if (debug) console.log("getBool", name, res);
        return res;
    }

    /**
     * Toggles a boolean parameter between true and false
     * @param name The name or hash of the parameter
     */
    toggleBool(name: string | number) {
        this.setBool(name, !this.getBool(name));
    }

    /**@deprecated use setFloat */
    SetFloat(name: string | number, val: number) { this.setFloat(name, val); }

    /**
     * Sets a float parameter in the animator
     * @param name The name or hash of the parameter
     * @param val The float value to set
     */
    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); }

    /**
     * Gets a float parameter from the animator
     * @param name The name or hash of the parameter
     * @returns The value of the float parameter, or -1 if not found
     */
    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); }

    /**
     * Sets an integer parameter in the animator
     * @param name The name or hash of the parameter
     * @param val The integer value to set
     */
    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); }

    /**
     * Gets an integer parameter from the animator
     * @param name The name or hash of the parameter
     * @returns The value of the integer parameter, or -1 if not found
     */
    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); }

    /**
     * Activates a trigger parameter in the animator
     * @param name The name or hash of the trigger parameter
     */
    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); }

    /**
     * Resets a trigger parameter in the animator
     * @param name The name or hash of the trigger parameter
     */
    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); }

    /**
     * Gets the state of a trigger parameter from the animator
     * @param name The name or hash of the trigger parameter
     * @returns The state of the trigger parameter
     */
    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(); }
    /** 
     * Checks if the animator is currently in a transition between states
     * @returns True if the animator is currently blending between animations
     */
    isInTransition(): boolean {
        return this.runtimeAnimatorController?.isInTransition() ?? false;
    }

    /**@deprecated use setSpeed */
    SetSpeed(speed: number) { return this.setSpeed(speed); }

    /**
     * Sets the playback speed of the animator
     * @param speed The new playback speed multiplier
     */
    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);
    }

    /** 
     * Sets a random playback speed between the min and max values
     * @param minMax Object with x (minimum) and y (maximum) speed values
     */
    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);
    }

    /**
     * Sets a random normalized time offset for animations between min (x) and max (y) values
     * @param minMax Object with x (min) and y (max) values for the offset range
     */
    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);
        }
    }
}