/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <stdint.h>

#include <algorithm>

#include "mozilla/Maybe.h"
#include "mozilla/dom/Attr.h"
#include "mozilla/dom/CharacterData.h"
#include "mozilla/dom/Element.h"
#include "nsAtom.h"
#include "nsAttrName.h"
#include "nsINode.h"
#include "nsNameSpaceManager.h"
#include "nsPrintfCString.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nsUnicharUtils.h"
#include "txLog.h"
#include "txXMLUtils.h"
#include "txXPathTreeWalker.h"

using namespace mozilla;
using namespace mozilla::dom;

txXPathTreeWalker::txXPathTreeWalker(const txXPathTreeWalker& aOther) = default;

txXPathTreeWalker::txXPathTreeWalker(const txXPathNode& aNode)
    : mPosition(aNode) {}

void txXPathTreeWalker::moveToRoot() {
  if (mPosition.isDocument()) {
    return;
  }

  Document* root = mPosition.mNode->GetUncomposedDoc();
  if (root) {
    mPosition.mIndex = txXPathNode::eDocument;
    mPosition.mNode = root;
  } else {
    nsINode* rootNode = mPosition.Root();

    NS_ASSERTION(rootNode->IsContent(), "root of subtree wasn't an nsIContent");

    mPosition.mIndex = txXPathNode::eContent;
    mPosition.mNode = rootNode;
  }
}

bool txXPathTreeWalker::moveToElementById(const nsAString& aID) {
  if (aID.IsEmpty()) {
    return false;
  }

  Document* doc = mPosition.mNode->GetUncomposedDoc();

  nsCOMPtr<nsIContent> content;
  if (doc) {
    content = doc->GetElementById(aID);
  } else {
    // We're in a disconnected subtree, search only that subtree.
    nsINode* rootNode = mPosition.Root();

    NS_ASSERTION(rootNode->IsContent(), "root of subtree wasn't an nsIContent");

    content =
        nsContentUtils::MatchElementId(static_cast<nsIContent*>(rootNode), aID);
  }

  if (!content) {
    return false;
  }

  mPosition.mIndex = txXPathNode::eContent;
  mPosition.mNode = content;

  return true;
}

bool txXPathTreeWalker::moveToFirstAttribute() {
  if (!mPosition.isContent()) {
    return false;
  }

  return moveToValidAttribute(0);
}

bool txXPathTreeWalker::moveToNextAttribute() {
  // XXX an assertion should be enough here with the current code
  if (!mPosition.isAttribute()) {
    return false;
  }

  return moveToValidAttribute(mPosition.mIndex + 1);
}

bool txXPathTreeWalker::moveToValidAttribute(uint32_t aStartIndex) {
  NS_ASSERTION(!mPosition.isDocument(), "documents doesn't have attrs");

  if (!mPosition.Content()->IsElement()) {
    return false;
  }

  Element* element = mPosition.Content()->AsElement();
  uint32_t total = element->GetAttrCount();
  if (aStartIndex >= total) {
    return false;
  }

  uint32_t index;
  for (index = aStartIndex; index < total; ++index) {
    const nsAttrName* name = element->GetAttrNameAt(index);

    // We need to ignore XMLNS attributes.
    if (name->NamespaceID() != kNameSpaceID_XMLNS) {
      mPosition.mIndex = index;

      return true;
    }
  }
  return false;
}

bool txXPathTreeWalker::moveToNamedAttribute(nsAtom* aLocalName,
                                             int32_t aNSID) {
  if (!mPosition.isContent() || !mPosition.Content()->IsElement()) {
    return false;
  }

  Element* element = mPosition.Content()->AsElement();

  const nsAttrName* name;
  uint32_t i;
  for (i = 0; element->GetAttrNameAt(i, &name); ++i) {
    if (name->Equals(aLocalName, aNSID)) {
      mPosition.mIndex = i;

      return true;
    }
  }
  return false;
}

bool txXPathTreeWalker::moveToFirstChild() {
  if (mPosition.isAttribute()) {
    return false;
  }

  nsIContent* child = mPosition.mNode->GetFirstChild();
  if (!child) {
    return false;
  }
  mPosition.mIndex = txXPathNode::eContent;
  mPosition.mNode = child;

  return true;
}

bool txXPathTreeWalker::moveToLastChild() {
  if (mPosition.isAttribute()) {
    return false;
  }

  nsIContent* child = mPosition.mNode->GetLastChild();
  if (!child) {
    return false;
  }

  mPosition.mIndex = txXPathNode::eContent;
  mPosition.mNode = child;

  return true;
}

bool txXPathTreeWalker::moveToNextSibling() {
  if (!mPosition.isContent()) {
    return false;
  }

  nsINode* sibling = mPosition.mNode->GetNextSibling();
  if (!sibling) {
    return false;
  }

  mPosition.mNode = sibling;

  return true;
}

