//=============================================================================
// 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/io/Logger.h"

#include "smtk/session/aeva/CellSelection.h"
#include "smtk/session/aeva/operators/ProportionalEdit.h"

#include "smtk/model/Face.h"
#include "smtk/model/Model.h"

#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/ComponentItem.h"
#include "smtk/attribute/DoubleItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/VoidItem.h"

#include "smtk/geometry/queries/BoundingBox.h"

#include "smtk/operation/MarkGeometry.h"

#include "smtk/resource/Component.h"

#include <vtkDataArray.h>
#include <vtkDataObject.h>
#include <vtkDataSetSurfaceFilter.h>
#include <vtkExtractSurface.h>
#include <vtkPolyData.h>

#include "vtk/aeva/ext/vtkProportionalEditFilter.h"

#include "smtk/session/aeva/ProportionalEdit_xml.h"

// On Windows MSVC 2015+, something is included that defines
// a macro named ERROR to be 0. This causes smtkErrorMacro()
// to expand into garbage (because smtk::io::Logger::ERROR
// gets expanded to smtk::io::Logger::0).
#ifdef ERROR
#undef ERROR
#endif

namespace smtk
{
namespace session
{
namespace aeva
{

bool ProportionalEdit::ableToOperate()
{
  if (!this->Superclass::ableToOperate())
  {
    return false;
  }

  smtk::session::aeva::Resource::Ptr resource;
  smtk::session::aeva::Session::Ptr session;
  auto result = this->createResult(smtk::operation::Operation::Outcome::FAILED);
  this->prepareResourceAndSession(result, resource, session);

  smtk::model::EntityPtr input = this->parameters()->associations()->valueAs<smtk::model::Entity>();
  return input && !dynamic_cast<CellSelection*>(input.get());
}

ProportionalEdit::Result ProportionalEdit::operateInternal()
{
  // Access the associated resource and session for the operation
  smtk::session::aeva::Resource::Ptr resource;
  smtk::session::aeva::Session::Ptr session;
  auto result = this->createResult(smtk::operation::Operation::Outcome::FAILED);
  this->prepareResourceAndSession(result, resource, session);

  // Access the input surface to deform
  smtk::model::EntityPtr input = this->parameters()->associations()->valueAs<smtk::model::Entity>();

  // Access the influence radius
  smtk::attribute::DoubleItemPtr influenceRadiusItem =
    this->parameters()->findDouble("influence radius");
  double influenceRadius = influenceRadiusItem->value();

  // Access whether projection mode is enabled
  smtk::attribute::IntItemPtr projectionEnabledItem =
    this->parameters()->findInt("projection enabled");
  auto projectionEnabled = static_cast<vtkTypeBool>(projectionEnabledItem->value());

  // Access the anchor point coordinate
  smtk::attribute::DoubleItemPtr anchorPointItem = this->parameters()->findDouble("anchor point");
  std::array<double, 3> anchorPoint = {
    anchorPointItem->value(0), anchorPointItem->value(1), anchorPointItem->value(2)
  };

  // Access the displacement vector
  smtk::attribute::DoubleItemPtr displacementPointItem =
    this->parameters()->findDouble("displacement vector");
  std::array<double, 3> anchorPointDisplacement = { displacementPointItem->value(0),
    displacementPointItem->value(1),
    displacementPointItem->value(2) };

  // Access the projection direction
  smtk::attribute::DoubleItemPtr projectionDirectionItem =
    this->parameters()->findDouble("projection vector");
  std::array<double, 3> projectionDirection = { projectionDirectionItem->value(0),
    projectionDirectionItem->value(1),
    projectionDirectionItem->value(2) };

  vtkSmartPointer<vtkDataObject> inputData;
  if (!(inputData = ProportionalEdit::storage(input)))
  {
    smtkErrorMacro(this->log(), "Input has no geometric data.");
    return result;
  }

  // Access the geometric data corresponding to the input face
  vtkSmartPointer<vtkPolyData> inputPD = vtkPolyData::SafeDownCast(inputData);

  // If the geometric data is not a polydata...
  if (!inputPD)
  {
    //...extract its surface as polydata.
    vtkNew<vtkDataSetSurfaceFilter> extractSurface;
    extractSurface->PassThroughCellIdsOn();
    extractSurface->SetInputDataObject(inputData);
    extractSurface->Update();
    inputPD = extractSurface->GetOutput();
  }
  // If we still have no input polydata, there's not much we can do.
  if (!inputPD)
  {
    smtkErrorMacro(smtk::io::Logger::instance(), "Could not generate surface polydata.");
    return result;
  }

  // Run the vtk proportional editing filter on the face's geometry
  vtkNew<vtkProportionalEditFilter> propEdit;
  propEdit->SetInfluenceRadius(influenceRadius);
  propEdit->SetProjected(projectionEnabled);
  propEdit->SetProjectionDirection(
    projectionDirection[0], projectionDirection[1], projectionDirection[2]);
  // propEdit->SetFalloff(vtkProportionalEditFilter::Smooth);
  propEdit->SetAnchorPointCoords(anchorPoint[0], anchorPoint[1], anchorPoint[2]);
  propEdit->SetAnchorPointDisplacement(
    anchorPointDisplacement[0], anchorPointDisplacement[1], anchorPointDisplacement[2]);
  // propEdit->SetGenerateEditDistance(true);
  // propEdit->SetGenerateEditVector(true);
  // propEdit->PassThroughCellIdsOn();
  propEdit->SetInputDataObject(inputPD);
  propEdit->PrintSelf(std::cout, vtkIndent(4));
  propEdit->Update();
  vtkPolyData* outputPD = propEdit->GetOutput();

  // Assign the geometric data for the input face
  session->addStorage(input->id(), outputPD);

  // Reassign the result to indicate success
  result->findInt("outcome")->setValue(static_cast<int>(ProportionalEdit::Outcome::SUCCEEDED));

  // Mark the input face to update its representative geometry
  smtk::operation::MarkGeometry markGeometry(resource);
  markGeometry.markModified(input);

  // Add the input face to the operation result's "modified" item
  smtk::attribute::ComponentItem::Ptr modified = result->findComponent("modified");
  modified->appendValue(input);

  return result;
}

const char* ProportionalEdit::xmlDescription() const
{
  return ProportionalEdit_xml;
}
}
}
}
