import Tone from "../../core/Tone";
import "../core/Buffer";
import "../Source";
import "../source/TickSource";
import "../BufferSource";

/**
 *  @class  Tone.Player is an audio file player with start, loop, and stop functions.
 *
 *  @constructor
 *  @extends {Tone.Source}
 *  @param {string|AudioBuffer} url Either the AudioBuffer or the url from
 *                                  which to load the AudioBuffer
 *  @param {Function=} onload The function to invoke when the buffer is loaded.
 *                            Recommended to use Tone.Buffer.on('load') instead.
 *  @example
 * var player = new Tone.Player("./path/to/sample.mp3").toMaster();
 * //play as soon as the buffer is loaded
 * player.autostart = true;
 */
Tone.Player = function(url){

	var options;
	if (url instanceof Tone.Buffer && url.loaded){
		url = url.get();
		options = Tone.Player.defaults;
	} else {
		options = Tone.defaults(arguments, ["url", "onload"], Tone.Player);
	}
	Tone.Source.call(this, options);

	/**
	 *  If the file should play as soon
	 *  as the buffer is loaded.
	 *  @type {Boolean}
	 *  @example
	 * //will play as soon as it's loaded
	 * var player = new Tone.Player({
	 * 	"url" : "./path/to/sample.mp3",
	 * 	"autostart" : true,
	 * }).toMaster();
	 */
	this.autostart = options.autostart;

	/**
	 *  the buffer
	 *  @private
	 *  @type {Tone.Buffer}
	 */
	this._buffer = new Tone.Buffer({
		"url" : options.url,
		"onload" : this._onload.bind(this, options.onload),
		"reverse" : options.reverse
	});
	if (url instanceof AudioBuffer){
		this._buffer.set(url);
	}

	/**
	 *  if the buffer should loop once it's over
	 *  @type {Boolean}
	 *  @private
	 */
	this._loop = options.loop;

	/**
	 *  if 'loop' is true, the loop will start at this position
	 *  @type {Time}
	 *  @private
	 */
	this._loopStart = options.loopStart;

	/**
	 *  if 'loop' is true, the loop will end at this position
	 *  @type {Time}
	 *  @private
	 */
	this._loopEnd = options.loopEnd;

	/**
	 *  the playback rate
	 *  @private
	 *  @type {Number}
	 */
	this._playbackRate = options.playbackRate;

	/**
	 *  All of the active buffer source nodes
	 *  @type {Array<Tone.BufferSource>}
	 *  @private
	 */
	this._activeSources = [];

	/**
	 *  The fadeIn time of the amplitude envelope.
	 *  @type {Time}
	 */
	this.fadeIn = options.fadeIn;

	/**
	 *  The fadeOut time of the amplitude envelope.
	 *  @type {Time}
	 */
	this.fadeOut = options.fadeOut;
};

Tone.extend(Tone.Player, Tone.Source);

/**
 *  the default parameters
 *  @static
 *  @const
 *  @type {Object}
 */
Tone.Player.defaults = {
	"onload" : Tone.noOp,
	"playbackRate" : 1,
	"loop" : false,
	"autostart" : false,
	"loopStart" : 0,
	"loopEnd" : 0,
	"reverse" : false,
	"fadeIn" : 0,
	"fadeOut" : 0
};

/**
 *  Load the audio file as an audio buffer.
 *  Decodes the audio asynchronously and invokes
 *  the callback once the audio buffer loads.
 *  Note: this does not need to be called if a url
 *  was passed in to the constructor. Only use this
 *  if you want to manually load a new url.
 * @param {string} url The url of the buffer to load.
 *                     Filetype support depends on the
 *                     browser.
 *  @param  {Function=} callback The function to invoke once
 *                               the sample is loaded.
 *  @returns {Promise}
 */
Tone.Player.prototype.load = function(url, callback){
	return this._buffer.load(url, this._onload.bind(this, callback));
};

/**
 * Internal callback when the buffer is loaded.
 * @private
 */
Tone.Player.prototype._onload = function(callback){
	callback = Tone.defaultArg(callback, Tone.noOp);
	callback(this);
	if (this.autostart){
		this.start();
	}
};

/**
 * Internal callback when the buffer is done playing.
 * @private
 */
