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

  Program:   ParaView
  Module:    vtkPTSDefinitionManager.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 "vtkPTSDefinitionManager.h"

#include "vtkClientSession.h"
#include "vtkCollection.h"
#include "vtkLayoutGenerator.h"
#include "vtkObjectFactory.h"
#include "vtkPropertyWidgetDecorator.h"
#include "vtkProxyAdapter.h"
#include "vtkProxyDefinitionManager.h"
#include "vtkRemotingCoreUtilities.h"
#include "vtkSMInputProperty.h"
#include "vtkSMProperty.h"
#include "vtkSMPropertyGroup.h"
#include "vtkSMProxy.h"
#include "vtkSMSessionProxyManager.h"

#include <exception> // for std::runtime_error

class vtkPTSDefinitionManager::vtkInternals
{
public:
  const std::thread::id OwnerTID{ std::this_thread::get_id() };

  vtkSmartPointer<vtkClientSession> Session;

  std::vector<rxcpp::subscriber<vtkPTSDefinitionManager::PrototypesT>> Subscribers;

  void UnsubscribeAll()
  {
    for (auto& subscriber : this->Subscribers)
    {
      subscriber.on_completed();
    }
  }

  ~vtkInternals() { this->UnsubscribeAll(); }

  vtkPTSDefinitionManager::PrototypesT GetPrototypes(const std::string& groupName = "") const
  {
    if (!this->Session)
    {
      return {};
    }

    vtkPTSDefinitionManager::PrototypesT result;
    auto* pxm = this->Session->GetProxyManager();
    auto* dmgr = this->Session->GetProxyDefinitionManager();
    for (const auto& item : dmgr->GetDefinitions(groupName))
    {
      if (auto* prototype = pxm->GetPrototypeProxy(item.GetGroup().c_str(), item.GetName().c_str()))
      {
        result.push_back(prototype);
      }
    }

    return result;
  }
};

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

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

//-----------------------------------------------------------------------------
void vtkPTSDefinitionManager::SetSession(vtkClientSession* session)
{
  auto& internals = (*this->Internals);
  vtkRemotingCoreUtilities::EnsureThread(internals.OwnerTID);
  if (internals.Session != session)
  {
    internals.Session = session;
    auto prototypes = internals.GetPrototypes();
    for (auto& subscriber : internals.Subscribers)
    {
      subscriber.on_next(prototypes);
    }
  }
}

//-----------------------------------------------------------------------------
vtkClientSession* vtkPTSDefinitionManager::GetSession() const
{
  const auto& internals = (*this->Internals);
  vtkRemotingCoreUtilities::EnsureThread(internals.OwnerTID);
  return internals.Session;
}

//-----------------------------------------------------------------------------
vtkSmartPointer<vtkProxyAdapter> vtkPTSDefinitionManager::GetDefinition(vtkSMProxy* smproxy) const
{
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);
  auto& internals = (*this->Internals);
  if (internals.Session == nullptr)
  {
    throw std::runtime_error("Session was not set!");
  }
  auto proxyAdapter = vtk::TakeSmartPointer(vtkProxyAdapter::New());
  proxyAdapter->SetSMProxy(smproxy);

  return proxyAdapter;
}

//-----------------------------------------------------------------------------
vtkSmartPointer<vtkProxyAdapter> vtkPTSDefinitionManager::GetDefinition(
  const std::string& groupName, const std::string& proxyName) const
{
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);
  auto& internals = (*this->Internals);
  if (internals.Session == nullptr)
  {
    throw std::runtime_error("Session was not set!");
  }
  vtkSMProxy* smproxy =
    internals.Session->GetProxyManager()->GetPrototypeProxy(groupName.c_str(), proxyName.c_str());
  auto proxyAdapter = vtk::TakeSmartPointer(vtkProxyAdapter::New());
  proxyAdapter->SetSMProxy(smproxy);

  return proxyAdapter;
}
//-----------------------------------------------------------------------------
rxcpp::observable<vtkPTSDefinitionManager::PrototypesT>
vtkPTSDefinitionManager::GetDefinitionsObservable() const
{
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);

  // create a cold observable; the initial prototype listing will be generated
  // on subscription.
  auto observable = rxcpp::observable<>::create<vtkPTSDefinitionManager::PrototypesT>(
    [this](rxcpp::subscriber<vtkPTSDefinitionManager::PrototypesT> subscriber) {
      auto& internals = (*this->Internals);
      internals.Subscribers.push_back(subscriber);
      subscriber.on_next(internals.GetPrototypes());
    }).distinct_until_changed();

  return observable;
}

