“use strict”;

const CookieJar = require(“tough-cookie”).CookieJar;

const NodeImpl = require(“./Node-impl”).implementation; const isNodeImpl = require(“../generated/Node”).isImpl;

const NODE_TYPE = require(“../node-type”); const memoizeQuery = require(“../../utils”).memoizeQuery; const firstChildWithHTMLLocalName = require(“../helpers/traversal”).firstChildWithHTMLLocalName; const firstChildWithHTMLLocalNames = require(“../helpers/traversal”).firstChildWithHTMLLocalNames; const firstDescendantWithHTMLLocalName = require(“../helpers/traversal”).firstDescendantWithHTMLLocalName; const whatwgURL = require(“whatwg-url”); const domSymbolTree = require(“../helpers/internal-constants”).domSymbolTree; const stripAndCollapseASCIIWhitespace = require(“../helpers/strings”).stripAndCollapseASCIIWhitespace; const DOMException = require(“../../web-idl/DOMException”); const HtmlToDom = require(“../../browser/htmltodom”).HtmlToDom; const History = require(“../generated/History”); const Location = require(“../generated/Location”); const createHTMLCollection = require(“../html-collection”).create; const idlUtils = require(“../generated/utils”); const validateName = require(“../helpers/validate-names”).name; const validateAndExtract = require(“../helpers/validate-names”).validateAndExtract; const resourceLoader = require(“../../browser/resource-loader”);

const clone = require(“../node”).clone; const generatedAttr = require(“../generated/Attr”); const listOfElementsWithQualifiedName = require(“../node”).listOfElementsWithQualifiedName; const listOfElementsWithNamespaceAndLocalName = require(“../node”).listOfElementsWithNamespaceAndLocalName; const listOfElementsWithClassNames = require(“../node”).listOfElementsWithClassNames; const Comment = require(“../generated/Comment”); const ProcessingInstruction = require(“../generated/ProcessingInstruction”); const CDATASection = require(“../generated/CDATASection”); const Text = require(“../generated/Text”); const DocumentFragment = require(“../generated/DocumentFragment”); const DOMImplementation = require(“../generated/DOMImplementation”); const ParentNodeImpl = require(“./ParentNode-impl”).implementation; const HTMLElement = require(“../generated/HTMLElement”); const HTMLUnknownElement = require(“../generated/HTMLUnknownElement”); const TreeWalker = require(“../generated/TreeWalker”);

const CustomEvent = require(“../generated/CustomEvent”); const ErrorEvent = require(“../generated/ErrorEvent”); const Event = require(“../generated/Event”); const FocusEvent = require(“../generated/FocusEvent”); const HashChangeEvent = require(“../generated/HashChangeEvent”); const KeyboardEvent = require(“../generated/KeyboardEvent”); const MessageEvent = require(“../generated/MessageEvent”); const MouseEvent = require(“../generated/MouseEvent”); const PopStateEvent = require(“../generated/PopStateEvent”); const ProgressEvent = require(“../generated/ProgressEvent”); const TouchEvent = require(“../generated/TouchEvent”); const UIEvent = require(“../generated/UIEvent”);

function clearChildNodes(node) {

for (let child = domSymbolTree.firstChild(node); child; child = domSymbolTree.firstChild(node)) {
  node.removeChild(child);
}

}

function setInnerHTML(document, node, html) {

// Clear the children first:
if (node._templateContents) {
  clearChildNodes(node._templateContents);
} else {
  clearChildNodes(node);
}

if (html !== "") {
  if (node.nodeName === "#document") {
    document._htmlToDom.appendHtmlToDocument(html, node);
  } else {
    document._htmlToDom.appendHtmlToElement(html, node);
  }
}

}

