import { Observable } from "@babylonjs/core/Misc/observable";
/**
 * Stream audio player
 *
 * This class is used to play the audio from the stream
 *
 * It is suitable for playing long sounds because it plays even if all of the audio is not loaded
 *
 * Wrapper of `HTMLAudioElement` which handles audio playback permission issues gracefully
 */
export class StreamAudioPlayer {
    /**
     * On load error observable
     *
     * This observable is notified when the audio load is failed
     */
    onLoadErrorObservable;
    /**
     * On duration changed observable
     *
     * This observable is notified when the audio duration is changed
     */
    onDurationChangedObservable;
    /**
     * On mute state changed observable
     *
     * This observable is notified when the mute state is changed
     */
    onPlaybackRateChangedObservable;
    /**
     * On mute state changed observable
     *
     * This observable is notified when the mute state is changed
     */
    onMuteStateChangedObservable;
    /**
     * On play observable
     *
     * This observable is notified when the player is played
     */
    onPlayObservable;
    /**
     * On pause observable
     *
     * This observable is notified when the player is paused
     */
    onPauseObservable;
    /**
     * On seek observable
     *
     * This observable is notified when the player is seeked
     */
    onSeekObservable;
    _audio;
    _duration;
    _playbackRate;
    _isVirtualPlay;
    _virtualStartTime;
    _virtualPaused;
    _virtualPauseCurrentTime;
    _metadataLoaded;
    _bindedDispose;
    _disposeObservableObject;
    /**
     * Create a stream audio player
     *
     * In general disposeObservable should be `Scene` of Babylon.js
     *
     * @param disposeObservable Objects that limit the lifetime of this instance
     */
    constructor(disposeObservable) {
        this.onLoadErrorObservable = new Observable();
        this.onDurationChangedObservable = new Observable();
        this.onPlaybackRateChangedObservable = new Observable();
        this.onMuteStateChangedObservable = new Observable();
        this.onPlayObservable = new Observable();
        this.onPauseObservable = new Observable();
        this.onSeekObservable = new Observable();
        const audio = this._audio = new Audio();
        audio.loop = false;
        audio.autoplay = false;
        this._duration = 0;
        this._playbackRate = 1;
        this._isVirtualPlay = false;
        this._virtualStartTime = 0;
        this._virtualPaused = true;
        this._virtualPauseCurrentTime = 0;
        this._metadataLoaded = false;
        audio.ondurationchange = this._onDurationChanged;
        audio.onerror = this._onLoadError;
        audio.onplaying = this._onPlay;
        audio.onpause = this._onPause;
        audio.onseeked = this._onSeek;
        this._bindedDispose = this.dispose.bind(this);
        this._disposeObservableObject = disposeObservable;
        if (this._disposeObservableObject !== null) {
            this._disposeObservableObject.onDisposeObservable.add(this._bindedDispose);
        }
    }
    _onDurationChanged = () => {
        this._duration = this._audio.duration;
        if (this._isVirtualPlay) {
            this._isVirtualPlay = false;
            this.onMuteStateChangedObservable.notifyObservers();
        }
        this._virtualPaused = true;
        this._virtualPauseCurrentTime = 0;
        this._metadataLoaded = true;
        this.onDurationChangedObservable.notifyObservers();
    };
    _onLoadError = () => {
        this._duration = 0;
        if (this._isVirtualPlay) {
            this._isVirtualPlay = false;
            this.onMuteStateChangedObservable.notifyObservers();
        }
        this._virtualPaused = true;
        this._virtualPauseCurrentTime = 0;
        this._metadataLoaded = false;
        this.onLoadErrorObservable.notifyObservers();
        this.onDurationChangedObservable.notifyObservers();
    };
    _onPlay = () => {
        if (!this._isVirtualPlay) {
            this._audio.playbackRate = this._playbackRate;
        }
        this.onPlayObservable.notifyObservers();
    };
    _onPause = () => {
        if (!this._isVirtualPlay) {
            this.onPauseObservable.notifyObservers();
        }
        else {
            if (this._virtualPaused) {
                this.onPauseObservable.notifyObservers();
            }
        }
    };
    _ignoreSeekedEventOnce = false;
    _onSeek = () => {
        if (this._ignoreSeekedEventOnce) {
            this._ignoreSeekedEventOnce = false;
            return;
        }
        this.onSeekObservable.notifyObservers();
    };
    /**
     * Audio duration (in seconds)
     */
    get duration() {
        return this._duration;
    }
    /**
     * Current time (in seconds)
     *
     * This property may be slow to update
     */
    get currentTime() {
        if (this._isVirtualPlay) {
            if (this._virtualPaused) {
                return this._virtualPauseCurrentTime;
            }
            else {
                const computedTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate;
                if (computedTime > this._duration) {
                    this._virtualPaused = true;
                    this._virtualPauseCurrentTime = this._duration;
                    this._onPause();
                    return this._virtualPauseCurrentTime;
                }
                else {
                    return computedTime;
                }
            }
        }
        else {
            return this._audio.currentTime;
        }
    }
    set currentTime(value) {
        if (this._isVirtualPlay) {
            if (this._virtualPaused) {
                this._virtualPauseCurrentTime = value;
            }
            else {
                this._virtualStartTime = performance.now() / 1000 - value / this._playbackRate;
            }
            this._onSeek();
        }
        else {
            this._audio.currentTime = value;
        }
    }
    /** @internal */
    _setCurrentTimeWithoutNotify(value) {
        if (this._isVirtualPlay) {
            if (this._virtualPaused) {
                this._virtualPauseCurrentTime = value;
            }
            else {
                this._virtualStartTime = performance.now() / 1000 - value / this._playbackRate;
            }
        }
        else {
            this._ignoreSeekedEventOnce = true;
            this._audio.currentTime = value;
        }
    }
    /**
     * Volume (0.0 to 1.0)
     */
    get volume() {
        return this._audio.volume;
    }
    set volume(value) {
        this._audio.volume = value;
    }
    /**
     * Whether the audio is muted
     */
    get muted() {
        return this._isVirtualPlay;
    }
    /**
     * Mute the audio
     */
    mute() {
        if (this._isVirtualPlay)
            return;
        this._isVirtualPlay = true;
        this._virtualStartTime = performance.now() / 1000 - this._audio.currentTime / this._playbackRate;
        this._virtualPaused = this._audio.paused;
        this._virtualPauseCurrentTime = this._audio.currentTime;
        this._audio.pause();
        this.onMuteStateChangedObservable.notifyObservers();
    }
    /**
     * Unmute the audio
     *
     * Unmute is possible failed if user interaction is not performed
     * @returns Whether the audio is unmuted
     */
    async unmute() {
        if (!this._isVirtualPlay)
            return false;
        let notAllowedError = false;
        this._ignoreSeekedEventOnce = true;
        if (this._virtualPaused) {
            this._audio.currentTime = this._virtualPauseCurrentTime;
        }
        else {
            this._audio.currentTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate;
            try {
                await this._audio.play();
                this._audio.playbackRate = this._playbackRate;
            }
            catch (e) {
                if (!(e instanceof DOMException && e.name === "NotAllowedError"))
                    throw e;
                notAllowedError = true;
            }
        }
        if (!notAllowedError) {
            this._isVirtualPlay = false;
            this._virtualPaused = true;
            this._virtualPauseCurrentTime = 0;
            this.onMuteStateChangedObservable.notifyObservers();
            return true;
        }
        return false;
    }
    /**
     * Playback rate (0.07 to 16.0)
     */
    get playbackRate() {
        return this._playbackRate;
    }
    set playbackRate(value) {
        this._setPlaybackRateWithoutNotify(value);
        this.onPlaybackRateChangedObservable.notifyObservers();
    }
    /** @internal */
    _setPlaybackRateWithoutNotify(value) {
        if (this._isVirtualPlay && !this._virtualPaused) {
            const nowInSec = performance.now() / 1000;
            const currentTime = (nowInSec - this._virtualStartTime) * this._playbackRate;
            this._virtualStartTime = nowInSec - currentTime / value;
        }
        this._playbackRate = value;
        this._audio.playbackRate = value;
    }
    /**
     * Determines whether or not the browser should adjust the pitch of the audio to compensate for changes to the playback rate made by setting
     */
    get preservesPitch() {
        return this._audio.preservesPitch;
    }
    set preservesPitch(value) {
        this._audio.preservesPitch = value;
    }
    /**
     * Whether the player is paused
     */
    get paused() {
        if (this._isVirtualPlay) {
            return this._virtualPaused;
        }
        else {
            return this._audio.paused;
        }
    }
    /**
     * Audio source URL
     */
    get source() {
        return this._audio.src;
    }
    set source(value) {
        if (value === this._audio.src)
            return;
        this._audio.src = value;
        this._metadataLoaded = false;
        if (this._isVirtualPlay) {
            this._isVirtualPlay = false;
            this.onMuteStateChangedObservable.notifyObservers();
        }
        this._virtualPaused = true;
        this._virtualPauseCurrentTime = 0;
        this._audio.load();
    }
    /**
     * Whether the audio metadata(durations) is loaded
     */
    get metadataLoaded() {
        return this._metadataLoaded;
    }
    async _virtualPlay() {
        if (this._metadataLoaded) {
            if (this._virtualPaused) {
                this._virtualStartTime = performance.now() / 1000 - this._virtualPauseCurrentTime / this._playbackRate;
                this._virtualPaused = false;
            }
            if (!this._isVirtualPlay) {
                this._isVirtualPlay = true;
                this.onMuteStateChangedObservable.notifyObservers();
            }
            this._onPlay();
        }
        else {
            await new Promise((resolve, reject) => {
                const onDurationChanged = () => {
                    if (this._virtualPaused) {
                        this._virtualStartTime = performance.now() / 1000 - this._virtualPauseCurrentTime / this._playbackRate;
                        this._virtualPaused = false;
                    }
                    if (!this._isVirtualPlay) {
                        this._isVirtualPlay = true;
                        this.onMuteStateChangedObservable.notifyObservers();
                    }
                    this._onPlay();
                    this.onLoadErrorObservable.removeCallback(onLoadError);
                    resolve();
                };
                const onLoadError = () => {
                    this.onDurationChangedObservable.removeCallback(onDurationChanged);
                    reject(new DOMException("The media resource indicated by the src attribute or assigned media provider object was not suitable.", "NotSupportedError"));
                };
                this.onDurationChangedObservable.addOnce(onDurationChanged);
                this.onLoadErrorObservable.addOnce(onLoadError);
            });
        }
    }
    _playRequestBlocking = false;
    /**
     * Play the audio from the current position
     *
     * If context don't have permission to play the audio, play audio in a mute state
     */
    async play() {
        if (this._isVirtualPlay && !this._virtualPaused)
            return;
        if (this._isVirtualPlay) {
            await this._virtualPlay();
            return;
        }
        if (this._playRequestBlocking)
            return;
        this._playRequestBlocking = true;
        try {
            await this._audio.play();
        }
        catch (e) {
            if (e instanceof DOMException && e.name === "NotAllowedError") {
                await this._virtualPlay();
            }
            else {
                throw e;
            }
        }
        finally {
            this._playRequestBlocking = false;
        }
    }
    /**
     * Pause the audio
     */
    pause() {
        if (this._isVirtualPlay) {
            if (this._virtualPaused)
                return;
            this._virtualPaused = true;
            this._virtualPauseCurrentTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate;
            this._onPause();
        }
        else {
            this._audio.pause();
        }
    }
    /**
     * Dispose the player
     */
    dispose() {
        const audio = this._audio;
        audio.pause();
        audio.ondurationchange = null;
        audio.onerror = null;
        audio.onplaying = null;
        audio.onpause = null;
        audio.onseeked = null;
        this._audio.remove();
        this.onLoadErrorObservable.clear();
        this.onDurationChangedObservable.clear();
        this.onPlaybackRateChangedObservable.clear();
        this.onMuteStateChangedObservable.clear();
        this.onPlayObservable.clear();
        this.onPauseObservable.clear();
        this.onSeekObservable.clear();
        if (this._disposeObservableObject !== null) {
            this._disposeObservableObject.onDisposeObservable.removeCallback(this._bindedDispose);
        }
    }
}
