import { Logger } from "@babylonjs/core/Misc/logger";
import { Observable } from "@babylonjs/core/Misc/observable";
import { MmdMesh } from "../mmdMesh";
import { MmdStandardMaterialProxy } from "../mmdStandardMaterialProxy";
import { MmdMetadataEncoder } from "./mmdMetadataEncoder";
import { MmdWasmModel } from "./mmdWasmModel";
import { WasmSpinlock } from "./wasmSpinlock";
/**
 * MMD WASM runtime animation evaluation type
 */
export var MmdWasmRuntimeAnimationEvaluationType;
(function (MmdWasmRuntimeAnimationEvaluationType) {
    /**
     * Immediate animation evaluation for the current frame
     */
    MmdWasmRuntimeAnimationEvaluationType[MmdWasmRuntimeAnimationEvaluationType["Immediate"] = 0] = "Immediate";
    /**
     * Buffered animation evaluation for the next frame
     *
     * Asynchronous Multi-thread optimization applies when possible
     *
     * If you are using havok or ammo.js physics, all matrix computations are computed before the physics stage (the transformAfterPhysics property is ignored)
     */
    MmdWasmRuntimeAnimationEvaluationType[MmdWasmRuntimeAnimationEvaluationType["Buffered"] = 1] = "Buffered";
})(MmdWasmRuntimeAnimationEvaluationType || (MmdWasmRuntimeAnimationEvaluationType = {}));
/**
 * MMD WASM runtime orchestrates several MMD components (models, camera, audio)
 *
 * MMD WASM runtime handles updates and synchronization of MMD components
 *
 * It can also create and remove runtime components
 */
