import { AnimationClip, AnimationMixer, AxesHelper, Euler, LoopOnce, Quaternion, Vector3 } from "three";
import { isDevEnvironment } from "../engine/debug/index.js";
import { AnimationUtils } from "../engine/engine_animation.js";
import { Mathf } from "../engine/engine_math.js";
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
import { assign, TypeSerializer } from "../engine/engine_serialization_core.js";
import { isAnimationAction } from "../engine/engine_three_utils.js";
import { TypeStore } from "../engine/engine_typestore.js";
import { deepClone, getParam } from "../engine/engine_utils.js";
import { AnimatorConditionMode, AnimatorControllerParameterType, AnimatorStateInfo, createMotion } from "../engine/extensions/NEEDLE_animator_controller_model.js";
const debug = getParam("debuganimatorcontroller");
const debugRootMotion = getParam("debugrootmotion");
/**
 * Generates a hash code for a string
 * @param str - The string to hash
 * @returns A numeric hash value
 */
function stringToHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        const char = str.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;
        hash = hash & hash;
    }
    return hash;
}
/**
 * Controls the playback of animations using a state machine architecture.
 *
 * The AnimatorController manages animation states, transitions between states,
 * and parameters that affect those transitions. It is used by the {@link Animator}
 * component to control animation behavior on 3D models.
 *
 * Use the static method {@link AnimatorController.createFromClips} to create
 * an animator controller from a set of animation clips.
 *
 * @category Animation and Sequencing
 * @group Utilities
 */
