text: "use strict";
  
  const parseContentType = require("content-type-parser");
  const sniffHTMLEncoding = require("html-encoding-sniffer");
  const whatwgEncoding = require("whatwg-encoding");
  const parseDataUrl = require("../utils").parseDataUrl;
  const fs = require("fs");
  const request = require("request");
  const documentBaseURLSerialized = require("../living/helpers/document-base-url").documentBaseURLSerialized;
  const NODE_TYPE = require("../living/node-type");
  
  /* eslint-disable no-restricted-modules */
  // TODO: stop using the built-in URL in favor of the spec-compliant whatwg-url package
  // This legacy usage is in the process of being purged.
  const URL = require("url");
  /* eslint-enable no-restricted-modules */
  
  const IS_BROWSER = Object.prototype.toString.call(process) !== "[object process]";
  
  function createResourceLoadHandler(element, resourceUrl, document, loadCallback) {
    if (loadCallback === undefined) {
      loadCallback = () => {
        // do nothing
      };
    }
    return (err, data, response) => {
      const ev = document.createEvent("HTMLEvents");
  
      if (!err) {
        try {
          loadCallback.call(element, data, resourceUrl, response);
          ev.initEvent("load", false, false);
        } catch (e) {
          err = e;
        }
      }
  
      if (err) {
        if (!err.isAbortError) {
          ev.initEvent("error", false, false);
          ev.error = err;
          element.dispatchEvent(ev);
  
          const error = new Error(`Could not load ${element.localName}: "${resourceUrl}"`);
          error.detail = err;
          error.type = "resource loading";
  
          document._defaultView._virtualConsole.emit("jsdomError", error);
        }
      } else {
        element.dispatchEvent(ev);
      }
    };
  }
  
  exports.readFile = function (filePath, options, callback) {
    const readableStream = fs.createReadStream(filePath);
  
    let data = new Buffer(0);
  
    readableStream.on("error", callback);
  
    readableStream.on("data", chunk => {
      data = Buffer.concat([data, chunk]);
    });
  
    const defaultEncoding = options.defaultEncoding;
    const detectMetaCharset = options.detectMetaCharset;
  
    readableStream.on("end", () => {
      // Not passing default encoding means binary
      if (defaultEncoding) {
        const encoding = detectMetaCharset ? sniffHTMLEncoding(data, { defaultEncoding }) :
                         whatwgEncoding.getBOMEncoding(data) || defaultEncoding;
        const decoded = whatwgEncoding.decode(data, encoding);
        callback(null, decoded, { headers: { "content-type": "text/plain;charset=" + encoding } });
      } else {
        callback(null, data);
      }
    });
  
    return {
      abort() {
        readableStream.destroy();
        const error = new Error("request canceled by user");
        error.isAbortError = true;
        callback(error);
      }
    };
  };
  
  function readDataUrl(dataUrl, options, callback) {
    const defaultEncoding = options.defaultEncoding;
    try {
      const data = parseDataUrl(dataUrl);
      // If default encoding does not exist, pass on binary data.
      if (defaultEncoding) {
        const contentType = parseContentType(data.type) || parseContentType("text/plain");
        const sniffOptions = {
          transportLayerEncodingLabel: contentType.get("charset"),
          defaultEncoding
        };
  
        const encoding = options.detectMetaCharset ? sniffHTMLEncoding(data.buffer, sniffOptions) :
                         whatwgEncoding.getBOMEncoding(data.buffer) ||
                         whatwgEncoding.labelToName(contentType.get("charset")) || defaultEncoding;
        const decoded = whatwgEncoding.decode(data.buffer, encoding);
  
        contentType.set("charset", encoding);
        data.type = contentType.toString();
  
        callback(null, decoded, { headers: { "content-type": data.type } });
      } else {
        callback(null, data.buffer, { headers: { "content-type": data.type } });
      }
    } catch (err) {
      callback(err, null);
    }
    return null;
  }
  
  // NOTE: request wraps tough-cookie cookie jar
  // (see: https://github.com/request/request/blob/master/lib/cookies.js).
  // Therefore, to pass our cookie jar to the request, we need to create
  // request's wrapper and monkey patch it with our jar.
  function wrapCookieJarForRequest(cookieJar) {
    const jarWrapper = request.jar();
    jarWrapper._jar = cookieJar;
    return jarWrapper;
  }
  
  function fetch(urlObj, options, callback) {
    if (urlObj.protocol === "data:") {
      return readDataUrl(urlObj.href, options, callback);
    } else if (urlObj.hostname) {
      return exports.download(urlObj, options, callback);
    }
    const filePath = urlObj.pathname
      .replace(/^file:\/\//, "")
      .replace(/^\/([a-z]):\//i, "$1:/")
      .replace(/%20/g, " ");
    return exports.readFile(filePath, options, callback);
  }
  
  exports.enqueue = function (element, resourceUrl, callback) {
    const document = element.nodeType === NODE_TYPE.DOCUMENT_NODE ? element : element._ownerDocument;
  
    if (document._queue) {
      const loadHandler = createResourceLoadHandler(element, resourceUrl || document.URL, document, callback);
      return document._queue.push(loadHandler);
    }
  
    return () => {
      // do nothing in queue-less documents
    };
  };
  
  exports.download = function (url, options, callback) {
    const requestOptions = {
      pool: options.pool,
      agent: options.agent,
      agentOptions: options.agentOptions,
      agentClass: options.agentClass,
      strictSSL: options.strictSSL,
      gzip: true,
      jar: wrapCookieJarForRequest(options.cookieJar),
      encoding: null,
      headers: {
        "User-Agent": options.userAgent,
        "Accept-Language": "en",
        Accept: options.accept || "*/*"
      }
    };
    if (options.referrer && !IS_BROWSER) {
      requestOptions.headers.referer = options.referrer;
    }
    if (options.proxy) {
      requestOptions.proxy = options.proxy;
    }
    Object.assign(requestOptions.headers, options.headers);
  
    const defaultEncoding = options.defaultEncoding;
    const detectMetaCharset = options.detectMetaCharset;
  
    const req = request(url, requestOptions, (error, response, bufferData) => {
      if (!error) {
        // If default encoding does not exist, pass on binary data.
        if (defaultEncoding) {
          const contentType = parseContentType(response.headers["content-type"]) || parseContentType("text/plain");
          const sniffOptions = {
            transportLayerEncodingLabel: contentType.get("charset"),
            defaultEncoding
          };
  
          const encoding = detectMetaCharset ? sniffHTMLEncoding(bufferData, sniffOptions) :
                          whatwgEncoding.getBOMEncoding(bufferData) ||
                          whatwgEncoding.labelToName(contentType.get("charset")) || defaultEncoding;
          const decoded = whatwgEncoding.decode(bufferData, encoding);
  
          contentType.set("charset", encoding);
          response.headers["content-type"] = contentType.toString();
  
          callback(null, decoded, response);
        } else {
          callback(null, bufferData, response);
        }
      } else {
        callback(error, null, response);
      }
    });
    return {
      abort() {
        req.abort();
        const error = new Error("request canceled by user");
        error.isAbortError = true;
        callback(error);
      }
    };
  };
  
  exports.load = function (element, urlString, options, callback) {
    const document = element._ownerDocument;
    const documentImpl = document.implementation;
  
    if (!documentImpl._hasFeature("FetchExternalResources", element.tagName.toLowerCase())) {
      return;
    }
  
    if (documentImpl._hasFeature("SkipExternalResources", urlString)) {
      return;
    }
  
    const urlObj = URL.parse(urlString);
    const enqueued = exports.enqueue(element, urlString, callback);
    const customLoader = document._customResourceLoader;
    const requestManager = document._requestManager;
    const cookieJar = document._cookieJar;
  
    options.accept = element._accept;
    options.cookieJar = cookieJar;
    options.referrer = document.URL;
    options.pool = document._pool;
    options.agentOptions = document._agentOptions;
    options.strictSSL = document._strictSSL;
    options.proxy = document._proxy;
    options.userAgent = document._defaultView.navigator.userAgent;
  
    let req = null;
    function wrappedEnqueued() {
      if (req && requestManager) {
        requestManager.remove(req);
      }
      // do not trigger if the window is closed
      if (element._ownerDocument && element._ownerDocument.defaultView.document) {
        enqueued.apply(this, arguments);
      }
    }
    if (typeof customLoader === "function") {
      req = customLoader({
        element,
        url: urlObj,
        cookie: cookieJar.getCookieStringSync(urlObj, { http: true }),
        baseUrl: documentBaseURLSerialized(document),
        defaultFetch(fetchCallback) {
          return fetch(urlObj, options, fetchCallback);
        }
      },
      wrappedEnqueued);
    } else {
      req = fetch(urlObj, options, wrappedEnqueued);
    }
    if (req && requestManager) {
      requestManager.add(req);
    }
  };
  