class ResourceQueue {

constructor(paused) {
  this.paused = Boolean(paused);
}

push(callback) {
  const q = this;
  const item = {
    prev: q.tail,
    check() {
      if (!q.paused && !this.prev && this.fired) {
        callback(this.err, this.data, this.response);
        if (this.next) {
          this.next.prev = null;
          this.next.check();
        } else { // q.tail===this
          q.tail = null;
        }
      }
    }
  };
  if (q.tail) {
    q.tail.next = item;
  }
  q.tail = item;
  return (err, data, response) => {
    item.fired = 1;
    item.err = err;
    item.data = data;
    item.response = response;
    item.check();
  };
}

resume() {
  if (!this.paused) {
    return;
  }
  this.paused = false;

  let head = this.tail;
  while (head && head.prev) {
    head = head.prev;
  }
  if (head) {
    head.check();
  }
}

}

class RequestManager {

constructor() {
  this.openedRequests = [];
}

add(req) {
  this.openedRequests.push(req);
}

remove(req) {
  const idx = this.openedRequests.indexOf(req);
  if (idx !== -1) {
    this.openedRequests.splice(idx, 1);
  }
}

close() {
  for (const openedRequest of this.openedRequests) {
    openedRequest.abort();
  }
  this.openedRequests = [];
}

size() {
  return this.openedRequests.length;
}

}

function pad(number) {

if (number < 10) {
  return "0" + number;
}
return number;

}

function toLastModifiedString(date) {

return pad(date.getMonth() + 1) +
  "/" + pad(date.getDate()) +
  "/" + date.getFullYear() +
  " " + pad(date.getHours()) +
  ":" + pad(date.getMinutes()) +
  ":" + pad(date.getSeconds());

}

const nonInheritedTags = new Set([

"article", "section", "nav", "aside", "hgroup", "header", "footer", "address", "dt",
"dd", "figure", "figcaption", "main", "em", "strong", "small", "s", "cite", "dfn", "abbr",
"ruby", "rt", "rp", "code", "var", "samp", "kbd", "i", "b", "u", "mark", "bdi", "bdo", "wbr"

]);

const eventInterfaceTable = {

customevent: CustomEvent,
errorevent: ErrorEvent,
event: Event,
events: Event,
focusevent: FocusEvent,
hashchangeevent: HashChangeEvent,
htmlevents: Event,
keyboardevent: KeyboardEvent,
messageevent: MessageEvent,
mouseevent: MouseEvent,
mouseevents: MouseEvent,
popstateevent: PopStateEvent,
progressevent: ProgressEvent,
svgevents: Event,
touchevent: TouchEvent,
uievent: UIEvent,
uievents: UIEvent

};