//-----------------------------------------------------------------------------
rxcpp::observable<vtkPTSDefinitionManager::PrototypesT>
vtkPTSDefinitionManager::GetDefinitionsObservable(const std::string& groupName) const
{
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);

  return this->GetDefinitionsObservable().transform([groupName](PrototypesT& prototypes) {
    auto iter =
      std::remove_if(prototypes.begin(), prototypes.end(), [groupName](vtkSMProxy* prototype) {
        // TODO: we can provide a variant of this map function
        // that looks up definitions in a predefined XML/JSON configuration
        // file.
        return prototype == nullptr || prototype->GetXMLGroup() == nullptr ||
          strcmp(prototype->GetXMLGroup(), groupName.c_str()) != 0;
      });
    prototypes.erase(iter, prototypes.end());
    return prototypes;
  });
}

//-----------------------------------------------------------------------------
vtkPTSDefinitionManager::PrototypesT vtkPTSDefinitionManager::GetCompatibleDefinitions(
  const std::vector<vtkSmartPointer<vtkSMProxy>>& smproxies, const std::string& groupName) const
{
  // TODO we can bundle reason for rejection returning vector<pair<prototype,string>>
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);
  auto& internals = (*this->Internals);
  if (internals.Session == nullptr)
  {
    throw std::runtime_error("Session was not set!");
  }

  if (smproxies.empty())
  {
    vtkLogF(ERROR, "No proxies provided");
    return {};
  }

  PrototypesT prototypes = internals.GetPrototypes(groupName);

  auto isProxyCompatible = [&smproxies](vtkSMProxy* prototype) {
    bool keep = true;
    auto* input = vtkSMInputProperty::SafeDownCast(prototype->GetProperty("Input"));
    if (!input)
    {
      keep = false;
    }
    // TODO: Handle case where a proxy has multiple input properties.
    if (keep && !input->GetMultipleInput() && smproxies.size() > 1)
    {
      keep = false;
    }
    if (keep)
    {
      input->RemoveAllUncheckedProxies();
      for (int i = 0; i < smproxies.size(); i++)
      {
        input->AddUncheckedInputConnection(smproxies[i], 0); // TODO support non zero output ports
      }
      if (!input->IsInDomains())
      {
        keep = false;
      }
      input->RemoveAllUncheckedProxies();
    }
#if 0
    if(!keep && prototype->GetXMLName())
    {
      vtkLogF(ERROR,"REJECTED %s",prototype->GetXMLName());
      }
    if(keep && prototype->GetXMLName())
    {
      vtkLogF(WARNING,"ACCEPTED %s",prototype->GetXMLName());
      }
#endif
    return !keep;
  };

  auto iter = std::remove_if(prototypes.begin(), prototypes.end(), isProxyCompatible);
  prototypes.erase(iter, prototypes.end());

  return prototypes;
}

//-----------------------------------------------------------------------------
vtkSmartPointer<vtkPVXMLElement> vtkPTSDefinitionManager::GetLayout(vtkSMProxy* smproxy) const
{
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);
  auto& internals = (*this->Internals);
  if (internals.Session == nullptr)
  {
    throw std::runtime_error("Session was not set!");
  }
  vtkSmartPointer<vtkPVXMLElement> xml = vtkLayoutGenerator::GetLayout(smproxy);

  return xml;
}

