//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt 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 "smtk/session/aeva/plugin/pqSMTKProportionalEditItemWidget.h"

#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKResource.h"
#include "smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h"
#include "smtk/session/aeva/Operation.h"
#include "smtk/session/aeva/Resource.h"
#include "smtk/session/aeva/Session.h"
#include "smtk/session/aeva/plugin/pqProportionalEditPropertyWidget.h"

#include "smtk/attribute/DoubleItem.h"
#include "smtk/attribute/GroupItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/ValueItem.h"

#include "smtk/io/Logger.h"

#include "pqActiveObjects.h"
#include "pqApplicationCore.h"
#include "pqDataRepresentation.h"
#include "pqImplicitPlanePropertyWidget.h"
#include "pqObjectBuilder.h"
#include "pqPipelineSource.h"
#include "pqServer.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkPVXMLElement.h"
#include "vtkPolyData.h"
#include "vtkSMNewWidgetRepresentationProxy.h"
#include "vtkSMPropertyGroup.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMProxy.h"
#include "vtkSMSourceProxy.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#ifdef ERROR
#undef ERROR
#endif

using qtItem = smtk::extension::qtItem;
using qtAttributeItemInfo = smtk::extension::qtAttributeItemInfo;

pqSMTKProportionalEditItemWidget::pqSMTKProportionalEditItemWidget(
  const smtk::extension::qtAttributeItemInfo& info,
  Qt::Orientation orient)
  : pqSMTKAttributeItemWidget(info, orient)
  , m_projection(false)
{
  this->createWidget();
}

pqSMTKProportionalEditItemWidget::~pqSMTKProportionalEditItemWidget() = default;

qtItem* pqSMTKProportionalEditItemWidget::createSphereItemWidget(const qtAttributeItemInfo& info)
{
  return new pqSMTKProportionalEditItemWidget(info);
}

qtItem* pqSMTKProportionalEditItemWidget::createCylinderItemWidget(const qtAttributeItemInfo& info)
{
  auto* item = new pqSMTKProportionalEditItemWidget(info);
  item->setProjectionEnabled(true);
  return item;
}

bool pqSMTKProportionalEditItemWidget::createProxyAndWidget(vtkSMProxy*& proxy,
  pqInteractivePropertyWidget*& widget)
{
  ItemBindings binding;
  std::vector<smtk::attribute::ValueItemPtr> items;
  bool haveItems = this->fetchProportionalEditItems(binding, items);
  if (!haveItems || binding == ItemBindings::Invalid)
  {
    smtkErrorMacro(smtk::io::Logger::instance(), "Could not find items for widget.");
    return false;
  }

  // I. Create the ParaView widget and a proxy for its representation.
  pqApplicationCore* paraViewApp = pqApplicationCore::instance();
  pqServer* server = paraViewApp->getActiveServer();
  pqObjectBuilder* builder = paraViewApp->getObjectBuilder();

  proxy = builder->createProxy("implicit_functions", "ProportionalEditFilter", server, "");
  if (!proxy)
  {
    return false;
  }
  auto* proportionalEditWidget =
    new pqProportionalEditPropertyWidget(proxy, proxy->GetPropertyGroup(0));
  proportionalEditWidget->setProjectionEnabled(m_projection);
  widget = proportionalEditWidget;

  // II. Initialize the properties.
  m_p->m_pvwidget = widget;
  this->updateWidgetFromItem();
  auto* widgetProxy = widget->widgetProxy();
  widgetProxy->UpdateVTKObjects();
  // vtkSMPropertyHelper(widgetProxy, "RotationEnabled").Set(false);
  QObject::connect(
    proportionalEditWidget, SIGNAL(changeAvailable()), this, SLOT(updateItemFromWidget()));

  return widget != nullptr;
}

