/*******************************************************************************
 * @author Lukasz Jagodzinski <a href="mailto:l.jagodzinsk@samsung.com">l.jagodzinsk@samsung.com</a>
 * 
 * Copyright (c) 2012 Samsung Electronics All Rights Reserved.
 ******************************************************************************/
var app = (function () {
    'use strict';

    var _files, _context, _source, _gainNode, _filterNode, _equalizerNodes, _analyserNode, _generationMethods, _timer, _option,
        _init, _loadSound, _isSoundPlaying, _playSound, _generateAndPlaySound, _stopSound, _setPlaybackRate, _createRoutingGraph, _startTimer, _stopTimer, _timerFunction, _draw,
        _$playbackRate, _$stop, _$filters, _$equalizer;

    _generationMethods = {};
    _generationMethods.oscillator = function generateSoundWithOscillator() {
        var source;

        source = _context.createOscillator();
        source.type = source.TRIANGLE; /* 0 - Sine wave, 1 - square wave, 2 - sawtooth wave, 3 - triangle wave */
        source.frequency.value = 100;
        source.detune.value = 400;

        return source;
    };

    _generationMethods.manual = function generateSoundManually() {
        var source, buffer, data, i;

        buffer = _context.createBuffer(1, 44100, 44100);
        data = buffer.getChannelData(0);
        for (i = 0; i < data.length; i++) {
            data[i] = (Math.random() - 0.5) * 2;
        }
        source = _context.createBufferSource();
        source.loop = true;
        source.buffer = buffer;

        return source;
    };

    _init = function () {
        var i, frequencies, onAppExitListener, onListItemClickListener, onVolumeChangeListener, onOptionsChangeListener, onFilterTypeChangeListener, onFilterFrequencyChangeListener, onFilterQualityChangeListener, onFilterGainChangeListener, onEqualizerChangeListener, onPlaybackRateChangeListener, initializeUI;

        _context = new webkitAudioContext(); /* Create contex object. */
        _source = null;
        _option = 'disabled';

        /* Create nodes. */
        _gainNode = _context.createGainNode(); // createGain()
        _filterNode = _context.createBiquadFilter();
        _analyserNode = _context.createAnalyser();
        _equalizerNodes = [
            _context.createBiquadFilter(),
            _context.createBiquadFilter(),
            _context.createBiquadFilter(),
            _context.createBiquadFilter(),
            _context.createBiquadFilter(),
            _context.createBiquadFilter()
        ];
        /* Initialize nodes. */
        _filterNode.type = 0;
        _filterNode.frequency.value = 10000;
        _filterNode.Q.value = 0;
        _filterNode.gain.value = 0;
        frequencies = [50, 160, 500, 1600, 5000, 20000];
        for (i = 0; i < 6; i++) {
            _equalizerNodes[i].frequency.value = frequencies[i];
            _equalizerNodes[i].type = 5;
            _equalizerNodes[i].Q.value = 2;
        }
        /* Connect available nodes to make a routing graph.
         * Enable volume level manipulation and sound wave visualization. */
        _gainNode.connect(_analyserNode);
        _analyserNode.connect(_context.destination);
        _filterNode.connect(_gainNode);
        _equalizerNodes[0].connect(_equalizerNodes[1]);
        _equalizerNodes[1].connect(_equalizerNodes[2]);
        _equalizerNodes[2].connect(_equalizerNodes[3]);
        _equalizerNodes[3].connect(_equalizerNodes[4]);
        _equalizerNodes[4].connect(_equalizerNodes[5]);
        _equalizerNodes[5].connect(_gainNode);


        /* onAppExitListener stops visualization timer and exits application. */
        onAppExitListener = function () {
            _stopTimer();
            tizen.application.getCurrentApplication().exit();
        };

        /* onListItemClickListener plays the clicked sound. */
        onListItemClickListener = function () {
            var a, name, uri, file;

            a = $(this);
            name = a.data('name');
            uri = a.data('uri');

            /* If there is URI to the file try to load it and then play. If
             * there is no URI just try to generate and play sound. */
            if (uri) {
                file = {
                    name: name,
                    uri: uri
                };
                _loadSound(file, function () {
                    _playSound(a.data('name'));
                });
            } else {
                _generateAndPlaySound(a.data('name'));
            }
        };

        /* onVolumeChangeListener changes volume of the sound. */
        onVolumeChangeListener = function () {
            /* Slider's values range between 0 and 200 but the GainNode's
             * default value is 1 what is standard volume level. We have to
             * dived slider's value by 100, but first we convert string value to
             * the integer value. */
            _gainNode.gain.value = parseInt(this.value, 10) / 100;
        };

        /* Listeners that shows/hides options. */
        onOptionsChangeListener = function () {
            _option = this.value;

            switch (this.value) {
            case 'filters':
                _$filters.show();
                _$equalizer.hide();
                break;
            case 'equalizer':
                _$filters.hide();
                _$equalizer.show();
                break;
            case 'disabled':
                _$filters.hide();
                _$equalizer.hide();
                break;
            }

            _createRoutingGraph();
        };
        onFilterTypeChangeListener = function () {
            _filterNode.type = parseInt(this.value, 10);
        };
        onFilterFrequencyChangeListener = function () {
            _filterNode.frequency.value = parseInt(this.value, 10);
        };
        onFilterQualityChangeListener = function () {
            _filterNode.Q.value = parseFloat(this.value);
        };
        onFilterGainChangeListener = function () {
            _filterNode.gain.value = parseFloat(this.value);
        };

        /* Listener for the equalizer. */
        onEqualizerChangeListener = function () {
            var index;

            index = parseInt($(this).attr('id').replace('equalizer-', ''), 10);
            _equalizerNodes[index].gain.value = parseInt(this.value, 10);
        };

        /* onPlaybackRateChangeListener changes the speed with which sound is played. */
        onPlaybackRateChangeListener = function () {
            _setPlaybackRate(parseFloat(this.value));
        };

        /* Function initializes all fields in the application. */
        initializeUI = function () {
            _$playbackRate = $('#playback-rate');
            _$stop = $('#stop');
            _$filters = $('#filters');
            _$equalizer = $('#equalizer');
            
            $('#close').on('click', onAppExitListener);
            $('li a').each(function () {
                $(this).on('click', onListItemClickListener);
            });

            $('#volume').on('change', onVolumeChangeListener);
            _$playbackRate.on('change', onPlaybackRateChangeListener);

            $('#options').on('change', onOptionsChangeListener);

            $('#filter-type').on('change', onFilterTypeChangeListener);
            $('#filter-frequency').on('change', onFilterFrequencyChangeListener);
            $('#filter-quality').on('change', onFilterQualityChangeListener);
            $('#filter-gain').on('change', onFilterGainChangeListener);

            $('input[id^="equalizer-"]').on('change', onEqualizerChangeListener);

            _$stop.on('click', _stopSound);
        };

        initializeUI();

        _startTimer();
    };

    _loadSound = function (file, successCallback, errorCallback) {
        var xhr, isLoaded, onRequestLoad, onRequestError, onDecodeAudioDataSuccess, onDecodeAudioDataError, doXHRRequest;

        /* Stop execution if sound file to load was not given. */
        if (!file) {
            return;
        }

        /* Set default values. */
        _files = _files || [];
        successCallback = successCallback || function successCallback() {};
        errorCallback = errorCallback || function errorCallback(msg) {
            alert(msg);
        };

        /* Check if file with the same name is already in the list. */
        isLoaded = false;
        $.each(_files, function isFileAlreadyLoaded(i) {
            if (_files[i].name === file.name) {
                /* Set flag indicating that file is already loaded and stop
                 * 'each' function. */
                isLoaded = true;
                return false;
            }
        });

        /* When audio data is decoded add it to the files list. */
        onDecodeAudioDataSuccess = function (buffer) {
            if (!buffer) {
                errorCallback('Error decoding file ' + file.uri + ' data.');
                tlib.view.hideLoader();
                return;
            }
            /* Add sound file to loaded sounds list when loading succeeded. */
            _files.push({
                name: file.name,
                uri: file.uri,
                buffer: buffer
            });
            /* Hide loading indicator. */
            tlib.view.hideLoader();
            /* Execute callback function. */
            successCallback();
        };
        /* Display error message when audio data decoding failed. */
        onDecodeAudioDataError = function (error) {
            errorCallback('Error decoding file ' + file.uri + ' data.' + error);
            tlib.view.hideLoader();
        };

        /* When loading file is finished try to decode its audio data. */
        onRequestLoad = function () {
            /* Decode audio data. */
            _context.decodeAudioData(xhr.response, onDecodeAudioDataSuccess, onDecodeAudioDataError);
        };
        /* Display error message when file loading failed. */
        onRequestError = function () {
            errorCallback('XHR error when loading file ' + file.uri + '.');
            tlib.view.hideLoader();
        };

        /* Do AJAX request. */
        doXHRRequest = function () {
            xhr = new XMLHttpRequest();
            xhr.open('GET', file.uri, true);
            xhr.responseType = 'arraybuffer';
            xhr.onload = onRequestLoad;
            xhr.onloadstart = tlib.view.showLoader;
            xhr.onerror = onRequestError;
            xhr.send();
        };

        /* Try to load data. If it comes from external source check whether
         * Internet connection is active. If it's already loaded just execute
         * callback function. */
        if (isLoaded) {
            successCallback();
        } else if (file.uri) {
            if (file.uri.indexOf('http://') === 0 || file.uri.indexOf('https://') === 0) {
                tlib.network.isInternetConnection(function isInternetConnectionCallback(isActive) {
                    if (isActive) {
                        doXHRRequest();
                    } else {
                        errorCallback('There is no active Internet connection.');
                    }
                });
            } else {
                doXHRRequest();
            }
        }
    };

    _isSoundPlaying = function () {
        return _source && _source.playbackState === _source.PLAYING_STATE;
    };

    _playSound = function (name) {
        tlib.view.showLoader();

        /* Look for the sound buffer in files list and play sound from that buffer. */
        $.each(_files, function (i, file) {
            if (file.name === name) {
                _stopSound();

                /* Create SourceNode and add buffer to it. */
                _source = _context.createBufferSource();
                _source.buffer = file.buffer;
                /* Connect nodes to create routing graph. */
                _createRoutingGraph();
                _source.noteOn(0); // start()
                /* Set playback rate to a new value because it could be changed
                 * while no sound was played. */
                _setPlaybackRate(_$playbackRate.val());
                tlib.view.hideLoader();

                return false;
            }
        });
    };

    _generateAndPlaySound = function (name) {
        if (_generationMethods.hasOwnProperty(name)) {
            _stopSound();

            /* Generate sound with the given method. */
            _source = _generationMethods[name]();
            /* Connect nodes to create routing graph. */
            _createRoutingGraph();
            _source.noteOn(0); // start()
            /* Set playback rate to a new value because it could be changed
             * while no sound was played. */
            _setPlaybackRate(_$playbackRate.val());
        }
    };

    _stopSound = function () {
        /* Check whether there is any source. */
        if (_source && _isSoundPlaying()) {
            _source.noteOff(0); // stop()
            _source = null;
        }
    };

    _setPlaybackRate = function (val) {
        /* Changing playback rate  */
        if (_isSoundPlaying()) {
            /* We have to check existence of playbackRate property in case of
             * OscillatorNode in which we can change that value. */
            if (_source.hasOwnProperty('playbackRate')) {
                _source.playbackRate.value = val;
            }
        }
    };

    _createRoutingGraph = function () {
        if (!_source) {
            return;
        }

        /* First disconnect source node form any node it's connected to.
         * We do it to make sure there is only one route in graph. */
        _source.disconnect(0);

        switch (_option) {
        case 'filters':
            _source.connect(_filterNode);
            break;
        case 'equalizer':
            _source.connect(_equalizerNodes[0]);
            break;
        case 'disabled':
            _source.connect(_gainNode);
            break;
        }
    };

    _startTimer = function () {
        /* Draw sound wave spectrum every 10 milliseconds. */
        _timer = setInterval(_timerFunction, 10);
    };

    _stopTimer = function () {
        /* Reset timer for drawing sound wave spectrum. */
        if (_timer) {
            clearInterval(_timer);
            _timer = null;
        }
    };

    _timerFunction = function () {
        _draw();
        if (_isSoundPlaying()) {
            _$stop.show();
        } else {
            _$stop.hide();
        }
    };

    _draw = function () {
        var canvas, context, width, height, barWidth, barHeight, barSpacing, frequencyData, barCount, loopStep, i, hue;

        canvas = $('canvas')[0];
        context = canvas.getContext('2d');
        width = canvas.width;
        height = canvas.height;
        barWidth = 10;
        barSpacing = 2;

        context.clearRect(0, 0, width, height);
        frequencyData = new Uint8Array(_analyserNode.frequencyBinCount);
        _analyserNode.getByteFrequencyData(frequencyData);
        barCount = Math.round(width / (barWidth + barSpacing));
        loopStep = Math.floor(frequencyData.length / barCount);

        for (i = 0; i < barCount; i++) {
            barHeight = frequencyData[i * loopStep];
            hue = parseInt(120 * (1 - (barHeight / 255)), 10);
            context.fillStyle = 'hsl(' + hue + ',75%,50%)';
            context.fillRect(((barWidth + barSpacing) * i) + (barSpacing / 2), height, barWidth - barSpacing, -barHeight);
        }
    };

    return {
        /**
         * Initiates user interface and generate files list.
         */
        init: _init,
        /**
         * Load sound file specified by the file parameter.
         * @param {Object} file It should be object with two properties 'name' - internal file name in the list and 'uri' - uri/url to the file.
         * @param {Function} successCallback
         * @param {Function} errorCallback
         */
        loadSound: _loadSound,
        /**
         * Play previously loaded sound.
         * @param {String} name Internal file name to be played.
         */
        playSound: _playSound,
        /**
         * Stops sound that is being currently played.
         */
        stopSound: _stopSound,
        /**
         * Generate sound source by synthesizing it.
         * @param {String} name Name of the method by which sound will be created.
         */
        generateAndPlaySound: _generateAndPlaySound
    };
}());

$(document).ready(function () {
    app.init();
});