//-----------------------------------------------------------------------------
vtkSmartPointer<vtkPVXMLElement> vtkPTSDefinitionManager::GetLayout(
  const std::string& groupName, const std::string& proxyName) const
{
  vtkRemotingCoreUtilities::EnsureThread(this->Internals->OwnerTID);
  auto& internals = (*this->Internals);
  if (internals.Session == nullptr)
  {
    throw std::runtime_error("Session was not set!");
  }
  vtkSMProxy* smproxy =
    internals.Session->GetProxyManager()->GetPrototypeProxy(groupName.c_str(), proxyName.c_str());

  return this->GetLayout(smproxy);
}
//-----------------------------------------------------------------------------
std::vector<vtkSmartPointer<vtkPropertyWidgetDecorator>>
vtkPTSDefinitionManager::GetPropertyDecorators(
  vtkSMProxy* proxy, const std::string& propertyName) const
{
  if (proxy == nullptr)
  {
    vtkLogF(ERROR, " proxy is nullptr !");
    return {};
  }
  vtkSMProperty* property = proxy->GetProperty(propertyName.c_str());

  if (property == nullptr)
  {
    vtkLogF(ERROR, "could not locaty property '%s' in proxy '%s'", proxy->GetXMLName(),
      propertyName.c_str());
    return {};
  }

  std::vector<vtkSmartPointer<vtkPropertyWidgetDecorator>> result;

  if (auto* hints = property->GetHints())
  {
    vtkNew<vtkCollection> collection;
    hints->FindNestedElementByName("PropertyWidgetDecorator", collection.GetPointer());
    for (int cc = 0; cc < collection->GetNumberOfItems(); cc++)
    {
      vtkPVXMLElement* elem = vtkPVXMLElement::SafeDownCast(collection->GetItemAsObject(cc));
      if (elem && elem->GetAttribute("type"))
      {
        result.emplace_back(vtkPropertyWidgetDecorator::create(elem, proxy));
      }
    }
  }
  return result;
}

//-----------------------------------------------------------------------------
std::vector<vtkSmartPointer<vtkPropertyWidgetDecorator>>
vtkPTSDefinitionManager::GetPropertyGroupDecorators(
  vtkSMProxy* proxy, const std::string& propertyGroupName) const
{
  if (proxy == nullptr)
  {
    vtkLogF(ERROR, " proxy is nullptr !");
    return {};
  }
  size_t n = proxy->GetNumberOfPropertyGroups();
  vtkSMPropertyGroup* group = nullptr;
  for (size_t i = 0; i < proxy->GetNumberOfPropertyGroups(); i++)
  {
    if (propertyGroupName == proxy->GetPropertyGroup(i)->GetName())
    {
      group = proxy->GetPropertyGroup(i);
      break;
    }
  }

  if (group == nullptr)
  {
    vtkLogF(ERROR, "could not locate group '%s' in proxy '%s'", proxy->GetXMLName(),
      propertyGroupName.c_str());
    return {};
  }

  std::vector<vtkSmartPointer<vtkPropertyWidgetDecorator>> result;

  if (auto* hints = group->GetHints())
  {
    vtkNew<vtkCollection> collection;
    hints->FindNestedElementByName("PropertyWidgetDecorator", collection.GetPointer());
    for (int cc = 0; cc < collection->GetNumberOfItems(); cc++)
    {
      vtkPVXMLElement* elem = vtkPVXMLElement::SafeDownCast(collection->GetItemAsObject(cc));
      if (elem && elem->GetAttribute("type"))
      {
        result.emplace_back(vtkPropertyWidgetDecorator::create(elem, proxy));
      }
    }
  }
  return result;
}

//-----------------------------------------------------------------------------
void vtkPTSDefinitionManager::UnsubscribeAll()
{
  this->Internals->UnsubscribeAll();
}

//-----------------------------------------------------------------------------
void vtkPTSDefinitionManager::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);
  const auto& internals = (*this->Internals);
  os << indent << "Session: " << internals.Session << "\n";
}