void pqSMTKProportionalEditItemWidget::updateItemFromWidgetInternal()
{
  vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
  std::vector<smtk::attribute::ValueItemPtr> items;
  ItemBindings binding;
  if (!this->fetchProportionalEditItems(binding, items))
  {
    smtkErrorMacro(smtk::io::Logger::instance(),
      "Item widget has an update but the item(s) do not exist or are not sized properly.");
    return;
  }

  // Values held by widget
  double r;
  int projectionEnabled;
  vtkVector3d point;
  vtkVector3d displacement;
  vtkVector3d direction;

  vtkSMPropertyHelper rHelper(widget, "InfluenceRadius");
  vtkSMPropertyHelper projEnaHelper(widget, "ProjectionEnabled");
  vtkSMPropertyHelper ptHelper(widget, "AnchorPoint");
  vtkSMPropertyHelper dispHelper(widget, "Displacement");

  rHelper.Get(&r, 1);
  projEnaHelper.Get(&projectionEnabled, 1);
  ptHelper.Get(point.GetData(), 3);
  dispHelper.Get(displacement.GetData(), 3);
  bool didChange = false;

  // Current values held in items:
  double curR;
  int curProjEna;
  vtkVector3d curPt;
  vtkVector3d curDisp;
  vtkVector3d curDir;

  // Translate widget values to item values and fetch current item values:
  curR = *std::static_pointer_cast<smtk::attribute::DoubleItem>(items[0])->begin();
  curProjEna = *std::static_pointer_cast<smtk::attribute::IntItem>(items[1])->begin();
  curPt =
    vtkVector3d(&(*(std::static_pointer_cast<smtk::attribute::DoubleItem>(items[2])->begin())));
  curDisp =
    vtkVector3d(&(*(std::static_pointer_cast<smtk::attribute::DoubleItem>(items[3])->begin())));
  if (binding == ItemBindings::CylinderRadiusPointDisplacementDirection)
  {
    vtkSMPropertyHelper DirHelper(widget, "Projection");
    DirHelper.Get(direction.GetData(), 3);
    curDir =
      vtkVector3d(&(*(std::static_pointer_cast<smtk::attribute::DoubleItem>(items[4])->begin())));
  }
  else
  {
    // do nothing
  }
  switch (binding)
  {
    case ItemBindings::SphereRadiusPointDisplacement:
      if (curPt != point || curDisp != displacement || curR != r || curProjEna != projectionEnabled)
      {
        didChange = true;
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[0])->setValue(r);
        std::static_pointer_cast<smtk::attribute::IntItem>(items[1])->setValue(projectionEnabled);
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[2])->setValues(
          point.GetData(), point.GetData() + 3);
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[3])->setValues(
          displacement.GetData(), displacement.GetData() + 3);
      }
      break;
    case ItemBindings::CylinderRadiusPointDisplacementDirection:
      if (curPt != point || curDisp != displacement || curR != r || curDir != direction ||
        curProjEna != projectionEnabled)
      {
        didChange = true;
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[0])->setValue(r);
        std::static_pointer_cast<smtk::attribute::IntItem>(items[1])->setValue(projectionEnabled);
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[2])->setValues(
          point.GetData(), point.GetData() + 3);
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[3])->setValues(
          displacement.GetData(), displacement.GetData() + 3);
        std::static_pointer_cast<smtk::attribute::DoubleItem>(items[4])->setValues(
          direction.GetData(), direction.GetData() + 3);
      }
      break;
    case ItemBindings::Invalid:
    default:
      smtkErrorMacro(smtk::io::Logger::instance(), "Unable to determine item binding.");
      break;
  }

  if (didChange)
  {
    emit modified();
  }
}

void pqSMTKProportionalEditItemWidget::updateWidgetFromItemInternal()
{
  vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
  std::vector<smtk::attribute::ValueItemPtr> items;
  ItemBindings binding;
  if (!this->fetchProportionalEditItems(binding, items))
  {
    smtkErrorMacro(smtk::io::Logger::instance(),
      "Item signaled an update but the item(s) do not exist or are not sized properly.");
    return;
  }

  // Unlike updateItemFromWidget, we don't care if we cause ParaView an unnecessary update;
  // we might cause an extra render but we won't accidentally mark a resource as modified.
  // Since there's no need to compare new values to old, this is simpler than updateItemFromWidget:

  double radius = *std::static_pointer_cast<smtk::attribute::DoubleItem>(items[0])->begin();
  int projectionEnabled = *std::static_pointer_cast<smtk::attribute::IntItem>(items[1])->begin();
  vtkVector3d point(&(*(std::static_pointer_cast<smtk::attribute::DoubleItem>(items[2])->begin())));
  vtkVector3d displacement(
    &(*(std::static_pointer_cast<smtk::attribute::DoubleItem>(items[3])->begin())));

  vtkSMPropertyHelper(widget, "InfluenceRadius").Set(&radius, 1);
  vtkSMPropertyHelper(widget, "ProjectionEnabled").Set(&projectionEnabled, 1);
  vtkSMPropertyHelper(widget, "AnchorPoint").Set(point.GetData(), 3);
  vtkSMPropertyHelper(widget, "Displacement").Set(displacement.GetData(), 3);
  switch (binding)
  {
    case ItemBindings::SphereRadiusPointDisplacement:
      break;
    case ItemBindings::CylinderRadiusPointDisplacementDirection:
    {
      vtkVector3d projection(
        &(*(std::static_pointer_cast<smtk::attribute::DoubleItem>(items[4])->begin())));
      vtkSMPropertyHelper(widget, "Projection").Set(projection.GetData(), 3);
    }
    break;
    case ItemBindings::Invalid:
    default:
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "Unhandled item binding.");
    }
    break;
  }

  // Also pass the geometry input to the widget rep, so the modification can be previewed.
  auto assoc = this->item()->attribute()->associations();
  if (assoc)
  {
    smtk::model::EntityPtr input = assoc->valueAs<smtk::model::Entity>();
    if (input)
    {
      auto resource = input->resource();
      auto uuid = input->id();
      auto* behavior = pqSMTKBehavior::instance();
      pqSMTKResource* pvResource = behavior->getPVResource(resource);
      vtkSMSourceProxy* resourceProxy = pvResource ? pvResource->getSourceProxy() : nullptr;
      if (!pvResource || !resourceProxy)
      {
        smtkErrorMacro(smtk::io::Logger::instance(), "Could not fetch surface proxy.");
        return;
      }
      vtkSMPropertyHelper(widget, "Surface").Set(resourceProxy);
      vtkSMPropertyHelper(widget, "SurfaceID").Set(uuid.toString().c_str());
    }
  }
}

