/**
 * EventSource
 * https://github.com/Yaffle/EventSource
 *
 * Released under the MIT License (MIT)
 * https://github.com/Yaffle/EventSource/blob/master/LICENSE.md
 */

/*jslint indent: 2, vars: true, plusplus: true */
/*global setTimeout, clearTimeout */

( function ( global ) {
	'use strict';

	var setTimeout = global.setTimeout;
	var clearTimeout = global.clearTimeout;
	var XMLHttpRequest = global.XMLHttpRequest;
	var XDomainRequest = global.XDomainRequest;
	var NativeEventSource = global.EventSource;
	var document = global.document;

	if ( Object.create == null ) {
		Object.create = function ( C ) {
			function F() {}
			F.prototype = C;
			return new F();
		};
	}

	var k = function () {};

	function XHRWrapper( xhr ) {
		this.withCredentials = false;
		this.responseType = '';
		this.readyState = 0;
		this.status = 0;
		this.statusText = '';
		this.responseText = '';
		this.onprogress = k;
		this.onreadystatechange = k;
		this._contentType = '';
		this._xhr = xhr;
		this._sendTimeout = 0;
		this._abort = k;
	}

	XHRWrapper.prototype.open = function ( method, url ) {
		this._abort( true );

		var that = this;
		var xhr = this._xhr;
		var state = 1;
		var timeout = 0;

		this._abort = function ( silent ) {
			if ( that._sendTimeout !== 0 ) {
				clearTimeout( that._sendTimeout );
				that._sendTimeout = 0;
			}
			if ( state === 1 || state === 2 || state === 3 ) {
				state = 4;
				xhr.onload = k;
				xhr.onerror = k;
				xhr.onabort = k;
				xhr.onprogress = k;
				xhr.onreadystatechange = k;
				// IE 8 - 9: XDomainRequest#abort() does not fire any event
				// Opera < 10: XMLHttpRequest#abort() does not fire any event
				xhr.abort();
				if ( timeout !== 0 ) {
					clearTimeout( timeout );
					timeout = 0;
				}
				if ( ! silent ) {
					that.readyState = 4;
					that.onreadystatechange();
				}
			}
			state = 0;
		};

		var onStart = function () {
			if ( state === 1 ) {
				//state = 2;
				var status = 0;
				var statusText = '';
				var contentType = undefined;
				if ( ! ( 'contentType' in xhr ) ) {
					try {
						status = xhr.status;
						statusText = xhr.statusText;
						contentType = xhr.getResponseHeader( 'Content-Type' );
					} catch ( error ) {
						// IE < 10 throws exception for `xhr.status` when xhr.readyState === 2 || xhr.readyState === 3
						// Opera < 11 throws exception for `xhr.status` when xhr.readyState === 2
						// https://bugs.webkit.org/show_bug.cgi?id=29121
						status = 0;
						statusText = '';
						contentType = undefined;
						// Firefox < 14, Chrome ?, Safari ?
						// https://bugs.webkit.org/show_bug.cgi?id=29658
						// https://bugs.webkit.org/show_bug.cgi?id=77854
					}
				} else {
					status = 200;
					statusText = 'OK';
					contentType = xhr.contentType;
				}
				if ( status !== 0 ) {
					state = 2;
					that.readyState = 2;
					that.status = status;
					that.statusText = statusText;
					that._contentType = contentType;
					that.onreadystatechange();
				}
			}
		};
		var onProgress = function () {
			onStart();
			if ( state === 2 || state === 3 ) {
				state = 3;
				var responseText = '';
				try {
					responseText = xhr.responseText;
				} catch ( error ) {
					// IE 8 - 9 with XMLHttpRequest
				}
				that.readyState = 3;
				that.responseText = responseText;
				that.onprogress();
			}
		};
		var onFinish = function () {
			// Firefox 52 fires "readystatechange" (xhr.readyState === 4) without final "readystatechange" (xhr.readyState === 3)
			// IE 8 fires "onload" without "onprogress"
			onProgress();
			if ( state === 1 || state === 2 || state === 3 ) {
				state = 4;
				if ( timeout !== 0 ) {
					clearTimeout( timeout );
					timeout = 0;
				}
				that.readyState = 4;
				that.onreadystatechange();
			}
		};
		var onReadyStateChange = function () {
			if ( xhr != undefined ) {
				// Opera 12
				if ( xhr.readyState === 4 ) {
					onFinish();
				} else if ( xhr.readyState === 3 ) {
					onProgress();
				} else if ( xhr.readyState === 2 ) {
					onStart();
				}
			}
		};
		var onTimeout = function () {
			timeout = setTimeout( function () {
				onTimeout();
			}, 500 );
			if ( xhr.readyState === 3 ) {
				onProgress();
			}
		};

		// XDomainRequest#abort removes onprogress, onerror, onload
		xhr.onload = onFinish;
		xhr.onerror = onFinish;
		// improper fix to match Firefox behaviour, but it is better than just ignore abort
		// see https://bugzilla.mozilla.org/show_bug.cgi?id=768596
		// https://bugzilla.mozilla.org/show_bug.cgi?id=880200
		// https://code.google.com/p/chromium/issues/detail?id=153570
		// IE 8 fires "onload" without "onprogress
		xhr.onabort = onFinish;

		// https://bugzilla.mozilla.org/show_bug.cgi?id=736723
		if (
			! ( 'sendAsBinary' in XMLHttpRequest.prototype ) &&
			! ( 'mozAnon' in XMLHttpRequest.prototype )
		) {
			xhr.onprogress = onProgress;
		}

		// IE 8 - 9 (XMLHTTPRequest)
		// Opera < 12
		// Firefox < 3.5
		// Firefox 3.5 - 3.6 - ? < 9.0
		// onprogress is not fired sometimes or delayed
		// see also #64
		xhr.onreadystatechange = onReadyStateChange;

		if ( 'contentType' in xhr ) {
			url +=
				( url.indexOf( '?', 0 ) === -1 ? '?' : '&' ) + 'padding=true';
		}
		xhr.open( method, url, true );

		if ( 'readyState' in xhr ) {
			// workaround for Opera 12 issue with "progress" events
			// #91
			timeout = setTimeout( function () {
				onTimeout();
			}, 0 );
		}
	};
	XHRWrapper.prototype.abort = function () {
		this._abort( false );
	};
	XHRWrapper.prototype.getResponseHeader = function ( name ) {
		return this._contentType;
	};
	XHRWrapper.prototype.setRequestHeader = function ( name, value ) {
		var xhr = this._xhr;
		if ( 'setRequestHeader' in xhr ) {
			xhr.setRequestHeader( name, value );
		}
	};
	XHRWrapper.prototype.send = function () {
		// loading indicator in Safari < ? (6), Chrome < 14, Firefox
		if (
			! ( 'ontimeout' in XMLHttpRequest.prototype ) &&
			document != undefined &&
			document.readyState != undefined &&
			document.readyState !== 'complete'
		) {
			var that = this;
			that._sendTimeout = setTimeout( function () {
				that._sendTimeout = 0;
				that.send();
			}, 4 );
			return;
		}

		var xhr = this._xhr;
		// withCredentials should be set after "open" for Safari and Chrome (< 19 ?)
		xhr.withCredentials = this.withCredentials;
		xhr.responseType = this.responseType;
		try {
			// xhr.send(); throws "Not enough arguments" in Firefox 3.0
			xhr.send( undefined );
		} catch ( error1 ) {
			// Safari 5.1.7, Opera 12
			throw error1;
		}
	};

	function XHRTransport( xhr ) {
		this._xhr = new XHRWrapper( xhr );
	}

	XHRTransport.prototype.open = function (
		onStartCallback,
		onProgressCallback,
		onFinishCallback,
		url,
		withCredentials,
		headers
	) {
		var xhr = this._xhr;
		xhr.open( 'GET', url );
		var offset = 0;
		xhr.onprogress = function () {
			var responseText = xhr.responseText;
			var chunk = responseText.slice( offset );
			offset += chunk.length;
			onProgressCallback( chunk );
		};
		xhr.onreadystatechange = function () {
			if ( xhr.readyState === 2 ) {
				var status = xhr.status;
				var statusText = xhr.statusText;
				var contentType = xhr.getResponseHeader( 'Content-Type' );
				onStartCallback( status, statusText, contentType );
			} else if ( xhr.readyState === 4 ) {
				onFinishCallback();
			}
		};
		xhr.withCredentials = withCredentials;
		xhr.responseType = 'text';
		for ( var name in headers ) {
			if ( Object.prototype.hasOwnProperty.call( headers, name ) ) {
				xhr.setRequestHeader( name, headers[ name ] );
			}
		}
		xhr.send();
	};

	XHRTransport.prototype.cancel = function () {
		var xhr = this._xhr;
		xhr.abort();
	};

	function EventTarget() {
		this._listeners = Object.create( null );
	}

	function throwError( e ) {
		setTimeout( function () {
			throw e;
		}, 0 );
	}

	EventTarget.prototype.dispatchEvent = function ( event ) {
		event.target = this;
		var typeListeners = this._listeners[ event.type ];
		if ( typeListeners != undefined ) {
			var length = typeListeners.length;
			for ( var i = 0; i < length; i += 1 ) {
				var listener = typeListeners[ i ];
				try {
					if ( typeof listener.handleEvent === 'function' ) {
						listener.handleEvent( event );
					} else {
						listener.call( this, event );
					}
				} catch ( e ) {
					throwError( e );
				}
			}
		}
	};
	EventTarget.prototype.addEventListener = function ( type, listener ) {
		type = String( type );
		var listeners = this._listeners;
		var typeListeners = listeners[ type ];
		if ( typeListeners == undefined ) {
			typeListeners = [];
			listeners[ type ] = typeListeners;
		}
		var found = false;
		for ( var i = 0; i < typeListeners.length; i += 1 ) {
			if ( typeListeners[ i ] === listener ) {
				found = true;
			}
		}
		if ( ! found ) {
			typeListeners.push( listener );
		}
	};
	EventTarget.prototype.removeEventListener = function ( type, listener ) {
		type = String( type );
		var listeners = this._listeners;
		var typeListeners = listeners[ type ];
		if ( typeListeners != undefined ) {
			var filtered = [];
			for ( var i = 0; i < typeListeners.length; i += 1 ) {
				if ( typeListeners[ i ] !== listener ) {
					filtered.push( typeListeners[ i ] );
				}
			}
			if ( filtered.length === 0 ) {
				delete listeners[ type ];
			} else {
				listeners[ type ] = filtered;
			}
		}
	};

	function Event( type ) {
		this.type = type;
		this.target = undefined;
	}

	function MessageEvent( type, options ) {
		Event.call( this, type );
		this.data = options.data;
		this.lastEventId = options.lastEventId;
	}

	MessageEvent.prototype = Object.create( Event.prototype );

	var WAITING = -1;
	var CONNECTING = 0;
	var OPEN = 1;
	var CLOSED = 2;

	var AFTER_CR = -1;
	var FIELD_START = 0;
	var FIELD = 1;
	var VALUE_START = 2;
	var VALUE = 3;

	var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i;

	var MINIMUM_DURATION = 1000;
	var MAXIMUM_DURATION = 18000000;

	var parseDuration = function ( value, def ) {
		var n = parseInt( value, 10 );
		if ( n !== n ) {
			n = def;
		}
		return clampDuration( n );
	};
	var clampDuration = function ( n ) {
		return Math.min( Math.max( n, MINIMUM_DURATION ), MAXIMUM_DURATION );
	};

	var fire = function ( that, f, event ) {
		try {
			if ( typeof f === 'function' ) {
				f.call( that, event );
			}
		} catch ( e ) {
			throwError( e );
		}
	};

	function EventSourcePolyfill( url, options ) {
		EventTarget.call( this );

		this.onopen = undefined;
		this.onmessage = undefined;
		this.onerror = undefined;

		this.url = undefined;
		this.readyState = undefined;
		this.withCredentials = undefined;

		this._close = undefined;

		start( this, url, options );
	}

	function start( es, url, options ) {
		url = String( url );
		var withCredentials =
			options != undefined && Boolean( options.withCredentials );

		var initialRetry = clampDuration( 1000 );
		var heartbeatTimeout = clampDuration( 45000 );

		var lastEventId = '';
		var retry = initialRetry;
		var wasActivity = false;
		var headers =
			options != undefined && options.headers != undefined
				? JSON.parse( JSON.stringify( options.headers ) )
				: undefined;
		var CurrentTransport =
			options != undefined && options.Transport != undefined
				? options.Transport
				: XDomainRequest != undefined
				? XDomainRequest
				: XMLHttpRequest;
		var transport = new XHRTransport( new CurrentTransport() );
		var timeout = 0;
		var currentState = WAITING;
		var dataBuffer = '';
		var lastEventIdBuffer = '';
		var eventTypeBuffer = '';

		var textBuffer = '';
		var state = FIELD_START;
		var fieldStart = 0;
		var valueStart = 0;

		var onStart = function ( status, statusText, contentType ) {
			if ( currentState === CONNECTING ) {
				if (
					status === 200 &&
					contentType != undefined &&
					contentTypeRegExp.test( contentType )
				) {
					currentState = OPEN;
					wasActivity = true;
					retry = initialRetry;
					es.readyState = OPEN;
					var event = new Event( 'open' );
					es.dispatchEvent( event );
					fire( es, es.onopen, event );
				} else {
					var message = '';
					if ( status !== 200 ) {
						if ( statusText ) {
							statusText = statusText.replace( /\s+/g, ' ' );
						}
						message =
							"EventSource's response has a status " +
							status +
							' ' +
							statusText +
							' that is not 200. Aborting the connection.';
					} else {
						message =
							"EventSource's response has a Content-Type specifying an unsupported type: " +
							( contentType == undefined
								? '-'
								: contentType.replace( /\s+/g, ' ' ) ) +
							'. Aborting the connection.';
					}
					throwError( new Error( message ) );
					close();
					var event = new Event( 'error' );
					es.dispatchEvent( event );
					fire( es, es.onerror, event );
				}
			}
		};

		var onProgress = function ( textChunk ) {
			if ( currentState === OPEN ) {
				var n = -1;
				for ( var i = 0; i < textChunk.length; i += 1 ) {
					var c = textChunk.charCodeAt( i );
					if (
						c === '\n'.charCodeAt( 0 ) ||
						c === '\r'.charCodeAt( 0 )
					) {
						n = i;
					}
				}
				var chunk =
					( n !== -1 ? textBuffer : '' ) +
					textChunk.slice( 0, n + 1 );
				textBuffer =
					( n === -1 ? textBuffer : '' ) + textChunk.slice( n + 1 );
				if ( chunk !== '' ) {
					wasActivity = true;
				}
				for (
					var position = 0;
					position < chunk.length;
					position += 1
				) {
					var c = chunk.charCodeAt( position );
					if ( state === AFTER_CR && c === '\n'.charCodeAt( 0 ) ) {
						state = FIELD_START;
					} else {
						if ( state === AFTER_CR ) {
							state = FIELD_START;
						}
						if (
							c === '\r'.charCodeAt( 0 ) ||
							c === '\n'.charCodeAt( 0 )
						) {
							if ( state !== FIELD_START ) {
								if ( state === FIELD ) {
									valueStart = position + 1;
								}
								var field = chunk.slice(
									fieldStart,
									valueStart - 1
								);
								var value = chunk.slice(
									valueStart +
										( valueStart < position &&
										chunk.charCodeAt( valueStart ) ===
											' '.charCodeAt( 0 )
											? 1
											: 0 ),
									position
								);
								if ( field === 'data' ) {
									dataBuffer += '\n';
									dataBuffer += value;
								} else if ( field === 'id' ) {
									lastEventIdBuffer = value;
								} else if ( field === 'event' ) {
									eventTypeBuffer = value;
								} else if ( field === 'retry' ) {
									initialRetry = parseDuration(
										value,
										initialRetry
									);
									retry = initialRetry;
								} else if ( field === 'heartbeatTimeout' ) {
									heartbeatTimeout = parseDuration(
										value,
										heartbeatTimeout
									);
									if ( timeout !== 0 ) {
										clearTimeout( timeout );
										timeout = setTimeout( function () {
											onTimeout();
										}, heartbeatTimeout );
									}
								}
							}
							if ( state === FIELD_START ) {
								if ( dataBuffer !== '' ) {
									lastEventId = lastEventIdBuffer;
									if ( eventTypeBuffer === '' ) {
										eventTypeBuffer = 'message';
									}
									var event = new MessageEvent(
										eventTypeBuffer,
										{
											data: dataBuffer.slice( 1 ),
											lastEventId: lastEventIdBuffer,
										}
									);
									es.dispatchEvent( event );
									if ( eventTypeBuffer === 'message' ) {
										fire( es, es.onmessage, event );
									}
									if ( currentState === CLOSED ) {
										return;
									}
								}
								dataBuffer = '';
								eventTypeBuffer = '';
							}
							state =
								c === '\r'.charCodeAt( 0 )
									? AFTER_CR
									: FIELD_START;
						} else {
							if ( state === FIELD_START ) {
								fieldStart = position;
								state = FIELD;
							}
							if ( state === FIELD ) {
								if ( c === ':'.charCodeAt( 0 ) ) {
									valueStart = position + 1;
									state = VALUE_START;
								}
							} else if ( state === VALUE_START ) {
								state = VALUE;
							}
						}
					}
				}
			}
		};

		var onFinish = function () {
			if ( currentState === OPEN || currentState === CONNECTING ) {
				currentState = WAITING;
				if ( timeout !== 0 ) {
					clearTimeout( timeout );
					timeout = 0;
				}
				timeout = setTimeout( function () {
					onTimeout();
				}, retry );
				retry = clampDuration(
					Math.min( initialRetry * 16, retry * 2 )
				);

				es.readyState = CONNECTING;
				var event = new Event( 'error' );
				es.dispatchEvent( event );
				fire( es, es.onerror, event );
			}
		};

		var close = function () {
			currentState = CLOSED;
			transport.cancel();
			if ( timeout !== 0 ) {
				clearTimeout( timeout );
				timeout = 0;
			}
			es.readyState = CLOSED;
		};

		var onTimeout = function () {
			timeout = 0;

			if ( currentState !== WAITING ) {
				if ( ! wasActivity ) {
					throwError(
						new Error(
							'No activity within ' +
								heartbeatTimeout +
								' milliseconds. Reconnecting.'
						)
					);
					transport.cancel();
				} else {
					wasActivity = false;
					timeout = setTimeout( function () {
						onTimeout();
					}, heartbeatTimeout );
				}
				return;
			}

			wasActivity = false;
			timeout = setTimeout( function () {
				onTimeout();
			}, heartbeatTimeout );

			currentState = CONNECTING;
			dataBuffer = '';
			eventTypeBuffer = '';
			lastEventIdBuffer = lastEventId;
			textBuffer = '';
			fieldStart = 0;
			valueStart = 0;
			state = FIELD_START;

			// https://bugzilla.mozilla.org/show_bug.cgi?id=428916
			// Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers.
			var requestURL = url;
			if (
				url.slice( 0, 5 ) !== 'data:' &&
				url.slice( 0, 5 ) !== 'blob:'
			) {
				requestURL =
					url +
					( url.indexOf( '?', 0 ) === -1 ? '?' : '&' ) +
					'lastEventId=' +
					encodeURIComponent( lastEventId );
			}
			var requestHeaders = {};
			requestHeaders[ 'Accept' ] = 'text/event-stream';
			if ( headers != undefined ) {
				for ( var name in headers ) {
					if (
						Object.prototype.hasOwnProperty.call( headers, name )
					) {
						requestHeaders[ name ] = headers[ name ];
					}
				}
			}
			try {
				transport.open(
					onStart,
					onProgress,
					onFinish,
					requestURL,
					withCredentials,
					requestHeaders
				);
			} catch ( error ) {
				close();
				throw error;
			}
		};

		es.url = url;
		es.readyState = CONNECTING;
		es.withCredentials = withCredentials;
		es._close = close;

		onTimeout();
	}

	EventSourcePolyfill.prototype = Object.create( EventTarget.prototype );
	EventSourcePolyfill.prototype.CONNECTING = CONNECTING;
	EventSourcePolyfill.prototype.OPEN = OPEN;
	EventSourcePolyfill.prototype.CLOSED = CLOSED;
	EventSourcePolyfill.prototype.close = function () {
		this._close();
	};

	EventSourcePolyfill.CONNECTING = CONNECTING;
	EventSourcePolyfill.OPEN = OPEN;
	EventSourcePolyfill.CLOSED = CLOSED;
	EventSourcePolyfill.prototype.withCredentials = undefined;

	global.EventSourcePolyfill = EventSourcePolyfill;
	global.NativeEventSource = NativeEventSource;

	if (
		XMLHttpRequest != undefined &&
		( NativeEventSource == undefined ||
			! ( 'withCredentials' in NativeEventSource.prototype ) )
	) {
		// Why replace a native EventSource ?
		// https://bugzilla.mozilla.org/show_bug.cgi?id=444328
		// https://bugzilla.mozilla.org/show_bug.cgi?id=831392
		// https://code.google.com/p/chromium/issues/detail?id=260144
		// https://code.google.com/p/chromium/issues/detail?id=225654
		// ...
		global.EventSource = EventSourcePolyfill;
	}
} )( typeof window !== 'undefined' ? window : this );