bool txXPathTreeWalker::moveToPreviousSibling() {
  if (!mPosition.isContent()) {
    return false;
  }

  nsINode* sibling = mPosition.mNode->GetPreviousSibling();
  if (!sibling) {
    return false;
  }

  mPosition.mNode = sibling;

  return true;
}

bool txXPathTreeWalker::moveToParent() {
  if (mPosition.isDocument()) {
    return false;
  }

  if (mPosition.isAttribute()) {
    mPosition.mIndex = txXPathNode::eContent;

    return true;
  }

  nsINode* parent = mPosition.mNode->GetParentNode();
  if (!parent) {
    return false;
  }

  mPosition.mIndex = mPosition.mNode->GetParent() ? txXPathNode::eContent
                                                  : txXPathNode::eDocument;
  mPosition.mNode = parent;

  return true;
}

/* static */
bool txXPathNodeUtils::getAttr(const txXPathNode& aNode, nsAtom* aLocalName,
                               int32_t aNSID, nsAString& aValue) {
  if (aNode.isDocument() || aNode.isAttribute() ||
      !aNode.Content()->IsElement()) {
    return false;
  }

  return aNode.Content()->AsElement()->GetAttr(aNSID, aLocalName, aValue);
}

/* static */
already_AddRefed<nsAtom> txXPathNodeUtils::getLocalName(
    const txXPathNode& aNode) {
  if (aNode.isDocument()) {
    return nullptr;
  }

  if (aNode.isContent()) {
    if (aNode.mNode->IsElement()) {
      RefPtr<nsAtom> localName = aNode.Content()->NodeInfo()->NameAtom();
      return localName.forget();
    }

    if (aNode.mNode->IsProcessingInstruction()) {
      return NS_Atomize(aNode.mNode->NodeName());
    }

    return nullptr;
  }

  // This is an attribute node, so we necessarily come from an element.
  RefPtr<nsAtom> localName =
      aNode.Content()->AsElement()->GetAttrNameAt(aNode.mIndex)->LocalName();

  return localName.forget();
}

nsAtom* txXPathNodeUtils::getPrefix(const txXPathNode& aNode) {
  if (aNode.isDocument()) {
    return nullptr;
  }

  if (aNode.isContent()) {
    // All other nsIContent node types but elements have a null prefix
    // which is what we want here.
    return aNode.Content()->NodeInfo()->GetPrefixAtom();
  }

  return aNode.Content()->AsElement()->GetAttrNameAt(aNode.mIndex)->GetPrefix();
}

/* static */
void txXPathNodeUtils::getLocalName(const txXPathNode& aNode,
                                    nsAString& aLocalName) {
  if (aNode.isDocument()) {
    aLocalName.Truncate();

    return;
  }

  if (aNode.isContent()) {
    if (aNode.mNode->IsElement()) {
      mozilla::dom::NodeInfo* nodeInfo = aNode.Content()->NodeInfo();
      nodeInfo->GetName(aLocalName);
      return;
    }

    if (aNode.mNode->IsProcessingInstruction()) {
      // PIs don't have a nodeinfo but do have a name
      // XXXbz Not actually true, but this function looks like it wants
      // different things from elements and PIs for "local name"...
      aLocalName = aNode.mNode->NodeName();
      return;
    }

    aLocalName.Truncate();

    return;
  }

  aNode.Content()
      ->AsElement()
      ->GetAttrNameAt(aNode.mIndex)
      ->LocalName()
      ->ToString(aLocalName);

  // Check for html
  if (aNode.Content()->NodeInfo()->NamespaceEquals(kNameSpaceID_None) &&
      aNode.Content()->IsHTMLElement()) {
    nsContentUtils::ASCIIToUpper(aLocalName);
  }
}

/* static */
void txXPathNodeUtils::getNodeName(const txXPathNode& aNode, nsAString& aName) {
  if (aNode.isDocument()) {
    aName.Truncate();

    return;
  }

  if (aNode.isContent()) {
    // Elements and PIs have a name
    if (aNode.mNode->IsElement() ||
        aNode.mNode->NodeType() == nsINode::PROCESSING_INSTRUCTION_NODE) {
      aName = aNode.Content()->NodeName();
      return;
    }

    aName.Truncate();

    return;
  }

  aNode.Content()
      ->AsElement()
      ->GetAttrNameAt(aNode.mIndex)
      ->GetQualifiedName(aName);
}

/* static */
int32_t txXPathNodeUtils::getNamespaceID(const txXPathNode& aNode) {
  if (aNode.isDocument()) {
    return kNameSpaceID_None;
  }

  if (aNode.isContent()) {
    return aNode.Content()->GetNameSpaceID();
  }

  return aNode.Content()
      ->AsElement()
      ->GetAttrNameAt(aNode.mIndex)
      ->NamespaceID();
}