class DocumentImpl extends NodeImpl {

constructor(args, privateData) {
  super(args, privateData);

  this._ownerDocument = this;
  this.nodeType = NODE_TYPE.DOCUMENT_NODE;
  if (!privateData.options) {
    privateData.options = {};
  }
  if (!privateData.options.parsingMode) {
    privateData.options.parsingMode = "xml";
  }

  this._parsingMode = privateData.options.parsingMode;
  this._htmlToDom = new HtmlToDom(privateData.core, privateData.options.parser, privateData.options.parsingMode);

  this._implementation = DOMImplementation.createImpl([], {
    core: this._core,
    ownerDocument: this
  });

  this._defaultView = privateData.options.defaultView || null;
  this._global = privateData.options.global;
  this._documentElement = null;
  this._ids = Object.create(null);
  this._attached = true;
  this._currentScript = null;
  this._cookieJar = privateData.options.cookieJar;
  if (this._cookieJar === undefined) {
    this._cookieJar = new CookieJar(null, { looseMode: true });
  }

  this._contentType = privateData.options.contentType;
  this._encoding = privateData.options.encoding;

  const urlOption = privateData.options.url === undefined ? "about:blank" : privateData.options.url;
  const parsed = whatwgURL.parseURL(urlOption);
  if (parsed === "failure") {
    throw new TypeError(`Could not parse "${urlOption}" as a URL`);
  }

  this._URL = parsed;
  this._origin = whatwgURL.serializeURLToUnicodeOrigin(parsed);

  this._location = Location.createImpl([], { relevantDocument: this });
  this._history = History.createImpl([], {
    window: this._defaultView,
    document: this,
    actAsIfLocationReloadCalled: () => this._location.reload()
  });

  if (privateData.options.cookie) {
    const cookies = Array.isArray(privateData.options.cookie) ?
      privateData.options.cookie : [privateData.options.cookie];
    const document = this;

    cookies.forEach(cookieStr => {
      document._cookieJar.setCookieSync(cookieStr, document.URL, { ignoreError: true });
    });
  }

  this._activeNodeIterators = [];
  this._activeNodeIteratorsMax = privateData.options.concurrentNodeIterators === undefined ?
                                10 :
                                Number(privateData.options.concurrentNodeIterators);

  if (isNaN(this._activeNodeIteratorsMax)) {
    throw new TypeError("The 'concurrentNodeIterators' option must be a Number");
  }

  if (this._activeNodeIteratorsMax < 0) {
    throw new RangeError("The 'concurrentNodeIterators' option must be a non negative Number");
  }

  this._referrer = privateData.options.referrer || "";
  this._lastModified = toLastModifiedString(privateData.options.lastModified || new Date());
  this._queue = new ResourceQueue(privateData.options.deferClose);
  this._customResourceLoader = privateData.options.resourceLoader;
  this._pool = privateData.options.pool;
  this._agentOptions = privateData.options.agentOptions;
  this._strictSSL = privateData.options.strictSSL;
  this._proxy = privateData.options.proxy;
  this._requestManager = new RequestManager();
  this.readyState = "loading";

  this._lastFocusedElement = null;

  // Add level2 features
  this.implementation._addFeature("core", "2.0");
  this.implementation._addFeature("html", "2.0");
  this.implementation._addFeature("xhtml", "2.0");
  this.implementation._addFeature("xml", "2.0");
}

_defaultElementBuilder(document, tagName) {
  if (nonInheritedTags.has(tagName.toLowerCase())) {
    return HTMLElement.create([], {
      core: this._core,
      ownerDocument: this,
      localName: tagName
    });
  }
  return HTMLUnknownElement.create([], {
    core: this._core,
    ownerDocument: this,
    localName: tagName
  });
}

get contentType() {
  return this._contentType || (this._parsingMode === "xml" ? "application/xml" : "text/html");
}
get compatMode() {
  return this._parsingMode === "xml" || this.doctype ? "CSS1Compat" : "BackCompat";
}
get charset() {
  return this._encoding;
}
get characterSet() {
  return this._encoding;
}
get inputEncoding() {
  return this._encoding;
}
get doctype() {
  for (const childNode of domSymbolTree.childrenIterator(this)) {
    if (childNode.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) {
      return childNode;
    }
  }
  return null;
}
get URL() {
  return whatwgURL.serializeURL(this._URL);
}
get documentURI() {
  return whatwgURL.serializeURL(this._URL);
}
get origin() {
  return this._origin;
}
get location() {
  return this._defaultView ? this._location : null;
}
get documentElement() {
  if (this._documentElement) {
    return this._documentElement;
  }

  for (const childNode of domSymbolTree.childrenIterator(this)) {
    if (childNode.nodeType === NODE_TYPE.ELEMENT_NODE) {
      this._documentElement = childNode;
      return childNode;
    }
  }

  return null;
}

get implementation() {
  return this._implementation;
}
set implementation(implementation) {
  this._implementation = implementation;
}

get defaultView() {
  return this._defaultView;
}

get currentScript() {
  return this._currentScript;
}

get activeElement() {
  if (this._lastFocusedElement) {
    return this._lastFocusedElement;
  }

  return this.body;
}

hasFocus() {
  return Boolean(this._lastFocusedElement);
}

toString() {
  return "[object HTMLDocument]";
}

_createElementWithCorrectElementInterface(name, namespace) {
  // https://dom.spec.whatwg.org/#concept-element-interface
  // TODO: eventually we should re-write the element-builder system to be namespace aware, but for now it is not.
  const builder = this._elementBuilders[name.toLowerCase()] || this._defaultElementBuilder.bind(this);
  const elem = builder(this, name, namespace);
  return idlUtils.implForWrapper(elem);
}

appendChild(/* Node */ arg) {
  if (this.documentElement && arg.nodeType === NODE_TYPE.ELEMENT_NODE) {
    throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR);
  }
  return super.appendChild(arg);
}

