
/**
 * Tutorials:
 * http://www.html5rocks.com/en/tutorials/webaudio/games/
 * http://www.html5rocks.com/en/tutorials/webaudio/positional_audio/ <- +1 as it is three.js
 * http://www.html5rocks.com/en/tutorials/webaudio/intro/
 *
 * Spec:
 * https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
 *
 * Chromium Demo:
 * http://chromium.googlecode.com/svn/trunk/samples/audio/index.html  <- running page
 * http://code.google.com/p/chromium/source/browse/trunk/samples/audio/ <- source
*/


/**
 * Notes on removing tQuery dependancy
 * * some stuff depends on tQuery
 * * find which one
 * * tQuery.Webaudio got a world link for the listener
 *   * do a plugin with followListener(world), unfollowListener(world)
 * * namespace become WebAudio.* instead of WebAudio.*
*/

//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//		WebAudio							//
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////


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

/**
 * Main class to handle webkit audio
 * 
 * TODO make the clip detector from http://www.html5rocks.com/en/tutorials/webaudio/games/
 *
 * @class Handle webkit audio API
 *
 * @param {tQuery.World} [world] the world on which to run 
*/
WebAudio	= function(){
	// sanity check - the api MUST be available
	if( WebAudio.isAvailable === false ){
		this._addRequiredMessage();
		// Throw an error to stop execution
		throw new Error('WebAudio API is required and not available.')	
	}
	
	// create the context
	this._ctx	= new AudioContext();
	// setup internal variable
	this._muted	= false;
	this._volume	= 1;

	// setup the end of the node chain
	// TODO later code the clipping detection from http://www.html5rocks.com/en/tutorials/webaudio/games/ 
	this._gainNode	= this._ctx.createGain();
	this._compressor= this._ctx.createDynamicsCompressor();
	this._gainNode.connect( this._compressor );
	this._compressor.connect( this._ctx.destination );	

	// init page visibility
	this._pageVisibilityCtor();	
};


/**
 * vendor.js way to make plugins ala jQuery
 * @namespace
*/
WebAudio.fn	= WebAudio.prototype;


/**
 * destructor
*/
WebAudio.prototype.destroy	= function(){
	this._pageVisibilityDtor();
};

/**
 * @return {Boolean} true if it is available or not
*/
WebAudio.isAvailable	= window.AudioContext ? true : false;

//////////////////////////////////////////////////////////////////////////////////
//		comment								//
//////////////////////////////////////////////////////////////////////////////////

WebAudio.prototype._addRequiredMessage = function(parent) {
	// handle defaults arguements
	parent	= parent || document.body;
	// message directly taken from Detector.js
	var domElement = document.createElement( 'div' );
	domElement.style.fontFamily	= 'monospace';
	domElement.style.fontSize	= '13px';
	domElement.style.textAlign	= 'center';
	domElement.style.background	= '#eee';
	domElement.style.color		= '#000';
	domElement.style.padding	= '1em';
	domElement.style.width		= '475px';
	domElement.style.margin		= '5em auto 0';
	domElement.innerHTML		= [
		'Your browser does not seem to support <a href="https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html">WebAudio API</a>.<br />',
		'Try with <a href="https://www.google.com/intl/en/chrome/browser/">Chrome Browser</a>.'
	].join( '\n' );
	// add it to the parent
	parent.appendChild(domElement);
}

//////////////////////////////////////////////////////////////////////////////////
//										//
//////////////////////////////////////////////////////////////////////////////////

/**
 * get the audio context
 *
 * @returns {AudioContext} the audio context
*/
WebAudio.prototype.context	= function(){
	return this._ctx;
};

/**
 * Create a sound
 *
 * @returns {WebAudio.Sound} the sound just created
*/
WebAudio.prototype.createSound	= function()
{
	var webaudio	= this;
	var sound	= new WebAudio.Sound(webaudio);
	return sound;
}