/* static */
void txXPathNodeUtils::getNamespaceURI(const txXPathNode& aNode,
                                       nsAString& aURI) {
  nsNameSpaceManager::GetInstance()->GetNameSpaceURI(getNamespaceID(aNode),
                                                     aURI);
}

/* static */
uint16_t txXPathNodeUtils::getNodeType(const txXPathNode& aNode) {
  if (aNode.isDocument()) {
    return txXPathNodeType::DOCUMENT_NODE;
  }

  if (aNode.isContent()) {
    return aNode.mNode->NodeType();
  }

  return txXPathNodeType::ATTRIBUTE_NODE;
}

/* static */
void txXPathNodeUtils::appendNodeValue(const txXPathNode& aNode,
                                       nsAString& aResult) {
  if (aNode.isAttribute()) {
    const nsAttrName* name =
        aNode.Content()->AsElement()->GetAttrNameAt(aNode.mIndex);

    if (aResult.IsEmpty()) {
      aNode.Content()->AsElement()->GetAttr(name->NamespaceID(),
                                            name->LocalName(), aResult);
    } else {
      nsAutoString result;
      aNode.Content()->AsElement()->GetAttr(name->NamespaceID(),
                                            name->LocalName(), result);
      aResult.Append(result);
    }

    return;
  }

  if (aNode.isDocument() || aNode.mNode->IsElement() ||
      aNode.mNode->IsDocumentFragment()) {
    nsContentUtils::AppendNodeTextContent(aNode.mNode, true, aResult,
                                          mozilla::fallible);

    return;
  }

  MOZ_ASSERT(aNode.mNode->IsCharacterData());
  static_cast<CharacterData*>(aNode.Content())->AppendTextTo(aResult);
}

/* static */
bool txXPathNodeUtils::isWhitespace(const txXPathNode& aNode) {
  NS_ASSERTION(aNode.isContent() && isText(aNode), "Wrong type!");

  return aNode.Content()->TextIsOnlyWhitespace();
}

/* static */
txXPathNode txXPathNodeUtils::getOwnerDocument(const txXPathNode& aNode) {
  return txXPathNode(aNode.mNode->OwnerDoc());
}

const char gPrintfFmt[] = "id0x%" PRIxPTR;
const char gPrintfFmtAttr[] = "id0x%" PRIxPTR "-%010i";

/* static */
nsresult txXPathNodeUtils::getXSLTId(const txXPathNode& aNode,
                                     const txXPathNode& aBase,
                                     nsAString& aResult) {
  uintptr_t nodeid =
      ((uintptr_t)aNode.mNode.get()) - ((uintptr_t)aBase.mNode.get());
  if (!aNode.isAttribute()) {
    CopyASCIItoUTF16(nsPrintfCString(gPrintfFmt, nodeid), aResult);
  } else {
    CopyASCIItoUTF16(nsPrintfCString(gPrintfFmtAttr, nodeid, aNode.mIndex),
                     aResult);
  }

  return NS_OK;
}

/* static */
nsresult txXPathNodeUtils::getBaseURI(const txXPathNode& aNode,
                                      nsAString& aURI) {
  return aNode.mNode->GetBaseURI(aURI);
}

