/** @license
 * eventsource.js
 * Available under MIT License (MIT)
 * https://github.com/Yaffle/EventSource/
 */

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

(function (global) {
  "use strict";

  var setTimeout = global.setTimeout;
  var clearTimeout = global.clearTimeout;

  var k = function () {
  };

  function XHRTransport(xhr, onStartCallback, onProgressCallback, onFinishCallback, thisArg) {
    this._internal = new XHRTransportInternal(xhr, onStartCallback, onProgressCallback, onFinishCallback, thisArg);
  }

  XHRTransport.prototype.open = function (url, withCredentials) {
    this._internal.open(url, withCredentials);
  };

  XHRTransport.prototype.cancel = function () {
    this._internal.cancel();
  };

  function XHRTransportInternal(xhr, onStartCallback, onProgressCallback, onFinishCallback, thisArg) {
    this.onStartCallback = onStartCallback;
    this.onProgressCallback = onProgressCallback;
    this.onFinishCallback = onFinishCallback;
    this.thisArg = thisArg;
    this.xhr = xhr;
    this.state = 0;
    this.charOffset = 0;
    this.offset = 0;
    this.url = "";
    this.withCredentials = false;
    this.timeout = 0;
  }

  XHRTransportInternal.prototype.onStart = function () {
    if (this.state === 1) {
      this.state = 2;
      var status = 0;
      var statusText = "";
      var contentType = undefined;
      if (!("contentType" in this.xhr)) {
        try {
          status = this.xhr.status;
          statusText = this.xhr.statusText;
          contentType = this.xhr.getResponseHeader("Content-Type");
        } catch (error) {
          // https://bugs.webkit.org/show_bug.cgi?id=29121
          status = 0;
          statusText = "";
          contentType = undefined;
          // FF < 14, WebKit
          // https://bugs.webkit.org/show_bug.cgi?id=29658
          // https://bugs.webkit.org/show_bug.cgi?id=77854
        }
      } else {
        status = 200;
        statusText = "OK";
        contentType = this.xhr.contentType;
      }
      if (contentType == undefined) {
        contentType = "";
      }
      this.onStartCallback.call(this.thisArg, status, statusText, contentType);
    }
  };
  XHRTransportInternal.prototype.onProgress = function () {
    this.onStart();
    if (this.state === 2 || this.state === 3) {
      this.state = 3;
      var responseText = "";
      try {
        responseText = this.xhr.responseText;
      } catch (error) {
        // IE 8 - 9 with XMLHttpRequest
      }
      var chunkStart = this.charOffset;
      var length = responseText.length;
      for (var i = this.offset; i < length; i += 1) {
        var c = responseText.charCodeAt(i);
        if (c === "\n".charCodeAt(0) || c === "\r".charCodeAt(0)) {
          this.charOffset = i + 1;
        }
      }
      this.offset = length;
      var chunk = responseText.slice(chunkStart, this.charOffset);
      this.onProgressCallback.call(this.thisArg, chunk);
    }
  };
  XHRTransportInternal.prototype.onFinish = function () {
    // IE 8 fires "onload" without "onprogress
    this.onProgress();
    if (this.state === 3) {
      this.state = 4;
      if (this.timeout !== 0) {
        clearTimeout(this.timeout);
        this.timeout = 0;
      }
      this.onFinishCallback.call(this.thisArg);
    }
  };
  XHRTransportInternal.prototype.onReadyStateChange = function () {
    if (this.xhr != undefined) { // Opera 12
      if (this.xhr.readyState === 4) {
        if (this.xhr.status === 0) {
          this.onFinish();
        } else {
          this.onFinish();
        }
      } else if (this.xhr.readyState === 3) {
        this.onProgress();
      } else if (this.xhr.readyState === 2) {
        // Opera 10.63 throws exception for `this.xhr.status`
        // this.onStart();
      }
    }
  };
  XHRTransportInternal.prototype.onTimeout2 = function () {
    this.timeout = 0;
    var tmp = (/^data\:([^,]*?)(base64)?,([\S]*)$/).exec(this.url);
    var contentType = tmp[1];
    var data = tmp[2] === "base64" ? global.atob(tmp[3]) : decodeURIComponent(tmp[3]);
    if (this.state === 1) {
      this.state = 2;
      this.onStartCallback.call(this.thisArg, 200, "OK", contentType);
    }
    if (this.state === 2 || this.state === 3) {
      this.state = 3;
      this.onProgressCallback.call(this.thisArg, data);
    }
    if (this.state === 3) {
      this.state = 4;
      this.onFinishCallback.call(this.thisArg);
    }
  };
  XHRTransportInternal.prototype.onTimeout1 = function () {
    this.timeout = 0;
    this.open(this.url, this.withCredentials);
  };
  XHRTransportInternal.prototype.onTimeout0 = function () {
    var that = this;
    this.timeout = setTimeout(function () {
      that.onTimeout0();
    }, 500);
    if (this.xhr.readyState === 3) {
      this.onProgress();
    }
  };
  XHRTransportInternal.prototype.handleEvent = function (event) {
    if (event.type === "load") {
      this.onFinish();
    } else if (event.type === "error") {
      this.onFinish();
    } else if (event.type === "abort") {
      // 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
      this.onFinish();
    } else if (event.type === "progress") {
      this.onProgress();
    } else if (event.type === "readystatechange") {
      this.onReadyStateChange();
    }
  };
  XHRTransportInternal.prototype.open = function (url, withCredentials) {
    if (this.timeout !== 0) {
      clearTimeout(this.timeout);
      this.timeout = 0;
    }

    this.url = url;
    this.withCredentials = withCredentials;

    this.state = 1;
    this.charOffset = 0;
    this.offset = 0;

    var that = this;

    var tmp = (/^data\:([^,]*?)(?:;base64)?,[\S]*$/).exec(url);
    if (tmp != undefined) {
      this.timeout = setTimeout(function () {
        that.onTimeout2();
      }, 0);
      return;
    }

    // loading indicator in Safari, Chrome < 14
    // loading indicator in Firefox
    // https://bugzilla.mozilla.org/show_bug.cgi?id=736723
    if ((!("ontimeout" in this.xhr) || ("sendAsBinary" in this.xhr) || ("mozAnon" in this.xhr)) && global.document != undefined && global.document.readyState != undefined && global.document.readyState !== "complete") {
      this.timeout = setTimeout(function () {
        that.onTimeout1();
      }, 4);
      return;
    }

    // XDomainRequest#abort removes onprogress, onerror, onload
    this.xhr.onload = function (event) {
      that.handleEvent({type: "load"});
    };
    this.xhr.onerror = function () {
      that.handleEvent({type: "error"});
    };
    this.xhr.onabort = function () {
      that.handleEvent({type: "abort"});
    };
    this.xhr.onprogress = function () {
      that.handleEvent({type: "progress"});
    };
    // IE 8-9 (XMLHTTPRequest)
    // Firefox 3.5 - 3.6 - ? < 9.0
    // onprogress is not fired sometimes or delayed
    // see also #64
    this.xhr.onreadystatechange = function () {
      that.handleEvent({type: "readystatechange"});
    };

    this.xhr.open("GET", url, true);

    // withCredentials should be set after "open" for Safari and Chrome (< 19 ?)
    this.xhr.withCredentials = withCredentials;

    this.xhr.responseType = "text";

    if ("setRequestHeader" in this.xhr) {
      // Request header field Cache-Control is not allowed by Access-Control-Allow-Headers.
      // "Cache-control: no-cache" are not honored in Chrome and Firefox
      // https://bugzilla.mozilla.org/show_bug.cgi?id=428916
      //this.xhr.setRequestHeader("Cache-Control", "no-cache");
      this.xhr.setRequestHeader("Accept", "text/event-stream");
      // Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers.
      //this.xhr.setRequestHeader("Last-Event-ID", this.lastEventId);
    }

    try {
      this.xhr.send(undefined);
    } catch (error1) {
      // Safari 5.1.7, Opera 12
      throw error1;
    }

    if (("readyState" in this.xhr) && global.opera != undefined) {
      // workaround for Opera issue with "progress" events
      this.timeout = setTimeout(function () {
        that.onTimeout0();
      }, 0);
    }
  };
  XHRTransportInternal.prototype.cancel = function () {
    if (this.state !== 0 && this.state !== 4) {
      this.state = 4;
      this.xhr.onload = k;
      this.xhr.onerror = k;
      this.xhr.onabort = k;
      this.xhr.onprogress = k;
      this.xhr.onreadystatechange = k;
      this.xhr.abort();
      if (this.timeout !== 0) {
        clearTimeout(this.timeout);
        this.timeout = 0;
      }
      this.onFinishCallback.call(this.thisArg);
    }
    this.state = 0;
  };

  function Map() {
    this._data = {};
  }

  Map.prototype.get = function (key) {
    return this._data[key + "~"];
  };
  Map.prototype.set = function (key, value) {
    this._data[key + "~"] = value;
  };
  Map.prototype["delete"] = function (key) {
    delete this._data[key + "~"];
  };

  function EventTarget() {
    this._listeners = new Map();
  }

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

  EventTarget.prototype.dispatchEvent = function (event) {
    event.target = this;
    var type = event.type.toString();
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners == undefined) {
      return;
    }
    var length = typeListeners.length;
    var listener = undefined;
    for (var i = 0; i < length; i += 1) {
      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, callback) {
    type = type.toString();
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners == undefined) {
      typeListeners = [];
      listeners.set(type, typeListeners);
    }
    for (var i = typeListeners.length; i >= 0; i -= 1) {
      if (typeListeners[i] === callback) {
        return;
      }
    }
    typeListeners.push(callback);
  };
  EventTarget.prototype.removeEventListener = function (type, callback) {
    type = type.toString();
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners == undefined) {
      return;
    }
    var length = typeListeners.length;
    var filtered = [];
    for (var i = 0; i < length; i += 1) {
      if (typeListeners[i] !== callback) {
        filtered.push(typeListeners[i]);
      }
    }
    if (filtered.length === 0) {
      listeners["delete"](type);
    } else {
      listeners.set(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 = Event.prototype;

  var XHR = global.XMLHttpRequest;
  var XDR = global.XDomainRequest;
  var isCORSSupported = XHR != undefined && (new XHR()).withCredentials != undefined;
  var Transport = isCORSSupported || (XHR != undefined && XDR == undefined) ? XHR : XDR;

  var WAITING = -1;
  var CONNECTING = 0;
  var OPEN = 1;
  var CLOSED = 2;
  var AFTER_CR = 3;
  var FIELD_START = 4;
  var FIELD = 5;
  var VALUE_START = 6;
  var VALUE = 7;
  var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i;

  var MINIMUM_DURATION = 1000;
  var MAXIMUM_DURATION = 18000000;

  var getDuration = function (value, def) {
    var n = value;
    if (n !== n) {
      n = def;
    }
    return (n < MINIMUM_DURATION ? MINIMUM_DURATION : (n > MAXIMUM_DURATION ? MAXIMUM_DURATION : n));
  };

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

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

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

    this.url = "";
    this.readyState = CONNECTING;
    this.withCredentials = false;

    this._internal = new EventSourceInternal(this, url, options);
  }

  function EventSourceInternal(es, url, options) {
    this.url = url.toString();
    this.readyState = CONNECTING;
    this.withCredentials = isCORSSupported && options != undefined && Boolean(options.withCredentials);

    this.es = es;
    this.initialRetry = getDuration(1000, 0);
    this.heartbeatTimeout = getDuration(45000, 0);

    this.lastEventId = "";
    this.retry = this.initialRetry;
    this.wasActivity = false;
    var CurrentTransport = options != undefined && options.Transport != undefined ? options.Transport : Transport;
    var xhr = new CurrentTransport();
    this.transport = new XHRTransport(xhr, this.onStart, this.onProgress, this.onFinish, this);
    this.timeout = 0;
    this.currentState = WAITING;
    this.dataBuffer = [];
    this.lastEventIdBuffer = "";
    this.eventTypeBuffer = "";

    this.state = FIELD_START;
    this.fieldStart = 0;
    this.valueStart = 0;

    this.es.url = this.url;
    this.es.readyState = this.readyState;
    this.es.withCredentials = this.withCredentials;

    this.onTimeout();
  }

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

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

  EventSourceInternal.prototype.onFinish = function () {
    if (this.currentState === OPEN || this.currentState === CONNECTING) {
      this.currentState = WAITING;
      if (this.timeout !== 0) {
        clearTimeout(this.timeout);
        this.timeout = 0;
      }
      if (this.retry > this.initialRetry * 16) {
        this.retry = this.initialRetry * 16;
      }
      if (this.retry > MAXIMUM_DURATION) {
        this.retry = MAXIMUM_DURATION;
      }
      var that = this;
      this.timeout = setTimeout(function () {
        that.onTimeout();
      }, this.retry);
      this.retry = this.retry * 2 + 1;

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

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

    this.wasActivity = false;
    var that = this;
    this.timeout = setTimeout(function () {
      that.onTimeout();
    }, this.heartbeatTimeout);

    this.currentState = CONNECTING;
    this.dataBuffer.length = 0;
    this.eventTypeBuffer = "";
    this.lastEventIdBuffer = this.lastEventId;
    this.fieldStart = 0;
    this.valueStart = 0;
    this.state = FIELD_START;

    var s = this.url.slice(0, 5);
    if (s !== "data:" && s !== "blob:") {
      s = this.url + ((this.url.indexOf("?", 0) === -1 ? "?" : "&") + "lastEventId=" + encodeURIComponent(this.lastEventId) + "&r=" + (Math.random() + 1).toString().slice(2));
    } else {
      s = this.url;
    }
    try {
      this.transport.open(s, this.withCredentials);
    } catch (error) {
      this.close();
      throw error;
    }
  };

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

  function F() {
    this.CONNECTING = CONNECTING;
    this.OPEN = OPEN;
    this.CLOSED = CLOSED;
  }
  F.prototype = EventTarget.prototype;

  EventSource.prototype = new F();

  EventSource.prototype.close = function () {
    this._internal.close();
  };

  F.call(EventSource);
  if (isCORSSupported) {
    EventSource.prototype.withCredentials = undefined;
  }

  var isEventSourceSupported = function () {
    // Opera 12 fails this test, but this is fine.
    return global.EventSource != undefined && ("withCredentials" in global.EventSource.prototype);
  };

  if (Transport != undefined && (global.EventSource == undefined || (isCORSSupported && !isEventSourceSupported()))) {
    // 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.NativeEventSource = global.EventSource;
    global.EventSource = EventSource;
  }

}(typeof window !== 'undefined' ? window : this));