/**
 * return the entry node in the master node chains
*/
WebAudio.prototype._entryNode	= function(){
	//return this._ctx.destination;
	return this._gainNode;
}

//////////////////////////////////////////////////////////////////////////////////
//		volume/mute							//
//////////////////////////////////////////////////////////////////////////////////

/**
 * getter/setter on the volume
*/
WebAudio.prototype.volume	= function(value){
	if( value === undefined )	return this._volume;
	// update volume
	this._volume	= value;
	// update actual volume IIF not muted
	if( this._muted  === false ){
		this._gainNode.gain.value	= this._volume;	
	}
	// return this for chained API
	return this;
};

/** 
 * getter/setter for mute
*/
WebAudio.prototype.mute	= function(value){
	if( value === undefined )	return this._muted;
	this._muted	= value;
	this._gainNode.gain.value	= this._muted ? 0 : this._volume;
	return this;	// for chained API
}

/**
 * to toggle the mute
*/
WebAudio.prototype.toggleMute	= function(){
	if( this.mute() )	this.mute(false);
	else			this.mute(true);
}

//////////////////////////////////////////////////////////////////////////////////
//		pageVisibility							//
//////////////////////////////////////////////////////////////////////////////////


WebAudio.prototype._pageVisibilityCtor	= function(){
	// shim to handle browser vendor
	this._pageVisibilityEventStr	= (document.hidden !== undefined	? 'visibilitychange'	:
		(document.mozHidden	!== undefined		? 'mozvisibilitychange'	:
		(document.msHidden	!== undefined		? 'msvisibilitychange'	:
		(document.webkitHidden	!== undefined		? 'webkitvisibilitychange' :
		console.assert(false, "Page Visibility API unsupported")
	))));
	this._pageVisibilityDocumentStr	= (document.hidden !== undefined ? 'hidden' :
		(document.mozHidden	!== undefined ? 'mozHidden' :
		(document.msHidden	!== undefined ? 'msHidden' :
		(document.webkitHidden	!== undefined ? 'webkitHidden' :
		console.assert(false, "Page Visibility API unsupported")
	))));
	// event handler for visibilitychange event
	this._$pageVisibilityCallback	= function(){
		var isHidden	= document[this._pageVisibilityDocumentStr] ? true : false;
		this.mute( isHidden ? true : false );
	}.bind(this);
	// bind the event itself
	document.addEventListener(this._pageVisibilityEventStr, this._$pageVisibilityCallback, false);
}

WebAudio.prototype._pageVisibilityDtor	= function(){
	// unbind the event itself
	document.removeEventListener(this._pageVisibilityEventStr, this._$pageVisibilityCallback, false);
}
/**
 * Constructor
 *
 * @class builder to generate nodes chains. Used in WebAudio.Sound
 * @param {AudioContext} audioContext the audio context
*/
WebAudio.NodeChainBuilder	= function(audioContext){
	console.assert(audioContext instanceof AudioContext);
	this._context	= audioContext;
	this._firstNode	= null;
	this._lastNode	= null;
	this._nodes	= {};
};

/**
 * creator
 * 
 * @param  {webkitAudioContext} 	audioContext the context	
 * @return {WebAudio.NodeChainBuider}	just created object
 */
WebAudio.NodeChainBuilder.create= function(audioContext){
	return new WebAudio.NodeChainBuilder(audioContext);
}

/**
 * destructor
*/
WebAudio.NodeChainBuilder.prototype.destroy	= function(){
};

//////////////////////////////////////////////////////////////////////////////////
//		getters								//
//////////////////////////////////////////////////////////////////////////////////

/**
 * getter for the nodes
*/
WebAudio.NodeChainBuilder.prototype.nodes	= function(){
	return this._nodes;
}

/**
 * @returns the first node of the chain
*/
WebAudio.NodeChainBuilder.prototype.first	= function(){
	return this._firstNode;
}