export class AnimatorController {
    /**
     * Creates an AnimatorController from a set of animation clips.
     * Each clip becomes a state in the controller's state machine.
     *
     * @param clips - The animation clips to use for creating states
     * @param options - Configuration options for the controller including looping behavior and transitions
     * @returns A new AnimatorController instance
     */
    static createFromClips(clips, options = { looping: false, autoTransition: true, transitionDuration: 0 }) {
        const states = [];
        for (let i = 0; i < clips.length; i++) {
            const clip = clips[i];
            const transitions = [];
            if (options.autoTransition !== false) {
                const dur = options.transitionDuration ?? 0;
                const normalizedDuration = dur / clip.duration;
                // automatically transition to self by default
                let nextState = i;
                if (options.autoTransition === undefined || options.autoTransition === true) {
                    nextState = (i + 1) % clips.length;
                }
                transitions.push({
                    exitTime: 1 - normalizedDuration,
                    offset: 0,
                    duration: dur,
                    hasExitTime: true,
                    destinationState: nextState,
                    conditions: [],
                });
            }
            const state = {
                name: clip.name,
                hash: i, // by using the index it's easy for users to call play(2) to play the clip at index 2
                motion: {
                    name: clip.name,
                    clip: clip,
                    isLooping: options?.looping ?? false,
                },
                transitions: transitions,
                behaviours: []
            };
            states.push(state);
        }
        const model = {
            name: "AnimatorController",
            guid: new InstantiateIdProvider(Date.now()).generateUUID(),
            parameters: [],
            layers: [{
                    name: "Base Layer",
                    stateMachine: {
                        defaultState: 0,
                        states: states
                    }
                }]
        };
        const controller = new AnimatorController(model);
        return controller;
    }
    /**
     * Plays an animation state by name or hash.
     *
     * @param name - The name or hash identifier of the state to play
     * @param layerIndex - The layer index (defaults to 0)
     * @param normalizedTime - The normalized time to start the animation from (0-1)
     * @param durationInSec - Transition duration in seconds
     */
    play(name, layerIndex = -1, normalizedTime = Number.NEGATIVE_INFINITY, durationInSec = 0) {
        if (layerIndex < 0)
            layerIndex = 0;
        else if (layerIndex >= this.model.layers.length) {
            console.warn("invalid layer");
            return;
        }
        const layer = this.model.layers[layerIndex];
        const sm = layer.stateMachine;
        for (const state of sm.states) {
            if (state.name === name || state.hash === name) {
                if (debug)
                    console.log("transition to ", state);
                this.transitionTo(state, durationInSec, normalizedTime);
                return;
            }
        }
        console.warn("Could not find " + name + " to play");
    }
    /**
     * Resets the controller to its initial state.
     */
    reset() {
        this.setStartTransition();
    }
    /**
     * Sets a boolean parameter value by name or hash.
     *
     * @param name - The name or hash identifier of the parameter
     * @param value - The boolean value to set
     */
    setBool(name, value) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = value);
    }
    /**
     * Gets a boolean parameter value by name or hash.
     *
     * @param name - The name or hash identifier of the parameter
     * @returns The boolean value of the parameter, or false if not found
     */
    getBool(name) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.find(p => p[key] === name)?.value ?? false;
    }
    /**
     * Sets a float parameter value by name or hash.
     *
     * @param name - The name or hash identifier of the parameter
     * @param val - The float value to set
     * @returns True if the parameter was found and set, false otherwise
     */
    setFloat(name, val) {
        const key = typeof name === "string" ? "name" : "hash";
        const filtered = this.model?.parameters?.filter(p => p[key] === name);
        filtered.forEach(p => p.value = val);
        return filtered?.length > 0;
    }
    /**
     * Gets a float parameter value by name or hash.
     *
     * @param name - The name or hash identifier of the parameter
     * @returns The float value of the parameter, or 0 if not found
     */
    getFloat(name) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.find(p => p[key] === name)?.value ?? 0;
    }
    /**
     * Sets an integer parameter value by name or hash.
     *
     * @param name - The name or hash identifier of the parameter
     * @param val - The integer value to set
     */
    setInteger(name, val) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = val);
    }
    /**
     * Gets an integer parameter value by name or hash.
     *
     * @param name - The name or hash identifier of the parameter
     * @returns The integer value of the parameter, or 0 if not found
     */
    getInteger(name) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.find(p => p[key] === name)?.value ?? 0;
    }
    /**
     * Sets a trigger parameter to active (true).
     * Trigger parameters are automatically reset after they are consumed by a transition.
     *
     * @param name - The name or hash identifier of the trigger parameter
     */
    setTrigger(name) {
        if (debug)
            console.log("SET TRIGGER", name);
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = true);
    }
    /**
     * Resets a trigger parameter to inactive (false).
     *
     * @param name - The name or hash identifier of the trigger parameter
     */
    resetTrigger(name) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = false);
    }
    /**
     * Gets the current state of a trigger parameter.
     *
     * @param name - The name or hash identifier of the trigger parameter
     * @returns The boolean state of the trigger, or false if not found
     */
    getTrigger(name) {
        const key = typeof name === "string" ? "name" : "hash";
        return this.model?.parameters?.find(p => p[key] === name)?.value ?? false;
    }
    /**
     * Checks if the controller is currently in a transition between states.
     *
     * @returns True if a transition is in progress, false otherwise
     */
    isInTransition() {
        return this._activeStates.length > 1;
    }
    /** Set the speed of the animator controller. Larger values will make the animation play faster. */
    setSpeed(speed) {
        this._speed = speed;
    }
    _speed = 1;
    /**
     * Finds an animation state by name or hash.
     * @deprecated Use findState instead
     *
     * @param name - The name or hash identifier of the state to find
     * @returns The found state or null if not found
     */
    FindState(name) { return this.findState(name); }
    /**
     * Finds an animation state by name or hash.
     *
     * @param name - The name or hash identifier of the state to find
     * @returns The found state or null if not found
     */
    findState(name) {
        if (!name)
            return null;
        if (Array.isArray(this.model.layers)) {
            for (const layer of this.model.layers) {
                for (const state of layer.stateMachine.states) {
                    if (state.name === name || state.hash == name)
                        return state;
                }
            }
        }
        return null;
    }
    /**
     * Gets information about the current playing animation state.
     *
     * @returns An AnimatorStateInfo object with data about the current state, or null if no state is active
     */
    getCurrentStateInfo() {
        if (!this._activeState)
            return null;
        const action = this._activeState.motion.action;
        if (!action)
            return null;
        const dur = this._activeState.motion.clip.duration;
        const normalizedTime = dur <= 0 ? 0 : Math.abs(action.time / dur);
        return new AnimatorStateInfo(this._activeState, normalizedTime, dur, this._speed);
    }
    /**
     * Gets the animation action currently playing.
     *
     * @returns The current animation action, or null if no action is playing
     */
    get currentAction() {
        if (!this._activeState)
            return null;
        const action = this._activeState.motion.action;
        if (!action)
            return null;
        return action;
    }
    // addState(state: State, layerIndex: number = 0) {
    //     if (!this.model) throw new Error("AnimatorController model is missing");
    //     if (layerIndex < 0 || layerIndex >= this.model.layers.length) {
    //         throw new Error(`Invalid layer index: ${layerIndex}`);
    //     }
    //     const layer = this.model.layers[layerIndex];
    //     if (!layer.stateMachine) {
    //         layer.stateMachine = { states: [], defaultState: 0 };
    //     }
    //     if (!layer.stateMachine.states) {
    //         layer.stateMachine.states = [];
    //     }
    //     if (state.hash === undefined) {
    //         state.hash = stringToHash(state.name || "state" + layer.stateMachine.states.length);
    //     }
    // }
    /**
     * The normalized time (0-1) to start playing the first state at.
     * This affects the initial state when the animator is first enabled.
     */
    normalizedStartOffset = 0;
    /**
     * The Animator component this controller is bound to.
     */
    animator;
    /**
     * The data model describing the animation states and transitions.
     */
    model;
    /**
     * Gets the engine context from the bound animator.
     */
    get context() { return this.animator?.context; }
    /**
     * Gets the animation mixer used by this controller.
     */
    get mixer() {
        return this._mixer;
    }
    /**
     * Cleans up resources used by this controller.
     * Stops all animations and unregisters the mixer from the animation system.
     */
    dispose() {
        this._mixer.stopAllAction();
        if (this.animator) {
            this._mixer.uncacheRoot(this.animator.gameObject);
            for (const action of this._activeStates) {
                if (action.motion.clip)
                    this.mixer.uncacheAction(action.motion.clip, this.animator.gameObject);
            }
        }
        this.context?.animations.unregisterAnimationMixer(this._mixer);
    }
    // applyRootMotion(obj: Object3D) {
    //     // this.internalApplyRootMotion(obj);
    // }
    /**
     * Binds this controller to an animator component.
     * Creates a new animation mixer and sets up animation actions.
     *
     * @param animator - The animator to bind this controller to
     */
    bind(animator) {
        if (!animator)
            console.error("AnimatorController.bind: animator is null");
        else if (this.animator !== animator) {
            if (this._mixer) {
                this._mixer.stopAllAction();
                this.context?.animations.unregisterAnimationMixer(this._mixer);
            }
            this.animator = animator;
            this._mixer = new AnimationMixer(this.animator.gameObject);
            this.context?.animations.registerAnimationMixer(this._mixer);
            this.createActions(this.animator);
        }
    }
    /**
     * Creates a deep copy of this controller.
     * Clones the model data but does not copy runtime state.
     *
     * @returns A new AnimatorController instance with the same configuration
     */
    clone() {
        if (typeof this.model === "string") {
            console.warn("AnimatorController has not been resolved, can not create model from string", this.model);
            return null;
        }
        if (debug)
            console.warn("AnimatorController clone()", this.model);
        // clone runtime controller but dont clone clip or action
        const clonedModel = deepClone(this.model, (_owner, _key, _value) => {
            if (_value === null || _value === undefined)
                return true;
            // dont clone three Objects
            if (_value.type === "Object3D" || _value.isObject3D === true)
                return false;
            // dont clone AnimationAction
            if (isAnimationAction(_value)) { //.constructor.name === "AnimationAction") {
                // console.log(_value);
                return false;
            }
            // dont clone AnimationClip
            if (_value["tracks"] !== undefined)
                return false;
            // when assigned __concreteInstance during serialization
            if (_value instanceof AnimatorController)
                return false;
            return true;
        });
        console.assert(clonedModel !== this.model);
        const controller = new AnimatorController(clonedModel);
        return controller;
    }
    /**
     * Updates the controller's state machine and animations.
     * Called each frame by the animator component.
     *
     * @param weight - The weight to apply to the animations (for blending)
     */
    update(weight) {
        if (!this.animator)
            return;
        this.evaluateTransitions();
        this.updateActiveStates(weight);
        // We want to update the animation mixer even if there is no active state (e.g. in cases where an empty animator controller is assigned and the timeline runs)
        // if (!this._activeState) return;
        const dt = this.animator.context.time.deltaTime;
        if (this.animator.applyRootMotion) {
            this.rootMotionHandler?.onBeforeUpdate(weight);
        }
        this._mixer.update(dt);
        if (this.animator.applyRootMotion) {
            this.rootMotionHandler?.onAfterUpdate(weight);
        }
    }
    _mixer;
    _activeState;
    /**
     * Gets the currently active animation state.
     *
     * @returns The active state or undefined if no state is active
     */
    get activeState() { return this._activeState; }
    constructor(model) {
        this.model = model;
        if (debug)
            console.log(this);
    }
    _activeStates = [];
    updateActiveStates(weight) {
        for (let i = 0; i < this._activeStates.length; i++) {
            const state = this._activeStates[i];
            const motion = state.motion;
            if (!motion.action) {
                this._activeStates.splice(i, 1);
                i--;
            }
            else {
                const action = motion.action;
                action.weight = weight;
                // console.log(action.getClip().name, action.getEffectiveWeight(), action.isScheduled());
                if ((action.getEffectiveWeight() <= 0 && !action.isRunning())) {
                    if (debug)
                        console.debug("REMOVE", state.name, action.getEffectiveWeight(), action.isRunning(), action.isScheduled());
                    this._activeStates.splice(i, 1);
                    i--;
                }
            }
        }
    }
    setStartTransition() {
        if (this.model.layers.length > 1 && (debug || isDevEnvironment())) {
            console.warn("Multiple layers are not supported yet " + this.animator?.name);
        }
        for (const layer of this.model.layers) {
            const sm = layer.stateMachine;
            if (sm.defaultState === undefined) {
                if (debug)
                    console.warn("AnimatorController default state is undefined, will assign state 0 as default", layer);
                sm.defaultState = 0;
            }
            const start = sm.states[sm.defaultState];
            this.transitionTo(start, 0, this.normalizedStartOffset);
            break;
        }
    }
    evaluateTransitions() {
        let didEnterStateThisFrame = false;
        if (!this._activeState) {
            this.setStartTransition();
            if (!this._activeState)
                return;
            didEnterStateThisFrame = true;
        }
        const state = this._activeState;
        const action = state.motion.action;
        let index = 0;
        for (const transition of state.transitions) {
            ++index;
            // transition without exit time and without condition that transition to itself are ignored
            if (!transition.hasExitTime && transition.conditions.length <= 0) {
                // if (this._activeState && this.getState(transition.destinationState, currentLayer)?.hash === this._activeState.hash)
                continue;
            }
            let allConditionsAreMet = true;
            for (const cond of transition.conditions) {
                if (!this.evaluateCondition(cond)) {
                    allConditionsAreMet = false;
                    break;
                }
            }
            if (!allConditionsAreMet)
                continue;
            if (debug && allConditionsAreMet) {
                // console.log("All conditions are met", transition);
            }
            if (action) {
                const dur = state.motion.clip.duration;
                const normalizedTime = dur <= 0 ? 1 : Math.abs(action.time / dur);
                let exitTime = transition.exitTime;
                // When the animation is playing backwards we need to check exit time inverted
                if (action.timeScale < 0) {
                    exitTime = 1 - exitTime;
                }
                let makeTransition = false;
                if (transition.hasExitTime) {
                    if (action.timeScale > 0)
                        makeTransition = normalizedTime >= transition.exitTime;
                    // When the animation is playing backwards we need to check exit time inverted
                    else if (action.timeScale < 0)
                        makeTransition = 1 - normalizedTime >= transition.exitTime;
                }
                else {
                    makeTransition = true;
                }
                if (makeTransition) {
                    // disable triggers for this transition
                    for (const cond of transition.conditions) {
                        const param = this.model.parameters.find(p => p.name === cond.parameter);
                        if (param?.type === AnimatorControllerParameterType.Trigger && param.value) {
                            param.value = false;
                        }
                    }
                    // if (transition.hasExitTime && transition.exitTime >= .9999) 
                    action.clampWhenFinished = true;
                    // else action.clampWhenFinished = false;
                    if (debug) {
                        const targetState = this.getState(transition.destinationState, 0);
                        console.log(`Transition to ${transition.destinationState} / ${targetState?.name}`, transition, "\nTimescale: " + action.timeScale, "\nNormalized time: " + normalizedTime.toFixed(3), "\nExit Time: " + exitTime, transition.hasExitTime);
                        // console.log(action.time, transition.exitTime);
                    }
                    this.transitionTo(transition.destinationState, transition.duration, transition.offset);
                    // use the first transition that matches all conditions and make the transition as soon as in range
                    return;
                }
            }
            else {
                this.transitionTo(transition.destinationState, transition.duration, transition.offset);
                return;
            }
            // if none of the transitions can be made continue searching for another transition meeting the conditions
        }
        // action.time += this.context.time.deltaTime
        // console.log(action?.time, action?.getEffectiveWeight())
        // update timescale
        if (action) {
            this.setTimescale(action, state);
        }
        let didTriggerLooping = false;
        if (state.motion.isLooping && action) {
            // we dont use the three loop state here because it prevents the transition check above
            // it is easier if we re-trigger loop here. 
            // We also can easily add the cycle offset settings from unity later
            if (action.time >= action.getClip().duration) {
                didTriggerLooping = true;
                action.reset();
                action.time = 0;
                action.play();
            }
            else if (action.time <= 0 && action.timeScale < 0) {
                didTriggerLooping = true;
                action.reset();
                action.time = action.getClip().duration;
                action.play();
            }
        }
        // call update state behaviours:
        if (!didTriggerLooping && state && !didEnterStateThisFrame && action && this.animator) {
            if (state.behaviours) {
                const duration = action?.getClip().duration;
                const normalizedTime = action.time / duration;
                const info = new AnimatorStateInfo(this._activeState, normalizedTime, duration, this._speed);
                for (const beh of state.behaviours) {
                    if (beh.instance) {
                        beh.instance.onStateUpdate?.call(beh.instance, this.animator, info, 0);
                    }
                }
            }
        }
    }
    setTimescale(action, state) {
        let speedFactor = state.speed ?? 1;
        if (state.speedParameter)
            speedFactor *= this.getFloat(state.speedParameter);
        if (speedFactor !== undefined) {
            action.timeScale = speedFactor * this._speed;
        }
    }
    getState(state, layerIndex) {
        if (typeof state === "number") {
            if (state == -1) {
                state = this.model.layers[layerIndex].stateMachine.defaultState; // exit state -> entry state
                if (state === undefined) {
                    if (debug)
                        console.warn("AnimatorController default state is undefined: ", this.model, "Layer: " + layerIndex);
                    state = 0;
                }
            }
            state = this.model.layers[layerIndex].stateMachine.states[state];
        }
        return state;
    }
    /**
     * These actions have been active previously but not faded out because we entered a state that has no real animation - no duration. In which case we hold the previously active actions until they are faded out.
     */
    _heldActions = [];
    releaseHeldActions(duration) {
        for (const prev of this._heldActions) {
            prev.fadeOut(duration);
        }
        this._heldActions.length = 0;
    }
    transitionTo(state, durationInSec, offsetNormalized) {
        if (!this.animator)
            return;
        const layerIndex = 0;
        state = this.getState(state, layerIndex);
        if (!state?.motion || !state.motion.clip || !(state.motion.clip instanceof AnimationClip)) {
            // if(debug) console.warn("State has no clip or motion", state);
            return;
        }
        const isSelf = this._activeState === state;
        if (isSelf) {
            const motion = state.motion;
            if (!motion.action_loopback && motion.clip) {
                // uncache action immediately resets the applied animation which breaks the root motion 
                // this happens if we have a transition to self and the clip is not cached yet
                const previousMatrix = this.rootMotionHandler ? this.animator.gameObject.matrix.clone() : null;
                this._mixer.uncacheAction(motion.clip, this.animator.gameObject);
                if (previousMatrix)
                    previousMatrix.decompose(this.animator.gameObject.position, this.animator.gameObject.quaternion, this.animator.gameObject.scale);
                motion.action_loopback = this.createAction(motion.clip);
            }
        }
        // call exit state behaviours
        if (this._activeState?.behaviours && this._activeState.motion.action) {
            const duration = this._activeState?.motion.clip.duration;
            const normalizedTime = this._activeState.motion.action.time / duration;
            const info = new AnimatorStateInfo(this._activeState, normalizedTime, duration, this._speed);
            for (const beh of this._activeState.behaviours) {
                beh.instance?.onStateExit?.call(beh.instance, this.animator, info, layerIndex);
            }
        }
        const prevAction = this._activeState?.motion.action;
        if (isSelf) {
            state.motion.action = state.motion.action_loopback;
            state.motion.action_loopback = prevAction;
        }
        const prev = this._activeState;
        this._activeState = state;
        const action = state.motion?.action;
        const clip = state.motion.clip;
        if (clip?.duration <= 0 && clip.tracks.length <= 0) {
            // if the new state doesn't have a valid clip / no tracks we don't fadeout the previous action and instead hold the previous action.
            if (prevAction) {
                this._heldActions.push(prevAction);
            }
        }
        else if (prevAction) {
            prevAction.fadeOut(durationInSec);
            this.releaseHeldActions(durationInSec);
        }
        if (action) {
            offsetNormalized = Math.max(0, Math.min(1, offsetNormalized));
            if (state.cycleOffsetParameter) {
                let val = this.getFloat(state.cycleOffsetParameter);
                if (typeof val === "number") {
                    if (val < 0)
                        val += 1;
                    offsetNormalized += val;
                    offsetNormalized %= 1;
                }
                else if (debug)
                    console.warn("AnimatorController cycle offset parameter is not a number", state.cycleOffsetParameter);
            }
            else if (typeof state.cycleOffset === "number") {
                offsetNormalized += state.cycleOffset;
                offsetNormalized %= 1;
            }
            if (action.isRunning())
                action.stop();
            action.reset();
            action.enabled = true;
            this.setTimescale(action, state);
            const duration = state.motion.clip.duration;
            // if we are looping to the same state we don't want to offset the current start time
            action.time = isSelf ? 0 : offsetNormalized * duration;
            if (action.timeScale < 0)
                action.time = duration - action.time;
            action.clampWhenFinished = true;
            action.setLoop(LoopOnce, 0);
            if (durationInSec > 0)
                action.fadeIn(durationInSec);
            else
                action.weight = 1;
            action.play();
            window.requestAnimationFrame(() => AnimationUtils.testIfRootCanAnimate(action));
            if (this.rootMotionHandler) {
                this.rootMotionHandler.onStart(action);
            }
            if (!this._activeStates.includes(state))
                this._activeStates.push(state);
            // call enter state behaviours
            if (this._activeState.behaviours) {
                const info = new AnimatorStateInfo(state, offsetNormalized, duration, this._speed);
                for (const beh of this._activeState.behaviours) {
                    beh.instance?.onStateEnter?.call(beh.instance, this.animator, info, layerIndex);
                }
            }
        }
        else if (debug) {
            if (!state["__warned_no_motion"]) {
                state["__warned_no_motion"] = true;
                console.warn("No action", state.motion, this);
            }
        }
        if (debug)
            console.log("TRANSITION FROM " + prev?.name + " TO " + state.name, durationInSec, prevAction, action, action?.getEffectiveTimeScale(), action?.getEffectiveWeight(), action?.isRunning(), action?.isScheduled(), action?.paused);
    }
    createAction(clip) {
        // uncache clip causes issues when multiple states use the same clip
        // this._mixer.uncacheClip(clip);
        // instead only uncache the action when one already exists to make sure
        // we get unique actions per state
        const existing = this._mixer.existingAction(clip);
        if (existing)
            this._mixer.uncacheAction(clip, this.animator?.gameObject);
        if (this.animator?.applyRootMotion) {
            if (!this.rootMotionHandler) {
                this.rootMotionHandler = new RootMotionHandler(this);
            }
            // TODO: find root bone properly
            const root = this.animator.gameObject;
            return this.rootMotionHandler.createClip(this._mixer, root, clip);
        }
        else {
            const action = this._mixer.clipAction(clip);
            return action;
        }
    }
    evaluateCondition(cond) {
        const param = this.model.parameters.find(p => p.name === cond.parameter);
        if (!param)
            return false;
        // console.log(param.name, param.value);
        switch (cond.mode) {
            case AnimatorConditionMode.If:
                return param.value === true;
            case AnimatorConditionMode.IfNot:
                return param.value === false;
            case AnimatorConditionMode.Greater:
                return param.value > cond.threshold;
            case AnimatorConditionMode.Less:
                return param.value < cond.threshold;
            case AnimatorConditionMode.Equals:
                return param.value === cond.threshold;
            case AnimatorConditionMode.NotEqual:
                return param.value !== cond.threshold;
        }
        return false;
    }
    createActions(_animator) {
        if (debug)
            console.log("AnimatorController createActions", this.model);
        for (const layer of this.model.layers) {
            const sm = layer.stateMachine;
            for (let index = 0; index < sm.states.length; index++) {
                const state = sm.states[index];
                // ensure we have a transitions array
                if (!state.transitions) {
                    state.transitions = [];
                }
                for (const t of state.transitions) {
                    // can happen if conditions are empty in blender - the exporter seems to skip empty arrays
                    if (!t.conditions)
                        t.conditions = [];
                }
                // ensure we have a motion even if none was exported
                if (!state.motion) {
                    if (debug)
                        console.warn("No motion", state);
                    state.motion = createMotion(state.name);
                    // console.warn("Missing motion", "AnimatorController: " + this.model.name, state);
                    // sm.states.splice(index, 1);
                    // index -= 1;
                    // continue;
                }
                // the clips array contains which animator has which animationclip
                if (this.animator && state.motion.clips) {
                    // TODO: we have to compare by name because on instantiate we clone objects but not the node object
                    const mapping = state.motion.clips?.find(e => e.node.name === this.animator?.gameObject?.name);
                    if (!mapping) {
                        if (debug || isDevEnvironment()) {
                            console.warn("Could not find clip for animator \"" + this.animator?.gameObject?.name + "\"", state.motion.clips.map(c => c.node.name));
                        }
                    }
                    else
                        state.motion.clip = mapping.clip;
                }
                // ensure we have a clip to blend to
                if (!state.motion.clip) {
                    if (debug)
                        console.warn("No clip assigned to state", state);
                    const clip = new AnimationClip(undefined, undefined, []);
                    state.motion.clip = clip;
                }
                if (state.motion?.clip) {
                    const clip = state.motion.clip;
                    if (clip instanceof AnimationClip) {
                        const action = this.createAction(clip);
                        state.motion.action = action;
                    }
                    else {
                        if (debug || isDevEnvironment())
                            console.warn("No valid animationclip assigned", state);
                    }
                }
                // create state machine behaviours
                if (state.behaviours && Array.isArray(state.behaviours)) {
                    for (const behaviour of state.behaviours) {
                        if (!behaviour?.typeName)
                            continue;
                        const type = TypeStore.get(behaviour.typeName);
                        if (type) {
                            const instance = new type();
                            if (instance.isStateMachineBehaviour) {
                                instance._context = this.context ?? undefined;
                                assign(instance, behaviour.properties);
                                behaviour.instance = instance;
                            }
                            if (debug)
                                console.log("Created animator controller behaviour", state.name, behaviour.typeName, behaviour.properties, instance);
                        }
                        else {
                            if (debug || isDevEnvironment())
                                console.warn("Could not find AnimatorBehaviour type: " + behaviour.typeName);
                        }
                    }
                }
            }
        }
    }
    /**
     * Yields all animation actions managed by this controller.
     * Iterates through all states in all layers and returns their actions.
     */
    *enumerateActions() {
        if (!this.model.layers)
            return;
        for (const layer of this.model.layers) {
            const sm = layer.stateMachine;
            for (let index = 0; index < sm.states.length; index++) {
                const state = sm.states[index];
                if (state?.motion) {
                    if (state.motion.action)
                        yield state.motion.action;
                    if (state.motion.action_loopback)
                        yield state.motion.action_loopback;
                }
            }
        }
    }
    // https://docs.unity3d.com/Manual/RootMotion.html
    rootMotionHandler;
}
/**
 * Wraps a KeyframeTrack to allow custom evaluation of animation values.
 * Used internally to modify animation behavior without changing the original data.
 */
