define(["../core/Tone", "../core/Emitter", "../core/Timeline", "../shim/AudioContext"], function(Tone){

	/**
	 *  @class Wrapper around the native AudioContext.
	 *  @extends {Tone.Emitter}
	 *  @param {AudioContext=} context optionally pass in a context
	 */
	Tone.Context = function(){

		Tone.Emitter.call(this);

		var options = Tone.defaults(arguments, ["context"], Tone.Context);

		if (!options.context){
			options.context = new Tone.global.AudioContext();
			if (!options.context){
				throw new Error("could not create AudioContext. Possibly too many AudioContexts running already.");
			}
		}
		this._context = options.context;
		//make sure it's not an AudioContext wrapper
		while (this._context.rawContext){
			this._context = this._context.rawContext;
		}
		// extend all of the methods
		for (var prop in this._context){
			this._defineProperty(this._context, prop);
		}

		/**
		 *  The default latency hint
		 *  @type  {String}
		 *  @private
		 */
		this._latencyHint = options.latencyHint;

		/**
		 *  An object containing all of the constants AudioBufferSourceNodes
		 *  @type  {Object}
		 *  @private
		 */
		this._constants = {};

		///////////////////////////////////////////////////////////////////////
		// WORKER
		///////////////////////////////////////////////////////////////////////

		/**
		 *  The amount of time events are scheduled
		 *  into the future
		 *  @type  {Number}
		 */
		this.lookAhead = options.lookAhead;

		/**
		 *  A reference to the actual computed update interval
		 *  @type  {Number}
		 *  @private
		 */
		this._computedUpdateInterval = 0;

		/**
		 *  A reliable callback method
		 *  @private
		 *  @type  {Ticker}
		 */
		this._ticker = new Ticker(this.emit.bind(this, "tick"), options.clockSource, options.updateInterval);

		///////////////////////////////////////////////////////////////////////
		// TIMEOUTS
		///////////////////////////////////////////////////////////////////////

		/**
		 *  All of the setTimeout events.
		 *  @type  {Tone.Timeline}
		 *  @private
		 */
		this._timeouts = new Tone.Timeline();

		/**
		 *  The timeout id counter
		 *  @private
		 *  @type {Number}
		 */
		this._timeoutIds = 0;

		this.on("tick", this._timeoutLoop.bind(this));

		//forward state change events
		this._context.onstatechange = function(e){
			this.emit("statechange", e);
		}.bind(this);
	};

	Tone.extend(Tone.Context, Tone.Emitter);
	Tone.Emitter.mixin(Tone.Context);

	/**
	 * defaults
	 * @static
	 * @type {Object}
	 */
	Tone.Context.defaults = {
		"clockSource" : "worker",
		"latencyHint" : "interactive",
		"lookAhead" : 0.1,
		"updateInterval" : 0.03
	};

	/**
	 * Is an instanceof Tone.Context
	 * @type {Boolean}
	 */
	Tone.Context.prototype.isContext = true;

	/**
	 *  Define a property on this Tone.Context.
	 *  This is used to extend the native AudioContext
	 *  @param  {AudioContext}  context
	 *  @param  {String}  prop
	 *  @private
	 */
	Tone.Context.prototype._defineProperty = function(context, prop){
		if (Tone.isUndef(this[prop])){
			Object.defineProperty(this, prop, {
				"get" : function(){
					if (typeof context[prop] === "function"){
						return context[prop].bind(context);
					} else {
						return context[prop];
					}
				},
				"set" : function(val){
					context[prop] = val;
				}
			});
		}
	};

	/**
	 *  The current audio context time
	 *  @return  {Number}
	 */
	Tone.Context.prototype.now = function(){
		return this._context.currentTime + this.lookAhead;
	};

	/**
	 *  The audio output destination. Alias for Tone.Master
	 *  @readyOnly
	 *  @type  {Tone.Master}
	 */
	Object.defineProperty(Tone.Context.prototype, "destination", {
		"get" : function(){
			if (!this.master){
				return this._context.destination;
			} else {
				return this.master;
			}
		}
	});

	/**
	 *  Starts the audio context from a suspended state. This is required
	 *  to initially start the AudioContext.
	 *  @return  {Promise}
	 */
	Tone.Context.prototype.resume = function(){
            /*
		if (this._context.state === "suspended" && this._context instanceof AudioContext){
			return this._context.resume();
		} else {
			return Promise.resolve();
		}
            */
            return Promise.resolve();
	};

	/**
	 *  Promise which is invoked when the context is running.
	 *  Tries to resume the context if it's not started.
	 *  @return  {Promise}
	 */
	Tone.Context.prototype.close = function(){
		var closePromise = Promise.resolve();
		//never close the global Tone.Context
		if (this !== Tone.global.TONE_AUDIO_CONTEXT){
			closePromise = this.rawContext.close();
		}
		return closePromise.then(function(){
			Tone.Context.emit("close", this);
		}.bind(this));
	};

	/**
	 *  Generate a looped buffer at some constant value.
	 *  @param  {Number}  val
	 *  @return  {BufferSourceNode}
	 */
	Tone.Context.prototype.getConstant = function(val){
		if (this._constants[val]){
			return this._constants[val];
		} else {
			var buffer = this._context.createBuffer(1, 128, this._context.sampleRate);
			var arr = buffer.getChannelData(0);
			for (var i = 0; i < arr.length; i++){
				arr[i] = val;
			}
			var constant = this._context.createBufferSource();
			constant.channelCount = 1;
			constant.channelCountMode = "explicit";
			constant.buffer = buffer;
			constant.loop = true;
			constant.start(0);
			this._constants[val] = constant;
			return constant;
		}
	};

	/**
	 *  The private loop which keeps track of the context scheduled timeouts
	 *  Is invoked from the clock source
	 *  @private
	 */
	Tone.Context.prototype._timeoutLoop = function(){
		var now = this.now();
		while (this._timeouts && this._timeouts.length && this._timeouts.peek().time <= now){
			this._timeouts.shift().callback();
		}
	};

	/**
	 *  A setTimeout which is gaurenteed by the clock source.
	 *  Also runs in the offline context.
	 *  @param  {Function}  fn       The callback to invoke
	 *  @param  {Seconds}    timeout  The timeout in seconds
	 *  @returns {Number} ID to use when invoking Tone.Context.clearTimeout
	 */
	Tone.Context.prototype.setTimeout = function(fn, timeout){
		this._timeoutIds++;
		var now = this.now();
		this._timeouts.add({
			"callback" : fn,
			"time" : now + timeout,
			"id" : this._timeoutIds
		});
		return this._timeoutIds;
	};

	/**
	 *  Clears a previously scheduled timeout with Tone.context.setTimeout
	 *  @param  {Number}  id  The ID returned from setTimeout
	 *  @return  {Tone.Context}  this
	 */
	Tone.Context.prototype.clearTimeout = function(id){
		this._timeouts.forEach(function(event){
			if (event.id === id){
				this.remove(event);
			}
		});
		return this;
	};

	/**
	 *  How often the Web Worker callback is invoked.
	 *  This number corresponds to how responsive the scheduling
	 *  can be. Context.updateInterval + Context.lookAhead gives you the
	 *  total latency between scheduling an event and hearing it.
	 *  @type {Number}
	 *  @memberOf Tone.Context#
	 *  @name updateInterval
	 */
	Object.defineProperty(Tone.Context.prototype, "updateInterval", {
		"get" : function(){
			return this._ticker.updateInterval;
		},
		"set" : function(interval){
			this._ticker.updateInterval = interval;
		}
	});

	/**
	 *  The unwrapped AudioContext.
	 *  @type {AudioContext}
	 *  @memberOf Tone.Context#
	 *  @name rawContext
	 *  @readOnly
	 */
	Object.defineProperty(Tone.Context.prototype, "rawContext", {
		"get" : function(){
			return this._context;
		}
	});

	/**
	 *  What the source of the clock is, either "worker" (Web Worker [default]),
	 *  "timeout" (setTimeout), or "offline" (none).
	 *  @type {String}
	 *  @memberOf Tone.Context#
	 *  @name clockSource
	 */
	Object.defineProperty(Tone.Context.prototype, "clockSource", {
		"get" : function(){
			return this._ticker.type;
		},
		"set" : function(type){
			this._ticker.type = type;
		}
	});

	/**
	 *  The type of playback, which affects tradeoffs between audio
	 *  output latency and responsiveness.
	 *
	 *  In addition to setting the value in seconds, the latencyHint also
	 *  accepts the strings "interactive" (prioritizes low latency),
	 *  "playback" (prioritizes sustained playback), "balanced" (balances
	 *  latency and performance), and "fastest" (lowest latency, might glitch more often).
	 *  @type {String|Seconds}
	 *  @memberOf Tone.Context#
	 *  @name latencyHint
	 *  @example
	 * //set the lookAhead to 0.3 seconds
	 * Tone.context.latencyHint = 0.3;
	 */
	Object.defineProperty(Tone.Context.prototype, "latencyHint", {
		"get" : function(){
			return this._latencyHint;
		},
		"set" : function(hint){
			var lookAhead = hint;
			this._latencyHint = hint;
			if (Tone.isString(hint)){
				switch (hint){
					case "interactive" :
						lookAhead = 0.1;
						this._context.latencyHint = hint;
						break;
					case "playback" :
						lookAhead = 0.8;
						this._context.latencyHint = hint;
						break;
					case "balanced" :
						lookAhead = 0.25;
						this._context.latencyHint = hint;
						break;
					case "fastest" :
						this._context.latencyHint = "interactive";
						lookAhead = 0.01;
						break;
				}
			}
			this.lookAhead = lookAhead;
			this.updateInterval = lookAhead/3;
		}
	});

	/**
	 *  Unlike other dispose methods, this returns a Promise
	 *  which executes when the context is closed and disposed
	 *  @returns {Promise} this
	 */
	Tone.Context.prototype.dispose = function(){
		return this.close().then(function(){
			Tone.Emitter.prototype.dispose.call(this);
			this._ticker.dispose();
			this._ticker = null;
			this._timeouts.dispose();
			this._timeouts = null;
			for (var con in this._constants){
				this._constants[con].disconnect();
			}
			this._constants = null;
		}.bind(this));
	};

	/**
	 * @class A class which provides a reliable callback using either
	 *        a Web Worker, or if that isn't supported, falls back to setTimeout.
	 * @private
	 */
	var Ticker = function(callback, type, updateInterval){

		/**
		 * Either "worker" or "timeout"
		 * @type {String}
		 * @private
		 */
		this._type = type;

		/**
		 * The update interval of the worker
		 * @private
		 * @type {Number}
		 */
		this._updateInterval = updateInterval;

		/**
		 * The callback to invoke at regular intervals
		 * @type {Function}
		 * @private
		 */
		this._callback = Tone.defaultArg(callback, Tone.noOp);

		//create the clock source for the first time
		this._createClock();
	};

	/**
	 * The possible ticker types
	 * @private
	 * @type {Object}
	 */
	Ticker.Type = {
		"Worker" : "worker",
		"Timeout" : "timeout",
		"Offline" : "offline"
	};

	/**
	 *  Generate a web worker
	 *  @return  {WebWorker}
	 *  @private
	 */
	Ticker.prototype._createWorker = function(){

		//URL Shim
		Tone.global.URL = Tone.global.URL || Tone.global.webkitURL;

		var blob = new Blob([
			//the initial timeout time
			"var timeoutTime = "+(this._updateInterval * 1000).toFixed(1)+";" +
			//onmessage callback
			"self.onmessage = function(msg){" +
			"	timeoutTime = parseInt(msg.data);" +
			"};" +
			//the tick function which posts a message
			//and schedules a new tick
			"function tick(){" +
			"	setTimeout(tick, timeoutTime);" +
			"	self.postMessage('tick');" +
			"}" +
			//call tick initially
			"tick();"
		]);
		var blobUrl = URL.createObjectURL(blob);
		var worker = new Worker(blobUrl);

		worker.onmessage = this._callback.bind(this);

		this._worker = worker;
	};

	/**
	 * Create a timeout loop
	 * @private
	 */
	Ticker.prototype._createTimeout = function(){
		this._timeout = setTimeout(function(){
			this._createTimeout();
			this._callback();
		}.bind(this), this._updateInterval * 1000);
	};

	/**
	 * Create the clock source.
	 * @private
	 */
	Ticker.prototype._createClock = function(){
		if (this._type === Ticker.Type.Worker){
			try {
				this._createWorker();
			} catch (e){
				// workers not supported, fallback to timeout
				this._type = Ticker.Type.Timeout;
				this._createClock();
			}
		} else if (this._type === Ticker.Type.Timeout){
			this._createTimeout();
		}
	};

	/**
	 * @memberOf Ticker#
	 * @type {Number}
	 * @name updateInterval
	 * @private
	 */
	Object.defineProperty(Ticker.prototype, "updateInterval", {
		"get" : function(){
			return this._updateInterval;
		},
		"set" : function(interval){
			this._updateInterval = Math.max(interval, 128/44100);
			if (this._type === Ticker.Type.Worker){
				this._worker.postMessage(Math.max(interval * 1000, 1));
			}
		}
	});

	/**
	 * The type of the ticker, either a worker or a timeout
	 * @memberOf Ticker#
	 * @type {Number}
	 * @name type
	 * @private
	 */
	Object.defineProperty(Ticker.prototype, "type", {
		"get" : function(){
			return this._type;
		},
		"set" : function(type){
			this._disposeClock();
			this._type = type;
			this._createClock();
		}
	});

	/**
	 * Clean up the current clock source
	 * @private
	 */
	Ticker.prototype._disposeClock = function(){
		if (this._timeout){
			clearTimeout(this._timeout);
			this._timeout = null;
		}
		if (this._worker){
			this._worker.terminate();
			this._worker.onmessage = null;
			this._worker = null;
		}
	};

	/**
	 * Clean up
	 * @private
	 */
	Ticker.prototype.dispose = function(){
		this._disposeClock();
		this._callback = null;
	};

	/**
	 *  Adds connect/disconnect methods
	 *  @private
	 */
	Tone.getContext(function(){

		var nativeConnect = AudioNode.prototype.connect;
		var nativeDisconnect = AudioNode.prototype.disconnect;

		//replace the old connect method
		function toneConnect(B, outNum, inNum){
			if (B.input){
				inNum = Tone.defaultArg(inNum, 0);
				if (Tone.isArray(B.input)){
					return this.connect(B.input[inNum]);
				} else {
					return this.connect(B.input, outNum, inNum);
				}
			} else {
				try {
					if (B instanceof AudioNode){
						nativeConnect.call(this, B, outNum, inNum);
						return B;
					} else {
						nativeConnect.call(this, B, outNum);
						return B;
					}
				} catch (e){
					throw new Error("error connecting to node: "+B+"\n"+e);
				}
			}
		}

		//replace the old disconnect method
		function toneDisconnect(B, outNum, inNum){
			if (B && B.input && Tone.isArray(B.input)){
				inNum = Tone.defaultArg(inNum, 0);
				this.disconnect(B.input[inNum], outNum, 0);
			} else if (B && B.input){
				this.disconnect(B.input, outNum, inNum);
			} else {
				try {
					if (B instanceof AudioParam){
						nativeDisconnect.call(this, B, outNum);
					} else {
						nativeDisconnect.apply(this, arguments);
					}
				} catch (e){
					throw new Error("error disconnecting node: "+B+"\n"+e);
				}
			}
		}

		if (AudioNode.prototype.connect !== toneConnect){
			AudioNode.prototype.connect = toneConnect;
			AudioNode.prototype.disconnect = toneDisconnect;
		}
	});

	// set the audio context initially, and if one is not already created
	if (Tone.supported && !Tone.initialized){			
		if (!Tone.global.TONE_AUDIO_CONTEXT){
			Tone.global.TONE_AUDIO_CONTEXT = new Tone.Context();
		}
		Tone.context = Tone.global.TONE_AUDIO_CONTEXT;

		// log on first initialization
		// allow optional silencing of this log
		if (!Tone.global.TONE_SILENCE_VERSION_LOGGING){
			var prefix = "v";
			if (Tone.version === "dev"){
				prefix = "";
			}
			var printString = " * Tone.js " + prefix + Tone.version + " * "; 
			// eslint-disable-next-line no-console
			console.log("%c" + printString, "background: #000; color: #fff");
		}
	} else if (!Tone.supported){
		// eslint-disable-next-line no-console
		console.warn("This browser does not support Tone.js");
	}

	return Tone.Context;
});