/**
 * @returns the last node of the chain
*/
WebAudio.NodeChainBuilder.prototype.last	= function(){
	return this._lastNode;
}

//////////////////////////////////////////////////////////////////////////////////
//										//
//////////////////////////////////////////////////////////////////////////////////

/**
 * add a node to the chain
 * @param {[type]} node       [description]
 * @param {[type]} properties [description]
 */
WebAudio.NodeChainBuilder.prototype._addNode	= function(node, properties)
{
	// update this._bufferSourceDst - needed for .cloneBufferSource()
	var lastIsBufferSource	= this._lastNode && ('playbackRate' in this._lastNode) ? true : false;
	if( lastIsBufferSource )	this._bufferSourceDst	= node;

	// connect this._lastNode to node if suitable
	if( this._lastNode !== null )	this._lastNode.connect(node);
	
	// update this._firstNode && this._lastNode
	if( this._firstNode === null )	this._firstNode	= node;
	this._lastNode	= node;
		
	// apply properties to the node
	for( var property in properties ){
		node[property]	= properties[property];
	}

	// for chained API
	return this;
};


//////////////////////////////////////////////////////////////////////////////////
//		creator for each type of nodes					//
//////////////////////////////////////////////////////////////////////////////////

/**
 * Clone the bufferSource. Used just before playing a sound
 * @returns {AudioBufferSourceNode} the clone AudioBufferSourceNode
*/
WebAudio.NodeChainBuilder.prototype.cloneBufferSource	= function(){
	console.assert(this._nodes.bufferSource, "no buffersource presents. Add one.");
	var orig	= this._nodes.bufferSource;
	var clone	= this._context.createBufferSource()
	clone.buffer		= orig.buffer;
	clone.playbackRate	= orig.playbackRate;
	clone.loop		= orig.loop;
	clone.connect(this._bufferSourceDst);
	return clone;
}

/**
 * add a bufferSource
 *
 * @param {Object} [properties] properties to set in the created node
*/
WebAudio.NodeChainBuilder.prototype.bufferSource	= function(properties){
	var node		= this._context.createBufferSource()
	this._nodes.bufferSource= node;
	return this._addNode(node, properties)
};

/**
 * add a createMediaStreamSource
 *
 * @param {Object} [properties] properties to set in the created node
*/
WebAudio.NodeChainBuilder.prototype.mediaStreamSource	= function(stream, properties){
//	console.assert( stream instanceof LocalMediaStream )
	var node		= this._context.createMediaStreamSource(stream)
	this._nodes.bufferSource= node;
	return this._addNode(node, properties)
};

/**
 * add a createMediaElementSource
 * @param  {HTMLElement} element    the element to add
 * @param {Object} [properties] properties to set in the created node
 */
WebAudio.NodeChainBuilder.prototype.mediaElementSource = function(element, properties){
	console.assert(element instanceof HTMLAudioElement || element instanceof HTMLVideoElement)
	var node		= this._context.createMediaElementSource(element)
	this._nodes.bufferSource= node;
	return this._addNode(node, properties)
};

/**
 * add a panner
 * 
 * @param {Object} [properties] properties to set in the created node
*/
WebAudio.NodeChainBuilder.prototype.panner	= function(properties){
	var node		= this._context.createPanner()
	this._nodes.panner	= node;
	return this._addNode(node, properties)
};

/**
 * add a analyser
 *
 * @param {Object} [properties] properties to set in the created node
*/
WebAudio.NodeChainBuilder.prototype.analyser	= function(properties){
	var node		= this._context.createAnalyser()
	this._nodes.analyser	= node;
	return this._addNode(node, properties)
};

/**
 * add a gainNode
 *
 * @param {Object} [properties] properties to set in the created node
*/
WebAudio.NodeChainBuilder.prototype.gainNode	= function(properties){
	var node		= this._context.createGain()
	this._nodes.gainNode	= node;
	return this._addNode(node, properties)
};