export class MmdWasmRuntime {
    /**
     * @internal
     */
    wasmInstance;
    /**
     * @internal
     */
    wasmInternal;
    /**
     * Spinlock for MMD WASM runtime to synchronize animation evaluation
     * @internal
     */
    lock;
    _usingWasmBackBuffer;
    _lastRequestAnimationFrameTime;
    _needToSyncEvaluate;
    _mmdMetadataEncoder;
    _physics;
    _models;
    _camera;
    _audioPlayer;
    _loggingEnabled;
    /** @internal */
    log;
    /** @internal */
    warn;
    /** @internal */
    error;
    _isRegistered;
    /**
     * This observable is notified when animation duration is changed
     */
    onAnimationDurationChangedObservable;
    /**
     * This observable is notified when animation is played
     */
    onPlayAnimationObservable;
    /**
     * This observable is notified when animation is paused
     */
    onPauseAnimationObservable;
    /**
     * This observable is notified when animation is seeked
     */
    onSeekAnimationObservable;
    /**
     * This observable is notified when animation is evaluated (usually every frame)
     */
    onAnimationTickObservable;
    _evaluationType;
    _currentFrameTime;
    _animationTimeScale;
    _animationPaused;
    _animationFrameTimeDuration;
    _useManualAnimationDuration;
    _needToInitializePhysicsModels;
    _needToInitializePhysicsModelsBuffer;
    _beforePhysicsBinded;
    _afterPhysicsBinded;
    _bindedDispose;
    _disposeObservableObject;
    /**
     * Creates a new MMD web assembly runtime
     *
     * For use external physics engine like ammo.js or havok, you need to set `physics` to instance of `IMmdPhysics`
     * @param wasmInstance MMD WASM instance
     * @param scene Objects that limit the lifetime of this instance
     * @param physics MMD physics
     */
    constructor(wasmInstance, scene = null, physics = null) {
        this.wasmInstance = wasmInstance;
        this.wasmInternal = wasmInstance.createMmdRuntime();
        this.lock = new WasmSpinlock(wasmInstance.createTypedArray(Uint8Array, this.wasmInternal.getLockStatePtr(), 1));
        this._usingWasmBackBuffer = false;
        this._lastRequestAnimationFrameTime = null;
        this._needToSyncEvaluate = true;
        this._mmdMetadataEncoder = new MmdMetadataEncoder();
        this._physics = physics;
        this._models = [];
        this._camera = null;
        this._audioPlayer = null;
        this._loggingEnabled = false;
        this.log = this._logDisabled;
        this.warn = this._warnDisabled;
        this.error = this._errorDisabled;
        this._isRegistered = false;
        this.onAnimationDurationChangedObservable = new Observable();
        this.onPlayAnimationObservable = new Observable();
        this.onPauseAnimationObservable = new Observable();
        this.onSeekAnimationObservable = new Observable();
        this.onAnimationTickObservable = new Observable();
        this._evaluationType = MmdWasmRuntimeAnimationEvaluationType.Immediate;
        this._currentFrameTime = 0;
        this._animationTimeScale = 1;
        this._animationPaused = true;
        this._animationFrameTimeDuration = 0;
        this._useManualAnimationDuration = false;
        this._needToInitializePhysicsModels = new Set();
        this._needToInitializePhysicsModelsBuffer = new Set();
        this._beforePhysicsBinded = null;
        this._afterPhysicsBinded = this.afterPhysics.bind(this);
        if (scene !== null) {
            this._bindedDispose = () => this.dispose(scene);
            this._disposeObservableObject = scene;
            if (this._disposeObservableObject !== null) {
                this._disposeObservableObject.onDisposeObservable.add(this._bindedDispose);
            }
        }
        else {
            this._bindedDispose = null;
            this._disposeObservableObject = null;
        }
    }
    /**
     * Dispose MMD WASM runtime
     *
     * Destroy all MMD models and unregister this runtime from scene
     * @param scene Scene
     */
    dispose(scene) {
        this.lock.wait(); // ensure that the runtime is not evaluating animations
        for (let i = 0; i < this._models.length; ++i)
            this._models[i].dispose();
        this._models.length = 0;
        this.setCamera(null);
        this.setAudioPlayer(null);
        this.onAnimationDurationChangedObservable.clear();
        this.onPlayAnimationObservable.clear();
        this.onPauseAnimationObservable.clear();
        this.onSeekAnimationObservable.clear();
        this.onAnimationTickObservable.clear();
        this._needToInitializePhysicsModels.clear();
        this.unregister(scene);
        this.wasmInternal.free();
        if (this._disposeObservableObject !== null && this._bindedDispose !== null) {
            this._disposeObservableObject.onDisposeObservable.removeCallback(this._bindedDispose);
        }
    }
    /**
     * Create MMD model from mesh that has MMD metadata
     *
     * The skeletons in the mesh where the MmdModel was created no longer follow the usual matrix update policy
     * @param mmdSkinnedMesh MmdSkinnedMesh
     * @param options Creation options
     * @returns MMD model
     * @throws {Error} if mesh is not `MmdSkinnedMesh`
     */
    createMmdModel(mmdSkinnedMesh, options = {}) {
        if (!MmdMesh.isMmdSkinnedMesh(mmdSkinnedMesh))
            throw new Error("Mesh validation failed.");
        return this.createMmdModelFromSkeleton(mmdSkinnedMesh, mmdSkinnedMesh.metadata.skeleton, options);
    }
    /**
     * Create MMD model from humanoid mesh and virtual skeleton
     *
     * this method is useful for supporting humanoid models, usually used by `HumanoidMmd`
     * @param mmdMesh MmdSkinnedMesh
     * @param skeleton Skeleton or Virtualized skeleton
     * @param options Creation options
     */
    createMmdModelFromSkeleton(mmdMesh, skeleton, options = {}) {
        if (options.materialProxyConstructor === undefined) {
            options.materialProxyConstructor = MmdStandardMaterialProxy;
        }
        if (options.buildPhysics === undefined) {
            options.buildPhysics = true;
        }
        this.lock.wait(); // ensure that the runtime is not evaluating animations
        // sync buffer temporarily
        const usingWasmBackBuffer = this._usingWasmBackBuffer;
        if (usingWasmBackBuffer) {
            this.wasmInternal.swapWorldMatrixBuffer();
            this._usingWasmBackBuffer = false;
        }
        const metadataEncoder = this._mmdMetadataEncoder;
        metadataEncoder.encodePhysics = options.buildPhysics;
        const metadataSize = metadataEncoder.computeSize(mmdMesh.metadata);
        const wasmRuntime = this.wasmInternal;
        const metadataBufferPtr = wasmRuntime.allocateBuffer(metadataSize);
        const metadataBuffer = this.wasmInstance.createTypedArray(Uint8Array, metadataBufferPtr, metadataSize);
        const wasmMorphIndexMap = metadataEncoder.encode(mmdMesh.metadata, skeleton.bones, metadataBuffer.array);
        const mmdModelPtr = wasmRuntime.createMmdModel(metadataBufferPtr, metadataSize);
        const model = new MmdWasmModel(this, mmdModelPtr, mmdMesh, skeleton, options.materialProxyConstructor, wasmMorphIndexMap, options.buildPhysics ? this._physics : null);
        this._models.push(model);
        const needToInitializePhysicsModels = this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered
            ? this._needToInitializePhysicsModelsBuffer
            : this._needToInitializePhysicsModels;
        needToInitializePhysicsModels.add(model);
        wasmRuntime.deallocateBuffer(metadataBufferPtr, metadataSize);
        // desync again
        if (usingWasmBackBuffer) {
            this.wasmInternal.swapWorldMatrixBuffer();
            this._usingWasmBackBuffer = true;
        }
        // because the model is created, the animation must be evaluated synchronously at least once
        this._needToSyncEvaluate = true;
        model.onCurrentAnimationChangedObservable.add(this._onAnimationChanged);
        return model;
    }
    /**
     * Destroy MMD model
     *
     * Dispose all resources used at MMD runtime and restores the skeleton to the usual matrix update policy
     *
     * After calling the `destroyMmdModel` once, the mesh is no longer able to `createMmdModel` because the metadata is lost
     * @param mmdModel MMD model to destroy
     * @throws {Error} if model is not found
     */
    destroyMmdModel(mmdModel) {
        mmdModel.dispose();
        const models = this._models;
        const index = models.indexOf(mmdModel);
        if (index < 0)
            throw new Error("Model not found.");
        models.splice(index, 1);
        this.lock.wait(); // ensure that the runtime is not evaluating animations
        this.wasmInternal.destroyMmdModel(mmdModel.ptr);
    }
    /**
     * Set camera to animate
     * @param camera MMD camera
     */
    setCamera(camera) {
        if (this._camera !== null) {
            this._camera.onCurrentAnimationChangedObservable.removeCallback(this._onAnimationChanged);
        }
        if (camera !== null) {
            camera.onCurrentAnimationChangedObservable.add(this._onAnimationChanged);
        }
        this._camera = camera;
        this._onAnimationChanged(camera?.currentAnimation ?? null);
    }
    _setAudioPlayerLastValue = null;
    /**
     * Set audio player to sync with animation
     *
     * If you set up audio Player while playing an animation, it try to play the audio from the current animation time
     * And returns Promise because this operation is asynchronous. In most cases, you don't have to await this Promise
     * @param audioPlayer Audio player
     * @returns Promise
     */
    async setAudioPlayer(audioPlayer) {
        if (this._audioPlayer === audioPlayer)
            return;
        this._setAudioPlayerLastValue = audioPlayer;
        if (this._audioPlayer !== null) {
            this._audioPlayer.onDurationChangedObservable.removeCallback(this._onAudioDurationChanged);
            this._audioPlayer.onPlaybackRateChangedObservable.removeCallback(this._onAudioPlaybackRateChanged);
            this._audioPlayer.onPlayObservable.removeCallback(this._onAudioPlay);
            this._audioPlayer.onPauseObservable.removeCallback(this._onAudioPause);
            this._audioPlayer.onSeekObservable.removeCallback(this._onAudioSeek);
            this._audioPlayer.pause();
        }
        this._audioPlayer = null;
        if (audioPlayer === null)
            return;
        if (!this._animationPaused) {
            const audioFrameTimeDuration = audioPlayer.duration * 30;
            if (this._currentFrameTime < audioFrameTimeDuration) {
                audioPlayer.currentTime = this._currentFrameTime / 30;
                await audioPlayer.play();
                if (this._setAudioPlayerLastValue !== audioPlayer) {
                    audioPlayer.pause();
                    return;
                }
            }
        }
        this._audioPlayer = audioPlayer;
        this._onAudioDurationChanged();
        audioPlayer.onDurationChangedObservable.add(this._onAudioDurationChanged);
        audioPlayer.onPlaybackRateChangedObservable.add(this._onAudioPlaybackRateChanged);
        audioPlayer.onPlayObservable.add(this._onAudioPlay);
        audioPlayer.onPauseObservable.add(this._onAudioPause);
        audioPlayer.onSeekObservable.add(this._onAudioSeek);
        audioPlayer._setPlaybackRateWithoutNotify(this._animationTimeScale);
    }
    /**
     * Register MMD runtime to scene
     *
     * register `beforePhysics` and `afterPhysics` to scene Observables
     *
     * If you need a more complex update method you can call `beforePhysics` and `afterPhysics` manually
     * @param scene Scene
     */
    register(scene) {
        if (this._isRegistered)
            return;
        this._isRegistered = true;
        this._beforePhysicsBinded = () => this.beforePhysics(scene.getEngine().getDeltaTime());
        scene.onBeforeAnimationsObservable.add(this._beforePhysicsBinded);
        scene.onBeforeRenderObservable.add(this._afterPhysicsBinded);
    }
    /**
     * Unregister MMD runtime from scene
     * @param scene Scene
     */
    unregister(scene) {
        if (!this._isRegistered)
            return;
        this._isRegistered = false;
        scene.onBeforeAnimationsObservable.removeCallback(this._beforePhysicsBinded);
        scene.onBeforeRenderObservable.removeCallback(this._afterPhysicsBinded);
        this._beforePhysicsBinded = null;
    }
    /**
     * Before the physics stage, update animations and run MMD runtime solvers
     *
     * @param deltaTime Delta time in milliseconds
     */
    beforePhysics(deltaTime) {
        let elapsedFrameTime = null;
        if (!this._animationPaused) {
            if (this._audioPlayer !== null && !this._audioPlayer.paused) { // sync animation time with audio time
                const audioPlayerCurrentTime = this._audioPlayer.currentTime;
                const timeDiff = audioPlayerCurrentTime - this._currentFrameTime / 30;
                const timeDiffAbs = Math.abs(timeDiff);
                if (timeDiffAbs < 0.05) { // synced
                    this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale;
                }
                else if (timeDiffAbs < 0.5) {
                    if (timeDiff < 0) { // animation is faster than audio
                        this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale * 0.9;
                    }
                    else { // animation is slower than audio
                        this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale * 1.1;
                    }
                }
                else {
                    if (2 * 30 < Math.abs(audioPlayerCurrentTime - this._currentFrameTime)) {
                        const needToInitializePhysicsModels = this._needToInitializePhysicsModels;
                        for (let i = 0; i < this._models.length; ++i) {
                            const model = this._models[i];
                            if (model.currentAnimation !== null) {
                                needToInitializePhysicsModels.add(model);
                            }
                        }
                    }
                    this._currentFrameTime = audioPlayerCurrentTime * 30;
                }
            }
            else { // only use delta time to calculate animation time
                this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale;
            }
            elapsedFrameTime = this._currentFrameTime;
            if (this._animationFrameTimeDuration <= elapsedFrameTime) {
                this._animationPaused = true;
                this._currentFrameTime = this._animationFrameTimeDuration;
                if (this._audioPlayer !== null && !this._audioPlayer.paused) {
                    this._audioPlayer.pause();
                }
                else {
                    this.onPauseAnimationObservable.notifyObservers();
                }
            }
        }
        if (this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered) {
            const models = this._models;
            // evaluate vertex / uv animations on javascript side
            // they can't be buffered
            {
                const lastRequestAnimationFrameTime = this._lastRequestAnimationFrameTime ?? elapsedFrameTime;
                for (let i = 0; i < models.length; ++i) {
                    const model = models[i];
                    if (model.currentAnimation?.wasmAnimate !== undefined) {
                        models[i].beforePhysicsAndWasm(lastRequestAnimationFrameTime);
                    }
                }
                if (lastRequestAnimationFrameTime !== null) {
                    if (this._camera !== null) {
                        this._camera.animate(lastRequestAnimationFrameTime);
                    }
                }
            }
            if (this.wasmInstance.MmdRuntime.bufferedUpdate === undefined) { // single thread environment fallback
                this.wasmInternal.beforePhysics(this._lastRequestAnimationFrameTime ?? undefined);
                this.wasmInternal.afterPhysics();
            }
            this.lock.wait(); // ensure that the runtime is not evaluating animations
            // desync buffer
            if (this._usingWasmBackBuffer === false) {
                this._usingWasmBackBuffer = true;
                this.wasmInternal.swapWorldMatrixBuffer();
            }
            // if there is no previous evaluated frame time, evaluate animation synchronously
            if (this._lastRequestAnimationFrameTime === null) {
                if (elapsedFrameTime !== null) {
                    // evaluate animations on javascript side
                    for (let i = 0; i < models.length; ++i) {
                        const model = models[i];
                        if (model.currentAnimation?.wasmAnimate === undefined) {
                            models[i].beforePhysicsAndWasm(elapsedFrameTime);
                        }
                    }
                    // compute world matrix on wasm side synchronously
                    this.wasmInternal.beforePhysics(elapsedFrameTime ?? undefined);
                    this.wasmInternal.afterPhysics();
                }
                else {
                    // if there is uninitialized new model, evaluate animation synchronously
                    if (this._needToSyncEvaluate) {
                        this._needToSyncEvaluate = false;
                        // compute world matrix on wasm side synchronously
                        this.wasmInternal.beforePhysics();
                        this.wasmInternal.afterPhysics();
                    }
                }
            }
            else {
                // if there is uninitialized new model, evaluate animation synchronously
                if (this._needToSyncEvaluate) {
                    this._needToSyncEvaluate = false;
                    // evaluate animations on javascript side
                    for (let i = 0; i < models.length; ++i) {
                        const model = models[i];
                        if (model.currentAnimation?.wasmAnimate === undefined) {
                            models[i].beforePhysicsAndWasm(this._lastRequestAnimationFrameTime);
                        }
                    }
                    // compute world matrix on wasm side synchronously
                    this.wasmInternal.beforePhysics(this._lastRequestAnimationFrameTime);
                    this.wasmInternal.afterPhysics();
                }
            }
            for (let i = 0; i < models.length; ++i)
                models[i].swapWorldTransformMatricesBuffer();
            this.wasmInternal.swapWorldMatrixBuffer();
            for (let i = 0; i < models.length; ++i) {
                const model = models[i];
                model.beforePhysics(); // sync body to bone
            }
            // update bone animation state on javascript side
            for (let i = 0; i < models.length; ++i) {
                const model = models[i];
                if (model.currentAnimation?.wasmAnimate === undefined) {
                    models[i].beforePhysicsAndWasm(elapsedFrameTime);
                }
            }
            // compute world matrix on wasm side asynchronously
            this.wasmInstance.MmdRuntime.bufferedUpdate?.(this.wasmInternal, elapsedFrameTime ?? undefined);
            this._lastRequestAnimationFrameTime = elapsedFrameTime;
            // physics initialization must be buffered 1 frame
            const needToInitializePhysicsModelsBuffer = this._needToInitializePhysicsModelsBuffer;
            for (const model of needToInitializePhysicsModelsBuffer) {
                model.initializePhysics();
            }
            needToInitializePhysicsModelsBuffer.clear();
            const needToInitializePhysicsModels = this._needToInitializePhysicsModels;
            for (const model of needToInitializePhysicsModels) {
                needToInitializePhysicsModelsBuffer.add(model);
            }
            needToInitializePhysicsModels.clear();
        }
        else {
            // sync buffer
            if (this._usingWasmBackBuffer === true) {
                const needToInitializePhysicsModelsBuffer = this._needToInitializePhysicsModelsBuffer;
                const needToInitializePhysicsModels = this._needToInitializePhysicsModels;
                for (const model of needToInitializePhysicsModelsBuffer) {
                    needToInitializePhysicsModels.add(model);
                }
                this.lock.wait(); // ensure that the runtime is not evaluating animations
                this._usingWasmBackBuffer = false;
                this._lastRequestAnimationFrameTime = null;
                this.wasmInternal.swapWorldMatrixBuffer();
            }
            const models = this._models;
            for (let i = 0; i < models.length; ++i)
                models[i].beforePhysicsAndWasm(elapsedFrameTime);
            this.wasmInternal.beforePhysics(elapsedFrameTime ?? undefined);
            for (let i = 0; i < models.length; ++i)
                models[i].beforePhysics();
            this._needToSyncEvaluate = false;
            const needToInitializePhysicsModels = this._needToInitializePhysicsModels;
            for (const model of needToInitializePhysicsModels) {
                model.initializePhysics();
            }
            needToInitializePhysicsModels.clear();
            if (elapsedFrameTime !== null) {
                if (this._camera !== null) {
                    this._camera.animate(elapsedFrameTime);
                }
            }
        }
        if (elapsedFrameTime !== null) {
            this.onAnimationTickObservable.notifyObservers();
        }
    }
    /**
     * After the physics stage, update physics and run MMD runtime solvers
     */
    afterPhysics() {
        const models = this._models;
        if (this._usingWasmBackBuffer) {
            for (let i = 0; i < models.length; ++i) {
                const model = models[i];
                model.afterPhysicsAndWasm();
                model.afterPhysics();
            }
        }
        else {
            for (let i = 0; i < models.length; ++i)
                models[i].afterPhysicsAndWasm();
            this.wasmInternal.afterPhysics();
            for (let i = 0; i < models.length; ++i)
                models[i].afterPhysics();
        }
    }
    _onAnimationChanged = (newAnimation) => {
        if (this._useManualAnimationDuration)
            return;
        const newAnimationDuration = newAnimation?.animation.endFrame ?? 0;
        if (this._animationFrameTimeDuration < newAnimationDuration) {
            this._animationFrameTimeDuration = newAnimationDuration;
        }
        else if (newAnimationDuration < this._animationFrameTimeDuration) {
            this._animationFrameTimeDuration = this._computeAnimationDuration();
        }
        this.onAnimationDurationChangedObservable.notifyObservers();
    };
    _computeAnimationDuration() {
        let duration = 0;
        const models = this._models;
        for (let i = 0; i < models.length; ++i) {
            const model = models[i];
            if (model.currentAnimation !== null) {
                duration = Math.max(duration, model.currentAnimation.animation.endFrame);
            }
        }
        if (this._camera !== null && this._camera.currentAnimation !== null) {
            duration = Math.max(duration, this._camera.currentAnimation.animation.endFrame);
        }
        if (this._audioPlayer !== null) {
            duration = Math.max(duration, this._audioPlayer.duration * 30);
        }
        return duration;
    }
    _onAudioDurationChanged = () => {
        if (!this._animationPaused) {
            const audioPlayer = this._audioPlayer;
            const currentTime = this._currentFrameTime / 30;
            if (currentTime < audioPlayer.duration) {
                audioPlayer._setCurrentTimeWithoutNotify(currentTime);
                audioPlayer.play().then(() => {
                    if (this._setAudioPlayerLastValue !== audioPlayer) {
                        audioPlayer.pause();
                        return;
                    }
                });
            }
        }
        if (this._useManualAnimationDuration)
            return;
        const audioFrameTimeDuration = this._audioPlayer.duration * 30;
        if (this._animationFrameTimeDuration < audioFrameTimeDuration) {
            this._animationFrameTimeDuration = audioFrameTimeDuration;
        }
        else {
            this._animationFrameTimeDuration = this._computeAnimationDuration();
        }
        this.onAnimationDurationChangedObservable.notifyObservers();
    };
    _onAudioPlaybackRateChanged = () => {
        this._animationTimeScale = this._audioPlayer.playbackRate;
    };
    _onAudioPlay = () => {
        this._playAnimationInternal();
    };
    _onAudioPause = () => {
        if (this._audioPlayer.currentTime === this._audioPlayer.duration)
            return;
        this._animationPaused = true;
        this.onPauseAnimationObservable.notifyObservers();
    };
    _onAudioSeek = () => {
        this._seekAnimationInternal(this._audioPlayer.currentTime * 30, this._animationPaused);
    };
    _playAnimationInternal() {
        if (!this._animationPaused)
            return;
        this._animationPaused = false;
        if (this._currentFrameTime === 0) {
            const models = this._models;
            const needToInitializePhysicsModels = this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered
                ? this._needToInitializePhysicsModelsBuffer
                : this._needToInitializePhysicsModels;
            for (let i = 0; i < models.length; ++i) {
                needToInitializePhysicsModels.add(models[i]);
            }
        }
        this.onPlayAnimationObservable.notifyObservers();
    }
    /**
     * Play animation from the current animation time
     *
     * If audio player is set, it try to play the audio from the current animation time
     *
     * It returns Promise because playing audio is asynchronous
     * @returns Promise
     */
    async playAnimation() {
        if (this._audioPlayer !== null && this._currentFrameTime < this._audioPlayer.duration * 30) {
            try {
                const currentTime = this._currentFrameTime / 30;
                if (0.05 < Math.abs(this._audioPlayer.currentTime - currentTime)) {
                    this._audioPlayer._setCurrentTimeWithoutNotify(currentTime);
                }
                await this._audioPlayer.play();
            }
            catch (e) {
                if (e instanceof DOMException && e.name === "NotSupportedError") {
                    this.error("Failed to play audio.");
                    this._playAnimationInternal();
                }
                else {
                    throw e;
                }
            }
        }
        else {
            this._playAnimationInternal();
        }
    }
    /**
     * Pause animation
     */
    pauseAnimation() {
        if (this._audioPlayer !== null && !this._audioPlayer.paused) {
            this._audioPlayer.pause();
            this._animationPaused = true;
        }
        else {
            this._animationPaused = true;
            this.onPauseAnimationObservable.notifyObservers();
        }
    }
    _seekAnimationInternal(frameTime, forceEvaluate) {
        if (2 * 30 < Math.abs(frameTime - this._currentFrameTime)) {
            const needToInitializePhysicsModels = this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered
                ? this._needToInitializePhysicsModelsBuffer
                : this._needToInitializePhysicsModels;
            for (let i = 0; i < this._models.length; ++i) {
                const model = this._models[i];
                if (model.currentAnimation !== null) {
                    needToInitializePhysicsModels.add(model);
                }
            }
        }
        this._currentFrameTime = frameTime;
        if (forceEvaluate) {
            const models = this._models;
            if (this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered) {
                this._lastRequestAnimationFrameTime = frameTime;
                this._needToSyncEvaluate = true;
            }
            else {
                this.lock.wait(); // ensure that the runtime is not evaluating animations
                for (let i = 0; i < models.length; ++i) {
                    const currentAnimation = models[i].currentAnimation;
                    if (currentAnimation !== null) {
                        if (currentAnimation.wasmAnimate !== undefined) {
                            currentAnimation.wasmAnimate(frameTime);
                            currentAnimation.animate(frameTime);
                        }
                        else {
                            currentAnimation.animate(frameTime);
                        }
                    }
                }
            }
            if (this._camera !== null && this._camera.currentAnimation !== null) {
                this._camera.animate(frameTime);
            }
            this.onAnimationTickObservable.notifyObservers();
        }
        this.onSeekAnimationObservable.notifyObservers();
    }
    /**
     * Seek animation to the specified frame time
     *
     * If you set forceEvaluate true, the animation is evaluated even if the animation is not playing
     *
     * If audio player is set and not paused, it try to play the audio from the seek time so it returns Promise
     * @param frameTime Time in 30fps frame
     * @param forceEvaluate Whether to force evaluate animation
     * @returns Promise
     */
    async seekAnimation(frameTime, forceEvaluate = false) {
        frameTime = Math.max(0, Math.min(frameTime, this._animationFrameTimeDuration));
        if (this._audioPlayer !== null) {
            if (!this._audioPlayer.paused) {
                this._audioPlayer.currentTime = frameTime / 30;
            }
            else if (!this._animationPaused && // animation playing but audio paused
                this._audioPlayer.currentTime * 30 < this._animationFrameTimeDuration && // is player exausted
                frameTime < this._audioPlayer.duration * 30 // is seek time in audio duration
            ) {
                try {
                    this._audioPlayer._setCurrentTimeWithoutNotify(frameTime / 30);
                    await this._audioPlayer.play();
                }
                catch (e) {
                    if (e instanceof DOMException && e.name === "NotSupportedError") {
                        this.error("Failed to play audio.");
                        this._seekAnimationInternal(frameTime, forceEvaluate);
                    }
                    else {
                        throw e;
                    }
                }
            }
            else {
                this._seekAnimationInternal(frameTime, forceEvaluate);
                this._audioPlayer?._setCurrentTimeWithoutNotify(frameTime / 30);
            }
        }
        else {
            this._seekAnimationInternal(frameTime, forceEvaluate);
        }
    }
    /**
     * Whether animation is playing
     */
    get isAnimationPlaying() {
        return !this._animationPaused;
    }
    /**
     * MMD models created by this runtime
     */
    get models() {
        return this._models;
    }
    /**
     * MMD camera
     */
    get camera() {
        return this._camera;
    }
    /**
     * Audio player
     */
    get audioPlayer() {
        return this._audioPlayer;
    }
    /**
     * Current animation time scale (default: 1)
     */
    get timeScale() {
        return this._animationTimeScale;
    }
    set timeScale(value) {
        this._animationTimeScale = value;
        if (this._audioPlayer !== null) {
            this._audioPlayer._setPlaybackRateWithoutNotify(value);
        }
    }
    /**
     * Current animation time in 30fps frame
     */
    get currentFrameTime() {
        return this._currentFrameTime;
    }
    /**
     * Current animation time in seconds
     */
    get currentTime() {
        return this._currentFrameTime / 30;
    }
    /**
     * Current animation duration in 30fps frame
     */
    get animationFrameTimeDuration() {
        return this._animationFrameTimeDuration;
    }
    /**
     * Current animation duration in seconds
     */
    get animationDuration() {
        return this._animationFrameTimeDuration / 30;
    }
    /**
     * Set animation duration manually
     *
     * When the difference between the length of the song and the length of the animation is large, it can be helpful to adjust the animation duration manually
     * @param frameTimeDuration Time in 30fps frame
     */
    setManualAnimationDuration(frameTimeDuration) {
        if (frameTimeDuration === null && !this._useManualAnimationDuration)
            return;
        if (frameTimeDuration === null) {
            this._useManualAnimationDuration = false;
            this._animationFrameTimeDuration = this._computeAnimationDuration();
        }
        else {
            this._useManualAnimationDuration = true;
            this._animationFrameTimeDuration = frameTimeDuration;
        }
        this.onAnimationDurationChangedObservable.notifyObservers();
    }
    /**
     * Animation evaluation type
     */
    get evaluationType() {
        return this._evaluationType;
    }
    set evaluationType(value) {
        if (this._evaluationType === value)
            return;
        if (value === MmdWasmRuntimeAnimationEvaluationType.Buffered) {
            this._evaluationType = value;
        }
        else {
            this._evaluationType = value;
        }
        const models = this._models;
        for (let i = 0; i < models.length; ++i) {
            models[i].onEvaluationTypeChanged(value);
        }
    }
    /**
     * Indicates whether the MMD WASM runtime is reading the back buffer of the WASM
     */
    get usingWasmBackBuffer() {
        return this._usingWasmBackBuffer;
    }
    /**
     * Enable or disable debug logging (default: false)
     */
    get loggingEnabled() {
        return this._loggingEnabled;
    }
    set loggingEnabled(value) {
        this._loggingEnabled = value;
        if (value) {
            this.log = this._logEnabled;
            this.warn = this._warnEnabled;
            this.error = this._errorEnabled;
        }
        else {
            this.log = this._logDisabled;
            this.warn = this._warnDisabled;
            this.error = this._errorDisabled;
        }
    }
    _logEnabled(message) {
        Logger.Log(message);
    }
    _logDisabled() {
        // do nothing
    }
    _warnEnabled(message) {
        Logger.Warn(message);
    }
    _warnDisabled() {
        // do nothing
    }
    _errorEnabled(message) {
        Logger.Error(message);
    }
    _errorDisabled() {
        // do nothing
    }
}