Tone.Player.prototype._onSourceEnd = function(source){
	var index = this._activeSources.indexOf(source);
	this._activeSources.splice(index, 1);
	if (this._activeSources.length === 0 && !this._synced){
		this._state.setStateAtTime(Tone.State.Stopped, Tone.now());
	}
};

/**
 *  Play the buffer at the given startTime. Optionally add an offset
 *  and/or duration which will play the buffer from a position
 *  within the buffer for the given duration.
 *
 *  @param  {Time} [startTime=now] When the player should start.
 *  @param  {Time} [offset=0] The offset from the beginning of the sample
 *                                 to start at.
 *  @param  {Time=} duration How long the sample should play. If no duration
 *                                is given, it will default to the full length
 *                                of the sample (minus any offset)
 *  @returns {Tone.Player} this
 *  @memberOf Tone.Player#
 *  @method start
 *  @name start
 */

/**
 *  Internal start method
 *  @private
 */
Tone.Player.prototype._start = function(startTime, offset, duration){
	//if it's a loop the default offset is the loopstart point
	if (this._loop){
		offset = Tone.defaultArg(offset, this._loopStart);
	} else {
		//otherwise the default offset is 0
		offset = Tone.defaultArg(offset, 0);
	}

	//compute the values in seconds
	offset = this.toSeconds(offset);
	
	//if it's synced, it should factor in the playback rate for computing the offset
	if (this._synced){
		offset *= this._playbackRate;
	}

	//compute the duration which is either the passed in duration of the buffer.duration - offset
	var computedDuration = Tone.defaultArg(duration, Math.max(this._buffer.duration - offset, 0));
	computedDuration = this.toSeconds(computedDuration);

	//scale it by the playback rate
	computedDuration = computedDuration / this._playbackRate;

	//get the start time
	startTime = this.toSeconds(startTime);

	//make the source
	var source = new Tone.BufferSource({
		"buffer" : this._buffer,
		"loop" : this._loop,
		"loopStart" : this._loopStart,
		"loopEnd" : this._loopEnd,
		"onended" : this._onSourceEnd.bind(this),
		"playbackRate" : this._playbackRate,
		"fadeIn" : this.fadeIn,
		"fadeOut" : this.fadeOut,
	}).connect(this.output);

	//set the looping properties
	if (!this._loop && !this._synced){
		//if it's not looping, set the state change at the end of the sample
		this._state.setStateAtTime(Tone.State.Stopped, startTime + computedDuration);
	}

	//add it to the array of active sources
	this._activeSources.push(source);

	//start it
	if (this._loop && Tone.isUndef(duration)){
		source.start(startTime, offset);
	} else {
		//subtract the fade out time
		source.start(startTime, offset, computedDuration - this.toSeconds(this.fadeOut));
	}
	return this;
};

/**
 *  Stop playback.
 *  @private
 *  @param  {Time} [time=now]
 *  @returns {Tone.Player} this
 */
Tone.Player.prototype._stop = function(time){
	time = this.toSeconds(time);
	this._activeSources.forEach(function(source){
		source.stop(time);
	});
	return this;
};

/**
 * Stop and then restart the player from the beginning (or offset)
 *  @param  {Time} [startTime=now] When the player should start.
 *  @param  {Time} [offset=0] The offset from the beginning of the sample
 *                                 to start at.
 *  @param  {Time=} duration How long the sample should play. If no duration
 *                                is given, it will default to the full length
 *                                of the sample (minus any offset)
 *  @returns {Tone.Player} this
 */
Tone.Player.prototype.restart = function(time, offset, duration){
	this._stop(time);
	this._start(time, offset, duration);
	return this;
};

/**
 *  Seek to a specific time in the player's buffer. If the
 *  source is no longer playing at that time, it will stop.
 *  If you seek to a time that
 *  @param {Time} offset The time to seek to.
 *  @param {Time=} time The time for the seek event to occur.
 *  @return {Tone.Player} this
 *  @example
 * source.start(0.2);
 * source.stop(0.4);
 */
Tone.Player.prototype.seek = function(offset, time){
	time = this.toSeconds(time);
	if (this._state.getValueAtTime(time) === Tone.State.Started){
		offset = this.toSeconds(offset);
		// if it's currently playing, stop it
		this._stop(time);
		//restart it at the given time
		this._start(time, offset);
	}
	return this;
};

