text: /* global fetch */
  /* global Headers */
  'use strict';
  
  var createBlob = require('./binary/blob.js');
  var utils = require('../utils');
  var readAsArrayBuffer = require('./binary/readAsArrayBuffer');
  
  function wrappedFetch() {
    var wrappedPromise = {};
  
    var promise = new utils.Promise(function(resolve, reject) {
      wrappedPromise.resolve = resolve;
      wrappedPromise.reject = reject;
    });
  
    var args = new Array(arguments.length);
  
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
  
    wrappedPromise.promise = promise;
  
    utils.Promise.resolve().then(function () {
      return fetch.apply(null, args);
    }).then(function(response) {
      wrappedPromise.resolve(response);
    }).catch(function(error) {
      wrappedPromise.reject(error);
    });
  
    return wrappedPromise;
  }
  
  function fetchRequest(options, callback) {
    var wrappedPromise, timer, response;
  
    var headers = new Headers();
  
    var fetchOptions = {
      method: options.method,
      credentials: 'include',
      headers: headers
    };
  
    if (options.json) {
      headers.set('Accept', 'application/json');
      headers.set('Content-Type', options.headers['Content-Type'] ||
        'application/json');
    }
  
    if (options.body && (options.body instanceof Blob)) {
      readAsArrayBuffer(options.body, function (arrayBuffer) {
        fetchOptions.body = arrayBuffer;
      });
    } else if (options.body &&
               options.processData &&
               typeof options.body !== 'string') {
      fetchOptions.body = JSON.stringify(options.body);
    } else if ('body' in options) {
      fetchOptions.body = options.body;
    } else {
      fetchOptions.body = null;
    }
  
    Object.keys(options.headers).forEach(function(key) {
      if (options.headers.hasOwnProperty(key)) {
        headers.set(key, options.headers[key]);
      }
    });
  
    wrappedPromise = wrappedFetch(options.url, fetchOptions);
  
    if (options.timeout > 0) {
      timer = setTimeout(function() {
        wrappedPromise.reject(new Error('Load timeout for resource: ' +
          options.url));
      }, options.timeout);
    }
  
    wrappedPromise.promise.then(function(fetchResponse) {
      response = {
        statusCode: fetchResponse.status
      };
  
      if (options.timeout > 0) {
        clearTimeout(timer);
      }
  
      if (response.statusCode >= 200 && response.statusCode < 300) {
        return options.binary ? fetchResponse.blob() : fetchResponse.text();
      }
  
      return fetchResponse.json();
    }).then(function(result) {
      if (response.statusCode >= 200 && response.statusCode < 300) {
        callback(null, response, result);
      } else {
        callback(result, response);
      }
    }).catch(function(error) {
      callback(error, response);
    });
  
    return {abort: wrappedPromise.reject};
  }
  
  function xhRequest(options, callback) {
  
    var xhr, timer, hasUpload;
  
    var abortReq = function () {
      xhr.abort();
    };
  
    if (options.xhr) {
      xhr = new options.xhr();
    } else {
      xhr = new XMLHttpRequest();
    }
  
    // cache-buster, specifically designed to work around IE's aggressive caching
    // see http://www.dashbay.com/2011/05/internet-explorer-caches-ajax/
    if (options.method === 'GET' && !options.cache) {
      var hasArgs = options.url.indexOf('?') !== -1;
      options.url += (hasArgs ? '&' : '?') + '_nonce=' + Date.now();
    }
  
    xhr.open(options.method, options.url);
    xhr.withCredentials = true;
  
    if (options.method === 'GET') {
      delete options.headers['Content-Type'];
    } else if (options.json) {
      options.headers.Accept = 'application/json';
      options.headers['Content-Type'] = options.headers['Content-Type'] ||
        'application/json';
      if (options.body &&
          options.processData &&
          typeof options.body !== "string") {
        options.body = JSON.stringify(options.body);
      }
    }
  
    if (options.binary) {
      xhr.responseType = 'arraybuffer';
    }
  
    if (!('body' in options)) {
      options.body = null;
    }
  
    for (var key in options.headers) {
      if (options.headers.hasOwnProperty(key)) {
        xhr.setRequestHeader(key, options.headers[key]);
      }
    }
  
    if (options.timeout > 0) {
      timer = setTimeout(abortReq, options.timeout);
      xhr.onprogress = function () {
        clearTimeout(timer);
        timer = setTimeout(abortReq, options.timeout);
      };
      if (typeof hasUpload === 'undefined') {
        // IE throws an error if you try to access it directly
        hasUpload = Object.keys(xhr).indexOf('upload') !== -1 &&
                    typeof xhr.upload !== 'undefined';
      }
      if (hasUpload) { // does not exist in ie9
        xhr.upload.onprogress = xhr.onprogress;
      }
    }
  
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) {
        return;
      }
  
      var response = {
        statusCode: xhr.status
      };
  
      if (xhr.status >= 200 && xhr.status < 300) {
        var data;
        if (options.binary) {
          data = createBlob([xhr.response || ''], {
            type: xhr.getResponseHeader('Content-Type')
          });
        } else {
          data = xhr.responseText;
        }
        callback(null, response, data);
      } else {
        var err = {};
        try {
          err = JSON.parse(xhr.response);
        } catch(e) {}
        callback(err, response);
      }
    };
  
    if (options.body && (options.body instanceof Blob)) {
      readAsArrayBuffer(options.body, function (arrayBuffer) {
        xhr.send(arrayBuffer);
      });
    } else {
      xhr.send(options.body);
    }
  
    return {abort: abortReq};
  }
  
  function testXhr() {
    try {
      new XMLHttpRequest();
      return true;
    } catch (err) {
      return false;
    }
  }
  
  var hasXhr = testXhr();
  
  module.exports = function(options, callback) {
    if (hasXhr || options.xhr) {
      return xhRequest(options, callback);
    } else {
      return fetchRequest(options, callback);
    }
  };
  