import { _audioContext } from './audio-listener.js';
import { Emitter } from '@wonderlandengine/api';
import { BufferPlayer, DEF_VOL, MIN_RAMP_TIME, MIN_VOLUME } from './audio-players.js';
/**
 * Enumerates the available channels within the AudioManager.
 * These channels can be utilized to regulate global volume.
 */
export var AudioChannel;
(function (AudioChannel) {
    /** Intended for sound effects. Connects to Master AudioChannel. */
    AudioChannel[AudioChannel["Sfx"] = 0] = "Sfx";
    /** Intended for music. Connects to Master AudioChannel. */
    AudioChannel[AudioChannel["Music"] = 1] = "Music";
    /** Connects directly to output. */
    AudioChannel[AudioChannel["Master"] = 2] = "Master";
})(AudioChannel || (AudioChannel = {}));
/**
 * Enumerates the possible states of playback for audio sources.
 */
export var PlayState;
(function (PlayState) {
    /** The source is ready to be played */
    PlayState[PlayState["Ready"] = 0] = "Ready";
    /** The source has started playing */
    PlayState[PlayState["Playing"] = 1] = "Playing";
    /** The source has stopped */
    PlayState[PlayState["Stopped"] = 2] = "Stopped";
    /** The source has paused */
    PlayState[PlayState["Paused"] = 3] = "Paused";
})(PlayState || (PlayState = {}));
/**
 * Default number of internal players.
 */
