/**
 * @author       Richard Davey <rich@phaser.io>
 * @author       Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 * @copyright    2013-2025 Phaser Studio Inc.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Base64ToArrayBuffer = require('../../utils/base64/Base64ToArrayBuffer');
var BaseSoundManager = require('../BaseSoundManager');
var Class = require('../../utils/Class');
var Events = require('../events');
var GameEvents = require('../../core/events');
var WebAudioSound = require('./WebAudioSound');
var GetFastValue = require('../../utils/object/GetFastValue');

/**
 * @classdesc
 * Web Audio API implementation of the Sound Manager.
 *
 * Not all browsers can play all audio formats.
 *
 * There is a good guide to what's supported: [Cross-browser audio basics: Audio codec support](https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics#Audio_Codec_Support).
 *
 * @class WebAudioSoundManager
 * @extends Phaser.Sound.BaseSoundManager
 * @memberof Phaser.Sound
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Game} game - Reference to the current game instance.
 */
var WebAudioSoundManager = new Class({

    Extends: BaseSoundManager,

    initialize:

    function WebAudioSoundManager (game)
    {
        /**
         * The AudioContext being used for playback.
         *
         * @name Phaser.Sound.WebAudioSoundManager#context
         * @type {AudioContext}
         * @since 3.0.0
         */
        this.context = this.createAudioContext(game);

        /**
         * Gain node responsible for controlling global muting.
         *
         * @name Phaser.Sound.WebAudioSoundManager#masterMuteNode
         * @type {GainNode}
         * @since 3.0.0
         */
        this.masterMuteNode = this.context.createGain();

        /**
         * Gain node responsible for controlling global volume.
         *
         * @name Phaser.Sound.WebAudioSoundManager#masterVolumeNode
         * @type {GainNode}
         * @since 3.0.0
         */
        this.masterVolumeNode = this.context.createGain();

        this.masterMuteNode.connect(this.masterVolumeNode);

        this.masterVolumeNode.connect(this.context.destination);

        /**
         * Destination node for connecting individual sounds to.
         *
         * @name Phaser.Sound.WebAudioSoundManager#destination
         * @type {AudioNode}
         * @since 3.0.0
         */
        this.destination = this.masterMuteNode;

        this.locked = this.context.state === 'suspended';

        BaseSoundManager.call(this, game);

        if (this.locked)
        {
            if (game.isBooted)
            {
                this.unlock();
            }
            else
            {
                game.events.once(GameEvents.BOOT, this.unlock, this);
            }
        }

        game.events.on(GameEvents.VISIBLE, this.onGameVisible, this);
    },

    /**
     * Internal handler for Phaser.Core.Events#VISIBLE.
     * 
     * Needed to handle resuming audio on iOS17/iOS18+ if you hide the browser, press
     * the home button, etc. See https://github.com/phaserjs/phaser/issues/6829
     *
     * @method Phaser.Sound.WebAudioSoundManager#onGameVisible
     * @private
     * @since 3.88.0
     */
    onGameVisible: function ()
    {
        var context = this.context;

        //  setTimeout to avoid weird audio artifacts (thanks Apple)
        window.setTimeout(function ()
        {

            if (context)
            {
                context.suspend();
                context.resume();
            }

        }, 100);
    },

    /**
     * Method responsible for instantiating and returning AudioContext instance.
     * If an instance of an AudioContext class was provided through the game config,
     * that instance will be returned instead. This can come in handy if you are reloading
     * a Phaser game on a page that never properly refreshes (such as in an SPA project)
     * and you want to reuse already instantiated AudioContext.
     *
     * @method Phaser.Sound.WebAudioSoundManager#createAudioContext
     * @since 3.0.0
     *
     * @param {Phaser.Game} game - Reference to the current game instance.
     *
     * @return {AudioContext} The AudioContext instance to be used for playback.
     */
    createAudioContext: function (game)
    {
        var audioConfig = game.config.audio;

        if (audioConfig.context)
        {
            audioConfig.context.resume();

            return audioConfig.context;
        }

        if (window.hasOwnProperty('AudioContext'))
        {
            return new AudioContext();
        }
        else if (window.hasOwnProperty('webkitAudioContext'))
        {
            return new window.webkitAudioContext();
        }
    },

    /**
     * This method takes a new AudioContext reference and then sets
     * this Sound Manager to use that context for all playback.
     *
     * As part of this call it also disconnects the master mute and volume
     * nodes and then re-creates them on the new given context.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setAudioContext
     * @since 3.21.0
     *
     * @param {AudioContext} context - Reference to an already created AudioContext instance.
     *
     * @return {this} The WebAudioSoundManager instance.
     */
    setAudioContext: function (context)
    {
        if (this.context)
        {
            this.context.close();
        }

        if (this.masterMuteNode)
        {
            this.masterMuteNode.disconnect();
        }

        if (this.masterVolumeNode)
        {
            this.masterVolumeNode.disconnect();
        }

        this.context = context;

        this.masterMuteNode = context.createGain();
        this.masterVolumeNode = context.createGain();

        this.masterMuteNode.connect(this.masterVolumeNode);
        this.masterVolumeNode.connect(context.destination);

        this.destination = this.masterMuteNode;

        return this;
    },

    /**
     * Adds a new sound into the sound manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#add
     * @since 3.0.0
     *
     * @param {string} key - Asset key for the sound.
     * @param {Phaser.Types.Sound.SoundConfig} [config] - An optional config object containing default sound settings.
     *
     * @return {Phaser.Sound.WebAudioSound} The new sound instance.
     */
    add: function (key, config)
    {
        var sound = new WebAudioSound(this, key, config);

        this.sounds.push(sound);

        return sound;
    },

    /**
     * Decode audio data into a format ready for playback via Web Audio.
     *
     * The audio data can be a base64 encoded string, an audio media-type data uri, or an ArrayBuffer instance.
     *
     * The `audioKey` is the key that will be used to save the decoded audio to the audio cache.
     *
     * Instead of passing a single entry you can instead pass an array of `Phaser.Types.Sound.DecodeAudioConfig`
     * objects as the first and only argument.
     *
     * Decoding is an async process, so be sure to listen for the events to know when decoding has completed.
     *
     * Once the audio has decoded it can be added to the Sound Manager or played via its key.
     *
     * @method Phaser.Sound.WebAudioSoundManager#decodeAudio
     * @fires Phaser.Sound.Events#DECODED
     * @fires Phaser.Sound.Events#DECODED_ALL
     * @since 3.18.0
     *
     * @param {(Phaser.Types.Sound.DecodeAudioConfig[]|string)} [audioKey] - The string-based key to be used to reference the decoded audio in the audio cache, or an array of audio config objects.
     * @param {(ArrayBuffer|string)} [audioData] - The audio data, either a base64 encoded string, an audio media-type data uri, or an ArrayBuffer instance.
     */
    decodeAudio: function (audioKey, audioData)
    {
        var audioFiles;

        if (!Array.isArray(audioKey))
        {
            audioFiles = [ { key: audioKey, data: audioData } ];
        }
        else
        {
            audioFiles = audioKey;
        }

        var cache = this.game.cache.audio;
        var remaining = audioFiles.length;

        for (var i = 0; i < audioFiles.length; i++)
        {
            var entry = audioFiles[i];

            var key = entry.key;
            var data = entry.data;

            if (typeof data === 'string')
            {
                data = Base64ToArrayBuffer(data);
            }

            var success = function (key, audioBuffer)
            {
                cache.add(key, audioBuffer);

                this.emit(Events.DECODED, key);

                remaining--;

                if (remaining === 0)
                {
                    this.emit(Events.DECODED_ALL);
                }
            }.bind(this, key);

            var failure = function (key, error)
            {
                //  eslint-disable-next-line no-console
                console.error('Error decoding audio: ' + key + ' - ', error ? error.message : '');

                remaining--;

                if (remaining === 0)
                {
                    this.emit(Events.DECODED_ALL);
                }
            }.bind(this, key);

            this.context.decodeAudioData(data, success, failure);
        }
    },

    /**
     * Sets the X and Y position of the Spatial Audio listener on this Web Audios context.
     *
     * If you call this method with no parameters it will default to the center-point of
     * the game canvas. Depending on the type of game you're making, you may need to call
     * this method constantly to reset the listener position as the camera scrolls.
     *
     * Calling this method does nothing on HTML5Audio.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setListenerPosition
     * @since 3.60.0
     *
     * @param {number} [x] - The x position of the Spatial Audio listener.
     * @param {number} [y] - The y position of the Spatial Audio listener.
     */
    setListenerPosition: function (x, y)
    {
        if (x === undefined) { x = this.game.scale.width / 2; }
        if (y === undefined) { y = this.game.scale.height / 2; }

        this.listenerPosition.set(x, y);

        return this;
    },

    /**
     * Unlocks Web Audio API on the initial input event.
     *
     * Read more about how this issue is handled here in [this article](https://medium.com/@pgoloskokovic/unlocking-web-audio-the-smarter-way-8858218c0e09).
     *
     * @method Phaser.Sound.WebAudioSoundManager#unlock
     * @since 3.0.0
     */
    unlock: function ()
    {
        var _this = this;

        var body = document.body;

        var unlockHandler = function unlockHandler ()
        {
            if (_this.context && body)
            {
                var bodyRemove = body.removeEventListener.bind(body);

                _this.context.resume().then(function ()
                {
                    bodyRemove('touchstart', unlockHandler);
                    bodyRemove('touchend', unlockHandler);
                    bodyRemove('mousedown', unlockHandler);
                    bodyRemove('mouseup', unlockHandler);
                    bodyRemove('keydown', unlockHandler);

                    _this.unlocked = true;

                }, function ()
                {
                    bodyRemove('touchstart', unlockHandler);
                    bodyRemove('touchend', unlockHandler);
                    bodyRemove('mousedown', unlockHandler);
                    bodyRemove('mouseup', unlockHandler);
                    bodyRemove('keydown', unlockHandler);
                });
            }
        };

        if (body)
        {
            body.addEventListener('touchstart', unlockHandler, false);
            body.addEventListener('touchend', unlockHandler, false);
            body.addEventListener('mousedown', unlockHandler, false);
            body.addEventListener('mouseup', unlockHandler, false);
            body.addEventListener('keydown', unlockHandler, false);
        }
    },

    /**
     * Method used internally for pausing sound manager if
     * Phaser.Sound.WebAudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.WebAudioSoundManager#onBlur
     * @protected
     * @since 3.0.0
     */
    onBlur: function ()
    {
        if (!this.locked)
        {
            this.context.suspend();
        }
    },

    /**
     * Method used internally for resuming sound manager if
     * Phaser.Sound.WebAudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.WebAudioSoundManager#onFocus
     * @protected
     * @since 3.0.0
     */
    onFocus: function ()
    {
        var context = this.context;

        if (context && !this.locked && (context.state === 'suspended' || context.state === 'interrupted'))
        {
            context.resume();
        }
    },

    /**
     * Update method called on every game step.
     *
     * Removes destroyed sounds and updates every active sound in the game.
     *
     * @method Phaser.Sound.WebAudioSoundManager#update
     * @protected
     * @fires Phaser.Sound.Events#UNLOCKED
     * @since 3.0.0
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta)
    {
        var listener = this.context.listener;

        var x = GetFastValue(this.listenerPosition, 'x', null);
        var y = GetFastValue(this.listenerPosition, 'y', null);


        if (listener && listener.positionX !== undefined)
        {
            if (x && x !== this._spatialx)
            {
                this._spatialx = listener.positionX.value = x;
            }
            if (y && y !== this._spatialy)
            {
                this._spatialy = listener.positionY.value = y;
            }
        }

        // Firefox doesn't currently implement positionX, positionY and positionZ properties on AudioListener,
        // falling back on AudioListener.prototype.setPosition() method. @see https://developer.mozilla.org/en-US/docs/Web/API/AudioListener/setPosition
        else if (listener)
        {
            if (x && x !== this._spatialx)
            {
                this._spatialx = x;
            }
            if (y && y !== this._spatialy)
            {
                this._spatialy = y;
            }

            var z = GetFastValue(listener, 'z', 0);

            listener.setPosition(this._spatialx || 0, this._spatialy || 0, z);
        }

        BaseSoundManager.prototype.update.call(this, time, delta);

        //  Resume interrupted audio on iOS only if the game has focus
        if (!this.gameLostFocus)
        {
            this.onFocus();
        }
    },

    /**
     * Calls Phaser.Sound.BaseSoundManager#destroy method
     * and cleans up all Web Audio API related stuff.
     *
     * @method Phaser.Sound.WebAudioSoundManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.destination = null;
        this.masterVolumeNode.disconnect();
        this.masterVolumeNode = null;
        this.masterMuteNode.disconnect();
        this.masterMuteNode = null;

        if (this.game.config.audio.context)
        {
            this.context.suspend();
        }
        else
        {
            var _this = this;

            this.context.close().then(function ()
            {
                _this.context = null;
            });
        }

        this.game.events.off(GameEvents.VISIBLE, this.onGameVisible, this);

        BaseSoundManager.prototype.destroy.call(this);
    },

    /**
     * Sets the muted state of all this Sound Manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setMute
     * @fires Phaser.Sound.Events#GLOBAL_MUTE
     * @since 3.3.0
     *
     * @param {boolean} value - `true` to mute all sounds, `false` to unmute them.
     *
     * @return {Phaser.Sound.WebAudioSoundManager} This Sound Manager.
     */
    setMute: function (value)
    {
        this.mute = value;

        return this;
    },

    /**
     * @name Phaser.Sound.WebAudioSoundManager#mute
     * @type {boolean}
     * @fires Phaser.Sound.Events#GLOBAL_MUTE
     * @since 3.0.0
     */
    mute: {

        get: function ()
        {
            return (this.masterMuteNode.gain.value === 0);
        },

        set: function (value)
        {
            this.masterMuteNode.gain.setValueAtTime(value ? 0 : 1, 0);

            this.emit(Events.GLOBAL_MUTE, this, value);
        }

    },

    /**
     * Sets the volume of this Sound Manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setVolume
     * @fires Phaser.Sound.Events#GLOBAL_VOLUME
     * @since 3.3.0
     *
     * @param {number} value - The global volume of this Sound Manager.
     *
     * @return {Phaser.Sound.WebAudioSoundManager} This Sound Manager.
     */
    setVolume: function (value)
    {
        this.volume = value;

        return this;
    },

    /**
     * @name Phaser.Sound.WebAudioSoundManager#volume
     * @type {number}
     * @fires Phaser.Sound.Events#GLOBAL_VOLUME
     * @since 3.0.0
     */
    volume: {

        get: function ()
        {
            return this.masterVolumeNode.gain.value;
        },

        set: function (value)
        {
            this.masterVolumeNode.gain.setValueAtTime(value, 0);

            this.emit(Events.GLOBAL_VOLUME, this, value);
        }

    }

});

module.exports = WebAudioSoundManager;
