import { AnimationAction, AnimationClip, AnimationMixer, KeyframeTrack, Object3D, PropertyBinding, Vector3Like } from "three";

import { isDevEnvironment } from "./debug/index.js";
import type { Context } from "./engine_context.js";
import { GLTF, IAnimationComponent, Model } from "./engine_types.js";
import { TypeStore } from "./engine_typestore.js";

/**
 * Registry for animation related data. Use {@link registerAnimationMixer} to register an animation mixer instance.  
 * Can be accessed from {@link Context.animations} and is used internally e.g. when exporting GLTF files.
 * @category Animation
 */
export class AnimationsRegistry {

    readonly context: Context
    readonly mixers: AnimationMixer[] = []

    constructor(context: Context) {
        this.context = context;
    }

    /** @hidden @internal */
    onDestroy() {
        this.mixers.forEach(mixer => mixer.stopAllAction());
        this.mixers.length = 0;
    }

    /**
     * Register an animation mixer instance.
     */
    registerAnimationMixer(mixer: AnimationMixer): void {
        if (!mixer) {
            console.warn("AnimationsRegistry.registerAnimationMixer called with null or undefined mixer")
            return;
        }
        if (this.mixers.includes(mixer)) return;
        this.mixers.push(mixer);
    }
    /**
     * Unregister an animation mixer instance.
     */
    unregisterAnimationMixer(mixer: AnimationMixer | null | undefined): void {
        if (!mixer) {
            console.warn("AnimationsRegistry.unregisterAnimationMixer called with null or undefined mixer")
            return;
        }
        const index = this.mixers.indexOf(mixer);
        if (index === -1) return;
        this.mixers.splice(index, 1);
    }

}


/**
 * Utility class for working with animations.
 */
export class AnimationUtils {

    /**
     * Tests if the root object of an AnimationAction can be animated. Objects where matrixAutoUpdate or matrixWorldAutoUpdate is set to false may not animate correctly.
     * @param action The AnimationAction to test
     * @param allowLog Whether to allow logging warnings. Default is false, which only allows logging in development environments.
     * @returns True if the root object can be animated, false otherwise
     */
    static testIfRootCanAnimate(action: AnimationAction, allowLog?: boolean): boolean {
        const root = action.getRoot();

        if (root && (root.userData.static || root.matrixAutoUpdate === false || root.matrixWorldAutoUpdate === false)) {
            if (allowLog === true || (allowLog === undefined && isDevEnvironment()))
                console.warn(`AnimationUtils: The root object (${root.name || root.type}) of this AnimationAction has matrixAutoUpdate or matrixWorldAutoUpdate set to false. This may prevent the animation from working correctly. If the object is marked as static, try to change it to dynamic.`, { static: root.userData.static, name: root.userData.name, tag: root.userData.tag, matrixAutoUpdate: root.matrixAutoUpdate, matrixWorldAutoUpdate: root.matrixWorldAutoUpdate });
            return false;
        }
        return true;
    }

    /**
     * Tries to get the animation actions from an animation mixer.
     * @param mixer The animation mixer to get the actions from
     * @returns The actions or null if the mixer is invalid
     */
    static tryGetActionsFromMixer(mixer: AnimationMixer): Array<AnimationAction> | null {
        const actions = mixer["_actions"] as Array<AnimationAction>;
        if (!actions) return null;
        return actions;
    }

    static tryGetAnimationClipsFromObjectHierarchy(obj: Object3D, target?: Array<AnimationClip>): Array<AnimationClip> {
        if (!target) target = new Array<AnimationClip>();

        if (!obj) {
            return target;
        }
        else if (obj.animations) {
            target.push(...obj.animations);
        }
        if (obj.children) {
            for (const child of obj.children) {
                this.tryGetAnimationClipsFromObjectHierarchy(child, target);
            }
        }
        return target;
    }