class TrackEvaluationWrapper {
    track;
    createdInterpolant;
    originalEvaluate;
    customEvaluate;
    constructor(track, evaluate) {
        this.track = track;
        const t = track;
        const createOriginalInterpolator = t.createInterpolant.bind(track);
        t.createInterpolant = () => {
            t.createInterpolant = createOriginalInterpolator;
            this.createdInterpolant = createOriginalInterpolator();
            this.originalEvaluate = this.createdInterpolant.evaluate.bind(this.createdInterpolant);
            this.customEvaluate = time => {
                if (!this.originalEvaluate)
                    return;
                const res = this.originalEvaluate(time);
                return evaluate(time, res);
            };
            this.createdInterpolant.evaluate = this.customEvaluate;
            return this.createdInterpolant;
        };
    }
    ;
    dispose() {
        if (this.createdInterpolant && this.originalEvaluate) {
            this.createdInterpolant.evaluate = this.originalEvaluate;
        }
        this.track = undefined;
        this.createdInterpolant = null;
        this.originalEvaluate = undefined;
        this.customEvaluate = undefined;
    }
}
/**
 * Handles root motion extraction from animation tracks.
 * Captures movement from animations and applies it to the root object.
 */
class RootMotionAction {
    static lastObjPosition = {};
    static lastObjRotation = {};
    // we remove the first keyframe rotation from the space rotation when updating
    static firstKeyframeRotation = {};
    // this is used to rotate the space on clip end / start (so the transform direction is correct)
    static spaceRotation = {};
    static effectiveSpaceRotation = {};
    static clipOffsetRotation = {};
    set action(val) {
        this._action = val;
    }
    get action() {
        return this._action;
    }
    get cacheId() {
        return this.root.uuid;
    }
    _action;
    root;
    clip;
    positionWrapper = null;
    rotationWrapper = null;
    context;
    positionChange = new Vector3();
    rotationChange = new Quaternion();
    constructor(context, root, clip, positionTrack, rotationTrack) {
        // console.log(this, positionTrack, rotationTrack);
        this.context = context;
        this.root = root;
        this.clip = clip;
        if (!RootMotionAction.firstKeyframeRotation[this.cacheId])
            RootMotionAction.firstKeyframeRotation[this.cacheId] = new Quaternion();
        if (rotationTrack) {
            const values = rotationTrack.values;
            RootMotionAction.firstKeyframeRotation[this.cacheId]
                .set(values[0], values[1], values[2], values[3]);
        }
        if (!RootMotionAction.spaceRotation[this.cacheId])
            RootMotionAction.spaceRotation[this.cacheId] = new Quaternion();
        if (!RootMotionAction.effectiveSpaceRotation[this.cacheId])
            RootMotionAction.effectiveSpaceRotation[this.cacheId] = new Quaternion();
        RootMotionAction.clipOffsetRotation[this.cacheId] = new Quaternion();
        if (rotationTrack) {
            RootMotionAction.clipOffsetRotation[this.cacheId]
                .set(rotationTrack.values[0], rotationTrack.values[1], rotationTrack.values[2], rotationTrack.values[3])
                .invert();
        }
        this.handlePosition(clip, positionTrack);
        this.handleRotation(clip, rotationTrack);
    }
    onStart(action) {
        if (action.getClip() !== this.clip)
            return;
        if (!RootMotionAction.lastObjRotation[this.cacheId]) {
            RootMotionAction.lastObjRotation[this.cacheId] = this.root.quaternion.clone();
        }
        const lastRotation = RootMotionAction.lastObjRotation[this.cacheId];
        // const firstKeyframe = RootMotionAction.firstKeyframeRotation[this.this.cacheId];
        // lastRotation.invert().premultiply(firstKeyframe).invert();
        RootMotionAction.spaceRotation[this.cacheId].copy(lastRotation);
        if (debugRootMotion) {
            const euler = new Euler().setFromQuaternion(lastRotation);
            console.log("START", this.clip.name, Mathf.toDegrees(euler.y), this.root.position.z);
        }
    }
    getClipRotationOffset() {
        return RootMotionAction.clipOffsetRotation[this.cacheId];
    }
    _prevTime = 0;
    handlePosition(_clip, track) {
        if (track) {
            const root = this.root;
            if (debugRootMotion)
                root.add(new AxesHelper());
            if (!RootMotionAction.lastObjPosition[this.cacheId])
                RootMotionAction.lastObjPosition[this.cacheId] = this.root.position.clone();
            const valuesDiff = new Vector3();
            const valuesPrev = new Vector3();
            // const rotation = new Quaternion();
            this.positionWrapper = new TrackEvaluationWrapper(track, (time, value) => {
                const weight = this.action.getEffectiveWeight();
                // reset for testing
                if (debugRootMotion) {
                    if (root.position.length() > 8)
                        root.position.set(0, root.position.y, 0);
                }
                if (time > this._prevTime) {
                    valuesDiff.set(value[0], value[1], value[2]);
                    valuesDiff.sub(valuesPrev);
                    valuesDiff.multiplyScalar(weight);
                    valuesDiff.applyQuaternion(this.getClipRotationOffset());
                    // RootMotionAction.effectiveSpaceRotation[id].slerp(RootMotionAction.spaceRotation[id], weight);
                    valuesDiff.applyQuaternion(root.quaternion);
                    this.positionChange.copy(valuesDiff);
                    // this.root.position.add(valuesDiff);
                }
                valuesPrev.fromArray(value);
                this._prevTime = time;
                value[0] = 0;
                value[1] = 0;
                value[2] = 0;
                return value;
            });
        }
    }
    static identityQuaternion = new Quaternion();
    handleRotation(clip, track) {
        if (track) {
            if (debugRootMotion) {
                const arr = track.values;
                const firstKeyframe = new Euler().setFromQuaternion(new Quaternion(arr[0], arr[1], arr[2], arr[3]));
                console.log(clip.name, track.name, "FIRST ROTATION IN TRACK", Mathf.toDegrees(firstKeyframe.y));
                const i = track.values.length - 4;
                const lastKeyframe = new Quaternion().set(arr[i], arr[i + 1], arr[i + 2], arr[i + 3]);
                const euler = new Euler().setFromQuaternion(lastKeyframe);
                console.log(clip.name, track.name, "LAST ROTATION IN TRACK", Mathf.toDegrees(euler.y));
            }
            // if (!RootMotionAction.lastObjRotation[root.uuid]) RootMotionAction.lastObjRotation[root.uuid] = new Quaternion();
            // const temp = new Quaternion();
            let prevTime = 0;
            const valuesPrev = new Quaternion();
            const valuesDiff = new Quaternion();
            // const summedRot = new Quaternion();
            this.rotationWrapper = new TrackEvaluationWrapper(track, (time, value) => {
                // root.quaternion.copy(RootMotionAction.lastObjRotation[root.uuid]);
                if (time > prevTime) {
                    valuesDiff.set(value[0], value[1], value[2], value[3]);
                    valuesPrev.invert();
                    valuesDiff.multiply(valuesPrev);
                    // if(weight < .99) valuesDiff.slerp(RootMotionAction.identityQuaternion, 1 - weight);
                    this.rotationChange.copy(valuesDiff);
                    // root.quaternion.multiply(valuesDiff);
                }
                // else
                //     root.quaternion.multiply(this.getClipRotationOffset());
                // RootMotionAction.lastObjRotation[root.uuid].copy(root.quaternion);
                valuesPrev.fromArray(value);
                prevTime = time;
                value[0] = 0;
                value[1] = 0;
                value[2] = 0;
                value[3] = 1;
                return value;
            });
        }
    }
    // private lastPos: Vector3 = new Vector3();
    onBeforeUpdate(_weight) {
        this.positionChange.set(0, 0, 0);
        this.rotationChange.set(0, 0, 0, 1);
    }
    onAfterUpdate(weight) {
        if (!this.action)
            return false;
        weight *= this.action.getEffectiveWeight();
        if (weight <= 0)
            return false;
        this.positionChange.multiplyScalar(weight);
        this.rotationChange.slerp(RootMotionAction.identityQuaternion, 1 - weight);
        return true;
    }
}
/**
 * Manages root motion for a character.
 * Extracts motion from animation tracks and applies it to the character's transform.
 */