/**
 *  Set the loop start and end. Will only loop if loop is
 *  set to true.
 *  @param {Time} loopStart The loop end time
 *  @param {Time} loopEnd The loop end time
 *  @returns {Tone.Player} this
 *  @example
 * //loop 0.1 seconds of the file.
 * player.setLoopPoints(0.2, 0.3);
 * player.loop = true;
 */
Tone.Player.prototype.setLoopPoints = function(loopStart, loopEnd){
	this.loopStart = loopStart;
	this.loopEnd = loopEnd;
	return this;
};

/**
 * If loop is true, the loop will start at this position.
 * @memberOf Tone.Player#
 * @type {Time}
 * @name loopStart
 */
Object.defineProperty(Tone.Player.prototype, "loopStart", {
	get : function(){
		return this._loopStart;
	},
	set : function(loopStart){
		this._loopStart = loopStart;
		//get the current source
		this._activeSources.forEach(function(source){
			source.loopStart = loopStart;
		});
	}
});

/**
 * If loop is true, the loop will end at this position.
 * @memberOf Tone.Player#
 * @type {Time}
 * @name loopEnd
 */
Object.defineProperty(Tone.Player.prototype, "loopEnd", {
	get : function(){
		return this._loopEnd;
	},
	set : function(loopEnd){
		this._loopEnd = loopEnd;
		//get the current source
		this._activeSources.forEach(function(source){
			source.loopEnd = loopEnd;
		});
	}
});

/**
 * The audio buffer belonging to the player.
 * @memberOf Tone.Player#
 * @type {Tone.Buffer}
 * @name buffer
 */
Object.defineProperty(Tone.Player.prototype, "buffer", {
	get : function(){
		return this._buffer;
	},
	set : function(buffer){
		this._buffer.set(buffer);
	}
});

/**
 * If the buffer should loop once it's over.
 * @memberOf Tone.Player#
 * @type {Boolean}
 * @name loop
 */
Object.defineProperty(Tone.Player.prototype, "loop", {
	get : function(){
		return this._loop;
	},
	set : function(loop){
		//if no change, do nothing
		if (this._loop === loop){
			return;
		}
		this._loop = loop;
		//set the loop of all of the sources
		this._activeSources.forEach(function(source){
			source.loop = loop;
		});
		if (loop){
			//remove the next stopEvent
			var stopEvent = this._state.getNextState(Tone.State.Stopped, this.now());
			if (stopEvent){
				this._state.cancel(stopEvent.time);
			}
		}
	}
});

/**
 * The playback speed. 1 is normal speed. This is not a signal because
 * Safari and iOS currently don't support playbackRate as a signal.
 * @memberOf Tone.Player#
 * @type {Number}
 * @name playbackRate
 */
Object.defineProperty(Tone.Player.prototype, "playbackRate", {
	get : function(){
		return this._playbackRate;
	},
	set : function(rate){
		this._playbackRate = rate;
		var now = this.now();

		//cancel the stop event since it's at a different time now
		var stopEvent = this._state.getNextState(Tone.State.Stopped, now);
		if (stopEvent){
			this._state.cancel(stopEvent.time);
		}

		//set all the sources
		this._activeSources.forEach(function(source){
			source.cancelStop();
			source.playbackRate.setValueAtTime(rate, now);
		});
	}
});

/**
 * The direction the buffer should play in
 * @memberOf Tone.Player#
 * @type {Boolean}
 * @name reverse
 */
Object.defineProperty(Tone.Player.prototype, "reverse", {
	get : function(){
		return this._buffer.reverse;
	},
	set : function(rev){
		this._buffer.reverse = rev;
	}
});

/**
 * If all the buffer is loaded
 * @memberOf Tone.Player#
 * @type {Boolean}
 * @name loaded
 * @readOnly
 */
Object.defineProperty(Tone.Player.prototype, "loaded", {
	get : function(){
		return this._buffer.loaded;
	}
});

/**
 *  Dispose and disconnect.
 *  @return {Tone.Player} this
 */
Tone.Player.prototype.dispose = function(){
	//disconnect all of the players
	this._activeSources.forEach(function(source){
		source.dispose();
	});
	this._activeSources = null;
	Tone.Source.prototype.dispose.call(this);
	this._buffer.dispose();
	this._buffer = null;
	return this;
};

export default Tone.Player;