//=============================================================================
// 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/Predicates.h"
#include "smtk/session/aeva/operators/PointProximityFeature.h"

#include "smtk/view/Selection.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/attribute/VoidItem.h"

#include "smtk/operation/MarkGeometry.h"

#include "vtkCellArray.h"
#include "vtkCellData.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkDistancePolyDataFilter.h"
#include "vtkDoubleArray.h"
#include "vtkGlyph3D.h"
#include "vtkImplicitPolyDataDistance.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPoints.h"
#include "vtkPolyData.h"
#include "vtkPolyDataNormals.h"
#include "vtkPolyDataWriter.h"
#include "vtkThreshold.h"
#include "vtkUnstructuredGrid.h"
#include "vtkUnstructuredGridWriter.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>
#include <limits>

#include "smtk/session/aeva/operators/PointProximityFeature_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
{

template<bool UseNormals>
class Proximilator
{
public:
  Proximilator(smtk::session::aeva::PointProximityFeature* op,
    smtk::session::aeva::Resource::Ptr resource,
    vtkPolyData* target,
    double maxDistance,
    double angle,
    smtk::attribute::ComponentItem::Ptr created,
    smtk::attribute::ComponentItem::Ptr modified)
    : Op(op)
    , Resource(std::move(resource))
    , Target(target)
    , MaxDistance(maxDistance)
    , Angle(angle)
    , AngleTolerance(std::cos(vtkMath::RadiansFromDegrees(angle)))
    , Created(std::move(created))
    , Modified(std::move(modified))
  {
    if (UseNormals)
    {
      this->TargetSurfNormals = this->Target->GetPointData()->GetNormals();
    }
    this->DistanceToTarget->SetInput(this->Target);
    this->Session = this->Resource ? this->Resource->session() : nullptr;
  }

  bool operator()(const smtk::model::Entity::Ptr& assoc, vtkSmartPointer<vtkDataSet> source)
  {
    using smtk::session::aeva::SurfaceWithNormals;
    smtk::operation::MarkGeometry geomMarker(this->Resource);
    vtkSmartPointer<vtkPolyData> assocSurf;
    vtkSmartPointer<vtkDataArray> assocSurfNormals;
    // Find the name of the global ID array for the input source as
    // it is carried through the pipeline and will need to be set
    // on the output.
    auto* igid = smtk::session::aeva::Session::globalIds(source, vtkDataObject::CELL);
    std::string gidName = igid ? igid->GetName() : "null";

    if (UseNormals)
    {
      SurfaceWithNormals(source, assocSurf, assocSurfNormals);
      // assocSurfNormals is cell normals, but we want point normals:
      assocSurfNormals = assocSurf->GetPointData()->GetNormals();
      source = assocSurf; // Needed?
    }

    // Turn points from origin data into cells via vtkGlyph3D:
    vtkNew<vtkGlyph3D> pointsAsCells;
    vtkNew<vtkPolyData> vertexPoly;
    vtkNew<vtkCellArray> vertexCA;
    vtkNew<vtkPoints> vertexPt;
    vertexPt->SetNumberOfPoints(1);
    vertexPt->SetPoint(0, 0., 0., 0.);
    vtkIdType vid = 0;
    vertexCA->InsertNextCell(1, &vid);
    vertexPoly->SetVerts(vertexCA);
    vertexPoly->SetPoints(vertexPt);
    pointsAsCells->SetInputDataObject(source);
    pointsAsCells->SetSourceData(vertexPoly);
    pointsAsCells->FillCellDataOn();
    pointsAsCells->GeneratePointIdsOn();
    pointsAsCells->OrientOff();
    pointsAsCells->Update();
    auto* pac = pointsAsCells->GetOutput();

    // Now choose which vertex cells are proximal to the target
    // and add an array of distances to the points/cells
    // indicating this selection:
    vtkNew<vtkDoubleArray> selectionVals;
    selectionVals->SetName("SelectionValue");
    selectionVals->SetNumberOfTuples(pac->GetNumberOfCells());
    double minDistanceHit = std::numeric_limits<double>::max();
    double maxDistanceHit = std::numeric_limits<double>::lowest();
    // Iterate over points looking for points close to the target:
    vtkIdType const* conn;
    vtkIdType nnn = pac->GetNumberOfElements(vtkDataObject::POINT);
    vtkIdType nnc = pac->GetNumberOfElements(vtkDataObject::CELL);
    for (vtkIdType ii = 0; ii < nnc; ++ii)
    {
      // Check whether any point is both close enough and has a normal pointing
      // opposite the associated surface's normal.
      vtkVector3d pt;
      vtkVector3d targetPt;
      vtkIdType npts;
      pac->GetCellPoints(ii, npts, conn);
      pac->GetPoint(conn[0], pt.GetData()); // NOLINT(clang-analyzer-core.NullDereference)
#if 0
      if (npts <= 0)
      {
        std::cerr << "ERROR!!!!!\nERROR!!!!! cell " << ii << " npts " << npts << "\n";
      }
      else if (npts > 1)
      {
        std::cerr << "ERROR!!!!!\nERROR!!!!! cell " << ii << " npts " << npts << " > 1\n";
      }
      if (conn[0] < 0 || conn[0] >= nnn)
      {
        std::cerr << "ERROR!!!!!\nERROR!!!!! cell " << ii << " npts " << npts << " conn " << conn[0] << " > nnn " << nnn << "\n";
      }
#endif
      bool pass = false;
      auto dist = this->DistanceToTarget->EvaluateFunctionAndGetClosestPoint(
        pt.GetData(), targetPt.GetData());
      // Track the min/max distances in case no cells are selected.
      if (ii == 0)
      {
        minDistanceHit = dist;
        maxDistanceHit = dist;
      }
      else
      {
        if (dist < minDistanceHit)
        {
          minDistanceHit = dist;
        }
        else if (dist > maxDistanceHit)
        {
          maxDistanceHit = dist;
        }
      }
      if (dist < this->MaxDistance)
      {
        vtkIdType targetPtId = this->Target->FindPoint(pt[0], pt[1], pt[2]);
        if (targetPtId >= 0)
        {
          if (UseNormals)
          {
            vtkVector3d tn;
            vtkVector3d an;
            this->TargetSurfNormals->GetTuple(targetPtId, tn.GetData());
            assocSurfNormals->GetTuple(conn[0], an.GetData());
            if (-an.Dot(tn) > this->AngleTolerance)
            {
              selectionVals->SetValue(conn[0], dist);
              pass = true;
            }
          }
          else
          {
            selectionVals->SetValue(conn[0], dist);
            pass = true;
          }
        }
      }
      if (!pass)
      {
        selectionVals->SetValue(conn[0], 2 * this->MaxDistance);
      }
    }

    // Add the selection array to the glyphed point vertices:
    pointsAsCells->GetOutput()->GetCellData()->AddArray(selectionVals);
#if 0
    vtkNew<vtkPolyDataWriter> pacw;
    pacw->SetInputDataObject(0, pointsAsCells->GetOutput());
    pacw->SetFileName("/tmp/pac.vtk");
    pacw->Write();
#endif

    // Choose only the points in range to become the output:
    vtkNew<vtkThreshold> threshold;
    threshold->SetInputDataObject(0, pointsAsCells->GetOutput());
    threshold->SetInputArrayToProcess(
      0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "SelectionValue");
    threshold->SetLowerThreshold(-this->MaxDistance);
    threshold->SetUpperThreshold(this->MaxDistance);
    threshold->SetThresholdFunction(vtkThreshold::THRESHOLD_BETWEEN);
    threshold->Update();
    auto* td = threshold->GetOutput();

#if 0
    vtkNew<vtkUnstructuredGridWriter> wri;
    wri->SetInputDataObject(0, td);
    wri->SetFileName("/tmp/td.vtk");
    wri->Write();
#endif

    // Convert the glyph vertices to polydata (threshold always outputs an unstructured grid):
    vtkNew<vtkDataSetSurfaceFilter> extractSurface;
    extractSurface->SetInputConnection(threshold->GetOutputPort());
    extractSurface->PassThroughCellIdsOn();
    extractSurface->PassThroughPointIdsOn();
    extractSurface->Update();
#if 0
    vtkNew<vtkPolyDataWriter> pwri;
    pwri->SetInputDataObject(0, extractSurface->GetOutput());
    pwri->SetFileName("/tmp/es.vtk");
    pwri->Write();
#endif

    vtkNew<vtkPolyData> geom;
    geom->ShallowCopy(extractSurface->GetOutput());
    // 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");

    // Now – while the global IDs are passed along through the pipeline
    // above – they are not marked as global IDs any longer. So mark them.
    // Also, they are only available as cell data, but should be point IDs.
    auto* ogid = geom->GetPointData()->GetArray(gidName.c_str());
    auto* ogci = geom->GetCellData()->GetArray(gidName.c_str());
    if (!ogci)
    {
      auto* cellData = geom->GetCellData();
      for (int ii = 0; ii < cellData->GetNumberOfArrays(); ++ii)
      {
        ogci = vtkIntArray::SafeDownCast(cellData->GetArray(ii));
        if (ogci)
        {
          break;
        }
      }
    }
    geom->GetPointData()->SetGlobalIds(ogci);
    if (ogci)
    {
      geom->GetCellData()->RemoveArray(ogci->GetName());
    }

    if (geom->GetNumberOfCells() == 0)
    {
      smtkInfoMacro(this->Op->log(),
        "No input or distance too small. Found distances in range [" << minDistanceHit << "  "
                                                                     << maxDistanceHit << "].");
      return false;
    }

    smtk::view::Selection::Ptr selection;
    if (auto managers = this->Op->managers())
    {
      selection = managers->template get<smtk::view::Selection::Ptr>();
    }
    auto resultNameItem = this->Op->parameters()->findString("save result as");
    if (selection && !resultNameItem->isEnabled())
    {
      auto cellSelection = smtk::session::aeva::CellSelection::instance();
      if (cellSelection)
      {
        cellSelection->replaceData(geom);
        this->Modified->appendValue(cellSelection);
      }
      else
      {
        cellSelection =
          smtk::session::aeva::CellSelection::create(this->Resource, geom, this->Op->manager());
        this->Session->addStorage(cellSelection->id(), geom);
        geomMarker.markModified(cellSelection);
        this->Created->appendValue(cellSelection);
      }
      std::set<smtk::model::EntityPtr> selected;
      selected.insert(cellSelection);
      int selectedValue = selection->selectionValueFromLabel("selected");
      selection->modifySelection(selected,
        "point proximity feature",
        selectedValue,
        smtk::view::SelectionAction::UNFILTERED_REPLACE,
        /* bitwise */ true,
        /* notify */ false);
    }
    else
    {
      std::string resultName = "selected by proximity";
      if (resultNameItem->isEnabled() && resultNameItem->isSet() &&
        !resultNameItem->value().empty())
      {
        resultName = resultNameItem->value();
      }
      auto face = this->Resource->addFace();
      face.setName(resultName);
      smtk::model::Face(assoc).owningModel().addCell(face);
      auto fcomp = face.entityRecord();
      this->Session->addStorage(fcomp->id(), geom);
      this->Session->setPrimary(*fcomp, false);
      this->Created->appendValue(fcomp);
      geomMarker.markModified(fcomp);
    }
    return true;
  }

protected:
  smtk::session::aeva::PointProximityFeature* Op;
  smtk::session::aeva::Resource::Ptr Resource;
  smtk::session::aeva::Session::Ptr Session;
  vtkSmartPointer<vtkPolyData> Target;
  vtkSmartPointer<vtkDataArray> TargetSurfNormals;
  double MaxDistance;
  double Angle;
  double AngleTolerance;
  smtk::attribute::ComponentItem::Ptr Created;
  smtk::attribute::ComponentItem::Ptr Modified;
  vtkNew<vtkImplicitPolyDataDistance> DistanceToTarget;
};

} // anonymous namepace