export const DEF_PLAYER_COUNT = 32;
const SHIFT_AMOUNT = 16;
const MAX_NUMBER_OF_INSTANCES = (1 << SHIFT_AMOUNT) - 1;
export class AudioManager {
    /** The emitter will notify all listeners about the PlayState of a unique ID.
     *
     * @remarks
     * - READY will be emitted if all sources of a given source ID have loaded.
     * - PLAYING / STOPPED / PAUSED are only emitted for play IDs that are returned by the play() method.
     * - If you want to check the status for a source ID, convert the play ID of the message using the
     *   getSourceIdFromPlayId() method.
     *
     * @see getSourceIdFromPlayId
     * @example
     * ```js
     * const music = audioManager.play(Sounds.Music);
     * audioManager.emitter.add((msg) => {
     *    if (msg.id === music) {
     *          console.log(msg.state);
     *    }
     * });
     * ```
     */
    emitter = new Emitter();
    /**
     * Sets the random function the manager will use for selecting buffers.
     *
     * @remarks Default random function is Math.random()
     * @param func Function that should be used for select the buffer.
     */
    randomBufferSelectFunction = Math.random;
    /* Cache for decoded audio buffers */
    _bufferCache = [];
    /* Simple, fast cache for players */
    _playerCache = [];
    _playerCacheIndex = 0;
    _amountOfFreePlayers = DEF_PLAYER_COUNT;
    /* Counts how many times a sourceId has played. Resets to 0 after {@link MAX_NUMBER_OF_INSTANCES }. */
    _instanceCounter = [];
    _masterGain;
    _musicGain;
    _sfxGain;
    _unlocked = false;
    _autoplayStorage = [];
    /**
     * Constructs a AudioManager.
     *
     * Uses the default amount of players.
     * @see DEF_PLAYER_COUNT
     * @example
     * ```js
     * // AudioManager can't be constructed in a non-browser environment!
     * export const am = window.AudioContext ? new AudioManager() : null!;
     * ```
     */
    constructor() {
        this._unlockAudioContext();
        this._sfxGain = new GainNode(_audioContext);
        this._masterGain = new GainNode(_audioContext);
        this._musicGain = new GainNode(_audioContext);
        this._sfxGain.connect(this._masterGain);
        this._musicGain.connect(this._masterGain);
        this._masterGain.connect(_audioContext.destination);
        for (let i = 0; i < DEF_PLAYER_COUNT; i++) {
            this._playerCache.push(new BufferPlayer(this));
        }
    }
    async load(path, id) {
        if (id < 0) {
            console.error('audio-manager: Negative IDs are not valid! Skipping ${path}.');
            return;
        }
        const paths = Array.isArray(path) ? path : [path];
        if (!this._bufferCache[id]) {
            this._bufferCache[id] = [];
        }
        this._instanceCounter[id] = -1;
        for (let i = 0; i < paths.length; i++) {
            const response = await fetch(paths[i]);
            const arrayBuffer = await response.arrayBuffer();
            const audioBuffer = await _audioContext.decodeAudioData(arrayBuffer);
            this._bufferCache[id].push(audioBuffer);
        }
        /* Init the instanceCounter */
        this._instanceCounter[id] = 0;
        this.emitter.notify({ id: id, state: PlayState.Ready });
    }
    async loadBatch(...pair) {
        return Promise.all(pair.map((p) => this.load(p[0], p[1])));
    }
    play(id, config) {
        if (this._instanceCounter[id] == -1) {
            console.warn(`audio-manager: Tried to play audio that is still decoding: ${id}`);
            return -1;
        }
        const bufferList = this._bufferCache[id];
        if (!bufferList) {
            console.error(`audio-manager: No audio source is associated with identifier: ${id}`);
            return -1;
        }
        if (!this._unlocked) {
            return -1;
        }
        const player = this._getAvailablePlayer();
        if (!player) {
            console.warn(`audio-manager: All players are busy and no low priority player could be found to free up to play ${id}.`);
            return -1;
        }
        const unique_id = this._generateUniqueId(id);
        /* Decode playConfig */
        if (config?.priority) {
            /* Priority players get pushed to the end of the list and cant be retrieved to free up */
            this._amountOfFreePlayers--;
            let index = this._playerCache.indexOf(player);
            this._playerCache.splice(index, 1);
            this._playerCache.push(player);
            player.priority = true;
        }
        else {
            player.priority = false;
        }
        player.playId = unique_id;
        player.buffer = this._selectRandomBuffer(bufferList);
        player.looping = config?.loop ?? false;
        player.position = config?.position;
        player.playOffset = config?.playOffset ?? 0;
        player.channel = config?.channel ?? AudioChannel.Sfx;
        player.volume = config?.volume ?? DEF_VOL;
        player.play();
        return unique_id;
    }
    _playWithUniqueId(uniqueId, config) {
        const id = this.getSourceIdFromPlayId(uniqueId);
        const bufferList = this._bufferCache[id];
        if (!bufferList) {
            console.error(`audio-manager: No audio source is associated with identifier: ${id}`);
            return;
        }
        const player = this._getAvailablePlayer();
        if (!player) {
            console.warn(`audio-manager: All players are busy and no low priority player could be found to free up.`);
            return;
        }
        /* Decode playConfig */
        if (config?.priority) {
            /* Priority players get pushed to the end of the list and cant be retrievd to free up */
            this._amountOfFreePlayers--;
            let index = this._playerCache.indexOf(player);
            this._playerCache.splice(index, 1);
            this._playerCache.push(player);
            player.priority = true;
        }
        else {
            player.priority = false;
        }
        player.playId = uniqueId;
        player.buffer = this._selectRandomBuffer(bufferList);
        player.looping = config?.loop ?? false;
        player.oneShot = config?.oneShot ?? false;
        player.position = config?.position;
        player.playOffset = config?.playOffset ?? 0;
        player.channel = config?.channel ?? AudioChannel.Sfx;
        player.volume = config?.volume ?? DEF_VOL;
        player.play();
    }
    playOneShot(id, config) {
        if (!config)
            this.play(id, { oneShot: true });
        config.loop = false;
        config.priority = false;
        config.oneShot = true;
        this.play(id, config);
    }
    /**
     * Advances the _playerCacheIndex and stops the player on that position.
     *
     * @returns A BufferPlayer with PlayState.Stopped, or undefined if no player can be stopped.
     */
    _getAvailablePlayer() {
        if (this._amountOfFreePlayers < 1)
            return;
        /* Advance cache pointer */
        this._playerCacheIndex = (this._playerCacheIndex + 1) % this._amountOfFreePlayers;
        const player = this._playerCache[this._playerCacheIndex];
        /* Make player available if unavailable */
        player.stop();
        return player;
    }
    autoplay(id, config) {
        if (this._unlocked) {
            return this.play(id, config);
        }
        const uniqueId = this._generateUniqueId(id);
        this._autoplayStorage.push([uniqueId, config]);
        return uniqueId;
    }
    stop(playId) {
        this._playerCache.forEach((player) => {
            if (player.playId === playId) {
                player.stop();
                return;
            }
        });
    }
    pause(playId) {
        this._playerCache.forEach((player) => {
            if (player.playId === playId) {
                player.pause();
                return;
            }
        });
    }
    resume(playId) {
        this._playerCache.forEach((player) => {
            if (player.playId === playId) {
                player.resume();
                return;
            }
        });
    }
    stopOneShots() {
        this._playerCache.forEach((player) => {
            if (player.oneShot) {
                player.stop();
                return;
            }
        });
    }
    resumeAll() {
        this._playerCache.forEach((player) => {
            player.resume();
        });
    }
    pauseAll() {
        this._playerCache.forEach((player) => {
            player.pause();
        });
    }
    stopAll() {
        this._playerCache.forEach((player) => {
            player.stop();
        });
    }
    setGlobalVolume(channel, volume, time = 0) {
        volume = Math.max(MIN_VOLUME, volume);
        time = _audioContext.currentTime + Math.max(MIN_RAMP_TIME, time);
        switch (channel) {
            case AudioChannel.Music:
                this._musicGain.gain.linearRampToValueAtTime(volume, time);
                break;
            case AudioChannel.Sfx:
                this._sfxGain.gain.linearRampToValueAtTime(volume, time);
                break;
            case AudioChannel.Master:
                this._masterGain.gain.linearRampToValueAtTime(volume, time);
                break;
            default:
                return;
        }
    }
    remove(id) {
        if (id < 0)
            return;
        this.stop(id);
        this._bufferCache[id] = undefined;
        this._instanceCounter[id] = -1;
    }
    removeAll() {
        this.stopAll();
        this._bufferCache.length = 0;
        this._instanceCounter.length = 0;
    }
    getSourceIdFromPlayId(playId) {
        return playId >> SHIFT_AMOUNT;
    }
    get amountOfFreePlayers() {
        return this._amountOfFreePlayers;
    }
    _selectRandomBuffer(bufferList) {
        return bufferList[Math.floor(this.randomBufferSelectFunction() * bufferList.length)];
    }
    _generateUniqueId(id) {
        let instanceCount = this._instanceCounter[id];
        if (!instanceCount)
            instanceCount = 0;
        else if (instanceCount === -1)
            return -1;
        const unique_id = (id << SHIFT_AMOUNT) + instanceCount;
        this._instanceCounter[id] = (instanceCount + 1) % MAX_NUMBER_OF_INSTANCES;
        return unique_id;
    }
    /**
     * @warning This function is for internal use only!
     */
    _returnPriorityPlayer(player) {
        if (!player.priority)
            return;
        /* We start looking from the back, because priority players are always in the back */
        for (let i = this._playerCache.length - 1; i >= 0; i--) {
            if (this._playerCache[i] === player) {
                this._playerCache.splice(i, 1);
                this._playerCache.unshift(player);
                this._amountOfFreePlayers++;
                return;
            }
        }
    }
    _unlockAudioContext() {
        const unlockHandler = () => {
            _audioContext.resume().then(() => {
                window.removeEventListener('click', unlockHandler);
                window.removeEventListener('touch', unlockHandler);
                window.removeEventListener('keydown', unlockHandler);
                window.removeEventListener('mousedown', unlockHandler);
                this._unlocked = true;
                for (const audio of this._autoplayStorage) {
                    this._playWithUniqueId(audio[0], audio[1]);
                }
                this._autoplayStorage.length = 0;
            });
        };
        window.addEventListener('click', unlockHandler);
        window.addEventListener('touch', unlockHandler);
        window.addEventListener('keydown', unlockHandler);
        window.addEventListener('mousedown', unlockHandler);
    }
}
export class EmptyAudioManager {
    async load(path, id) { }
    async loadBatch(...pair) { }
    play(id, config) {
        return -1;
    }
    playOneShot(id, config) { }
    autoplay(id, config) {
        return -1;
    }
    stop(playId) { }
    pause(playId) { }
    resume(playId) { }
    stopOneShots() { }
    resumeAll() { }
    pauseAll() { }
    stopAll() { }
    setGlobalVolume(channel, volume, time) { }
    remove(id) { }
    removeAll() { }
    getSourceIdFromPlayId(playId) {
        return -1;
    }
    get amountOfFreePlayers() {
        return -1;
    }
}
/**
 * Global instance of a AudioManager.
 *
 * @remarks
 * To construct an AudioManager, the WebAudio API is needed. For non-browser environments, like during the packaging
 * step of the wonderland editor, the globalAudioManager is set to an `EmptyAudioManager`.
 * It enables the usage of `load()` and `loadBatch()` in top-level code.
 *
 * @warning
 * ⚠️ Only load() and loadBatch() can be used in top-level code ⚠️
 */
export const globalAudioManager = window.AudioContext
    ? new AudioManager()
    : new EmptyAudioManager();