/**
 * sound instance
 *
 * @class Handle one sound for WebAudio
 *
 * @param {tQuery.World} [world] the world on which to run
 * @param {WebAudio.NodeChainBuilder} [nodeChain] the nodeChain to use
*/
WebAudio.Sound	= function(webaudio, nodeChain){
	this._webaudio	= webaudio;
	this._context	= this._webaudio.context();

	console.assert( this._webaudio instanceof WebAudio );

	// create a default NodeChainBuilder if needed
	if( nodeChain === undefined ){
		nodeChain	= new WebAudio.NodeChainBuilder(this._context)
					.bufferSource().gainNode().analyser().panner();
	}
	// setup this._chain
	console.assert( nodeChain instanceof WebAudio.NodeChainBuilder );
	this._chain	= nodeChain;
	// connect this._chain.last() node to this._webaudio._entryNode()
	this._chain.last().connect( this._webaudio._entryNode() );
	
	// create some alias
	this._source	= this._chain.nodes().bufferSource;
	this._gainNode	= this._chain.nodes().gainNode;
	this._analyser	= this._chain.nodes().analyser;
	this._panner	= this._chain.nodes().panner;
	
	// sanity check
	console.assert(this._source	, "no bufferSource: not yet supported")
	console.assert(this._gainNode	, "no gainNode: not yet supported")
	console.assert(this._analyser	, "no analyser: not yet supported")
	console.assert(this._panner	, "no panner: not yet supported")
};

WebAudio.Sound.create	= function(webaudio, nodeChain){
	return new WebAudio.Sound(webaudio,  nodeChain);
}

/**
 * destructor
*/
WebAudio.Sound.prototype.destroy	= function(){
	// disconnect from this._webaudio
	this._chain.last().disconnect();
	// destroy this._chain
	this._chain.destroy();
	this._chain	= null;
};

/**
 * vendor.js way to make plugins ala jQuery
 * @namespace
*/
WebAudio.Sound.fn	= WebAudio.Sound.prototype;

//////////////////////////////////////////////////////////////////////////////////
//										//
//////////////////////////////////////////////////////////////////////////////////

/**
 * getter of the chain nodes
*/
WebAudio.Sound.prototype.nodes	= function(){
	return this._chain.nodes();
};

/**
 * @returns {Boolean} true if the sound is playable, false otherwise
*/
WebAudio.Sound.prototype.isPlayable	= function(){
	return this._source.buffer ? true : false;
};

/**
 * play the sound
 *
 * @param {Number} [time] time when to play the sound
*/
WebAudio.Sound.prototype.play		= function(time, offset){
	// handle parameter polymorphism
	if( time ===  undefined )	time	= 0;
	if( offset ===  undefined )	offset	= 0;
	// if not yet playable, ignore
	// - usefull when the sound download isnt yet completed
	if( this.isPlayable() === false )	return;
	// clone the bufferSource
	var clonedNode	= this._chain.cloneBufferSource();
	// set the noteOn
	clonedNode.start(time, offset);
	// create the source object
	var source	= {
		node	: clonedNode,
		stop	: function(time){
			if( time ===  undefined )	time	= 0;
			this.node.stop(time);
			return source;	// for chained API
		}
	}
	// return it
	return source;
};

/**
 * getter/setter on the volume
 *
 * @param {Number} [value] the value to set, if not provided, get current value
*/
WebAudio.Sound.prototype.volume	= function(value){
	if( value === undefined )	return this._gainNode.gain.value;
	this._gainNode.gain.value	= value;
	return this;	// for chained API
};


/**
 * getter/setter on the loop
 * 
 * @param {Number} [value] the value to set, if not provided, get current value
*/
WebAudio.Sound.prototype.loop	= function(value){
	if( value === undefined )	return this._source.loop;
	this._source.loop	= value;
	return this;	// for chained API
};

