/*=========================================================================

  Program:   ParaView
  Module:    vtkProxyDefinitionManager.cxx

  Copyright (c) Kitware, Inc.
  All rights reserved.
  See Copyright.txt or http://www.paraview.org/HTML/Copyright.html for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/
#include "vtkProxyDefinitionManager.h"

#include "vtkLogger.h"
#include "vtkNew.h"
#include "vtkObjectFactory.h"
#include "vtkPVXMLElement.h"
#include "vtkPVXMLParser.h"
#include "vtkSMPropertyTypes.h"
#include "vtkSMProxyDefinitionsIterator.h"
#include "vtkSmartPointer.h"

#include <exception>
#include <map>
#include <mutex>
#include <set>
#include <sstream>
#include <thread>
#include <vector>

class vtkProxyDefinitionManager::vtkInternals
{
public:
  const std::thread::id OwnerTID{ std::this_thread::get_id() };
  std::recursive_mutex Mutex;
  std::map<std::string, std::map<std::string, std::shared_ptr<pugi::xml_document>>> Definitions;

  // TODO: ASYNC ParaView master sets `ReplaceOverrideInParent` = false when loading plugins.
  bool ReplaceOverrideInParent{ true };

  std::shared_ptr<pugi::xml_document> Find(const std::string& group, const std::string& name)
  {
    auto it1 = this->Definitions.find(group);
    if (it1 != this->Definitions.end())
    {
      auto it2 = it1->second.find(name);
      if (it2 != it1->second.end())
      {
        return it2->second;
      }
    }
    return nullptr;
  }

  void AddDefinition(
    const std::string& group, const std::string& name, const pugi::xml_node& element)
  {
    if (strcmp(element.name(), "Extension") == 0)
    {
      auto coreXML = this->Find(group, name);
      if (!coreXML)
      {
        vtkLogF(ERROR, "Extension specified for unknown proxy type (%s, %s). Skipping",
          group.c_str(), name.c_str());
        return;
      }

      // we cannot modify the XML directly for thread safety. we need to create
      // a clone.

      std::shared_ptr<pugi::xml_document> doc = std::make_shared<pugi::xml_document>();
      auto clone = doc->append_copy(*coreXML);
      for (auto child : element.children())
      {
        clone.append_copy(child);
      }
      // replace previous definition
      this->Definitions[group][name] = doc;
    }
    else
    {
      std::shared_ptr<pugi::xml_document> doc = std::make_shared<pugi::xml_document>();
      doc->append_copy(element);
      this->Definitions[group][name] = doc;
    }
  }

  static void ExtractMetaInformation(const pugi::xml_node& proxy,
    std::map<std::string, std::shared_ptr<pugi::xml_node>>& propertyMap)
  {
    auto propertyTypeNames = vtkSMPropertyTypes::GetPropertyClassNames();

    for (const auto& child : proxy.children())
    {
      std::string className = std::string("vtkSM") + child.name();
      if (!child || !propertyTypeNames.count(className))
      {
        continue;
      }
      auto exposedName = child.attribute("exposed_name");
      auto givenName = child.attribute("name");
      if (!exposedName.empty())
      {
        propertyMap[exposedName.as_string()] = std::make_shared<pugi::xml_node>(child);
      }
      else if (!givenName.empty())
      {
        propertyMap[givenName.as_string()] = std::make_shared<pugi::xml_node>(child);
      }
      else
      {
        vtkLogF(
          ERROR, "Child does not have a given name or exposed name. Abort for debugging purpose.");
        std::terminate();
      }
    }
  }

  /**
   * Carefully merge properties from `source` and `dest` into `dest`.
   * When `source` overrides `dest`, the properties are moved from
   * `source` -> `dest` replacing any identically named properties in
   * `source`.
   *
   * @warning This method aborts when there are any conflicts since
   * there really is no sane way to handle merge conflicts automatically.
   *
   */
  void MergeProperties(
    pugi::xml_node& source, pugi::xml_node& dest, std::set<std::string>& overridenProperties)
  {
    vtkLogScopeF(TRACE, "%s", __func__);
    // meta-data of dest.
    std::map<std::string, std::shared_ptr<pugi::xml_node>> dstProperties;
    vtkInternals::ExtractMetaInformation(dest, dstProperties);

    // meta-data of source.
    std::map<std::string, std::shared_ptr<pugi::xml_node>> srcProperties;
    vtkInternals::ExtractMetaInformation(source, srcProperties);

    // Look for property conflicts between properties from `source` and `dest`.
    // When there's a conflict, `source` can override `dest` properties if
    // xml author specified `override=1` on the src property or on the src proxy.
    for (const auto& dstPropertyIter : dstProperties)
    {
      const auto name = dstPropertyIter.first;
      const auto properties = dstPropertyIter.second;
      if (!srcProperties.count(name))
      {
        // nothing to do.
        continue;
      }

      auto srcProperty = srcProperties[name];
      auto srcPropertyOverrideFlag = srcProperty->attribute("override");
      auto srcProxyOverrideFlag = srcProperty->parent().attribute("override");

      bool replace = this->ReplaceOverrideInParent;
      replace &= ((!srcPropertyOverrideFlag.empty() && srcPropertyOverrideFlag.as_int() == 1) ||
        (!srcProxyOverrideFlag.empty() && srcProxyOverrideFlag.as_int() == 1));

      if (replace)
      {
        // Replace overriden proxy definition in `dest` with the new one from `source`
        auto& dstProperty = dstProperties[name];
        dest.remove_child(*dstProperty);
        dest.append_copy(*srcProperty);
        // store the old property from src so we can skip copying it later.
        overridenProperties.insert(name);
      }
      else
      {
        // carelessly written child proxy. let's give tips and abort.
        vtkLogF(ERROR,
          "\n###############################################################################\n"
          "The property `%s` in proxy `%s` conflicts with another identical property in\n"
          "the hierarchy. Please use `override=\"1\"` to clarify intent to override.\n"
          "Abort for debugging purposes.\n"
          "###############################################################################",
          name.c_str(), source.attribute("name").as_string());
        std::terminate();
      }
    }
  }

  /**
   * The documentation of `source` overrides the documentation of destination.
   */
  void MergeDocumentation(pugi::xml_node& source, pugi::xml_node& dest)
  {
    vtkLogScopeF(TRACE, "%s", __func__);
    if (!source.child("Documentation").empty() && !dest.child("Documentation").empty())
    {
      dest.remove_child("Documentation");
    }
  }

  /**
   * Convenient method to copy all children from source into destination.
   * Use `skip` to provide a set of names not to be copied.
   */
  void CopyChildren(pugi::xml_node& source, pugi::xml_node& dest, std::set<std::string>& skip)
  {
    vtkLogScopeF(TRACE, "%s", __func__);
    for (const auto& srcChild : source.children())
    {
      if (skip.count(srcChild.attribute("name").value()))
      {
        // for properties <Property name="propname"/>
        continue;
      }
      if (skip.count(srcChild.name()))
      {
        // generic skip
        continue;
      }
      dest.append_copy(srcChild);
    }
  }

  /**
   * Convenient method to copy all attributes from source into destination.
   * Use `skip` to provide a set of attributes not to be copied.
   */
  void CopyAttributes(pugi::xml_node& source, pugi::xml_node& dest, std::set<std::string>& skip)
  {
    vtkLogScopeF(TRACE, "%s", __func__);
    for (const auto& srcAttr : source.attributes())
    {
      if (skip.count(srcAttr.name()))
      {
        continue;
      }
      auto destAttr = dest.append_attribute(srcAttr.name());
      destAttr.set_value(srcAttr.value());
    }
  }

  /**
   * Convert the hierarchial arrangement into a linear assortment of properties.
   * The hierarchy should be ordered such that the root base class is the last element.
   */
  pugi::xml_document FlattenTree(const std::vector<std::shared_ptr<pugi::xml_node>>& hierarchy)
  {
    vtkLogScopeF(TRACE, "%s", __func__);
    auto flatDoc = pugi::xml_document();
    auto dest = flatDoc.append_child();
    std::set<std::string> overridenProperties;

    for (auto it = hierarchy.rbegin(); it != hierarchy.rend(); ++it)
    {
      auto proxy = (*it);
      auto& source = *proxy;
      vtkLogF(TRACE, "Processing (%s) in hierarchy", proxy->attribute("name").as_string());
      dest.set_name(source.name());
      this->MergeProperties(source, dest, overridenProperties);
      this->MergeDocumentation(source, dest);
      for (const auto& prop : overridenProperties)
      {
        vtkLogF(TRACE, "Overriden property: %s", prop.c_str());
      }
      this->CopyChildren(source, dest, overridenProperties);
      overridenProperties.clear();
    }
    if (!hierarchy.empty() && (hierarchy.front() != nullptr))
    {
      std::set<std::string> toSkip = { "base_proxygroup", "base_proxyname" };
      auto realDefinition = *(hierarchy.front());
      this->CopyAttributes(realDefinition, dest, toSkip);
    }
    return flatDoc;
  }
};

