/*=========================================================================
 *
 *  Copyright NumFOCUS
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         https://www.apache.org/licenses/LICENSE-2.0.txt
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *=========================================================================*/

#include "itkDOMNode.h"

#include <sstream>

namespace itk
{

DOMNode::DOMNode() = default;

void
DOMNode::SetParent(DOMNode * node)
{
  this->m_Parent = node;
}

DOMNode *
DOMNode::GetParent()
{
  return this->m_Parent;
}

const DOMNode *
DOMNode::GetParent() const
{
  return this->m_Parent;
}

/** Retrieve an attribute by key (return an empty string if not found). */
std::string
DOMNode::GetAttribute(const std::string & key) const
{
  // "id" is a special attribute
  if (StringTools::MatchWith(key, "id"))
  {
    return this->GetID();
  }

  auto i = this->m_Attributes.find(key);
  if (i == this->m_Attributes.end())
  {
    return "";
  }
  else
  {
    return i->second;
  }
}

/** Check whether has an attribute. */
bool
DOMNode::HasAttribute(const std::string & key) const
{
  auto i = this->m_Attributes.find(key);
  return (i != this->m_Attributes.end());
}

/** Add or replace an attribute. */
void
DOMNode::SetAttribute(const std::string & key, const std::string & value)
{
  // "id" is a special attribute
  if (StringTools::MatchWith(key, "id"))
  {
    this->SetID(value);
    return;
  }

  auto i = this->m_Attributes.find(key);
  if (i == this->m_Attributes.end())
  {
    // add a new attribute
    this->m_Attributes[key] = value;
    // keep track of the adding order
    i = this->m_Attributes.find(key);
    this->m_OrderedAttributes.push_back(&(*i));
  }
  else
  {
    // replace an existing attribute
    i->second = value;
  }
}

/** Remove an attribute by key (throw exception if not found). */
void
DOMNode::RemoveAttribute(const std::string & key)
{
  auto i = this->m_Attributes.find(key);
  if (i == this->m_Attributes.end())
  {
    itkExceptionMacro("attribute does not exist");
  }

  // remove it from the ordered container first
  for (auto j = this->m_OrderedAttributes.begin(); j != this->m_OrderedAttributes.end(); ++j)
  {
    if ((*j)->first == key)
    {
      this->m_OrderedAttributes.erase(j);
      break;
    }
  }

  // then remove it from the map container
  this->m_Attributes.erase(i);
}

/**
 * Return all attributes, in the order of being parsed or added (default),
 * or alphabetic order of the attribute keys (keepOriginalOrder = false).
 */
void
DOMNode::GetAllAttributes(AttributesListType & output, bool keepOriginalOrder) const
{
  if (keepOriginalOrder)
  {
    for (auto orderedAttribute : this->m_OrderedAttributes)
    {
      output.push_back(*orderedAttribute);
    }
  }
  else
  {
    for (const auto & attribute : this->m_Attributes)
    {
      output.push_back(attribute);
    }
  }
}

/** Remove all attributes. */
void
DOMNode::RemoveAllAttributes()
{
  this->m_Attributes.clear();
  this->m_OrderedAttributes.clear();
}

/** Get number of children. */
DOMNode::SizeType
DOMNode::GetNumberOfChildren() const
{
  return this->m_Children.size();
}

/** Return all children. */
void
DOMNode::GetAllChildren(ChildrenListType & output)
{
  for (IdentifierType i = 0; i < static_cast<IdentifierType>(this->GetNumberOfChildren()); ++i)
  {
    DOMNode * node = this->GetChild(i);
    output.push_back(node);
  }
}

/** Return all children for read-only access. */
void
DOMNode::GetAllChildren(ConstChildrenListType & output) const
{
  for (IdentifierType i = 0; i < static_cast<IdentifierType>(this->GetNumberOfChildren()); ++i)
  {
    const DOMNode * node = this->GetChild(i);
    output.push_back(node);
  }
}

/** Return all children of a same tag name. */
void
DOMNode::GetChildren(const std::string & tag, ChildrenListType & output)
{
  for (IdentifierType i = 0; i < static_cast<IdentifierType>(this->GetNumberOfChildren()); ++i)
  {
    DOMNode * node = this->GetChild(i);
    if (tag == node->GetName())
    {
      output.push_back(node);
    }
  }
}

/** Return all children of a same tag name for read-only access. */
void
DOMNode::GetChildren(const std::string & tag, ConstChildrenListType & output) const
{
  for (IdentifierType i = 0; i < static_cast<IdentifierType>(this->GetNumberOfChildren()); ++i)
  {
    const DOMNode * node = this->GetChild(i);
    if (tag == node->GetName())
    {
      output.push_back(node);
    }
  }
}

/** Remove all children. */
void
DOMNode::RemoveAllChildren()
{
  this->m_Children.clear();
}

/** Add a child in front of another child (throw exception if not able to add). */
void
DOMNode::AddChild(DOMNode * node, IdentifierType i)
{
  if (this->m_Children.empty())
  {
    this->AddChildAtEnd(node);
    return;
  }

  if (node == nullptr || this->ShareRoot(node))
  {
    itkExceptionMacro("not able to add child");
  }

  if (i < 0 || i >= static_cast<IdentifierType>(this->m_Children.size()))
  {
    itkExceptionMacro("not able to add child");
  }

  node->m_Parent = this;
  this->m_Children.insert(this->m_Children.begin() + i, Pointer(node));
}

/** Add a child in front of the children list (throw exception if not able to add). */
void
DOMNode::AddChildAtBegin(DOMNode * node)
{
  if (node == nullptr || this->ShareRoot(node))
  {
    itkExceptionMacro("not able to add child");
  }

  node->m_Parent = this;
  this->m_Children.insert(this->m_Children.begin(), Pointer(node));
}

/** Add a child at the end of the children list (throw exception if not able to add). */
void
DOMNode::AddChildAtEnd(DOMNode * node)
{
  if (node == nullptr || this->ShareRoot(node))
  {
    itkExceptionMacro("not able to add child");
  }

  node->m_Parent = this;
  this->m_Children.push_back(Pointer(node));
}

/** Replace a child (throw exception if not able to replace). */
void
DOMNode::SetChild(DOMNode * node, IdentifierType i)
{
  if (node == nullptr || this->ShareRoot(node))
  {
    itkExceptionMacro("not able to add child");
  }

  if (i < 0 || i >= static_cast<IdentifierType>(this->m_Children.size()))
  {
    itkExceptionMacro("not able to add child");
  }

  node->m_Parent = this;
  this->m_Children[i] = node;
}

/** Remove a child by index (throw exception if out of range). */
void
DOMNode::RemoveChild(IdentifierType i)
{
  if (i < 0 || i >= static_cast<IdentifierType>(m_Children.size()))
  {
    itkExceptionMacro("not able to remove child");
  }

  this->m_Children[i]->m_Parent = nullptr;
  this->m_Children.erase(this->m_Children.begin() + i);
}

/** Remove all attributes and children. */
void
DOMNode::RemoveAllAttributesAndChildren()
{
  this->RemoveAllAttributes();
  this->RemoveAllChildren();
}

/** Retrieve a child by index (return nullptr if out of range). */
DOMNode *
DOMNode::GetChild(IdentifierType i)
{
  if (i < 0 || i >= static_cast<IdentifierType>(this->m_Children.size()))
  {
    return nullptr;
  }
  else
  {
    return (DOMNode *)this->m_Children[i];
  }
}

/** Retrieve a child by index (return nullptr if out of range). */
const DOMNode *
DOMNode::GetChild(IdentifierType i) const
{
  // just use the non-const version and return it as a const,
  // this is safe as the non-const version actually doesn't modify the internal state.
  return const_cast<Self *>(this)->GetChild(i);
}

/** Retrieve a child by tag name and an index (multiple children can have a same tag name, return nullptr if no such
 * child). */
DOMNode *
DOMNode::GetChild(const std::string & tag, IdentifierType i)
{
  IdentifierType k = 0;
  for (IdentifierType j = 0; j < static_cast<IdentifierType>(this->GetNumberOfChildren()); ++j)
  {
    DOMNode * node = this->GetChild(j);
    if (tag == node->GetName())
    {
      if (k++ == i)
      {
        return node;
      }
    }
  }
  return nullptr;
}

/** Retrieve a child by tag name and an index (multiple children can have a same tag name, return nullptr if no such
 * child). */
const DOMNode *
DOMNode::GetChild(const std::string & tag, IdentifierType i) const
{
  // just use the non-const version and return it as a const,
  // this is safe as the non-const version actually doesn't modify the internal state.
  return const_cast<Self *>(this)->GetChild(tag, i);
}

/** Retrieve a child by its unique "id" attribute value (return nullptr if not found). */
DOMNode *
DOMNode::GetChildByID(const std::string & value)
{
  for (IdentifierType j = 0; j < static_cast<IdentifierType>(this->GetNumberOfChildren()); ++j)
  {
    DOMNode * node = this->GetChild(j);
    if (value == node->GetID())
    {
      return node;
    }
  }
  return nullptr;
}

/** Retrieve a child by its unique "id" attribute value (return nullptr if not found). */
const DOMNode *
DOMNode::GetChildByID(const std::string & value) const
{
  // just use the non-const version and return it as a const,
  // this is safe as the non-const version actually doesn't modify the internal state.
  return const_cast<Self *>(this)->GetChildByID(value);
}

/** Retrieve an older or younger sibling by distance (return nullptr if no such sibling). */
DOMNode *
DOMNode::GetSibling(OffsetType i)
{
  DOMNode * parent = this->GetParent();

  if (parent == nullptr)
  {
    return nullptr;
  }

  IdentifierType j;
  for (j = 0; j < static_cast<IdentifierType>(parent->GetNumberOfChildren()); ++j)
  {
    if (parent->GetChild(j) == this)
    {
      break;
    }
  }

  j += i;
  if (j < 0 || j >= static_cast<IdentifierType>(parent->GetNumberOfChildren()))
  {
    return nullptr;
  }
  else
  {
    return parent->GetChild(j);
  }
}

/** Retrieve an older or younger sibling by distance (return nullptr if no such sibling). */
const DOMNode *
DOMNode::GetSibling(OffsetType i) const
{
  // just use the non-const version and return it as a const,
  // this is safe as the non-const version actually doesn't modify the internal state.
  return const_cast<Self *>(this)->GetSibling(i);
}

/** Return the root node. */
DOMNode *
DOMNode::GetRoot()
{
  DOMNode * node = this->GetParent();

  if (node == nullptr)
  {
    return this;
  }
  else
  {
    return node->GetRoot();
  }
}

/** Return the root node. */
const DOMNode *
DOMNode::GetRoot() const
{
  // just use the non-const version and return it as a const,
  // this is safe as the non-const version actually doesn't modify the internal state.
  return const_cast<Self *>(this)->GetRoot();
}

/** Test whether the input node and this node share the same root. */
bool
DOMNode::ShareRoot(const DOMNode * node) const
{
  return (node != nullptr && node->GetRoot() == this->GetRoot());
}

/**
 * The following function finds a child or sibling or relative using a query string or path.
 * A path or QueryString consists of multiple following items that are separated by '/':
 *     -[n]           : an older sibling by distance 1 (when omitted) or n;
 *     +[n]           : a younger sibling by distance 1 (when omitted) or n;
 *     n              : a child at index n;
 *     <tag>[:n]      : a child at index 0 (when omitted) or n after filtering children with a tag name;
 *     ![:n]          : a child at index 0 (when omitted) or n within all text children;
 *     :<id>          : a child by id;
 *     .              : current node;
 *     ..             : parent node;
 *     /<rpath>       : absolute path (denote apath), search from the root.
 *
 * The method returns nullptr if queried node does not exist.
 */
DOMNode *
DOMNode::Find(const std::string & path)
{
  std::string s;

  std::string rpath;
  {
    size_t pos = path.find_first_of('/');
    if (pos == std::string::npos)
    {
      s = path;
      rpath = "";
    }
    else
    {
      s = path.substr(0, pos);
      rpath = path.substr(pos + 1);
    }
  }

  DOMNode * node = nullptr;

  // /<rpath>
  if (s.empty())
  {
    node = this->GetRoot();
  }

  // if it is an unsigned integer number
  else if (s[0] >= '0' && s[0] <= '9')
  {
    std::istringstream iss(s);
    unsigned int       i = 0;
    iss >> i;
    if (!iss.fail())
    {
      node = this->GetChild(IdentifierType(i));
    }
  }

  // +[n]
  else if (s[0] == '+')
  {
    if (s.size() > 1)
    {
      s = s.substr(1);
      std::istringstream iss(s);
      unsigned int       i = 0;
      iss >> i;
      if (!iss.fail())
      {
        node = this->GetSibling(OffsetType(i));
      }
    }
    else
    {
      node = this->GetSibling(1);
    }
  }

  // -[n]
  else if (s[0] == '-')
  {
    if (s.size() > 1)
    {
      s = s.substr(1);
      std::istringstream iss(s);
      unsigned int       i = 0;
      iss >> i;
      if (!iss.fail())
      {
        node = this->GetSibling(-OffsetType(i));
      }
    }
    else
    {
      node = this->GetSibling(-1);
    }
  }

  // :<id>
  else if (s[0] == ':')
  {
    s = s.substr(1);
    node = this->GetChildByID(s);
  }

  // ..
  else if (s == "..")
  {
    node = this->GetParent();
  }

  // .
  else if (s == ".")
  {
    node = this;
  }

  // <tag>[:n]
  else
  {
    size_t pos = s.find_first_of(':');
    if (pos != std::string::npos)
    {
      std::string s2 = s.substr(pos + 1);
      s = s.substr(0, pos);
      std::istringstream iss(s2);
      IdentifierType     i = 0;
      iss >> i;
      if (!iss.fail())
      {
        node = this->GetChild(s, i);
      }
    }
    else
    {
      node = this->GetChild(s);
    }
  }

  if (rpath.empty() || node == nullptr)
  {
    return node;
  }
  else
  {
    return node->Find(rpath);
  }
}

/**
 * The following function finds a child or sibling or relative using a query string or path.
 * The method returns nullptr if queried node does not exist.
 */
const DOMNode *
DOMNode::Find(const std::string & path) const
{
  // just use the non-const version and return it as a const,
  // this is safe as the non-const version actually doesn't modify the internal state.
  return const_cast<Self *>(this)->Find(path);
}

/** Return the path of this node within its root, in the form of a query string that uses only indices. */
std::string
DOMNode::GetPath() const
{
  std::string path = "";

  const DOMNode * parent = this->GetParent();
  if (parent == nullptr)
  {
    return path;
  }

  for (IdentifierType i = 0; i < static_cast<IdentifierType>(parent->GetNumberOfChildren()); ++i)
  {
    if (parent->GetChild(i) == this)
    {
      // set the global path
      path = parent->GetPath();
      path.append(1, '/');
      // add the local path
      std::string s;
      StringTools::FromData(s, i);
      path.append(s);
      // early exit as we've already found the path
      break;
    }
  }
  return path;
}

/** Get a child and cast it to a text node (return nullptr if out of range or not a text node). */
DOMTextNode *
DOMNode::GetTextChild(IdentifierType i)
{
  DOMNode * node = this->GetChild(i);
  return dynamic_cast<DOMTextNode *>(node);
}

/** Get a child and cast it to a text node (return nullptr if out of range or not a text node). */
const DOMTextNode *
DOMNode::GetTextChild(IdentifierType i) const
{
  const DOMNode * node = this->GetChild(i);
  return dynamic_cast<const DOMTextNode *>(node);
}

/** Generate a text node from a string and add/insert it as a child (see AddChild(node,i)). */
void
DOMNode::AddTextChild(const std::string & text, IdentifierType i)
{
  auto tnode = DOMTextNode::New();
  tnode->SetText(text);
  this->AddChild((DOMTextNode *)tnode, i);
}

/** Generate a text node from a string and add/insert in front of all children. */
void
DOMNode::AddTextChildAtBegin(const std::string & text)
{
  auto tnode = DOMTextNode::New();
  tnode->SetText(text);
  this->AddChildAtBegin((DOMTextNode *)tnode);
}

/** Generate a text node from a string and add/insert at the end of all children. */
void
DOMNode::AddTextChildAtEnd(const std::string & text)
{
  auto tnode = DOMTextNode::New();
  tnode->SetText(text);
  this->AddChildAtEnd((DOMTextNode *)tnode);
}

/** Generate a text node from a string and replace a child with it (see SetChild(node,i). */
void
DOMNode::SetTextChild(const std::string & text, IdentifierType i)
{
  auto tnode = DOMTextNode::New();
  tnode->SetText(text);
  this->SetChild((DOMTextNode *)tnode, i);
}

} // namespace itk
