// Copyright (c) 2012 Mansheng Yang
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

/*
 * Sound-Object-Builder class
 * An utility class used by sound-object-parser to create various sound object.
 * 
 * built-in sound object types:
 *  pause
 *  seq
 *  simul
 *  audio
 */
function soundObjectBuilder(context) {
    
    function copyTo(x, y) {
        for (var attr in x) {
            y[attr] = x[attr];
        }
    }
    
    function createBufferSource(buffer, pitch, originalPlaybackRate) {
        var playbackRate = pitch ? Math.pow(2, pitch / 12.0) : 1;
        playbackRate /= originalPlaybackRate === undefined ? 1 : originalPlaybackRate;
        var source = context.createBufferSource();
        source.buffer = buffer;
        source.connect(context.destination);
        source.playbackRate.value = playbackRate;
        return source;
    }
    
    //play function for sound objects that play the sound once
    function playOnce(time, controlParams) {
        var gain = controlParams.gain;
        var source = createBufferSource(this.buffer, this.pitch, this.originalPlaybackRate);
        this.source = source;
        this.source.gain.value = gain;
        source.noteOn(time);
        this.endTime = time + this.duration;
    }
    
    //play function for sound objects that play continuous sound
    function playContinuousSound(time, controlParams) {
        var gain = controlParams.gain;
        var params = this.ADSRParams || controlParams.ADSRParams;
        var a = params.a;
        var d = params.d;
        var r = params.r;
        var s = this.buffer.duration - a - d - r;
        var maxAmp = (params.maxAmp || 1.2) * gain;
        var sustainAmp = (params.sustainAmp || 1) * gain;
        
        var source = createBufferSource(this.buffer, this.pitch, this.originalPlaybackRate);
        source.loop = true;
        var aEndTime = time + a;
        var dEndTime = time + a + d;
        var sEndTime = time + this.duration;
        var rEndTime = sEndTime + r;
        
        source.gain.value = 0;
        source.gain.setValueAtTime(0, time);
        source.gain.linearRampToValueAtTime(maxAmp, aEndTime);
        source.gain.linearRampToValueAtTime(sustainAmp, dEndTime);
        source.gain.setValueAtTime(sustainAmp, sEndTime);
        source.gain.linearRampToValueAtTime(0, rEndTime);
        console.log(time);
        console.log(rEndTime);
        source.noteOn(time);
        source.noteOff(rEndTime);
        this.source = source;
        this.endTime = time + this.duration;
    }
    
    function stopAudio(time) {
        if (this.source === undefined) return;
        if (time > this.endTime) return;
        this.source.noteOff(time);
    }
    
    //create a sound object that plays the audio buffer
    function createAudioObject(buffer, duration, continuous, ADSRParams, originalPlaybackRate) {
        return {
            buffer: buffer,
            duration: duration,
            play: continuous ? playContinuousSound : playOnce,
            fit: simpleFit,
            repeat: continuous ? continuousRepeat : simpleRepeat,
            stop: stopAudio,
            ADSRParams: ADSRParams,
            originalPlaybackRate: originalPlaybackRate,
        };
    }
    
    //simple fit function for sound objects
    //return a copy of the sound object with the duration changed
    function simpleFit(duration) {
        var obj = new Object();
        copyTo(this, obj);
        obj.duration = duration;
        return obj;
    }
    
    //fit function for sound objects that are groups
    //return a new sound object group with 
    //a playList resulting from calling fit on the objects in the original playList
    function groupFit(duration) {
        var durationScale = duration / this.duration;
        //generate a new play list 
        var list = this.playList.map(function(obj) {
            //scale the duration of the sub sound object
            return obj.fit(obj.duration * durationScale);
        });
        var group = new Object();
        copyTo(this, group);
        group.duration = duration;
        group.playList = list;
        return group;
    }
    
    //create a sound object that plays the specified sound object according to the specified durations
    function simpleRepeat(durations) {
        var obj = this;
        var list = new Array();
        durations.forEach(function(duration) {
            list.push(obj.fit(duration));
        });
        return seq(list);
    }
    
    //repeat function for sound object with continuous sound, simply use the total duration
    function continuousRepeat(durations) {
        var obj = new Object();
        copyTo(this, obj);
        var totalDuration = 0;
        durations.forEach(function(dur) {
            totalDuration += dur;
        });
        obj.duration = totalDuration;
        return obj;
    }
    
    //play a list of sound objects sequentially
    function playSeq(list, startTime, controlParams) {
        var acc = 0;
        list.forEach(function(obj) {
            obj.play(startTime + acc, controlParams);
            acc += obj.duration;
        });
    }
    
    //play a list of sound objects simultaneously
    function playSimul(list, startTime, controlParams) {
        list.forEach(function(obj) {
            obj.play(startTime, controlParams);
        });
    }
    
    //stop function for groups
    function groupStop(time) {
        this.playList.forEach(function(obj) {
            obj.stop();
        });
    }
    
    //create a sound object that plays the specified sound objects sequentially
    function seq(objects) {
        var duration = 0;
        objects.forEach(function(obj) {
            duration += obj.duration;
        });
        return {
            duration: duration,
            playList: objects,
            play: function(time, controlParams) {
                playSeq(this.playList, time, controlParams);
            },
            fit: groupFit,
            repeat: simpleRepeat,
            stop: groupStop,
        };
    }
    
    //create a sound object that plays the specified sound objects simultaneously
    function simul(objects) { 
        var duration = 0;
        //all the elements in objects should have the same duration
        if (objects.length > 0) 
            duration = objects[0].duration;
            
        return {
            duration: duration,
            playList: objects,
            play: function(time, controlParams) {
                playSimul(this.playList, time, controlParams);
            },
            fit: groupFit,
            repeat: simpleRepeat,
            stop: groupStop,
        };
    }
    
    //create a pause sound object
    function pause(duration) {
        return {
            duration: duration,
            play: function() {},
            fit: simpleFit,
            repeat: simpleRepeat,
            stop: function() {},
        };
    }
    
    this.seq = seq;
    this.simul = simul;
    this.pause = pause;
    this.createAudioObject = createAudioObject;
    this.updateContext = function(newContext) {
        context = newContext;
    }
}