/* static */
int txXPathNodeUtils::comparePosition(const txXPathNode& aNode,
                                      const txXPathNode& aOtherNode) {
  // First check for equal nodes or attribute-nodes on the same element.
  if (aNode.mNode == aOtherNode.mNode) {
    if (aNode.mIndex == aOtherNode.mIndex) {
      return 0;
    }

    NS_ASSERTION(!aNode.isDocument() && !aOtherNode.isDocument(),
                 "documents should always have a set index");

    if (aNode.isContent() ||
        (!aOtherNode.isContent() && aNode.mIndex < aOtherNode.mIndex)) {
      return -1;
    }

    return 1;
  }

  // Get document for both nodes.
  Document* document = aNode.mNode->GetUncomposedDoc();
  Document* otherDocument = aOtherNode.mNode->GetUncomposedDoc();

  // If the nodes have different current documents, compare the document
  // pointers.
  if (document != otherDocument) {
    return document < otherDocument ? -1 : 1;
  }

  // Now either both nodes are in orphan trees, or they are both in the
  // same tree.

  // Get parents up the tree.
  AutoTArray<nsINode*, 8> parents, otherParents;
  nsINode* node = aNode.mNode;
  nsINode* otherNode = aOtherNode.mNode;
  nsINode* parent;
  nsINode* otherParent;
  while (node && otherNode) {
    parent = node->GetParentNode();
    otherParent = otherNode->GetParentNode();

    // Hopefully this is a common case.
    if (parent == otherParent) {
      if (!parent) {
        // Both node and otherNode are root nodes in respective orphan
        // tree.
        return node < otherNode ? -1 : 1;
      }

      const Maybe<uint32_t> indexOfNode = parent->ComputeIndexOf(node);
      const Maybe<uint32_t> indexOfOtherNode =
          parent->ComputeIndexOf(otherNode);
      if (MOZ_LIKELY(indexOfNode.isSome() && indexOfOtherNode.isSome())) {
        return *indexOfNode < *indexOfOtherNode ? -1 : 1;
      }
      // XXX Keep the odd traditional behavior for now.
      return indexOfNode.isNothing() && indexOfOtherNode.isSome() ? -1 : 1;
    }

    parents.AppendElement(node);
    otherParents.AppendElement(otherNode);
    node = parent;
    otherNode = otherParent;
  }

  while (node) {
    parents.AppendElement(node);
    node = node->GetParentNode();
  }
  while (otherNode) {
    otherParents.AppendElement(otherNode);
    otherNode = otherNode->GetParentNode();
  }

  // Walk back down along the parent-chains until we find where they split.
  int32_t total = parents.Length() - 1;
  int32_t otherTotal = otherParents.Length() - 1;
  NS_ASSERTION(total != otherTotal, "Can't have same number of parents");

  int32_t lastIndex = std::min(total, otherTotal);
  int32_t i;
  parent = nullptr;
  for (i = 0; i <= lastIndex; ++i) {
    node = parents.ElementAt(total - i);
    otherNode = otherParents.ElementAt(otherTotal - i);
    if (node != otherNode) {
      if (!parent) {
        // The two nodes are in different orphan subtrees.
        NS_ASSERTION(i == 0, "this shouldn't happen");
        return node < otherNode ? -1 : 1;
      }

      const Maybe<uint32_t> index = parent->ComputeIndexOf(node);
      const Maybe<uint32_t> otherIndex = parent->ComputeIndexOf(otherNode);
      if (MOZ_LIKELY(index.isSome() && otherIndex.isSome())) {
        NS_ASSERTION(*index != *otherIndex, "invalid index in comparePosition");
        return *index < *otherIndex ? -1 : 1;
      }
      NS_ASSERTION(false, "invalid index in comparePosition");
      // XXX Keep the odd traditional behavior for now.
      return index.isNothing() && otherIndex.isSome() ? -1 : 1;
    }

    parent = node;
  }

  // One node is a descendant of the other. The one with the shortest
  // parent-chain is first in the document.
  return total < otherTotal ? -1 : 1;
}

/* static */
Maybe<txXPathNode> txXPathNativeNode::createXPathNode(nsINode* aNode) {
  uint16_t nodeType = aNode->NodeType();
  if (nodeType == nsINode::ATTRIBUTE_NODE) {
    auto* attr = static_cast<Attr*>(aNode);

    NodeInfo* nodeInfo = attr->NodeInfo();
    Element* parent = attr->GetElement();
    if (!parent) {
      return Nothing();
    }

    uint32_t i, total = parent->GetAttrCount();
    for (i = 0; i < total; ++i) {
      const nsAttrName* name = parent->GetAttrNameAt(i);
      if (nodeInfo->Equals(name->LocalName(), name->NamespaceID())) {
        return Some(txXPathNode(parent, i));
      }
    }

    NS_ERROR("Couldn't find the attribute in its parent!");

    return Nothing();
  }

  uint32_t index;
  if (nodeType == nsINode::DOCUMENT_NODE) {
    index = txXPathNode::eDocument;
  } else {
    index = txXPathNode::eContent;
  }

  return Some(txXPathNode(aNode, index));
}

/* static */
nsINode* txXPathNativeNode::getNode(const txXPathNode& aNode) {
  if (!aNode.isAttribute()) {
    return aNode.mNode;
  }

  const nsAttrName* name =
      aNode.Content()->AsElement()->GetAttrNameAt(aNode.mIndex);

  nsAutoString namespaceURI;
  nsNameSpaceManager::GetInstance()->GetNameSpaceURI(name->NamespaceID(),
                                                     namespaceURI);

  nsCOMPtr<Element> element = do_QueryInterface(aNode.mNode);
  nsDOMAttributeMap* map = element->Attributes();
  return map->GetNamedItemNS(namespaceURI,
                             nsDependentAtomString(name->LocalName()));
}

/* static */
nsIContent* txXPathNativeNode::getContent(const txXPathNode& aNode) {
  NS_ASSERTION(aNode.isContent(),
               "Only call getContent on nsIContent wrappers!");
  return aNode.Content();
}

/* static */
Document* txXPathNativeNode::getDocument(const txXPathNode& aNode) {
  NS_ASSERTION(aNode.isDocument(),
               "Only call getDocument on Document wrappers!");
  return aNode.Document();
}