    /**
     * Assigns animations from a GLTF file to the objects in the scene.  
     * This method will look for objects in the scene that have animations and assign them to the correct objects.  
     * @param file The GLTF file to assign the animations from
     */
    static autoplayAnimations(file: Object3D | Pick<Model, "animations" | "scene">): Array<IAnimationComponent> | null {
        if (!file || !file.animations) {
            console.debug("No animations found in file");
            return null;
        }

        const scene = "scene" in file ? file.scene : file as Object3D;
        const animationComponents = new Array<IAnimationComponent>();

        for (let i = 0; i < file.animations.length; i++) {
            const animation = file.animations[i];
            if (!animation.tracks || animation.tracks.length <= 0) {
                console.warn("Animation has no tracks");
                continue;
            }
            for (const t in animation.tracks) {
                const track = animation.tracks[t];
                const parsedPath = PropertyBinding.parseTrackName(track.name);
                let obj = PropertyBinding.findNode(scene, parsedPath.nodeName);
                if (!obj) {
                    const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
                    // let obj = gltf.scene.getObjectByName(objectName);
                    // this finds unnamed objects that still have tracks targeting them
                    obj = scene.getObjectByProperty('uuid', objectName);

                    if (!obj) {
                        // console.warn("could not find " + objectName, animation, gltf.scene);
                        continue;
                    }
                }

                let animationComponent = findAnimationComponentInParent(obj) || findAnimationComponentInParent(scene);
                if (!animationComponent) {
                    const anim = TypeStore.get("Animation");
                    animationComponent = scene.addComponent(anim);
                    if (!animationComponent) {
                        console.error("Failed creating Animation component: No 'Animation' component found in TypeStore");
                        break;
                    }
                }
                animationComponents.push(animationComponent);
                if (animationComponent.addClip) {
                    animationComponent.addClip(animation);
                }
            }
        }
        return animationComponents;

        function findAnimationComponentInParent(obj): IAnimationComponent | null {
            if (!obj) return null;
            const components = obj.userData?.components;
            if (components && components.length > 0) {
                for (let i = 0; i < components.length; i++) {
                    const component = components[i] as IAnimationComponent;
                    if (component.isAnimationComponent === true) {
                        return component;
                    }
                }
            }
            return findAnimationComponentInParent(obj.parent);
        }
    }


    static emptyClip(): AnimationClip {
        return new AnimationClip("empty", 0, []);
    }

    static createScaleClip(options?: ScaleClipOptions): AnimationClip {
        const duration = options?.duration ?? 0.3;

        let baseScale: Vector3Like = { x: 1, y: 1, z: 1 };
        if (options?.scale !== undefined) {
            if (typeof options.scale === "number") {
                baseScale = { x: options.scale, y: options.scale, z: options.scale };
            }
            else {
                baseScale = options.scale;
            }
        }
        const type = options?.type ?? "linear";
        const scale = options?.scaleFactor ?? 1.2;

        const times = new Array<number>();
        const values = new Array<number>();
        switch (type) {
            case "linear":
                times.push(0, duration);
                values.push(
                    baseScale.x, baseScale.y, baseScale.z,
                    baseScale.x * scale, baseScale.y * scale, baseScale.z * scale
                );
                break;

            case "spring":
                times.push(0, duration * 0.3, duration * 0.5, duration * 0.7, duration * 0.9, duration);
                values.push(
                    baseScale.x, baseScale.y, baseScale.z,
                    baseScale.x * scale, baseScale.y * scale, baseScale.z * scale,
                    baseScale.x * 0.9, baseScale.y * 0.9, baseScale.z * 0.9,
                    baseScale.x * 1.05, baseScale.y * 1.05, baseScale.z * 1.05,
                    baseScale.x * 0.98, baseScale.y * 0.98, baseScale.z * 0.98,
                    baseScale.x, baseScale.y, baseScale.z
                );
                break;

        }

        const track = new KeyframeTrack(".scale", times, values);
        return new AnimationClip("scale", times[times.length - 1], [track]);
    }
}

/**
 * Type of scale animation to create.  
 * - "linear": Simple linear scale up animation.  
 * - "spring": Spring-like scale animation with overshoot and settling.  
 */
export type ScaleClipType = "linear" | "spring";

type ScaleClipOptions = {
    /**
     * Type of scale animation to create.
     * - "linear": Simple linear scale up animation.
     * - "spring": Spring-like scale animation with overshoot and settling.
     */
    type?: ScaleClipType,
    duration?: number,
    scale?: number | Vector3Like,
    scaleFactor?: number
}