vtkStandardNewMacro(vtkProxyDefinitionManager);
//----------------------------------------------------------------------------
vtkProxyDefinitionManager::vtkProxyDefinitionManager()
  : Internals(new vtkProxyDefinitionManager::vtkInternals())
{
}

//----------------------------------------------------------------------------
vtkProxyDefinitionManager::~vtkProxyDefinitionManager() = default;

//----------------------------------------------------------------------------
std::shared_ptr<pugi::xml_document> vtkProxyDefinitionManager::FindProxy(
  const std::string& group, const std::string& name) const
{
  auto& internals = (*this->Internals);
  std::lock_guard<std::recursive_mutex> lk(internals.Mutex);

  std::shared_ptr<pugi::xml_document> doc = internals.Find(group, name);
  if (doc == nullptr)
  {
    vtkLogF(TRACE, "Failed to find proxy definition (%s, %s)", group.c_str(), name.c_str());
    return nullptr;
  }
  else
  {
    vtkLogF(TRACE, "Found proxy definition (%s, %s)", group.c_str(), name.c_str());
  }

  auto originalDefinition = doc->first_child();
  auto baseGroup = originalDefinition.attribute("base_proxygroup");
  auto baseName = originalDefinition.attribute("base_proxyname");
  if (baseGroup.empty() || baseName.empty())
  {
    return doc;
  }

  // let's accumulate proxies into a hierarchy - if it exists.
  std::vector<std::shared_ptr<pugi::xml_node>> classHierarchy;
  int depth = 0;
  while (true)
  {
    classHierarchy.push_back(std::make_shared<pugi::xml_node>(originalDefinition));
    if (baseGroup.empty() || baseName.empty())
    {
      break;
    }
    vtkLogF(TRACE, "Found base(%s, %s)|depth=%d", baseGroup.value(), baseName.value(), depth++);

    auto baseDoc = this->FindProxy(baseGroup.as_string(), baseName.as_string());

    if (baseDoc == nullptr)
    {
      vtkLogF(ERROR,
        "Failed to locate base proxy definition (%s, %s). Abort for debugging purpose.",
        baseGroup.as_string(), baseName.as_string());
      std::terminate();
    }
    originalDefinition = baseDoc->first_child();
    baseGroup = originalDefinition.attribute("base_proxygroup");
    baseName = originalDefinition.attribute("base_proxyname");
  }

  auto flatDoc = std::make_shared<pugi::xml_document>(internals.FlattenTree(classHierarchy));
  internals.Definitions[group][name] = flatDoc;
  return internals.Definitions[group][name];
}

