import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import { MmdCompositeAnimation } from "./mmdCompositeAnimation";
/**
 * Mmd composite runtime camera animation
 *
 * An object with mmd composite animation and camera binding information
 */
export class MmdCompositeRuntimeCameraAnimation {
    /**
     * The animation data
     */
    animation;
    _camera;
    _runtimeAnimations;
    _onSpanAdded;
    _onSpanRemoved;
    constructor(animation, camera, runtimeAnimations, onSpanAdded, onSpanRemoved) {
        this.animation = animation;
        this._camera = camera;
        this._runtimeAnimations = runtimeAnimations;
        this._onSpanAdded = onSpanAdded;
        this._onSpanRemoved = onSpanRemoved;
        animation.onSpanAddedObservable.add(onSpanAdded);
        animation.onSpanRemovedObservable.add(onSpanRemoved);
    }
    static _ActiveAnimationSpans = [];
    static _ActiveRuntimeAnimations = [];
    static _CameraPosition = new Vector3();
    static _CameraRotation = new Vector3();
    /**
     * Update animation
     * @param frameTime Frame time in 30fps
     */
    animate(frameTime) {
        frameTime = Math.max(this.animation.startFrame, Math.min(this.animation.endFrame, frameTime));
        const spans = this.animation.spans;
        const runtimeAnimations = this._runtimeAnimations;
        const activeAnimationSpans = MmdCompositeRuntimeCameraAnimation._ActiveAnimationSpans;
        const activeRuntimeAnimations = MmdCompositeRuntimeCameraAnimation._ActiveRuntimeAnimations;
        for (let i = 0; i < spans.length; ++i) {
            const span = spans[i];
            const runtimeAnimation = runtimeAnimations[i];
            if (runtimeAnimation !== null && 0 < span.weight && span.isInSpan(frameTime)) {
                activeAnimationSpans.push(span);
                activeRuntimeAnimations.push(runtimeAnimation);
            }
        }
        let totalWeight = 0;
        for (let i = 0; i < activeAnimationSpans.length; ++i) {
            totalWeight += activeAnimationSpans[i].getEasedWeight(activeAnimationSpans[i].getFrameTime(frameTime));
        }
        const camera = this._camera;
        if (totalWeight === 0) { // avoid divide by zero
            // camera does not have rest pose
            // camera.position.setAll(0);
            // camera.rotation.setAll(0);
            // camera.distance = 0;
            // camera.fov = 0;
            activeAnimationSpans.length = 0;
            activeRuntimeAnimations.length = 0;
            return;
        }
        if (totalWeight === 1 && activeAnimationSpans.length === 1) { // for one animation, just animate it
            const span = activeAnimationSpans[0];
            const runtimeAnimation = activeRuntimeAnimations[0];
            runtimeAnimation.animate(span.getFrameTime(frameTime));
            activeAnimationSpans.length = 0;
            activeRuntimeAnimations.length = 0;
            return;
        }
        const normalizer = totalWeight < 1.0 ? 1.0 : 1.0 / totalWeight;
        const position = MmdCompositeRuntimeCameraAnimation._CameraPosition.setAll(0);
        const rotation = MmdCompositeRuntimeCameraAnimation._CameraRotation.setAll(0);
        let distance = 0;
        let fov = 0;
        for (let i = 0; i < activeAnimationSpans.length; ++i) {
            const span = activeAnimationSpans[i];
            const runtimeAnimation = activeRuntimeAnimations[i];
            const frameTimeInSpan = span.getFrameTime(frameTime);
            runtimeAnimation.animate(frameTimeInSpan);
            const weight = span.getEasedWeight(frameTimeInSpan) * normalizer;
            camera.position.scaleAndAddToRef(weight, position);
            camera.rotation.scaleAndAddToRef(weight, rotation);
            distance += camera.distance * weight;
            fov += camera.fov * weight;
        }
        camera.position.copyFrom(position);
        camera.rotation.copyFrom(rotation);
        camera.distance = distance;
        camera.fov = fov;
        activeAnimationSpans.length = 0;
        activeRuntimeAnimations.length = 0;
    }
    /**
     * Dispose. remove all event listeners
     */
    dispose() {
        if (this._onSpanAdded !== null) {
            this.animation.onSpanAddedObservable.removeCallback(this._onSpanAdded);
            this.animation.onSpanRemovedObservable.removeCallback(this._onSpanRemoved);
            this._onSpanAdded = null;
            this._onSpanRemoved = null;
        }
    }
    /**
     * Bind animation to camera
     * @param animation Animation to bind
     * @param camera Bind target
     * @returns MmdCompositeRuntimeCameraAnimation instance
     */
    static Create(animation, camera) {
        const runtimeAnimations = new Array(animation.spans.length).fill(null);
        const spans = animation.spans;
        for (let i = 0; i < spans.length; ++i) {
            const animation = spans[i].animation;
            if (animation.createRuntimeCameraAnimation !== undefined) {
                const runtimeAnimation = animation.createRuntimeCameraAnimation(camera);
                runtimeAnimations[i] = runtimeAnimation;
            }
            else if (animation.createRuntimeModelAnimation === undefined) {
                throw new Error(`animation ${animation.name} is not bindable. are you missing import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimation" or "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimationGroup"?`);
            }
        }
        const onSpanAdded = (span) => {
            const animation = span.animation;
            if (animation.createRuntimeCameraAnimation !== undefined) {
                const runtimeAnimation = animation.createRuntimeCameraAnimation(camera);
                runtimeAnimations.push(runtimeAnimation);
            }
            else if (animation.createRuntimeModelAnimation === undefined) {
                throw new Error(`animation ${animation.name} is not bindable. are you missing import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimation" or "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimationGroup"?`);
            }
            else {
                runtimeAnimations.push(null);
            }
        };
        const onSpanRemoved = (removeIndex) => {
            runtimeAnimations.splice(removeIndex, 1);
        };
        return new MmdCompositeRuntimeCameraAnimation(animation, camera, runtimeAnimations, onSpanAdded, onSpanRemoved);
    }
}
/**
 * Create runtime camera animation
 * @param camera Bind target
 * @returns MmdRuntimeCameraAnimation instance
 */
MmdCompositeAnimation.prototype.createRuntimeCameraAnimation = function (camera) {
    return MmdCompositeRuntimeCameraAnimation.Create(this, camera);
};
