//=========================================================================
//  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/widgets/pqSMTKAttributeItemWidgetP.h"
#include "smtk/session/aeva/plugin/pqProportionalEditPropertyWidget.h"

#include "smtk/attribute/DoubleItem.h"
#include "smtk/attribute/GroupItem.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 "vtkPVXMLElement.h"
#include "vtkSMNewWidgetRepresentationProxy.h"
#include "vtkSMPropertyGroup.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMProxy.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

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::DoubleItemPtr> 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);

  return widget != nullptr;
}

void pqSMTKProportionalEditItemWidget::updateItemFromWidgetInternal()
{
  vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
  std::vector<smtk::attribute::DoubleItemPtr> 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;
  vtkVector3d point;
  vtkVector3d displacement;
  vtkVector3d direction;

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

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

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

  // Translate widget values to item values and fetch current item values:
  curR = *items[0]->begin();
  curPt = vtkVector3d(&(*items[1]->begin()));
  curDisp = vtkVector3d(&(*items[2]->begin()));
  if (binding == ItemBindings::CylinderRadiusPointDisplacementDirection)
  {
    vtkSMPropertyHelper DirHelper(widget, "Projection");
    DirHelper.Get(direction.GetData(), 3);
    curDir = vtkVector3d(&(*items[3]->begin()));
  }
  else
  {
    // do nothing
  }
  switch (binding)
  {
    case ItemBindings::SphereRadiusPointDisplacement:
      if (curPt != point || curDisp != displacement || curR != r)
      {
        didChange = true;
        items[0]->setValue(r);
        items[1]->setValues(point.GetData(), point.GetData() + 3);
        items[2]->setValues(displacement.GetData(), displacement.GetData() + 3);
      }
      break;
    case ItemBindings::CylinderRadiusPointDisplacementDirection:
      if (curPt != point || curDisp != displacement || curR != r || curDir != direction)
      {
        didChange = true;
        items[0]->setValue(r);
        items[1]->setValues(point.GetData(), point.GetData() + 3);
        items[2]->setValues(displacement.GetData(), displacement.GetData() + 3);
        items[3]->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::DoubleItemPtr> 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 = items[0]->value(0);
  vtkVector3d point(&(*items[1]->begin()));
  vtkVector3d displacement(&(*items[2]->begin()));
  vtkSMPropertyHelper(widget, "InfluenceRadius").Set(&radius, 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(&(*items[3]->begin()));
      vtkSMPropertyHelper(widget, "Projection").Set(projection.GetData(), 3);
    }
    break;
    case ItemBindings::Invalid:
    default:
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "Unhandled item binding.");
    }
    break;
  }
}

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::DoubleItemPtr>& 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() < 3)
  {
    smtkErrorMacro(
      smtk::io::Logger::instance(), "Expected a group item with 1 group of 3 or more items.");
    return false;
  }

  // Find items in the group based on names in the configuration info:
  std::string influenceRadiusItemName;
  std::string anchorPointItemName;
  std::string displacementItemName;
  std::string projectionItemName;
  if (!m_itemInfo.component().attribute("InfluenceRadius", influenceRadiusItemName))
  {
    influenceRadiusItemName = "InfluenceRadius";
  }
  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 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 && anchorPointItem &&
    anchorPointItem->numberOfValues() == 3 && displacementItem &&
    displacementItem->numberOfValues() == 3)
  {
    items.push_back(influenceRadiusItem);
    items.push_back(anchorPointItem);
    items.push_back(displacementItem);
    if (!projectionItem)
    {
      binding = ItemBindings::SphereRadiusPointDisplacement;
      return true;
    }
    else if (projectionItem->numberOfValues() == 3)
    {
      items.push_back(projectionItem);
      binding = ItemBindings::CylinderRadiusPointDisplacementDirection;
      return true;
    }
  }

  binding = ItemBindings::Invalid;
  return false;
}
