#include "vtkLayoutGenerator.h"
#include "vtkCollection.h"
#include "vtkPVXMLElement.h"
#include "vtkSMOrderedPropertyIterator.h"
#include "vtkSMProperty.h"
#include "vtkSMPropertyGroup.h"
#include "vtkSMProxy.h"

#include <list>
#include <map>
#include <string>
#include <utility>

#include <vtk_pugixml.h>
namespace
{
std::string get_group_label(vtkSMPropertyGroup* smgroup)
{
  assert(smgroup != nullptr);
  auto label = smgroup->GetXMLLabel();
  if (label && label[0] != '\0')
  {
    return std::string(label);
  }
  // generate a unique string.
  std::ostringstream str;
  str << "__smgroup:" << smgroup;
  return str.str();
}

bool skip_property(vtkSMProperty* smproperty)
{
  if (smproperty->GetIsInternal() || std::string(smproperty->GetPanelVisibility()) == "never")
  {
    return true;
  }
  return false;
}

struct vtkInternals
{

  std::map<vtkSMProperty*, vtkSMPropertyGroup*> property_2_group_map;
  std::list<std::pair<vtkSMProperty*, std::string>> ordered_properties;
  std::map<std::string, decltype(ordered_properties)::iterator> group_end_iterators;

  void PopulateInternalStructures(vtkSMProxy* smproxy)
  {
    // step 1: iterate over all groups to populate `property_2_group_map`.
    //         this will make it easier to determine if a property belong to a
    //         group.
    for (size_t index = 0, num_groups = smproxy->GetNumberOfPropertyGroups(); index < num_groups;
         ++index)
    {
      auto smgroup = smproxy->GetPropertyGroup(index);
      for (unsigned int cc = 0, num_properties = smgroup->GetNumberOfProperties();
           cc < num_properties; ++cc)
      {
        if (auto smproperty = smgroup->GetProperty(cc))
        {
          property_2_group_map[smproperty] = smgroup;
        }
      }
    }

    // step 2: iterate over all properties and build an ordered list of properties
    //         that corresponds to the order in which the widgets will be rendered.
    //         this is generally same as the order in the XML with one exception,
    //         properties in groups with same label are placed next to each other.

    vtkNew<vtkSMOrderedPropertyIterator> opiter;
    opiter->SetProxy(smproxy);
    for (opiter->Begin(); !opiter->IsAtEnd(); opiter->Next())
    {
      auto smproperty = opiter->GetProperty();
      vtkSMPropertyGroup* smgroup = nullptr;
      try
      {
        smgroup = property_2_group_map.at(smproperty);
      }
      catch (std::out_of_range&)
      {
        ordered_properties.push_back(std::make_pair(smproperty, std::string(opiter->GetKey())));
        continue;
      }

      assert(smgroup != nullptr);
      const std::string xmllabel = ::get_group_label(smgroup);
      auto geiter = group_end_iterators.find(xmllabel);
      auto insert_pos = (geiter != group_end_iterators.end()) ? std::next(geiter->second)
                                                              : ordered_properties.end();

      group_end_iterators[xmllabel] = ordered_properties.insert(
        insert_pos, std::make_pair(smproperty, std::string(opiter->GetKey())));
    }
  }
};
}