class RootMotionHandler {
    controller;
    handler = [];
    root;
    basePosition = new Vector3();
    baseQuaternion = new Quaternion();
    baseRotation = new Euler();
    constructor(controller) {
        this.controller = controller;
    }
    createClip(mixer, root, clip) {
        this.root = root;
        let rootName = "";
        if (root && "name" in root) {
            rootName = root.name;
        }
        const positionTrack = this.findRootTrack(clip, ".position");
        const rotationTrack = this.findRootTrack(clip, ".quaternion");
        const handler = new RootMotionAction(this.controller.context, root, clip, positionTrack, rotationTrack);
        this.handler.push(handler);
        // it is important we do this after the handler is created
        // otherwise we can not hook into threejs interpolators
        const action = mixer.clipAction(clip);
        handler.action = action;
        return action;
    }
    onStart(action) {
        for (const handler of this.handler) {
            handler.onStart(action);
        }
    }
    onBeforeUpdate(weight) {
        // capture the position of the object
        this.basePosition.copy(this.root.position);
        this.baseQuaternion.copy(this.root.quaternion);
        for (const hand of this.handler)
            hand.onBeforeUpdate(weight);
    }
    summedPosition = new Vector3();
    summedRotation = new Quaternion();
    onAfterUpdate(weight) {
        if (weight <= 0)
            return;
        // TODO: blend weight properly with root motion (when using timeline blending with animator)
        // apply the accumulated changes
        this.root.position.copy(this.basePosition);
        this.root.quaternion.copy(this.baseQuaternion);
        this.summedPosition.set(0, 0, 0);
        this.summedRotation.set(0, 0, 0, 1);
        for (const entry of this.handler) {
            if (entry.onAfterUpdate(weight)) {
                this.summedPosition.add(entry.positionChange);
                this.summedRotation.multiply(entry.rotationChange);
            }
        }
        this.root.position.add(this.summedPosition);
        this.root.quaternion.multiply(this.summedRotation);
        // RootMotionAction.lastObjRotation[this.root.uuid].copy(this.root.quaternion);
    }
    findRootTrack(clip, name) {
        const tracks = clip.tracks;
        if (!tracks)
            return null;
        for (const track of tracks) {
            if (track.name.endsWith(name)) {
                return track;
            }
        }
        return null;
    }
}
/**
 * Serialization handler for AnimatorController instances.
 * Handles conversion between serialized data and runtime objects.
 */
class AnimatorControllerSerializator extends TypeSerializer {
    onSerialize(_, _context) {
    }
    onDeserialize(data, context) {
        if (context.type === AnimatorController && data?.__type === "AnimatorController")
            return new AnimatorController(data);
        return undefined;
    }
}
new AnimatorControllerSerializator(AnimatorController);
//# sourceMappingURL=AnimatorController.js.map