removeChild(/* Node */ arg) {
  const ret = super.removeChild(arg);
  if (arg === this._documentElement) {
    this._documentElement = null;// force a recalculation
  }
  return ret;
}

_descendantRemoved(parent, child) {
  if (child.tagName === "STYLE") {
    const index = this.styleSheets.indexOf(child.sheet);
    if (index > -1) {
      this.styleSheets.splice(index, 1);
    }
  }
}

write() {
  let text = "";
  for (let i = 0; i < arguments.length; ++i) {
    text += String(arguments[i]);
  }

  if (this._parsingMode === "xml") {
    throw new DOMException(DOMException.INVALID_STATE_ERR, "Cannot use document.write on XML documents");
  }

  if (this._writeAfterElement) {
    // If called from an script element directly (during the first tick),
    // the new elements are inserted right after that element.
    const tempDiv = this.createElement("div");
    setInnerHTML(this, tempDiv, text);

    let child = tempDiv.firstChild;
    let previous = this._writeAfterElement;
    const parent = this._writeAfterElement.parentNode;

    while (child) {
      const node = child;
      child = child.nextSibling;
      parent.insertBefore(node, previous.nextSibling);
      previous = node;
    }
  } else if (this.readyState === "loading") {
    // During page loading, document.write appends to the current element
    // Find the last child that has been added to the document.
    let node = this;
    while (node.lastChild && node.lastChild.nodeType === NODE_TYPE.ELEMENT_NODE) {
      node = node.lastChild;
    }
    setInnerHTML(this, node, text);
  } else if (text) {
    setInnerHTML(this, this, text);
  }
}

writeln() {
  const args = [];
  for (let i = 0; i < arguments.length; ++i) {
    args.push(arguments[i]);
  }
  args.push("\n");
  this.write.apply(this, args);
}

getElementById(id) {
  // return the first element
  return this._ids[id] && this._ids[id].length > 0 ? this._ids[id][0] : null;
}

get referrer() {
  return this._referrer || "";
}
get lastModified() {
  return this._lastModified;
}
get images() {
  return this.getElementsByTagName("IMG");
}
get embeds() {
  return this.getElementsByTagName("EMBED");
}
get plugins() {
  return this.embeds;
}
get links() {
  return createHTMLCollection(this, () => domSymbolTree.treeToArray(this, { filter(node) {
    return (node._localName === "a" || node._localName === "area") &&
           node.hasAttribute("href") &&
           node._namespaceURI === "http://www.w3.org/1999/xhtml";
  } }));
}
get forms() {
  return this.getElementsByTagName("FORM");
}
get scripts() {
  return this.getElementsByTagName("SCRIPT");
}
get anchors() {
  return createHTMLCollection(this, () => domSymbolTree.treeToArray(this, { filter(node) {
    return node._localName === "a" &&
           node.hasAttribute("name") &&
           node._namespaceURI === "http://www.w3.org/1999/xhtml";
  } }));
}
get applets() {
  return this.getElementsByTagName("APPLET");
}
open() {
  let child = domSymbolTree.firstChild(this);
  while (child) {
    this.removeChild(child);
    child = domSymbolTree.firstChild(this);
  }
  this._documentElement = null;
  this._modified();
  return this;
}
close() {
  this._queue.resume();

  // Set the readyState to 'complete' once all resources are loaded.
  // As a side-effect the document's load-event will be dispatched.
  resourceLoader.enqueue(this, null, function () {
    this.readyState = "complete";
    const ev = this.createEvent("HTMLEvents");
    ev.initEvent("DOMContentLoaded", false, false);
    this.dispatchEvent(ev);
  })(null, true);
}

