//=============================================================================
// 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/NormalFeature.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/StringItem.h"

#include "smtk/operation/MarkGeometry.h"

#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h"
#include "smtk/view/Selection.h"

#include "vtkCellData.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkDoubleArray.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkPolyDataNormals.h"
#include "vtkThreshold.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>

#include "smtk/session/aeva/NormalFeature_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

using smtk::common::UUID;

namespace smtk
{
namespace session
{
namespace aeva
{

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

  auto directionItem = this->parameters()->findDouble("direction");
  vtkVector3d direction;
  for (int ii = 0; ii < 3; ++ii)
  {
    direction[ii] = directionItem->value(ii);
  }
  if (direction.Normalize() < 1e-12)
  {
    smtkInfoMacro(this->log(), "Direction vector must not have zero length.");
    return false;
  }
  return true;
}

NormalFeature::Result NormalFeature::operateInternal()
{
  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);

  auto directionItem = this->parameters()->findDouble("direction");
  vtkVector3d direction;
  for (int ii = 0; ii < 3; ++ii)
  {
    direction[ii] = directionItem->value(ii);
  }
  if (direction.Normalize() < 1e-12)
  {
    smtkErrorMacro(this->log(), "Direction vector must not have zero length.");
    return result;
  }

  auto angleItem = this->parameters()->findDouble("angle");
  double angle = angleItem->value();
  double angleTol = std::cos(vtkMath::RadiansFromDegrees(angle));

  std::cout << "Dir " << direction << " angle ±" << angle << " tol " << angleTol << "\n";

  auto assocs = this->parameters()->associations();
  auto created = result->findComponent("created");
  smtk::operation::MarkGeometry geomMarker(resource);
  for (const auto& assoc : *assocs)
  {
    auto data = session->findStorage(assoc->id());
    if (!data)
    {
      continue;
    }
    vtkSmartPointer<vtkPolyData> surf(vtkPolyData::SafeDownCast(data));
    if (!surf)
    {
      vtkNew<vtkDataSetSurfaceFilter> extractSurface;
      extractSurface->PassThroughCellIdsOn();
      extractSurface->SetInputDataObject(data);
      extractSurface->Update();
      surf = extractSurface->GetOutput();
    }
    if (!surf)
    {
      smtkErrorMacro(this->log(), "Could not generate surface.");
      continue;
    }
    if (!surf->GetCellData()->GetNormals())
    {
      vtkNew<vtkPolyDataNormals> genNormals;
      genNormals->SetInputDataObject(surf);
      genNormals->ConsistencyOn();
      genNormals->SplittingOff();
      genNormals->ComputeCellNormalsOn();
      genNormals->Update();
      surf = genNormals->GetOutput();
    }
    vtkDataArray* cellNormals = surf->GetCellData()->GetNormals();
    vtkNew<vtkDoubleArray> dottedNormals;
    dottedNormals->SetName("SelectionValue");
    dottedNormals->SetNumberOfTuples(surf->GetNumberOfCells());
    for (vtkIdType ii = 0; ii < cellNormals->GetNumberOfTuples(); ++ii)
    {
      vtkVector3d norm;
      cellNormals->GetTuple(ii, norm.GetData());
      dottedNormals->SetValue(ii, norm.Dot(direction));
    }
    surf->GetCellData()->AddArray(dottedNormals);
    vtkNew<vtkThreshold> threshold;
    threshold->SetInputDataObject(surf);
    threshold->SetInputArrayToProcess(
      0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "SelectionValue");
    threshold->ThresholdBetween(angleTol, 1.1);

    vtkNew<vtkDataSetSurfaceFilter> extractSurface;
    extractSurface->SetInputConnection(threshold->GetOutputPort());
    extractSurface->Update();
    vtkNew<vtkPolyData> geom;
    geom->ShallowCopy(extractSurface->GetOutput());
    // Must remove both sets of normals or rendering artifacts will occur
    // (z-fighting with point normals, bad shading with cell normals).
    geom->GetCellData()->RemoveArray("SelectionValue");
    geom->GetCellData()->RemoveArray("Normals");
    geom->GetPointData()->RemoveArray("Normals");

    auto* wrapper = pqSMTKBehavior::instance()->builtinOrActiveWrapper();
    if (wrapper)
    {
      auto cellSelection = CellSelection::create(resource, geom, this->manager());
      session->addStorage(cellSelection->id(), geom);
      created->appendValue(cellSelection);
      geomMarker.markModified(cellSelection);
      std::set<smtk::model::EntityPtr> selected;
      selected.insert(cellSelection);
      auto selection = wrapper->smtkSelection();
      int selectedValue = selection->selectionValueFromLabel("selected");
      selection->modifySelection(selected,
        "normal feature",
        selectedValue,
        smtk::view::SelectionAction::UNFILTERED_REPLACE,
        /* bitwise */ true,
        /* notify */ false);
    }
    else
    {
      auto face = resource->addFace();
      face.setName("selected by normal");
      smtk::model::Face(std::dynamic_pointer_cast<smtk::model::Entity>(assoc))
        .owningModel()
        .addCell(face);
      auto fcomp = face.entityRecord();
      session->addStorage(fcomp->id(), geom);
      created->appendValue(fcomp);
      geomMarker.markModified(fcomp);
    }
  }

  result->findInt("outcome")->setValue(static_cast<int>(NormalFeature::Outcome::SUCCEEDED));
  return result;
}

const char* NormalFeature::xmlDescription() const
{
  return NormalFeature_xml;
}

}
}
}