//----------------------------------------------------------------------------
bool vtkProxyDefinitionManager::LoadConfigurationXML(const pugi::xml_node& root)
{
  if (!root)
  {
    return false;
  }
  if (!root.name() || strcmp(root.name(), "ServerManagerConfiguration") != 0)
  {
    // find nested ServerManagerConfiguration element and process that.
    return this->LoadConfigurationXML(root.child("ServerManagerConfiguration"));
  }

  auto& internals = (*this->Internals);
  std::unique_lock<std::recursive_mutex> lk(internals.Mutex);

  // Loop over the top-level elements: `ProxyGroup`
  for (pugi::xml_node group : root.children("ProxyGroup"))
  {
    const std::string groupName = group.attribute("name").as_string();

    // Loop over the top-level elements.
    for (pugi::xml_node proxy : group.children())
    {
      const std::string proxyName = proxy.attribute("name").as_string();
      if (!proxyName.empty())
      {
        internals.AddDefinition(groupName, proxyName, proxy);
      }
    }
  }

  lk.unlock(); // must unlock before firing the event.

  this->InvokeEvent(vtkProxyDefinitionManager::ProxyDefinitionsUpdated);
  return true;
}

//----------------------------------------------------------------------------
bool vtkProxyDefinitionManager::LoadConfigurationXML(const std::string& xmlcontents)
{
  pugi::xml_document doc;
  bool success = doc.load_buffer(xmlcontents.c_str(), xmlcontents.length());
  return success ? this->LoadConfigurationXML(doc) : false;
}

//----------------------------------------------------------------------------
vtkSMProxyDefinitionsRange vtkProxyDefinitionManager::GetDefinitions(const std::string& groupName)
{
  auto& internals = (*this->Internals);
  return vtkSMProxyDefinitionsRange(internals.Definitions, groupName);
}

//----------------------------------------------------------------------------
vtkSmartPointer<vtkPVXMLElement> vtkProxyDefinitionManager::FindProxyLegacy(
  const std::string& group, const std::string& name) const
{
  auto xmlDoc = this->FindProxy(group, name);
  if (xmlDoc)
  {
    std::ostringstream stream;
    xmlDoc->save(stream);
    return vtkPVXMLParser::ParseXML(stream.str().c_str());
  }

  return nullptr;
}

//----------------------------------------------------------------------------
void vtkProxyDefinitionManager::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);
}
