
// 1. TextDecoder
// 2. TextDecoder + stream option (Firefox 38 + - how to detect ?)
// 3. Promise.prototype.finally -
// 4. fetch +
// 5. Response.prototype.body +
// 6. ReadableStream.prototype.getReader (no Firefox support yet)
// 7. AbortController - (Firefox 57)

var s = "";
if (window.Uint8Array != null && window.TextDecoder != null) {
  var b = function (byte) {
    var x = new Uint8Array(1);
    x[0] = byte;
    return x;
  };
  var textDecoder = new TextDecoder();
  var chunk = textDecoder.decode(b(208), {stream: true}) + textDecoder.decode(b(176), {stream: true});
  s = chunk;
};

console.log('TextDecoder', typeof window.TextDecoder);
console.log('TextDecoder+stream', chunk === 'а');
console.log('Promise#finally', window.Promise != null ? typeof window.Promise.prototype.finally : typeof undefined);
console.log('fetch', typeof window.fetch);
console.log('Response#body', window.Response != null && 'body' in window.Response.prototype);
console.log('ReadableStream#getReader', window.ReadableStream != null ? typeof window.ReadableStream.prototype.getReader : typeof undefined);
console.log('AbortController', typeof window.AbortController);

// an example from https://fetch.spec.whatwg.org/
var url = 'https://matrixcalc.org/jstest/tests.php?events';

function consume(reader) {
  var responseText = '';
  var pump = function () {
    return reader.read().then(function (result) {
      if (result.done) {
        console.log('responseText', responseText);
        return;
      }
      responseText += String.fromCharCode.apply(undefined, result.value);
      return pump();
    });
  };
  return pump();
}

if (window.fetch != null && window.TextDecoder != null) {
  fetch(url).then(function (response) {
    var body = response.body;
    console.log('response.body', typeof body);
    if (body != null) {
      console.log('response.body.getReader', typeof body.getReader);
      if (typeof body.getReader == 'function') {
        consume(body.getReader());
      }
    }
  });
}

setTimeout(function () {
  console.log('done');
}, 1000);




  // Firefox < 40 (no "stream" option), IE, Edge
  function TextDecoderPolyfill() {
  }

  //TODO: streaming
  TextDecoderPolyfill.prototype.decode = function (octets) {
    var string = "";
    var i = 0;
    while (i < octets.length) {
      var octet = octets[i];
      var bytesNeeded = 0;
      var codePoint = 0;
      if (octet <= 0x7F) {
        bytesNeeded = 0;
        codePoint = octet & 0xFF;
      } else if (octet <= 0xDF) {
        bytesNeeded = 1;
        codePoint = octet & 0x1F;
      } else if (octet <= 0xEF) {
        bytesNeeded = 2;
        codePoint = octet & 0x0F;
      } else if (octet <= 0xF4) {
        bytesNeeded = 3;
        codePoint = octet & 0x07;
      }
      if (octets.length - i - bytesNeeded > 0) {
        var k = 0;
        while (k < bytesNeeded) {
          octet = octets[i + k + 1];
          codePoint = (codePoint << 6) | (octet & 0x3F);
          k += 1;
        }
      } else {
        codePoint = 0xFFFD;
        bytesNeeded = octets.length - i;
      }
      string += String.fromCodePoint(codePoint);
      i += bytesNeeded + 1;
    }
    return string
  };

  if (Promise.prototype.finally == undefined) {
    Promise.prototype.finally = function (callback) {
      return this.then(function (result) {
        return Promise.resolve(callback()).then(function () {
          return result;
        });
      }, function (error) {
        return Promise.resolve(callback()).then(function () {
          throw error;
        });
      });
    };
  }

  function FetchTransport() {
    //this.controller = undefined;
    this.reader = undefined;    
    this.lastRequestId = 1;
  }

  FetchTransport.prototype.open = function (onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
    // cache: "no-store"
    // https://bugs.chromium.org/p/chromium/issues/detail?id=453190
    this.cancel();
    var textDecoder = new TextDecoder();
    var that = this;
    var lastRequestId = this.lastRequestId;
    this.controller = new AbortController();
    fetch(url, {
      headers: headers,
      credentials: withCredentials ? "include" : "same-origin",
      signal: this.controller.signal
    }).then(function (response) {
      if (lastRequestId === that.lastRequestId) {
        that.reader = response.body.getReader();
        onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"));
        return new Promise(function (resolve, reject) {
          var readNextChunk = function () {
            if (that.reader != undefined) {
              that.reader.read().then(function (result) {
                if (result.done) {
                  //Note: bytes in textDecoder are ignored
                  resolve(undefined);
                } else {
                  var chunk = textDecoder.decode(result.value, {stream: true});
                  //var chunk = String.fromCharCode.apply(undefined, result.value);
                  onProgressCallback(chunk);
                  readNextChunk();
                }
              })["catch"](reject);
            } else {
              resolve(undefined);
            }
          };
          readNextChunk();
        });
      }
      return undefined;
    })["finally"](function () {
      onFinishCallback();
    });
  };

  FetchTransport.prototype.cancel = function () {
    this.lastRequestId += 1;
    if (this.reader != undefined) {
      this.reader.cancel();
      this.reader = undefined;
    }
    //if (this.controller != undefined) {
    //  this.controller.abort();
    //}
  };

  function XHRTransport(xhr) {
    this.xhr = 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 = new Map();
  }

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

  EventTarget.prototype.dispatchEvent = function (event) {
    event.target = this;
    var typeListeners = this._listeners.get(event.type);
    if (typeListeners != undefined) {
      typeListeners.forEach(function (listener) {
        try {
          if (typeof listener.handleEvent === "function") {
            listener.handleEvent(event);
          } else {
            listener.call(this, event);
          }
        } catch (e) {
          throwError(e);
        }
      }, this);
    }
  };
  EventTarget.prototype.addEventListener = function (type, listener) {
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners == undefined) {
      typeListeners = new Set();
      listeners.set(type, typeListeners);
    }
    typeListeners.add(listener);
  };
  EventTarget.prototype.removeEventListener = function (type, listener) {
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners != undefined) {
      typeListeners["delete"](listener);
      if (typeListeners.size === 0) {
        listeners["delete"](type);
      }
    }
  };
