(function (root, factory)
{
    if (typeof define === 'function' && define.amd)
    {
        // AMD. Register as an anonymous module.
        define(['videojs', 'wavesurfer'], factory);
    }
    else if (typeof module === 'object' && module.exports)
    {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('video.js'), require('wavesurfer.js'));
    }
    else
    {
        // Browser globals (root is window)
        root.returnExports = factory(root.videojs, root.WaveSurfer);
    }
}(this, function (videojs, WaveSurfer)
{
    var ERROR = 'error';
    var WARN = 'warn';
    var VjsComponent = videojs.getComponent('Component');

    /**
     * Draw a waveform for audio and video files in a video.js player.
     * @class
     * @augments videojs.Component
     */
    videojs.Waveform = videojs.extend(VjsComponent,
    {
        /**
         * The constructor function for the class.
         *
         * @param {(videojs.Player|Object)} player - Video.js player instance.
         * @param {Object} options - Player options.
         */
        constructor: function(player, options)
        {
            // run base component initializing with new options
            VjsComponent.call(this, player, options);

            // parse options
            this.waveReady = false;
            this.waveFinished = false;
            this.liveMode = false;
            this.debug = (this.options_.options.debug.toString() === 'true');
            this.msDisplayMax = parseFloat(this.options_.options.msDisplayMax);

            if (this.options_.options.src === 'live')
            {
                // check if the wavesurfer.js microphone plugin can be enabled
                try
                {
                    this.microphone = Object.create(WaveSurfer.Microphone);

                    // listen for events
                    this.microphone.on('deviceError', this.onWaveError.bind(this));

                    // enable audio input from a microphone
                    this.liveMode = true;
                    this.waveReady = true;

                    this.log('wavesurfer.js microphone plugin enabled.');
                }
                catch (TypeError)
                {
                    this.onWaveError('Could not find wavesurfer.js ' +
                        'microphone plugin!');
                }
            }

            // wait until player ui is ready
            this.player().one('ready', this.setupUI.bind(this));
        },

        /**
         * Player UI is ready.
         * @private
         */
        setupUI: function()
        {
            // customize controls
            this.player().bigPlayButton.hide();

            // the native controls don't work for this UI so disable
            // them no matter what
            if (this.player().usingNativeControls_ === true)
            {
                if (this.player().tech_.el_ !== undefined)
                {
                    this.player().tech_.el_.controls = false;
                }
            }

            // since video.js 5.11.0 the player won't start if no src is
            // set (see PR 3378), so fake it
            this.player().currentSrc = function()
            {
                return 'videojs-wavesurfer';
            };

            if (this.player().options_.controls)
            {
                // make sure controlBar is showing
                this.player().controlBar.show();
                this.player().controlBar.el().style.display = 'flex';

                // progress control isn't used by this plugin
                this.player().controlBar.progressControl.hide();

                // prevent controlbar fadeout
                this.player().on('userinactive', function(event)
                {
                    this.player().userActive(true);
                });

                // make sure time display is visible
                var uiElements = [this.player().controlBar.currentTimeDisplay,
                                  this.player().controlBar.timeDivider,
                                  this.player().controlBar.durationDisplay];
                for (var element in uiElements)
                {
                    // ignore when elements have been disabled by user
                    if (uiElements[element] !== undefined)
                    {
                        uiElements[element].el().style.display = 'block';
                        uiElements[element].show();
                    }
                }
                if (this.player().controlBar.remainingTimeDisplay !== undefined)
                {
                    this.player().controlBar.remainingTimeDisplay.hide();
                }
                if (this.player().controlBar.timeDivider !== undefined)
                {
                    this.player().controlBar.timeDivider.el().style.textAlign = 'center';
                    this.player().controlBar.timeDivider.el().style.width = '2em';
                }

                // disable play button until waveform is ready
                // (except when in live mode)
                if (!this.liveMode)
                {
                    this.player().controlBar.playToggle.hide();
                }
            }

            // waveform events
            this.surfer = Object.create(WaveSurfer);
            this.surfer.on('error', this.onWaveError.bind(this));
            this.surfer.on('finish', this.onWaveFinish.bind(this));

            this.surferReady = this.onWaveReady.bind(this);
            this.surferProgress = this.onWaveProgress.bind(this);
            this.surferSeek = this.onWaveSeek.bind(this);

            // only listen to these events when we're not in live mode
            if (!this.liveMode)
            {
                this.setupPlaybackEvents(true);
            }

            // player events
            this.player().on('play', this.onPlay.bind(this));
            this.player().on('pause', this.onPause.bind(this));
            this.player().on('volumechange', this.onVolumeChange.bind(this));
            this.player().on('fullscreenchange', this.onScreenChange.bind(this));

            // kick things off
            this.startPlayers();
        },

        /**
         * Starts or stops listening to events related to audio-playback.
         *
         * @param {boolean} enable - Start or stop listening to playback
         *     related events.
         * @private
         */
        setupPlaybackEvents: function(enable)
        {
            if (enable === false)
            {
                this.surfer.un('ready', this.surferReady);
                this.surfer.un('audioprocess', this.surferProgress);
                this.surfer.un('seek', this.surferSeek);
            }
            else if (enable === true)
            {
                this.surfer.on('ready', this.surferReady);
                this.surfer.on('audioprocess', this.surferProgress);
                this.surfer.on('seek', this.surferSeek);
            }
        },

        /**
         * Start the players.
         * @private
         */
        startPlayers: function()
        {
            var options = this.options_.options;

            // init waveform
            this.initialize(options);

            if (options.src !== undefined)
            {
                if (this.microphone === undefined)
                {
                    // show loading spinner
                    this.player().loadingSpinner.show();

                    // start loading file
                    this.load(options.src);
                }
                else
                {
                    // hide loading spinner
                    this.player().loadingSpinner.hide();

                    // connect microphone input to our waveform
                    options.wavesurfer = this.surfer;
                    this.microphone.init(options);
                }
            }
            else
            {
                // no valid src found, hide loading spinner
                this.player().loadingSpinner.hide();
            }
        },

        /**
         * Initializes the waveform.
         *
         * @param {Object} opts - Plugin options.
         * @private
         */
        initialize: function(opts)
        {
            this.originalHeight = this.player().options_.height;
            var controlBarHeight = this.player().controlBar.height();
            if (this.player().options_.controls === true && controlBarHeight === 0)
            {
                // the dimensions of the controlbar are not known yet, but we
                // need it now, so we can calculate the height of the waveform.
                // The default height is 30px, so use that instead.
                controlBarHeight = 30;
            }

            // set waveform element and dimensions
            // Set the container to player's container if "container" option is
            // not provided. If a waveform needs to be appended to your custom
            // element, then use below option. For example:
            // container: document.querySelector("#vjs-waveform")
            if (opts.container === undefined)
            {
                opts.container = this.el();
            }

            // set the height of generated waveform if user has provided height
            // from options. If height of waveform need to be customized then use
            // option below. For example: waveformHeight: 30
            if (opts.waveformHeight === undefined)
            {
                opts.height = this.player().height() - controlBarHeight;
            }
            else
            {
                opts.height = opts.waveformHeight;
            }

            // split channels
            if (opts.splitChannels && opts.splitChannels === true)
            {
                opts.height /= 2;
            }

            // customize waveform appearance
            this.surfer.init(opts);
        },

        /**
         * Start loading waveform data.
         *
         * @param {string|blob|file} url - Either the URL of the audio file,
         *     a Blob or a File object.
         */
        load: function(url)
        {
            if (url instanceof Blob || url instanceof File)
            {
                this.log('Loading object: ' + url);
                this.surfer.loadBlob(url);
            }
            else
            {
                this.log('Loading URL: ' + url);
                this.surfer.load(url);
            }
        },

        /**
         * Start/resume playback or microphone.
         */
        play: function()
        {
            if (this.liveMode)
            {
                // start/resume microphone visualization
                if (!this.microphone.active)
                {
                    this.log('Start microphone');
                    this.microphone.start();
                }
                else
                {
                    this.log('Resume microphone');
                    this.microphone.play();
                }
            }
            else
            {
                this.log('Start playback');

                // put video.js player UI in playback mode
                this.player().play();

                // start surfer playback
                this.surfer.play();
            }
        },

        /**
         * Pauses playback or microphone visualization.
         */
        pause: function()
        {
            if (this.liveMode)
            {
                // pause microphone visualization
                this.log('Pause microphone');
                this.microphone.pause();
            }
            else
            {
                // pause playback
                this.log('Pause playback');
                if (!this.waveFinished)
                {
                    this.surfer.pause();
                }
                else
                {
                    this.waveFinished = false;
                }

                this.setCurrentTime();
            }
        },

        /**
         * Remove the player and waveform.
         */
        destroy: function()
        {
            this.log('Destroying plugin');

            if (this.liveMode && this.microphone)
            {
                // destroy microphone plugin
                this.log('Destroying microphone');
                this.microphone.destroy();
            }

            this.surfer.destroy();
            this.player().dispose();
        },

        /**
         * Set the current volume.
         *
         * @param {number} volume - The new volume level.
         */
        setVolume: function(volume)
        {
            if (volume !== undefined)
            {
                this.log('Changing volume to: ' + volume);
                this.surfer.setVolume(volume);
            }
        },

        /**
         * Save waveform image as data URI.
         *
         * The default format is 'image/png'. Other supported types are
         * 'image/jpeg' and 'image/webp'.
         *
         * @param {string} [format=image/png] - String indicating the image format.
         * @param {number} [quality=1] - Number between 0 and 1 indicating image
         *     quality if the requested type is 'image/jpeg' or 'image/webp'.
         * @returns {string} The data URI of the image data.
         */
        exportImage: function(format, quality)
        {
            return this.surfer.exportImage(format, quality);
        },

        /**
         * Log message (if the debug option is enabled).
         * @private
         */
        log: function(args, logType)
        {
            if (this.debug === true)
            {
                if (logType === ERROR)
                {
                    videojs.log.error(args);
                }
                else if (logType === WARN)
                {
                    videojs.log.warn(args);
                }
                else
                {
                    videojs.log(args);
                }
            }
        },

        /**
         * Get the current time (in seconds) of the stream during playback.
         *
         * Returns 0 if no stream is available (yet).
         */
        getCurrentTime: function()
        {
            var currentTime = this.surfer.getCurrentTime();

            currentTime = isNaN(currentTime) ? 0 : currentTime;

            return currentTime;
        },

        /**
         * Updates the player's element displaying the current time.
         *
         * @param {number} [currentTime] - Current position of the playhead
         *     (in seconds).
         * @param {number} [duration] - Duration of the waveform (in seconds).
         * @private
         */
        setCurrentTime: function(currentTime, duration)
        {
            if (currentTime === undefined)
            {
                currentTime = this.surfer.getCurrentTime();
            }

            if (duration === undefined)
            {
                duration = this.surfer.getDuration();
            }

            currentTime = isNaN(currentTime) ? 0 : currentTime;
            duration = isNaN(duration) ? 0 : duration;
            var time = Math.min(currentTime, duration);

            // update control
            this.player().controlBar.currentTimeDisplay.contentEl(
                ).innerHTML = this.formatTime(time, duration);
        },

        /**
         * Get the duration of the stream in seconds.
         *
         * Returns 0 if no stream is available (yet).
         */
        getDuration: function()
        {
            var duration = this.surfer.getDuration();

            duration = isNaN(duration) ? 0 : duration;

            return duration;
        },

        /**
         * Updates the player's element displaying the duration time.
         *
         * @param {number} [duration] - Duration of the waveform (in seconds).
         * @private
         */
        setDuration: function(duration)
        {
            if (duration === undefined)
            {
                duration = this.surfer.getDuration();
            }

            duration = isNaN(duration) ? 0 : duration;

            // update control
            this.player().controlBar.durationDisplay.contentEl(
                ).innerHTML = this.formatTime(duration, duration);
        },

        /**
         * Wave ready event.
         *
         * Fired when audio is loaded, decoded and the waveform is drawn.
         *
         * @event waveReady
        */

        /**
         * Audio is loaded, decoded and the waveform is drawn.
         *
         * @fires waveReady
         * @private
         */
        onWaveReady: function()
        {
            this.waveReady = true;
            this.waveFinished = false;
            this.liveMode = false;

            this.log('Waveform is ready');
            this.player().trigger('waveReady');

            // update time display
            this.setCurrentTime();
            this.setDuration();

            // enable and show play button
            this.player().controlBar.playToggle.show();

            // hide loading spinner
            this.player().loadingSpinner.hide();

            // auto-play when ready (if enabled)
            if (this.player().options_.autoplay)
            {
                this.play();
            }
        },

        /**
         * Playback finish event.
         *
         * Fired when audio playback finished.
         *
         * @event playbackFinish
        */

        /**
         * Fires when audio playback completed.
         * @private
         */
        onWaveFinish: function()
        {
            this.log('Finished playback');

            this.player().trigger('playbackFinish');

            // check if player isn't paused already
            if (!this.player().paused())
            {
                // check if loop is enabled
                if (this.player().options_.loop)
                {
                    // reset waveform
                    this.surfer.stop();
                    this.play();
                }
                else
                {
                    // finished
                    this.waveFinished = true;

                    // pause player
                    this.player().pause();
                }
            }
        },

        /**
         * Fires continuously during audio playback.
         *
         * @param {number} time - Current time/location of the playhead.
         * @private
         */
        onWaveProgress: function(time)
        {
            this.setCurrentTime();
        },

        /**
         * Fires during seeking of the waveform.
         * @private
         */
        onWaveSeek: function()
        {
            this.setCurrentTime();
        },

        /**
         * Fired whenever the media in the player begins or resumes playback.
         * @private
         */
        onPlay: function()
        {
            // don't start playing until waveform's ready
            if (this.waveReady)
            {
                this.play();
            }
        },

        /**
         * Fired whenever the media in the player has been paused.
         * @private
         */
        onPause: function()
        {
            this.pause();
        },

        /**
         * Fired when the volume in the player changes.
         * @private
         */
        onVolumeChange: function()
        {
            var volume = this.player().volume();
            if (this.player().muted())
            {
                // muted volume
                volume = 0;
            }

            this.setVolume(volume);
        },

        /**
         * Fired when the player switches in or out of fullscreen mode.
         * @private
         */
        onScreenChange: function()
        {
            var isFullscreen = this.player().isFullscreen();
            var newHeight;

            if (!isFullscreen)
            {
                // restore original height
                newHeight = this.originalHeight;
            }
            else
            {
                // fullscreen height
                newHeight = window.outerHeight;
            }

            if (this.waveReady)
            {
                if (this.liveMode && !this.microphone.active)
                {
                    // we're in live mode but the microphone hasn't been
                    // started yet
                    return;
                }

                // destroy old drawing
                this.surfer.drawer.destroy();

                // set new height
                this.surfer.params.height = newHeight - this.player().controlBar.height();
                this.surfer.createDrawer();

                // redraw
                this.surfer.drawBuffer();

                // make sure playhead is restored at right position
                this.surfer.drawer.progress(this.surfer.backend.getPlayedPercents());
            }
        },

        /**
         * Waveform error.
         *
         * @param {string} error - The wavesurfer error.
         * @private
         */
        onWaveError: function(error)
        {
            this.log(error, ERROR);

            this.player().trigger('error', error);
        },

        /**
         * Format seconds as a time string, H:MM:SS, M:SS or M:SS:MMM.
         *
         * Supplying a guide (in seconds) will force a number of leading zeros
         * to cover the length of the guide.
         *
         * @param {number} seconds - Number of seconds to be turned into a
         *     string.
         * @param {number} guide - Number (in seconds) to model the string
         *     after.
         * @return {string} Time formatted as H:MM:SS, M:SS or M:SS:MMM, e.g.
         *     0:00:12.
         * @private
         */
        formatTime: function(seconds, guide)
        {
            // Default to using seconds as guide
            seconds = seconds < 0 ? 0 : seconds;
            guide = guide || seconds;
            var s = Math.floor(seconds % 60),
                m = Math.floor(seconds / 60 % 60),
                h = Math.floor(seconds / 3600),
                gm = Math.floor(guide / 60 % 60),
                gh = Math.floor(guide / 3600),
                ms = Math.floor((seconds - s) * 1000);

            // handle invalid times
            if (isNaN(seconds) || seconds === Infinity)
            {
                // '-' is false for all relational operators (e.g. <, >=) so this
                // setting will add the minimum number of fields specified by the
                // guide
                h = m = s = ms = '-';
            }

            // Check if we need to show milliseconds
            if (guide > 0 && guide < this.msDisplayMax)
            {
                if (ms < 100)
                {
                    if (ms < 10)
                    {
                        ms = '00' + ms;
                    }
                    else
                    {
                        ms = '0' + ms;
                    }
                }
                ms = ':' + ms;
            }
            else
            {
                ms = '';
            }

            // Check if we need to show hours
            h = (h > 0 || gh > 0) ? h + ':' : '';

            // If hours are showing, we may need to add a leading zero.
            // Always show at least one digit of minutes.
            m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':';

            // Check if leading zero is need for seconds
            s = ((s < 10) ? '0' + s : s);

            return h + m + s + ms;
        }

    });

    var createWaveform = function()
    {
        var props = {
            className: 'vjs-waveform',
            tabIndex: 0
        };
        return VjsComponent.prototype.createEl('div', props);
    };

    // plugin defaults
    var defaults = {
        // Display console log messages.
        debug: false,
        // msDisplayMax indicates the number of seconds that is
        // considered the boundary value for displaying milliseconds
        // in the time controls. An audio clip with a total length of
        // 2 seconds and a msDisplayMax of 3 will use the format
        // M:SS:MMM. Clips longer than msDisplayMax will be displayed
        // as M:SS or HH:MM:SS.
        msDisplayMax: 3
    };

    /**
     * Initialize the plugin.
     *
     * @param {Object} [options] - Configuration for the plugin.
     * @private
     */
    var wavesurferPlugin = function(options)
    {
        var settings = videojs.mergeOptions(defaults, options);
        var player = this;

        // create new waveform
        player.waveform = new videojs.Waveform(player,
        {
            'el': createWaveform(),
            'options': settings
        });

        // add waveform to dom
        player.el().appendChild(player.waveform.el());
    };

    // register the plugin
    videojs.plugin('wavesurfer', wavesurferPlugin);

    // return a function to define the module export
    return wavesurferPlugin;
}));