namespace smtk
{
namespace session
{
namespace aeva
{

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

  auto target = this->parameters()->findComponent("target")->value();
  auto resource = std::dynamic_pointer_cast<smtk::session::aeva::Resource>(target->resource());
  auto session = resource ? resource->session() : smtk::session::aeva::Session::Ptr();
  if (!session)
  {
    return false;
  }

  return target && PointProximityFeature::storage(target);
}

PointProximityFeature::Result PointProximityFeature::operateInternal()
{
  auto maxDistance = this->parameters()->findDouble("distance")->value();
  auto target = this->parameters()->findComponent("target")->value();
  auto angleItem = this->parameters()->findDouble("angle");
  bool checkNormals = angleItem->isEnabled();
  double angle = angleItem->value();

  auto resource = std::dynamic_pointer_cast<smtk::session::aeva::Resource>(target->resource());
  auto result = this->createResult(smtk::operation::Operation::Outcome::FAILED);

  vtkSmartPointer<vtkDataObject> targetData;
  if (!target || !(targetData = PointProximityFeature::storage(target)))
  {
    smtkErrorMacro(this->log(), "No target available or target has no geometric data.");
    return result;
  }

  vtkSmartPointer<vtkPolyData> targetSurf;
  vtkSmartPointer<vtkDataArray> targetSurfNormals;
  SurfaceWithNormals(targetData, targetSurf, targetSurfNormals);
  // targetSurfNormals is cell normals, but we want point normals:
  targetSurfNormals = targetSurf->GetPointData()->GetNormals();

  auto assocs = this->parameters()->associations();
  auto created = result->findComponent("created");
  auto modified = result->findComponent("modified");

  Proximilator<false> proximityWithoutNormals(
    this, resource, targetSurf, maxDistance, angle, created, modified);
  Proximilator<true> proximityWithNormals(
    this, resource, targetSurf, maxDistance, angle, created, modified);
  bool ok = true;

  for (const auto& assoc : *assocs)
  {
    auto ent = std::dynamic_pointer_cast<smtk::model::Entity>(assoc);
    auto* data = vtkDataSet::SafeDownCast(PointProximityFeature::storage(assoc));
    if (!ent || !data)
    {
      continue;
    }

    if (EstimateParametricDimension(data) == 2 && checkNormals)
    {
      ok |= proximityWithNormals(ent, data);
    }
    else
    {
      ok |= proximityWithoutNormals(ent, data);
    }
    if (!ok)
    {
      break;
    }
  }

  if (ok)
  {
    result->findInt("outcome")->setValue(
      static_cast<int>(PointProximityFeature::Outcome::SUCCEEDED));
  }
  else
  {
    // TODO: Remove any components we may have already created.
  }
  return result;
}

const char* PointProximityFeature::xmlDescription() const
{
  return PointProximityFeature_xml;
}

}
}
}