bool pqSMTKProportionalEditItemWidget::setProjectionEnabled(bool isProjectionEnabled)
{
  if (m_projection == isProjectionEnabled)
  {
    return false;
  }

  m_projection = isProjectionEnabled;
  if (m_p->m_pvwidget)
  {
    auto* widget = reinterpret_cast<pqProportionalEditPropertyWidget*>(m_p->m_pvwidget);
    widget->setProjectionEnabled(m_projection);
  }
  return true;
}

bool pqSMTKProportionalEditItemWidget::fetchProportionalEditItems(ItemBindings& binding,
  std::vector<smtk::attribute::ValueItemPtr>& items)
{
  items.clear();

  // Check to see if item is a group containing items of double-vector items.
  auto groupItem = m_itemInfo.itemAs<smtk::attribute::GroupItem>();
  if (!groupItem || groupItem->numberOfGroups() < 1 || groupItem->numberOfItemsPerGroup() < 4)
  {
    smtkErrorMacro(
      smtk::io::Logger::instance(), "Expected a group item with 1 group of 4 or more items.");
    return false;
  }

  // Find items in the group based on names in the configuration info:
  std::string influenceRadiusItemName;
  std::string projectionEnabledItemName;
  std::string anchorPointItemName;
  std::string displacementItemName;
  std::string projectionItemName;

  if (!m_itemInfo.component().attribute("InfluenceRadius", influenceRadiusItemName))
  {
    influenceRadiusItemName = "InfluenceRadius";
  }
  if (!m_itemInfo.component().attribute("ProjectionEnabled", projectionEnabledItemName))
  {
    projectionItemName = "ProjectionEnabled";
  }
  if (!m_itemInfo.component().attribute("AnchorPoint", anchorPointItemName))
  {
    anchorPointItemName = "AnchorPoint";
  }
  if (!m_itemInfo.component().attribute("Displacement", displacementItemName))
  {
    displacementItemName = "Displacement";
  }
  if (!m_itemInfo.component().attribute("Projection", projectionItemName))
  {
    projectionItemName = "Projection";
  }

  auto influenceRadiusItem =
    groupItem->findAs<smtk::attribute::DoubleItem>(influenceRadiusItemName);
  auto projectionEnabledItem =
    groupItem->findAs<smtk::attribute::IntItem>(projectionEnabledItemName);
  auto anchorPointItem = groupItem->findAs<smtk::attribute::DoubleItem>(anchorPointItemName);
  auto displacementItem = groupItem->findAs<smtk::attribute::DoubleItem>(displacementItemName);
  auto projectionItem = groupItem->findAs<smtk::attribute::DoubleItem>(projectionItemName);

  if (influenceRadiusItem && influenceRadiusItem->numberOfValues() == 1 && projectionEnabledItem &&
    projectionEnabledItem->numberOfValues() == 1 && anchorPointItem &&
    anchorPointItem->numberOfValues() == 3 && displacementItem &&
    displacementItem->numberOfValues() == 3)
  {
    items.push_back(influenceRadiusItem);
    items.push_back(projectionEnabledItem);
    items.push_back(anchorPointItem);
    items.push_back(displacementItem);
    if (!projectionItem)
    {
      binding = ItemBindings::SphereRadiusPointDisplacement;
      return true;
    }
    if (projectionItem->numberOfValues() == 3)
    {
      items.push_back(projectionItem);
      binding = ItemBindings::CylinderRadiusPointDisplacementDirection;
      return true;
    }
  }

  binding = ItemBindings::Invalid;
  return false;
}