/**
 * getter/setter on the source buffer
 * 
 * @param {Number} [value] the value to set, if not provided, get current value
*/
WebAudio.Sound.prototype.buffer	= function(value){
	if( value === undefined )	return this._source.buffer;
	this._source.buffer	= value;
	return this;	// for chained API
};


/**
 * Set parameter for the pannerCone
 *
 * @param {Number} innerAngle the inner cone hangle in radian
 * @param {Number} outerAngle the outer cone hangle in radian
 * @param {Number} outerGain the gain to apply when in the outerCone
*/
WebAudio.Sound.prototype.pannerCone	= function(innerAngle, outerAngle, outerGain)
{
	this._panner.coneInnerAngle	= innerAngle * 180 / Math.PI;
	this._panner.coneOuterAngle	= outerAngle * 180 / Math.PI;
	this._panner.coneOuterGain	= outerGain;
	return this;	// for chained API
};

/**
 * getter/setter on the pannerConeInnerAngle
 * 
 * @param {Number} value the angle in radian
*/
WebAudio.Sound.prototype.pannerConeInnerAngle	= function(value){
	if( value === undefined )	return this._panner.coneInnerAngle / 180 * Math.PI;
	this._panner.coneInnerAngle	= value * 180 / Math.PI;
	return this;	// for chained API
};

/**
 * getter/setter on the pannerConeOuterAngle
 *
 * @param {Number} value the angle in radian
*/
WebAudio.Sound.prototype.pannerConeOuterAngle	= function(value){
	if( value === undefined )	return this._panner.coneOuterAngle / 180 * Math.PI;
	this._panner.coneOuterAngle	= value * 180 / Math.PI;
	return this;	// for chained API
};

/**
 * getter/setter on the pannerConeOuterGain
 * 
 * @param {Number} value the value
*/
WebAudio.Sound.prototype.pannerConeOuterGain	= function(value){
	if( value === undefined )	return this._panner.coneOuterGain;
	this._panner.coneOuterGain	= value;
	return this;	// for chained API
};

/**
 * compute the amplitude of the sound (not sure at all it is the proper term)
 *
 * @param {Number} width the number of frequencyBin to take into account
 * @returns {Number} return the amplitude of the sound
*/
WebAudio.Sound.prototype.amplitude	= function(width)
{
	// handle paramerter
	width		= width !== undefined ? width : 2;
	// inint variable
	var analyser	= this._analyser;
	var freqByte	= new Uint8Array(analyser.frequencyBinCount);
	// get the frequency data
	analyser.getByteFrequencyData(freqByte);
	// compute the sum
	var sum	= 0;
	for(var i = 0; i < width; i++){
		sum	+= freqByte[i];
	}
	// complute the amplitude
	var amplitude	= sum / (width*256-1);
	// return ampliture
	return amplitude;
}

/**
 * Generate a sinusoid buffer.
 * FIXME should likely be in a plugin
*/
WebAudio.Sound.prototype.tone	= function(hertz, seconds){
	// handle parameter
	hertz	= hertz !== undefined ? hertz : 200;
	seconds	= seconds !== undefined ? seconds : 1;
	// set default value	
	var nChannels	= 1;
	var sampleRate	= 44100;
	var amplitude	= 2;
	// create the buffer
	var buffer	= this._webaudio.context().createBuffer(nChannels, seconds*sampleRate, sampleRate);
	var fArray	= buffer.getChannelData(0);
	// fill the buffer
	for(var i = 0; i < fArray.length; i++){
		var time	= i / buffer.sampleRate;
		var angle	= hertz * time * Math.PI;
		fArray[i]	= Math.sin(angle)*amplitude;
	}
	// set the buffer
	this.buffer(buffer);
	return this;	// for chained API
}


