"use strict";
/* eslint-disable no-unused-expressions */
() => `jsdom 7.x onward only works on Node.js 4 or newer: https://github.com/tmpvar/jsdom#install`;
/* eslint-enable no-unused-expressions */

const fs = require("fs");
const path = require("path");
const { CookieJar } = require("tough-cookie");
const MIMEType = require("whatwg-mimetype");

const { toFileUrl } = require("./jsdom/utils");
const documentFeatures = require("./jsdom/browser/documentfeatures");
const { domToHtml } = require("./jsdom/browser/domtohtml");
const Window = require("./jsdom/browser/Window");
const resourceLoader = require("./jsdom/browser/resource-loader");
const VirtualConsole = require("./jsdom/virtual-console");
const idlUtils = require("./jsdom/living/generated/utils");
const Blob = require("./jsdom/living/generated/Blob");

const whatwgURL = require("whatwg-url");

require("./jsdom/living"); // Enable living standard features

/* 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 canReadFilesFromFS = Boolean(fs.readFile); // in a browserify environment, this isn't present

exports.createVirtualConsole = function (options) {
  return new VirtualConsole(options);
};

exports.getVirtualConsole = function (window) {
  return window._virtualConsole;
};

exports.createCookieJar = function () {
  return new CookieJar(null, { looseMode: true });
};

exports.nodeLocation = function (node) {
  return idlUtils.implForWrapper(node).__location;
};

exports.reconfigureWindow = function (window, newProps) {
  if ("top" in newProps) {
    window._top = newProps.top;
  }
};

exports.changeURL = function (window, urlString) {
  const doc = idlUtils.implForWrapper(window._document);

  const url = whatwgURL.parseURL(urlString);

  if (url === null) {
    throw new TypeError(`Could not parse "${urlString}" as a URL`);
  }

  doc._URL = url;
  doc.origin = whatwgURL.serializeURLOrigin(doc._URL);
};

// Proxy to features module
Object.defineProperty(exports, "defaultDocumentFeatures", {
  enumerable: true,
  configurable: true,
  get() {
    return documentFeatures.defaultDocumentFeatures;
  },
  set(v) {
    documentFeatures.defaultDocumentFeatures = v;
  }
});

exports.jsdom = function (html, options) {
  if (options === undefined) {
    options = {};
  }
  if (options.parsingMode === undefined || options.parsingMode === "auto") {
    options.parsingMode = "html";
  }

  if (options.parsingMode !== "html" && options.parsingMode !== "xml") {
    throw new RangeError(`Invalid parsingMode option ${JSON.stringify(options.parsingMode)}; must be either "html", ` +
      `"xml", "auto", or undefined`);
  }

  options.encoding = options.encoding || "UTF-8";

  setGlobalDefaultConfig(options);

  // Back-compat hack: we have previously suggested nesting these under document, for jsdom.env at least.
  // So we need to support that.
  if (options.document) {
    if (options.document.cookie !== undefined) {
      options.cookie = options.document.cookie;
    }
    if (options.document.referrer !== undefined) {
      options.referrer = options.document.referrer;
    }
  }

  // Adapt old API `features: { ProcessExternalResources: ["script"] }` to the runScripts option.
  // This is part of a larger effort to eventually remove the document features infrastructure entirely. It's unclear
  // whether we'll kill the old API or document features first, but as long as old API survives, attempts to kill
  // document features will need this kind of adapter.
  if (!options.features) {
    options.features = exports.defaultDocumentFeatures;
  }
  if (options.features.ProcessExternalResources === undefined) {
    options.features.ProcessExternalResources = ["script"];
  }
  const ProcessExternalResources = options.features.ProcessExternalResources || [];
  if (ProcessExternalResources === "script" ||
      (ProcessExternalResources.includes && ProcessExternalResources.includes("script"))) {
    options.runScripts = "dangerously";
  }

  if (options.pretendToBeVisual !== undefined) {
    options.pretendToBeVisual = Boolean(options.pretendToBeVisual);
  } else {
    options.pretendToBeVisual = false;
  }

  options.storageQuota = options.storageQuota || 5000000;

  // List options explicitly to be clear which are passed through
  const window = new Window({
    parsingMode: options.parsingMode,
    parseOptions: options.parseOptions,
    contentType: options.contentType,
    encoding: options.encoding,
    url: options.url,
    lastModified: options.lastModified,
    referrer: options.referrer,
    cookieJar: options.cookieJar,
    cookie: options.cookie,
    resourceLoader: options.resourceLoader,
    deferClose: options.deferClose,
    concurrentNodeIterators: options.concurrentNodeIterators,
    virtualConsole: options.virtualConsole,
    pool: options.pool,
    agent: options.agent,
    agentClass: options.agentClass,
    agentOptions: options.agentOptions,
    strictSSL: options.strictSSL,
    proxy: options.proxy,
    userAgent: options.userAgent,
    runScripts: options.runScripts,
    pretendToBeVisual: options.pretendToBeVisual,
    storageQuota: options.storageQuota
  });

  const documentImpl = idlUtils.implForWrapper(window.document);
  documentFeatures.applyDocumentFeatures(documentImpl, options.features);

  if (options.created) {
    options.created(null, window.document.defaultView);
  }

  if (options.parsingMode === "html") {
    if (html === undefined || html === "") {
      html = "<html><head></head><body></body></html>";
    }

    window.document.write(html);
  } else if (options.parsingMode === "xml") {
    if (html !== undefined) {
      documentImpl._htmlToDom.appendToDocument(html, documentImpl);
    }
  }

  if (window.document.close && !options.deferClose) {
    window.document.close();
  }

  return window.document;
};

exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) {
  if (!window || !window.document) {
    return;
  }

  const implImpl = idlUtils.implForWrapper(window.document.implementation);
  const oldFeatures = implImpl._features;
  const oldRunScripts = window._runScripts;

  implImpl._addFeature("FetchExternalResources", ["script"]);
  documentFeatures.contextifyWindow(idlUtils.implForWrapper(window.document)._global);
  window._runScripts = "dangerously";

  const scriptEl = window.document.createElement("script");
  scriptEl.className = "jsdom";
  scriptEl.src = jqueryUrl;
  scriptEl.onload = scriptEl.onerror = () => {
    implImpl._features = oldFeatures;
    window._runScripts = oldRunScripts;
    // Can't un-contextify the window. Oh well. That's what we get for such magic behavior in old API.

    if (callback) {
      callback(window, window.jQuery);
    }
  };

  window.document.body.appendChild(scriptEl);
};

exports.env = exports.jsdom.env = function () {
  const config = getConfigFromEnvArguments(arguments);
  let req = null;

  if (config.file && canReadFilesFromFS) {
    req = resourceLoader.readFile(
      config.file,
      { defaultEncoding: config.defaultEncoding, detectMetaCharset: true },
      (err, text, res) => {
        if (err) {
          reportInitError(err, config);
          return;
        }

        const contentType = new MIMEType(res.headers["content-type"]);
        config.encoding = contentType.parameters.get("charset");
        setParsingModeFromExtension(config, config.file);

        config.html = text;
        processHTML(config);
      }
    );
  } else if (config.html !== undefined) {
    processHTML(config);
  } else if (config.url) {
    req = handleUrl(config);
  } else if (config.somethingToAutodetect !== undefined) {
    const url = URL.parse(config.somethingToAutodetect);
    if (url.protocol && url.hostname) {
      config.url = config.somethingToAutodetect;
      req = handleUrl(config.somethingToAutodetect);
    } else if (canReadFilesFromFS) {
      try {
        req = resourceLoader.readFile(
          config.somethingToAutodetect,
          { defaultEncoding: config.defaultEncoding, detectMetaCharset: true },
          (err, text, res) => {
            if (err) {
              if (err.code === "ENOENT" || err.code === "ENAMETOOLONG" || err.code === "ERR_INVALID_ARG_TYPE") {
                config.html = config.somethingToAutodetect;
                processHTML(config);
              } else {
                reportInitError(err, config);
              }
            } else {
              const contentType = new MIMEType(res.headers["content-type"]);
              config.encoding = contentType.parameters.get("charset");
              setParsingModeFromExtension(config, config.somethingToAutodetect);

              config.html = text;
              config.url = toFileUrl(config.somethingToAutodetect);
              processHTML(config);
            }
          }
        );
      } catch (err) {
        config.html = config.somethingToAutodetect;
        processHTML(config);
      }
    } else {
      config.html = config.somethingToAutodetect;
      processHTML(config);
    }
  }

  function handleUrl() {
    config.cookieJar = config.cookieJar || exports.createCookieJar();

    const options = {
      defaultEncoding: config.defaultEncoding,
      detectMetaCharset: true,
      headers: config.headers,
      pool: config.pool,
      strictSSL: config.strictSSL,
      proxy: config.proxy,
      cookieJar: config.cookieJar,
      userAgent: config.userAgent,
      agent: config.agent,
      agentClass: config.agentClass,
      agentOptions: config.agentOptions
    };

    const { fragment } = whatwgURL.parseURL(config.url);

    return resourceLoader.download(config.url, options, (err, responseText, res) => {
      if (err) {
        reportInitError(err, config);
        return;
      }

      // The use of `res.request.uri.href` ensures that `window.location.href`
      // is updated when `request` follows redirects.
      config.html = responseText;
      config.url = res.request.uri.href;
      if (fragment) {
        config.url += `#${fragment}`;
      }

      if (res.headers["last-modified"]) {
        config.lastModified = new Date(res.headers["last-modified"]);
      }

      const contentType = new MIMEType(res.headers["content-type"]);
      if (config.parsingMode === "auto") {
        if (contentType.isXML()) {
          config.parsingMode = "xml";
        }
      }
      config.contentType = contentType.essence;
      config.encoding = contentType.parameters.get("charset");

      processHTML(config);
    });
  }
  return req;
};

exports.serializeDocument = function (doc) {
  return domToHtml([idlUtils.implForWrapper(doc)]);
};

exports.blobToBuffer = function (blob) {
  return (Blob.is(blob) && idlUtils.implForWrapper(blob)._buffer) || undefined;
};

exports.evalVMScript = (window, script) => {
  return script.runInContext(idlUtils.implForWrapper(window._document)._global);
};

function processHTML(config) {
  const window = exports.jsdom(config.html, config).defaultView;
  const implImpl = idlUtils.implForWrapper(window.document.implementation);
  const features = JSON.parse(JSON.stringify(implImpl._features));

  let docsLoaded = 0;
  const totalDocs = config.scripts.length + config.src.length;

  if (!window || !window.document) {
    reportInitError(new Error("JSDOM: a window object could not be created."), config);
    return;
  }

  function scriptComplete() {
    docsLoaded++;

    if (docsLoaded >= totalDocs) {
      implImpl._features = features;

      process.nextTick(() => {
        if (config.onload) {
          config.onload(window);
        }
        if (config.done) {
          config.done(null, window);
        }
      });
    }
  }

  function handleScriptError() {
    // nextTick so that an exception within scriptComplete won't cause
    // another script onerror (which would be an infinite loop)
    process.nextTick(scriptComplete);
  }

  if (config.scripts.length > 0 || config.src.length > 0) {
    implImpl._addFeature("FetchExternalResources", ["script"]);

    for (const scriptSrc of config.scripts) {
      const script = window.document.createElement("script");
      script.className = "jsdom";
      script.onload = scriptComplete;
      script.onerror = handleScriptError;
      script.src = scriptSrc;

      window.document.body.appendChild(script);
    }

    for (const scriptText of config.src) {
      const script = window.document.createElement("script");
      script.onload = scriptComplete;
      script.onerror = handleScriptError;
      script.text = scriptText;

      window.document.documentElement.appendChild(script);
      window.document.documentElement.removeChild(script);
    }
  } else if (window.document.readyState === "complete") {
    scriptComplete();
  } else {
    window.addEventListener("load", scriptComplete);
  }
}

function setGlobalDefaultConfig(config) {
  config.parseOptions = { locationInfo: true };

  config.pool = config.pool !== undefined ? config.pool : { maxSockets: 6 };

  config.agentOptions = config.agentOptions !== undefined ?
                        config.agentOptions :
                        { keepAlive: true, keepAliveMsecs: 115 * 1000 };

  config.strictSSL = config.strictSSL !== undefined ? config.strictSSL : true;

  config.userAgent = config.userAgent ||
    `Node.js (${process.platform}; U; rv:${process.version}) AppleWebKit/537.36 (KHTML, like Gecko)`;
}

function getConfigFromEnvArguments(args) {
  const config = {};
  if (typeof args[0] === "object") {
    Object.assign(config, args[0]);
  } else {
    for (const arg of args) {
      switch (typeof arg) {
        case "string":
          config.somethingToAutodetect = arg;
          break;
        case "function":
          config.done = arg;
          break;
        case "object":
          if (Array.isArray(arg)) {
            config.scripts = arg;
          } else {
            Object.assign(config, arg);
          }
          break;
      }
    }
  }

  if (!config.done && !config.created && !config.onload) {
    throw new Error("Must pass a \"created\", \"onload\", or \"done\" option, or a callback, to jsdom.env");
  }

  if (config.somethingToAutodetect === undefined &&
      config.html === undefined && !config.file && !config.url) {
    throw new Error("Must pass a \"html\", \"file\", or \"url\" option, or a string, to jsdom.env");
  }

  config.scripts = ensureArray(config.scripts);
  config.src = ensureArray(config.src);
  config.parsingMode = config.parsingMode || "auto";

  config.features = config.features || {
    FetchExternalResources: false,
    SkipExternalResources: false,
    ProcessExternalResources: false // needed since we'll process it inside jsdom.jsdom()
  };

  if (!config.url && config.file) {
    config.url = toFileUrl(config.file);
  }

  config.defaultEncoding = config.defaultEncoding || "windows-1252";

  setGlobalDefaultConfig(config);

  if (config.scripts.length > 0 || config.src.length > 0) {
    config.features.ProcessExternalResources = ["script"];
  }
  return config;
}

function reportInitError(err, config) {
  if (config.created) {
    config.created(err);
  }
  if (config.done) {
    config.done(err);
  }
}

function ensureArray(value) {
  let array = value || [];
  if (typeof array === "string") {
    array = [array];
  }
  return array;
}

function setParsingModeFromExtension(config, filename) {
  if (config.parsingMode === "auto") {
    const ext = path.extname(filename);
    if (ext === ".xhtml" || ext === ".xml") {
      config.parsingMode = "xml";
    }
  }
}