vtkSmartPointer<vtkPVXMLElement> vtkLayoutGenerator::GetLayout(vtkSMProxy* smproxy)
{
  vtkLogger::Verbosity verbosity = vtkLogger::VERBOSITY_INFO;
  vtkInternals internals;
  internals.PopulateInternalStructures(smproxy);
  auto& property_2_group_map = internals.property_2_group_map;
  auto& ordered_properties = internals.ordered_properties;
  auto& group_end_iterators = internals.group_end_iterators;

  // Handle legacy-hidden properties: previously, developers hid properties from
  // panels by adding XML in the hints section. We need to ensure that that
  // works too. So, we'll scan the hints and create a list of properties to
  // hide.
  // QSet<QString> legacyHiddenProperties;
  // DetermineLegacyHiddenProperties(legacyHiddenProperties, smproxy);

  enum class EnumState
  {
    None = 0,  //< undefined
    Custom,    //< group is using a custom widget
    Collection //< group is simply grouping multiple property widgets together
  };
  std::map<vtkSMPropertyGroup*, EnumState> group_widget_status;

  // group-widget name uniquification helper.
  std::map<std::string, int> group_widget_names;

  vtkSmartPointer<vtkPVXMLElement> layout = vtk::TakeSmartPointer(vtkPVXMLElement::New());
  layout->SetName("ui");
  std::string id = std::string(smproxy->GetXMLGroup()) + "__" + smproxy->GetXMLName();
  layout->SetAttribute("id", id.c_str());

  // keep track of groups. easier than looking each time in the xml
  std::map<std::string, vtkSmartPointer<vtkPVXMLElement>> groups2XML;

  // step 3: now iterate over the `ordered_properties` list and create widgets
  for (auto& apair : ordered_properties)
  {
    auto smproperty = apair.first;
    const std::string& smkey = apair.second;
    vtkVLogScopeF(verbosity, "create property widget for  `%s`", smkey.c_str());
    if (smproperty == nullptr ||
      ::skip_property(smproperty)) // ::skip_property(smproperty, smkey, properties,
                                   // legacyHiddenProperties, this))
    {
      continue;
    }
    vtkSMPropertyGroup* smgroup = nullptr;
    try
    {
      smgroup = property_2_group_map.at(smproperty);
    }
    catch (std::out_of_range&)
    {
      vtkVLogF(verbosity, "property '%s' does not belong to a group", smkey.c_str());
    }

    vtkVLogIfF(verbosity, smgroup != nullptr, "part of property-group with label `%s` %p",
      (smgroup->GetXMLLabel() ? smgroup->GetXMLLabel() : "(unspecified)"), smgroup);

    vtkSmartPointer<vtkPVXMLElement> groupXML = nullptr;
    if (smgroup != nullptr)
    {
      auto gwsiter = group_widget_status.find(smgroup);
      if (gwsiter != group_widget_status.end() && gwsiter->second == EnumState::Custom)
      {
        // already created a custom widget for this group, skip
        // the property.
        vtkVLogF(verbosity, "skip since already handled in custom group widget");
        continue;
      }

      if (gwsiter == group_widget_status.end())
      {
        // first time we're encountering a property from this group.
        // let's see if we're creating a custom group widget or just a
        // multi-property group.
        auto& ref_state = group_widget_status[smgroup];
        ref_state = EnumState::None;

        if (const char* customGroupWidget = smgroup->GetPanelWidget())
        {
          ref_state = EnumState::Custom;
          // we just add a custom widget for this property's group,
          // continue on to the next property.
          vtkLogF(WARNING, "Skipping property '%s' because it uses a custom group widget `%s`",
            smproperty->GetXMLName(), customGroupWidget);
          continue;
        }

        // no custom widget created for the group, must be simply a
        // multi-property group. just update the state and fall-through
        // to create a widget for the property.
        ref_state = EnumState::Collection;

        // find the xml element corresponds to this group
        auto iter = groups2XML.find(smgroup->GetXMLLabel());
        if (iter == groups2XML.end())
        {
          const std::string xmllabel = ::get_group_label(smgroup);
          groupXML = vtk::TakeSmartPointer(vtkPVXMLElement::New());
          groupXML->SetName("group");
          groupXML->SetAttribute("name", xmllabel.c_str());
          groups2XML[xmllabel] = groupXML;
          layout->AddNestedElement(groupXML);
        }
        else
        {
          groupXML = iter->second;
        }
      }
      else
      {
        groupXML = groups2XML[::get_group_label(smgroup).c_str()];
      }
    }
    auto node = vtk::TakeSmartPointer(vtkPVXMLElement::New());
    node->SetName("input");
    node->SetAttribute("name", smkey.c_str());
    if (groupXML)
    {
      groupXML->AddNestedElement(node);
    }
    else
    {
      layout->AddNestedElement(node);
    }
  }
  return layout;
}

//------------------------------------------------------------------------------
std::string vtkLayoutGenerator::XMLToString(vtkSmartPointer<vtkPVXMLElement> xml)
{
  std::stringstream stream;
  xml->PrintXML(stream, vtkIndent());
  return stream.str();
}