/**
 * Put this function is .Sound with getByt as private callback
*/
WebAudio.Sound.prototype.makeHistogram	= function(nBar)
{	
	// get analyser node
	var analyser	= this._analyser;
	// allocate the private histo if needed - to avoid allocating at every frame
	//this._privHisto	= this._privHisto || new Float32Array(analyser.frequencyBinCount);
	this._privHisto	= this._privHisto || new Uint8Array(analyser.frequencyBinCount);
	// just an alias
	var freqData	= this._privHisto;

	// get the data
	//analyser.getFloatFrequencyData(freqData);
	analyser.getByteFrequencyData(freqData);
	//analyser.getByteTimeDomainData(freqData);

	/**
	 * This should be in imageprocessing.js almost
	*/
	var makeHisto	= function(srcArr, dstLength){
		var barW	= Math.floor(srcArr.length / dstLength);
		var nBar	= Math.floor(srcArr.length / barW);
		var arr		= []
		for(var x = 0, arrIdx = 0; x < srcArr.length; arrIdx++){
			var sum	= 0;
			for(var i = 0; i < barW; i++, x++){
				sum += srcArr[x];
			}
			var average	= sum/barW;
			arr[arrIdx]	= average;
		}
		return arr;
	}
	// build the histo
	var histo	= makeHisto(freqData, nBar);
	// return it
	return histo;
}

//////////////////////////////////////////////////////////////////////////////////
//										//
//////////////////////////////////////////////////////////////////////////////////

/**
 * Load a sound
 *
 * @param {String} url the url of the sound to load
 * @param {Function} onSuccess function to notify once the url is loaded (optional)
 * @param {Function} onError function to notify if an error occurs (optional)
*/
WebAudio.Sound.prototype.load = function(url, onSuccess, onError){
	// handle default arguments
	onError	= onError	|| function(){
		console.warn("unable to load sound "+url);
	}
	// try to load the user	
	this._loadAndDecodeSound(url, function(buffer){
		this._source.buffer	= buffer;
		onSuccess && onSuccess(this);
	}.bind(this), function(){
		onError && onError(this);
	}.bind(this));
	return this;	// for chained API
};

/**
 * Load and decode a sound
 *
 * @param {String} url the url where to get the sound
 * @param {Function} onLoad the function called when the sound is loaded and decoded (optional)
 * @param {Function} onError the function called when an error occured (optional)
*/
WebAudio.Sound.prototype._loadAndDecodeSound	= function(url, onLoad, onError){
	var context	= this._context;
	var request	= new XMLHttpRequest();
	request.open('GET', url, true);
	request.responseType	= 'arraybuffer';
	// Decode asynchronously
	request.onload	= function() {
		context.decodeAudioData(request.response, function(buffer) {
			onLoad && onLoad(buffer);
		}, function(){
			onError && onError();
		});
	};
	// actually start the request
	request.send();
}
/**
 * gowiththeflow.js - a javascript flow control micro library
 * https://github.com/jeromeetienne/gowiththeflow.js
*/
WebAudio.Flow	= function(){
	var self, stack = [], timerId = setTimeout(function(){ timerId = null; self._next(); }, 0);
	return self = {
		destroy : function(){ timerId && clearTimeout(timerId); },
		par	: function(callback, isSeq){
			if(isSeq || !(stack[stack.length-1] instanceof Array)) stack.push([]);
			stack[stack.length-1].push(callback);
			return self;
		},seq	: function(callback){ return self.par(callback, true);	},
		_next	: function(err, result){
			var errors = [], results = [], callbacks = stack.shift() || [], nbReturn = callbacks.length, isSeq = nbReturn == 1;
			for(var i = 0; i < callbacks.length; i++){
				(function(fct, index){
					fct(function(error, result){
						errors[index]	= error;
						results[index]	= result;		
						if(--nbReturn == 0)	self._next(isSeq?errors[0]:errors, isSeq?results[0]:results)
					}, err, result)
				})(callbacks[i], i);
			}
		}
	}
};
