// ----------------------------------------------------------------------------
// Buzz, a Javascript HTML5 Audio library
// Licensed under the MIT license.
// http://buzz.jaysalvat.com/
// ----------------------------------------------------------------------------
// Copyright (C) Jay Salvat
// http://jaysalvat.com/
// ----------------------------------------------------------------------------
/* jshint browser: true, node: true */
/* global define */

(function (context, factory) {
    "use strict";

    if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else {
        context.buzz = factory();
    }
})(this, function () {
    "use strict";

    var AudioContext = window.AudioContext || window.webkitAudioContext;

    var buzz = {
        defaults: {
            autoplay: false,
            crossOrigin: null,
            duration: 5000,
            formats: [],
            loop: false,
            placeholder: '--',
            preload: 'metadata',
            volume: 80,
            webAudioApi: false,
            document: window.document // iframe support
        },
        types: {
            'mp3': 'audio/mpeg',
            'ogg': 'audio/ogg',
            'wav': 'audio/wav',
            'aac': 'audio/aac',
            'm4a': 'audio/x-m4a'
        },
        sounds: [],
        el: document.createElement('audio'),

        getAudioContext: function() {
            if (this.audioCtx === undefined) {
                try {
                    this.audioCtx = AudioContext ? new AudioContext() : null;
                } catch (e) {
                    // There is a limit to how many contexts you can have, so fall back in case of errors constructing it
                    this.audioCtx = null;
                }
            }

            return this.audioCtx;
        },

        sound: function (src, options) {
            options = options || {};

            var doc = options.document || buzz.defaults.document;

            var pid = 0,
                events = [],
                eventsOnce = {},
                supported = buzz.isSupported();

            // publics
            this.load = function () {
                if (!supported) {
                    return this;
                }

                this.sound.load();

                return this;
            };

            this.play = function () {
                if (!supported) {
                    return this;
                }

                this.sound.play().catch(function () {});

                return this;
            };

            this.togglePlay = function () {
                if (!supported) {
                    return this;
                }

                if (this.sound.paused) {
                    this.sound.play().catch(function () {});
                } else {
                    this.sound.pause();
                }

                return this;
            };

            this.pause = function () {
                if (!supported) {
                    return this;
                }

                this.sound.pause();

                return this;
            };

            this.isPaused = function () {
                if (!supported) {
                    return null;
                }

                return this.sound.paused;
            };

            this.stop = function () {
                if (!supported ) {
                    return this;
                }

                this.sound.pause();
                this.setTime(0);

                return this;
            };

            this.isEnded = function () {
                if (!supported) {
                    return null;
                }

                return this.sound.ended;
            };

            this.loop = function () {
                if (!supported) {
                    return this;
                }

                this.sound.loop = 'loop';
                this.bind('ended.buzzloop', function () {
                    this.currentTime = 0;
                    this.play();
                });

                return this;
            };

            this.unloop = function () {
                if (!supported) {
                    return this;
                }

                this.sound.removeAttribute('loop');
                this.unbind('ended.buzzloop');

                return this;
            };

            this.mute = function () {
                if (!supported) {
                    return this;
                }

                this.sound.muted = true;

                return this;
            };

            this.unmute = function () {
                if (!supported) {
                    return this;
                }

                this.sound.muted = false;

                return this;
            };

            this.toggleMute = function () {
                if (!supported) {
                    return this;
                }

                this.sound.muted = !this.sound.muted;

                return this;
            };

            this.isMuted = function () {
                if (!supported) {
                    return null;
                }

                return this.sound.muted;
            };

            this.setVolume = function (volume) {
                if (!supported) {
                    return this;
                }

                if (volume < 0) {
                    volume = 0;
                }
                if (volume > 100) {
                    volume = 100;
                }

                this.volume = volume;
                this.sound.volume = volume / 100;

                return this;
            };

            this.getVolume = function () {
                if (!supported) {
                    return this;
                }

                return this.volume;
            };

            this.increaseVolume = function (value) {
                return this.setVolume(this.volume + (value || 1));
            };

            this.decreaseVolume = function (value) {
                return this.setVolume(this.volume - (value || 1));
            };

            this.setTime = function (time) {
                if (!supported) {
                    return this;
                }

                var set = true;
                this.whenReady(function () {
                    if (set === true) {
                        set = false;
                        this.sound.currentTime = time;
                    }
                });

                return this;
            };

            this.getTime = function () {
                if (!supported) {
                    return null;
                }

                var time = Math.round(this.sound.currentTime * 100) / 100;

                return isNaN(time) ? buzz.defaults.placeholder : time;
            };

            this.setPercent = function (percent) {
                if (!supported) {
                    return this;
                }

                return this.setTime(buzz.fromPercent(percent, this.sound.duration));
            };

            this.getPercent = function () {
                if (!supported) {
                    return null;
                }

                var percent = Math.round(buzz.toPercent(this.sound.currentTime, this.sound.duration));

                return isNaN(percent) ? buzz.defaults.placeholder : percent;
            };

            this.setSpeed = function (duration) {
                if (!supported) {
                    return this;
                }

                this.sound.playbackRate = duration;

                return this;
            };

            this.getSpeed = function () {
                if (!supported) {
                    return null;
                }

                return this.sound.playbackRate;
            };

            this.getDuration = function () {
                if (!supported) {
                    return null;
                }

                var duration = Math.round(this.sound.duration * 100) / 100;

                return isNaN(duration) ? buzz.defaults.placeholder : duration;
            };

            this.getPlayed = function () {
                if (!supported) {
                    return null;
                }

                return timerangeToArray(this.sound.played);
            };

            this.getBuffered = function () {
                if (!supported) {
                    return null;
                }

                return timerangeToArray(this.sound.buffered);
            };

            this.getSeekable = function () {
                if (!supported) {
                    return null;
                }

                return timerangeToArray(this.sound.seekable);
            };

            this.getErrorCode = function () {
                if (supported && this.sound.error) {
                    return this.sound.error.code;
                }

                return 0;
            };

            this.getErrorMessage = function () {
                if (!supported) {
                    return null;
                }

                switch(this.getErrorCode()) {
                    case 1:
                        return 'MEDIA_ERR_ABORTED';
                    case 2:
                        return 'MEDIA_ERR_NETWORK';
                    case 3:
                        return 'MEDIA_ERR_DECODE';
                    case 4:
                        return 'MEDIA_ERR_SRC_NOT_SUPPORTED';
                    default:
                        return null;
                }
            };

            this.getStateCode = function () {
                if (!supported) {
                    return null;
                }

                return this.sound.readyState;
            };

            this.getStateMessage = function () {
                if (!supported) {
                    return null;
                }

                switch(this.getStateCode()) {
                    case 0:
                        return 'HAVE_NOTHING';
                    case 1:
                        return 'HAVE_METADATA';
                    case 2:
                        return 'HAVE_CURRENT_DATA';
                    case 3:
                        return 'HAVE_FUTURE_DATA';
                    case 4:
                        return 'HAVE_ENOUGH_DATA';
                    default:
                        return null;
                }
            };

            this.getNetworkStateCode = function () {
                if (!supported) {
                    return null;
                }

                return this.sound.networkState;
            };

            this.getNetworkStateMessage = function () {
                if (!supported) {
                    return null;
                }

                switch(this.getNetworkStateCode()) {
                    case 0:
                        return 'NETWORK_EMPTY';
                    case 1:
                        return 'NETWORK_IDLE';
                    case 2:
                        return 'NETWORK_LOADING';
                    case 3:
                        return 'NETWORK_NO_SOURCE';
                    default:
                        return null;
                }
            };

            this.set = function (key, value) {
                if (!supported) {
                    return this;
                }

                this.sound[key] = value;

                return this;
            };

            this.get = function (key) {
                if (!supported) {
                  return null;
                }

                return key ? this.sound[key] : this.sound;
            };

            this.bind = function (types, func) {
                if (!supported) {
                    return this;
                }

                types = types.split(' ');

                var self = this,
                    efunc = function (e) { func.call(self, e); };

                for (var t = 0; t < types.length; t++) {
                    var type = types[t],
                        idx = type;
                        type = idx.split('.')[0];

                        events.push({ idx: idx, func: efunc });
                        this.sound.addEventListener(type, efunc, true);
                }

                return this;
            };

            this.unbind = function (types) {
                if (!supported) {
                    return this;
                }

                types = types.split(' ');

                for (var t = 0; t < types.length; t++) {
                    var idx = types[t],
                        type = idx.split('.')[0];

                    for (var i = 0; i < events.length; i++) {
                        var namespace = events[i].idx.split('.');
                        if (events[i].idx === idx || (namespace[1] && namespace[1] === idx.replace('.', ''))) {
                            this.sound.removeEventListener(type, events[i].func, true);
                            // remove event
                            events.splice(i, 1);
                        }
                    }
                }

                return this;
            };

            this.bindOnce = function (type, func) {
                if (!supported) {
                    return this;
                }

                var self = this;

                eventsOnce[pid++] = false;
                this.bind(type + '.' + pid, function () {
                   if (!eventsOnce[pid]) {
                       eventsOnce[pid] = true;
                       func.call(self);
                   }
                   self.unbind(type + '.' + pid);
                });

                return this;
            };

            this.trigger = function (types, detail) {
                if (!supported) {
                    return this;
                }

                types = types.split(' ');

                for (var t = 0; t < types.length; t++) {
                    var idx = types[t];

                    for (var i = 0; i < events.length; i++) {
                        var eventType = events[i].idx.split('.');

                        if (events[i].idx === idx || (eventType[0] && eventType[0] === idx.replace('.', ''))) {
                            var evt = doc.createEvent('HTMLEvents');

                            evt.initEvent(eventType[0], false, true);

                            evt.originalEvent = detail;

                            this.sound.dispatchEvent(evt);
                        }
                    }
                }

                return this;
            };

            this.fadeTo = function (to, duration, callback) {
                if (!supported) {
                    return this;
                }

                if (duration instanceof Function) {
                    callback = duration;
                    duration = buzz.defaults.duration;
                } else {
                    duration = duration || buzz.defaults.duration;
                }

                var from = this.volume,
                    delay = duration / Math.abs(from - to),
                    self = this,
                    fadeToTimeout;

                this.play();

                function doFade() {
                    clearTimeout(fadeToTimeout);

                    fadeToTimeout = setTimeout(function () {
                        if (from < to && self.volume < to) {
                            self.setVolume(self.volume += 1);
                            doFade();
                        } else if (from > to && self.volume > to) {
                            self.setVolume(self.volume -= 1);
                            doFade();
                        } else if (callback instanceof Function) {
                            callback.apply(self);
                        }
                    }, delay);
                }

                this.whenReady(function () {
                    doFade();
                });

                return this;
            };

            this.fadeIn = function (duration, callback) {
                if (!supported) {
                    return this;
                }

                return this.setVolume(0).fadeTo(100, duration, callback);
            };

            this.fadeOut = function (duration, callback) {
                if (!supported) {
                    return this;
                }

                return this.fadeTo(0, duration, callback);
            };

            this.fadeWith = function (sound, duration) {
                if (!supported) {
                    return this;
                }

                this.fadeOut(duration, function () {
                    this.stop();
                });

                sound.play().fadeIn(duration);

                return this;
            };

            this.whenReady = function (func) {
                if (!supported) {
                    return null;
                }

                var self = this;

                if (this.sound.readyState === 0) {
                    this.bind('canplay.buzzwhenready', function () {
                        func.call(self);
                    });
                } else {
                    func.call(self);
                }
            };

            this.addSource = function (src) {
                var self   = this,
                    source = doc.createElement('source');

                source.src = src;

                if (buzz.types[getExt(src)]) {
                    source.type = buzz.types[getExt(src)];
                }

                this.sound.appendChild(source);

                source.addEventListener('error', function (e) {
                    self.trigger('sourceerror', e);
                });

                return source;
            };

            // privates
            function timerangeToArray(timeRange) {
                var array = [],
                    length = timeRange.length - 1;

                for (var i = 0; i <= length; i++) {
                    array.push({
                        start: timeRange.start(i),
                        end: timeRange.end(i)
                    });
                }

                return array;
            }

            function getExt(filename) {
                return filename.split('.').pop();
            }

            // init
            if (supported && src) {

                for (var i in buzz.defaults) {
                    if (buzz.defaults.hasOwnProperty(i)) {
                        if (options[i] === undefined) {
                            options[i] = buzz.defaults[i];
                        }
                    }
                }

                this.sound = doc.createElement('audio');

                // Shoud we set crossOrigin?
                if (options.crossOrigin !== null) {
                    this.sound.crossOrigin = options.crossOrigin;
                }

                // Use web audio if possible to improve performance.
                if (options.webAudioApi) {
                    var audioCtx = buzz.getAudioContext();
                    if (audioCtx) {
                      this.source = audioCtx.createMediaElementSource(this.sound);
                      this.source.connect(audioCtx.destination);
                    }
                }

                if (src instanceof Array) {
                    for (var j in src) {
                        if (src.hasOwnProperty(j)) {
                            this.addSource(src[j]);
                        }
                    }
                } else if (options.formats.length) {
                    for (var k in options.formats) {
                        if (options.formats.hasOwnProperty(k)) {
                            this.addSource(src + '.' + options.formats[k]);
                        }
                    }
                } else {
                    this.addSource(src);
                }

                if (options.loop) {
                    this.loop();
                }

                if (options.autoplay) {
                    this.sound.autoplay = 'autoplay';
                }

                if (options.preload === true) {
                    this.sound.preload = 'auto';
                } else if (options.preload === false) {
                    this.sound.preload = 'none';
                } else {
                    this.sound.preload = options.preload;
                }

                this.setVolume(options.volume);

                buzz.sounds.push(this);
            }
        },

        group: function (sounds) {
            sounds = argsToArray(sounds, arguments);

            // publics
            this.getSounds = function () {
                return sounds;
            };

            this.add = function (soundArray) {
                soundArray = argsToArray(soundArray, arguments);

                for (var a = 0; a < soundArray.length; a++) {
                    sounds.push(soundArray[a]);
                }
            };

            this.remove = function (soundArray) {
                soundArray = argsToArray(soundArray, arguments);

                for (var a = 0; a < soundArray.length; a++) {
                    for (var i = 0; i < sounds.length; i++) {
                        if (sounds[i] === soundArray[a]) {
                            sounds.splice(i, 1);
                            break;
                        }
                    }
                }
            };

            this.load = function () {
                fn('load');

                return this;
            };

            this.play = function () {
                fn('play');

                return this;
            };

            this.togglePlay = function () {
                fn('togglePlay');

                return this;
            };

            this.pause = function (time) {
                fn('pause', time);

                return this;
            };

            this.stop = function () {
                fn('stop');

                return this;
            };

            this.mute = function () {
                fn('mute');

                return this;
            };

            this.unmute = function () {
                fn('unmute');

                return this;
            };

            this.toggleMute = function () {
                fn('toggleMute');

                return this;
            };

            this.setVolume = function (volume) {
                fn('setVolume', volume);

                return this;
            };

            this.increaseVolume = function (value) {
                fn('increaseVolume', value);

                return this;
            };

            this.decreaseVolume = function (value) {
                fn('decreaseVolume', value);

                return this;
            };

            this.loop = function () {
                fn('loop');

                return this;
            };

            this.unloop = function () {
                fn('unloop');

                return this;
            };

            this.setSpeed = function (speed) {
                fn('setSpeed', speed);

                return this;
            };

            this.setTime = function (time) {
                fn('setTime', time);

                return this;
            };

            this.set = function (key, value) {
                fn('set', key, value);

                return this;
            };

            this.bind = function (type, func) {
                fn('bind', type, func);

                return this;
            };

            this.unbind = function (type) {
                fn('unbind', type);

                return this;
            };

            this.bindOnce = function (type, func) {
                fn('bindOnce', type, func);

                return this;
            };

            this.trigger = function (type) {
                fn('trigger', type);

                return this;
            };

            this.fade = function (from, to, duration, callback) {
                fn('fade', from, to, duration, callback);

                return this;
            };

            this.fadeIn = function (duration, callback) {
                fn('fadeIn', duration, callback);

                return this;
            };

            this.fadeOut = function (duration, callback) {
                fn('fadeOut', duration, callback);

                return this;
            };

            // privates
            function fn() {
                var args = argsToArray(null, arguments),
                    func = args.shift();

                for (var i = 0; i < sounds.length; i++) {
                    sounds[i][func].apply(sounds[i], args);
                }
            }

            function argsToArray(array, args) {
                return (array instanceof Array) ? array : Array.prototype.slice.call(args);
            }
        },

        all: function () {
            return new buzz.group(buzz.sounds);
        },

        isSupported: function () {
            return !!buzz.el.canPlayType;
        },

        isOGGSupported: function () {
            return !!buzz.el.canPlayType && buzz.el.canPlayType('audio/ogg; codecs="vorbis"');
        },

        isWAVSupported: function () {
            return !!buzz.el.canPlayType && buzz.el.canPlayType('audio/wav; codecs="1"');
        },

        isMP3Supported: function () {
            return !!buzz.el.canPlayType && buzz.el.canPlayType('audio/mpeg;');
        },

        isAACSupported: function () {
            return !!buzz.el.canPlayType && (buzz.el.canPlayType('audio/x-m4a;') || buzz.el.canPlayType('audio/aac;'));
        },

        toTimer: function (time, withHours) {
            var h, m, s;

            h = Math.floor(time / 3600);
            h = isNaN(h) ? '--' : (h >= 10) ? h : '0' + h;
            m = withHours ? Math.floor(time / 60 % 60) : Math.floor(time / 60);
            m = isNaN(m) ? '--' : (m >= 10) ? m : '0' + m;
            s = Math.floor(time % 60);
            s = isNaN(s) ? '--' : (s >= 10) ? s : '0' + s;

            return withHours ? h + ':' + m + ':' + s : m + ':' + s;
        },

        fromTimer: function (time) {
            var splits = time.toString().split(':');

            if (splits && splits.length === 3) {
                time = (parseInt(splits[0], 10) * 3600) + (parseInt(splits[1], 10) * 60) + parseInt(splits[2], 10);
            }

            if (splits && splits.length === 2) {
                time = (parseInt(splits[0], 10) * 60) + parseInt(splits[1], 10);
            }

            return time;
        },

        toPercent: function (value, total, decimal) {
            var r = Math.pow(10, decimal || 0);

            return Math.round(((value * 100) / total) * r) / r;
        },

        fromPercent: function (percent, total, decimal) {
            var r = Math.pow(10, decimal || 0);

            return  Math.round(((total / 100) * percent) * r) / r;
        }
    };

    return buzz;
});