getElementsByName(elementName) {
  // TODO: should be NodeList, should be memoized
  return createHTMLCollection(this, () => domSymbolTree.treeToArray(this, { filter(node) {
    return node.getAttribute && node.getAttribute("name") === elementName;
  } }));
}

get title() {
  // TODO SVG

  const titleElement = firstDescendantWithHTMLLocalName(this, "title");
  let value = titleElement !== null ? titleElement.textContent : "";
  value = stripAndCollapseASCIIWhitespace(value);
  return value;
}

set title(val) {
  // TODO SVG

  const titleElement = firstDescendantWithHTMLLocalName(this, "title");
  const headElement = this.head;

  if (titleElement === null && headElement === null) {
    return;
  }

  let element;
  if (titleElement !== null) {
    element = titleElement;
  } else {
    element = this.createElement("title");
    headElement.appendChild(element);
  }

  element.textContent = val;
}

get head() {
  return this.documentElement ? firstChildWithHTMLLocalName(this.documentElement, "head") : null;
}

get body() {
  const documentElement = this.documentElement;
  if (!documentElement || documentElement._localName !== "html" ||
      documentElement._namespaceURI !== "http://www.w3.org/1999/xhtml") {
    return null;
  }

  return firstChildWithHTMLLocalNames(this.documentElement, new Set(["body", "frameset"]));
}

set body(value) {
  if (!HTMLElement.isImpl(value)) {
    throw new TypeError("Argument must be a HTMLElement");
  }
  if (value._namespaceURI !== "http://www.w3.org/1999/xhtml" ||
      (value._localName !== "body" && value._localName !== "frameset")) {
    throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Cannot set the body to a non-body/frameset element");
  }

  const bodyElement = this.body;
  if (value === bodyElement) {
    return;
  }

  if (bodyElement !== null) {
    bodyElement.parentNode.replaceChild(value, bodyElement);
    return;
  }

  const documentElement = this.documentElement;
  if (documentElement === null) {
    throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
      "Cannot set the body when there is no document element");
  }

  documentElement.appendChild(value);
}

_runRemovingSteps(oldNode, oldParent, oldPreviousSibling) {
  const listeners = DocumentImpl._removingSteps;
  for (let i = 0; i < listeners.length; ++i) {
    listeners[i](this, oldNode, oldParent, oldPreviousSibling);
  }
}

createEvent(type) {
  const typeLower = type.toLowerCase();
  const eventWrapper = eventInterfaceTable[typeLower] || null;

  if (!eventWrapper) {
    throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
      "The provided event type (\"" + type + "\") is invalid");
  }

  const impl = eventWrapper.createImpl([""]);
  impl._initializedFlag = false;
  return impl;
}

createProcessingInstruction(target, data) {
  validateName(target);

  if (data.indexOf("?>") !== -1) {
    throw new DOMException(DOMException.INVALID_CHARACTER_ERR,
      "Processing instruction data cannot contain the string \"?>\"");
  }

  return ProcessingInstruction.createImpl([], {
    core: this._core,
    ownerDocument: this,
    target,
    data
  });
}

// https://dom.spec.whatwg.org/#dom-document-createcdatasection
createCDATASection(data) {
  if (this._parsingMode === "html") {
    throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
      "Cannot create CDATA sections in HTML documents");
  }

  if (data.indexOf("]]>") !== -1) {
    throw new DOMException(DOMException.INVALID_CHARACTER_ERR,
      "CDATA section data cannot contain the string \"]]>\"");
  }

  return CDATASection.createImpl([], {
    core: this._core,
    ownerDocument: this,
    data
  });
}

createTextNode(data) {
  return Text.createImpl([], {
    core: this._core,
    ownerDocument: this,
    data
  });
}

createComment(data) {
  return Comment.createImpl([], {
    core: this._core,
    ownerDocument: this,
    data
  });
}

