text: // @flow
  
  import window from './window';
  import {extend, warnOnce, isWorker} from './util';
  import {isMapboxHTTPURL, hasCacheDefeatingSku} from './mapbox';
  import config from './config';
  import assert from 'assert';
  import {cacheGet, cachePut} from './tile_request_cache';
  import webpSupported from './webp_supported';
  import offscreenCanvasSupported from './offscreen_canvas_supported';
  
  import type {Callback} from '../types/callback';
  import type {Cancelable} from '../types/cancelable';
  
  /**
   * The type of a resource.
   * @private
   * @readonly
   * @enum {string}
   */
  const ResourceType = {
      Unknown: 'Unknown',
      Style: 'Style',
      Source: 'Source',
      Tile: 'Tile',
      Glyphs: 'Glyphs',
      SpriteImage: 'SpriteImage',
      SpriteJSON: 'SpriteJSON',
      Image: 'Image'
  };
  export {ResourceType};
  
  if (typeof Object.freeze == 'function') {
      Object.freeze(ResourceType);
  }
  
  /**
   * A `RequestParameters` object to be returned from Map.options.transformRequest callbacks.
   * @typedef {Object} RequestParameters
   * @property {string} url The URL to be requested.
   * @property {Object} headers The headers to be sent with the request.
   * @property {string} credentials `'same-origin'|'include'` Use 'include' to send cookies with cross-origin requests.
   */
  export type RequestParameters = {
      url: string,
      headers?: Object,
      method?: 'GET' | 'POST' | 'PUT',
      body?: string,
      type?: 'string' | 'json' | 'arrayBuffer',
      credentials?: 'same-origin' | 'include',
      collectResourceTiming?: boolean
  };
  
  export type ResponseCallback<T> = (error: ?Error, data: ?T, cacheControl: ?string, expires: ?string) => void;
  
  class AJAXError extends Error {
      status: number;
      url: string;
      constructor(message: string, status: number, url: string) {
          if (status === 401 && isMapboxHTTPURL(url)) {
              message += ': you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens-and-token-scopes';
          }
          super(message);
          this.status = status;
          this.url = url;
  
          // work around for https://github.com/Rich-Harris/buble/issues/40
          this.name = this.constructor.name;
          this.message = message;
      }
  
      toString() {
          return `${this.name}: ${this.message} (${this.status}): ${this.url}`;
      }
  }
  
  // Ensure that we're sending the correct referrer from blob URL worker bundles.
  // For files loaded from the local file system, `location.origin` will be set
  // to the string(!) "null" (Firefox), or "file://" (Chrome, Safari, Edge, IE),
  // and we will set an empty referrer. Otherwise, we're using the document's URL.
  /* global self */
  export const getReferrer = isWorker() ?
      () => self.worker && self.worker.referrer :
      () => (window.location.protocol === 'blob:' ? window.parent : window).location.href;
  
  // Determines whether a URL is a file:// URL. This is obviously the case if it begins
  // with file://. Relative URLs are also file:// URLs iff the original document was loaded
  // via a file:// URL.
  const isFileURL = url => /^file:/.test(url) || (/^file:/.test(getReferrer()) && !/^\w+:/.test(url));
  
  function makeFetchRequest(requestParameters: RequestParameters, callback: ResponseCallback<any>): Cancelable {
      const controller = new window.AbortController();
      const request = new window.Request(requestParameters.url, {
          method: requestParameters.method || 'GET',
          body: requestParameters.body,
          credentials: requestParameters.credentials,
          headers: requestParameters.headers,
          referrer: getReferrer(),
          signal: controller.signal
      });
      let complete = false;
      let aborted = false;
  
      const cacheIgnoringSearch = hasCacheDefeatingSku(request.url);
  
      if (requestParameters.type === 'json') {
          request.headers.set('Accept', 'application/json');
      }
  
      const validateOrFetch = (err, cachedResponse, responseIsFresh) => {
          if (aborted) return;
  
          if (err) {
              // Do fetch in case of cache error.
              // HTTP pages in Edge trigger a security error that can be ignored.
              if (err.message !== 'SecurityError') {
                  warnOnce(err);
              }
          }
  
          if (cachedResponse && responseIsFresh) {
              return finishRequest(cachedResponse);
          }
  
          if (cachedResponse) {
              // We can't do revalidation with 'If-None-Match' because then the
              // request doesn't have simple cors headers.
          }
  
          const requestTime = Date.now();
  
          window.fetch(request).then(response => {
              if (response.ok) {
                  const cacheableResponse = cacheIgnoringSearch ? response.clone() : null;
                  return finishRequest(response, cacheableResponse, requestTime);
  
              } else {
                  return callback(new AJAXError(response.statusText, response.status, requestParameters.url));
              }
          }).catch(error => {
              if (error.code === 20) {
                  // silence expected AbortError
                  return;
              }
              callback(new Error(error.message));
          });
      };
  
      const finishRequest = (response, cacheableResponse, requestTime) => {
          (
              requestParameters.type === 'arrayBuffer' ? response.arrayBuffer() :
              requestParameters.type === 'json' ? response.json() :
              response.text()
          ).then(result => {
              if (aborted) return;
              if (cacheableResponse && requestTime) {
                  // The response needs to be inserted into the cache after it has completely loaded.
                  // Until it is fully loaded there is a chance it will be aborted. Aborting while
                  // reading the body can cause the cache insertion to error. We could catch this error
                  // in most browsers but in Firefox it seems to sometimes crash the tab. Adding
                  // it to the cache here avoids that error.
                  cachePut(request, cacheableResponse, requestTime);
              }
              complete = true;
              callback(null, result, response.headers.get('Cache-Control'), response.headers.get('Expires'));
          }).catch(err => {
              if (!aborted) callback(new Error(err.message));
          });
      };
  
      if (cacheIgnoringSearch) {
          cacheGet(request, validateOrFetch);
      } else {
          validateOrFetch(null, null);
      }
  
      return {cancel: () => {
          aborted = true;
          if (!complete) controller.abort();
      }};
  }
  
  function makeXMLHttpRequest(requestParameters: RequestParameters, callback: ResponseCallback<any>): Cancelable {
      const xhr: XMLHttpRequest = new window.XMLHttpRequest();
  
      xhr.open(requestParameters.method || 'GET', requestParameters.url, true);
      if (requestParameters.type === 'arrayBuffer') {
          xhr.responseType = 'arraybuffer';
      }
      for (const k in requestParameters.headers) {
          xhr.setRequestHeader(k, requestParameters.headers[k]);
      }
      if (requestParameters.type === 'json') {
          xhr.responseType = 'text';
          xhr.setRequestHeader('Accept', 'application/json');
      }
      xhr.withCredentials = requestParameters.credentials === 'include';
      xhr.onerror = () => {
          callback(new Error(xhr.statusText));
      };
      xhr.onload = () => {
          if (((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) && xhr.response !== null) {
              let data: mixed = xhr.response;
              if (requestParameters.type === 'json') {
                  // We're manually parsing JSON here to get better error messages.
                  try {
                      data = JSON.parse(xhr.response);
                  } catch (err) {
                      return callback(err);
                  }
              }
              callback(null, data, xhr.getResponseHeader('Cache-Control'), xhr.getResponseHeader('Expires'));
          } else {
              callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url));
          }
      };
      xhr.send(requestParameters.body);
      return {cancel: () => xhr.abort()};
  }
  
  export const makeRequest = function(requestParameters: RequestParameters, callback: ResponseCallback<any>): Cancelable {
      // We're trying to use the Fetch API if possible. However, in some situations we can't use it:
      // - IE11 doesn't support it at all. In this case, we dispatch the request to the main thread so
      //   that we can get an accruate referrer header.
      // - Safari exposes window.AbortController, but it doesn't work actually abort any requests in
      //   some versions (see https://bugs.webkit.org/show_bug.cgi?id=174980#c2)
      // - Requests for resources with the file:// URI scheme don't work with the Fetch API either. In
      //   this case we unconditionally use XHR on the current thread since referrers don't matter.
      if (!isFileURL(requestParameters.url)) {
          if (window.fetch && window.Request && window.AbortController && window.Request.prototype.hasOwnProperty('signal')) {
              return makeFetchRequest(requestParameters, callback);
          }
          if (isWorker() && self.worker && self.worker.actor) {
              const queueOnMainThread = true;
              return self.worker.actor.send('getResource', requestParameters, callback, undefined, queueOnMainThread);
          }
      }
      return makeXMLHttpRequest(requestParameters, callback);
  };
  
  export const getJSON = function(requestParameters: RequestParameters, callback: ResponseCallback<Object>): Cancelable {
      return makeRequest(extend(requestParameters, {type: 'json'}), callback);
  };
  
  export const getArrayBuffer = function(requestParameters: RequestParameters, callback: ResponseCallback<ArrayBuffer>): Cancelable {
      return makeRequest(extend(requestParameters, {type: 'arrayBuffer'}), callback);
  };
  
  export const postData = function(requestParameters: RequestParameters, callback: ResponseCallback<string>): Cancelable {
      return makeRequest(extend(requestParameters, {method: 'POST'}), callback);
  };
  
  function sameOrigin(url) {
      const a: HTMLAnchorElement = window.document.createElement('a');
      a.href = url;
      return a.protocol === window.document.location.protocol && a.host === window.document.location.host;
  }
  
  const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=';
  
  function arrayBufferToImage(data: ArrayBuffer, callback: (err: ?Error, image: ?HTMLImageElement) => void, cacheControl: ?string, expires: ?string) {
      const img: HTMLImageElement = new window.Image();
      const URL = window.URL;
      img.onload = () => {
          callback(null, img);
          URL.revokeObjectURL(img.src);
      };
      img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.'));
      const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'});
      (img: any).cacheControl = cacheControl;
      (img: any).expires = expires;
      img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl;
  }
  
  function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, image: ?ImageBitmap) => void) {
      const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'});
      window.createImageBitmap(blob).then((imgBitmap) => {
          callback(null, imgBitmap);
      }).catch((e) => {
          callback(new Error(`Could not load image because of ${e.message}. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`));
      });
  }
  
  let imageQueue, numImageRequests;
  export const resetImageRequestQueue = () => {
      imageQueue = [];
      numImageRequests = 0;
  };
  resetImageRequestQueue();
  
  export const getImage = function(requestParameters: RequestParameters, callback: Callback<HTMLImageElement | ImageBitmap>): Cancelable {
      if (webpSupported.supported) {
          if (!requestParameters.headers) {
              requestParameters.headers = {};
          }
          requestParameters.headers.accept = 'image/webp,*/*';
      }
  
      // limit concurrent image loads to help with raster sources performance on big screens
      if (numImageRequests >= config.MAX_PARALLEL_IMAGE_REQUESTS) {
          const queued = {
              requestParameters,
              callback,
              cancelled: false,
              cancel() { this.cancelled = true; }
          };
          imageQueue.push(queued);
          return queued;
      }
      numImageRequests++;
  
      let advanced = false;
      const advanceImageRequestQueue = () => {
          if (advanced) return;
          advanced = true;
          numImageRequests--;
          assert(numImageRequests >= 0);
          while (imageQueue.length && numImageRequests < config.MAX_PARALLEL_IMAGE_REQUESTS) { // eslint-disable-line
              const request = imageQueue.shift();
              const {requestParameters, callback, cancelled} = request;
              if (!cancelled) {
                  request.cancel = getImage(requestParameters, callback).cancel;
              }
          }
      };
  
      // request the image with XHR to work around caching issues
      // see https://github.com/mapbox/mapbox-gl-js/issues/1470
      const request = getArrayBuffer(requestParameters, (err: ?Error, data: ?ArrayBuffer, cacheControl: ?string, expires: ?string) => {
  
          advanceImageRequestQueue();
  
          if (err) {
              callback(err);
          } else if (data) {
              if (offscreenCanvasSupported()) {
                  arrayBufferToImageBitmap(data, callback);
              } else {
                  arrayBufferToImage(data, callback, cacheControl, expires);
              }
          }
      });
  
      return {
          cancel: () => {
              request.cancel();
              advanceImageRequestQueue();
          }
      };
  };
  
  export const getVideo = function(urls: Array<string>, callback: Callback<HTMLVideoElement>): Cancelable {
      const video: HTMLVideoElement = window.document.createElement('video');
      video.muted = true;
      video.onloadstart = function() {
          callback(null, video);
      };
      for (let i = 0; i < urls.length; i++) {
          const s: HTMLSourceElement = window.document.createElement('source');
          if (!sameOrigin(urls[i])) {
              video.crossOrigin = 'Anonymous';
          }
          s.src = urls[i];
          video.appendChild(s);
      }
      return {cancel: () => {}};
  };
  