createElement(localName) {
  validateName(localName);
  if (this._parsingMode === "html") {
    localName = localName.toLowerCase();
  }

  const element = this._createElementWithCorrectElementInterface(localName, "http://www.w3.org/1999/xhtml");
  element._namespaceURI = "http://www.w3.org/1999/xhtml";
  element._localName = localName;

  return element;
}

createElementNS(namespace, qualifiedName) {
  namespace = namespace !== null ? String(namespace) : namespace;

  const extracted = validateAndExtract(namespace, qualifiedName);

  const element = this._createElementWithCorrectElementInterface(extracted.localName, extracted.namespace);
  element._namespaceURI = extracted.namespace;
  element._prefix = extracted.prefix;
  element._localName = extracted.localName;

  return element;
}

createDocumentFragment() {
  return DocumentFragment.createImpl([], { ownerDocument: this });
}

createAttribute(localName) {
  validateName(localName);

  if (this._parsingMode === "html") {
    localName = localName.toLowerCase();
  }

  return generatedAttr.createImpl([], { localName });
}

createAttributeNS(namespace, name) {
  if (namespace === undefined) {
    namespace = null;
  }
  namespace = namespace !== null ? String(namespace) : namespace;

  const extracted = validateAndExtract(namespace, name);
  return generatedAttr.createImpl([], {
    namespace: extracted.namespace,
    namespacePrefix: extracted.prefix,
    localName: extracted.localName
  });
}

// TODO: Add callback interface support to `webidl2js`
createTreeWalker(root, whatToShow, filter) {
  if (!isNodeImpl(root)) {
    throw new TypeError("First argument to createTreeWalker must be a Node");
  }

  return TreeWalker.createImpl([], {
    root,
    whatToShow,
    filter
  });
}

importNode(node, deep) {
  if (!isNodeImpl(node)) {
    throw new TypeError("First argument to importNode must be a Node");
  }
  deep = Boolean(deep);

  if (node.nodeType === NODE_TYPE.DOCUMENT_NODE) {
    throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot import a document node");
  }

  return clone(this._core, node, this, deep);
}

adoptNode(node) {
  if (!isNodeImpl(node)) {
    throw new TypeError("First argument to adoptNode must be a Node");
  }
  if (node.nodeType === NODE_TYPE.DOCUMENT_NODE) {
    throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot adopt a document node");
  }
  // TODO: Determine correct way to detect a shadow root
  // See also https://github.com/w3c/webcomponents/issues/182

  if (node.parentNode) {
    node.parentNode.removeChild(node);
  }
  node._ownerDocument = this;
  for (const descendant of domSymbolTree.treeIterator(node)) {
    descendant._ownerDocument = this;
  }

  return node;
}

get cookie() {
  return this._cookieJar.getCookieStringSync(this.URL, { http: false });
}
set cookie(cookieStr) {
  cookieStr = String(cookieStr);
  this._cookieJar.setCookieSync(cookieStr, this.URL, {
    http: false,
    ignoreError: true
  });
}

get styleSheets() {
  if (!this._styleSheets) {
    this._styleSheets = new this._core.StyleSheetList();
  }

  // TODO: each style and link element should register its sheet on creation
  // and remove it on removal.
  return this._styleSheets;
}

get hidden() {
  return true;
}

get visibilityState() {
  return "prerender";
}

}

idlUtils.mixin(DocumentImpl.prototype, ParentNodeImpl.prototype);

DocumentImpl._removingSteps = [];

DocumentImpl.prototype._elementBuilders = Object.create(null);

DocumentImpl.prototype.getElementsByTagName = memoizeQuery(function (qualifiedName) {

return listOfElementsWithQualifiedName(qualifiedName, this);

});

DocumentImpl.prototype.getElementsByTagNameNS = memoizeQuery(function (namespace, localName) {

return listOfElementsWithNamespaceAndLocalName(namespace, localName, this);

});

DocumentImpl.prototype.getElementsByClassName = memoizeQuery(function getElementsByClassName(classNames) {

return listOfElementsWithClassNames(classNames, this);

});

module.exports = {

implementation: DocumentImpl

};