//=============================================================================
// 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/AdjacencyFeature.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/operation/MarkGeometry.h"

#ifdef SMTK_ENABLE_PARAVIEW_SUPPORT
#include "pqApplicationCore.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h"
#endif

#include "smtk/view/Selection.h"

#include "vtkBoundingBox.h"
#include "vtkCellData.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkDistancePolyDataFilter.h"
#include "vtkDoubleArray.h"
#include "vtkImplicitPolyDataDistance.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkPolyDataNormals.h"
#include "vtkThreshold.h"
#include "vtkUnsignedCharArray.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>

#include "smtk/session/aeva/AdjacencyFeature_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 AdjacencyFeature::ableToOperate()
{
  if (!this->Superclass::ableToOperate())
  {
    return false;
  }

  auto targetItem = this->parameters()->findComponent("targets");
  auto assocs = this->parameters()->associations();

  bool ok = AdjacencyFeature::allValuesHaveStorage(*targetItem);
  ok &= AdjacencyFeature::allValuesHaveStorage(*assocs);

  return ok;
}

AdjacencyFeature::Result AdjacencyFeature::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 distanceItem = this->parameters()->findDouble("distance");
  bool checkDistance = distanceItem->isEnabled();
  auto maxDistance = distanceItem->value();
  auto targetsItem = this->parameters()->findComponent("targets");
  auto angleItem = this->parameters()->findDouble("angle");
  double angle = angleItem->value();
  double angleTol = std::cos(vtkMath::RadiansFromDegrees(angle));
  using Target = struct
  {
    vtkNew<vtkImplicitPolyDataDistance> DistanceFilter;
    vtkSmartPointer<vtkPolyData> Data;
    vtkBoundingBox Bounds;
    vtkVector3d Center;
  };
  std::vector<Target> targets;
  targets.resize(targetsItem->numberOfValues());
  for (std::size_t ii = 0; ii < targetsItem->numberOfValues(); ++ii)
  {
    auto target = this->parameters()->findComponent("targets")->value(ii);
    vtkSmartPointer<vtkDataObject> targetData;
    if (!target || !(targetData = Operation::storage(target)))
    {
      continue;
    }
    vtkSmartPointer<vtkDataArray> targetSurfNormals;
    SurfaceWithNormals(targetData, targets[ii].Data, targetSurfNormals);
    targets[ii].DistanceFilter->SetInput(targets[ii].Data);
    targets[ii].Bounds.SetBounds(targets[ii].Data->GetBounds());
    targets[ii].Bounds.GetCenter(targets[ii].Center.GetData());
  }

  auto assocs = this->parameters()->associations();
  auto created = result->findComponent("created");
  smtk::operation::MarkGeometry geomMarker(resource);
  for (const auto& assoc : *assocs)
  {
    auto data = Operation::storage(assoc);
    if (!data)
    {
      continue;
    }
    vtkSmartPointer<vtkPolyData> assocSurf;
    vtkSmartPointer<vtkDataArray> assocSurfNormals;
    SurfaceWithNormals(data, assocSurf, assocSurfNormals);

    vtkNew<vtkUnsignedCharArray> selectionVals;
    selectionVals->SetName("SelectionValue");
    selectionVals->SetNumberOfTuples(assocSurf->GetNumberOfCells());
    selectionVals->FillComponent(0, 0);
    vtkNew<vtkIdList> cellPoints;
    double minDistanceHit = std::numeric_limits<double>::max();
    double maxDistanceHit = std::numeric_limits<double>::min();
    vtkVector3d srcNormal;
    std::cout << (checkDistance ? "Checking" : "Ignoring") << " distance\n";
    for (vtkIdType ii = 0; ii < assocSurfNormals->GetNumberOfTuples(); ++ii)
    {
      assocSurfNormals->GetTuple(ii, srcNormal.GetData());
      assocSurf->GetCellPoints(ii, cellPoints);
      vtkVector3d cellCenter;
      if (cellPoints->GetNumberOfIds() == 0)
      {
        continue;
      }
      double weight = 1.0 / cellPoints->GetNumberOfIds();
      for (vtkIdType jj = 0; jj < cellPoints->GetNumberOfIds(); ++jj)
      {
        vtkVector3d pt;
        assocSurf->GetPoint(cellPoints->GetId(jj), pt.GetData());
        cellCenter = cellCenter + pt;
      }
      cellCenter = cellCenter * weight;
      bool pass = false;
      for (auto& target : targets)
      {
        vtkVector3d pointingVector = (target.Center - cellCenter).Normalized();
        if (pointingVector.Dot(srcNormal) > angleTol)
        {
          if (checkDistance)
          {
            vtkVector3d targetPt;
            auto dist = target.DistanceFilter->EvaluateFunctionAndGetClosestPoint(
              cellCenter.GetData(), targetPt.GetData());
            // Track the min/max distances in case no cells are selected.
            if (dist < minDistanceHit)
            {
              minDistanceHit = dist;
            }
            if (dist > maxDistanceHit)
            {
              maxDistanceHit = dist;
            }
            if (dist < maxDistance)
            {
              pass = true;
              break;
            }
          }
          else
          {
            pass = true;
            break;
          }
        }
      }
      selectionVals->SetValue(ii, pass ? 1 : 0);
    }
    assocSurf->GetCellData()->AddArray(selectionVals);

    vtkNew<vtkThreshold> threshold;
    threshold->SetInputDataObject(assocSurf);
    threshold->SetInputArrayToProcess(
      0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "SelectionValue");
    threshold->SetLowerThreshold(1);
    threshold->SetUpperThreshold(255);
    threshold->SetThresholdFunction(vtkThreshold::THRESHOLD_BETWEEN);

    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("Normals");
    geom->GetPointData()->RemoveArray("Normals");
    geom->GetCellData()->RemoveArray("SelectionValue");

    if (geom->GetNumberOfCells() == 0)
    {
      if (checkDistance)
      {
        smtkInfoMacro(this->log(),
          "No input or distance too small for " << assoc->name()
                                                << ". Found distances in range "
                                                   "["
                                                << minDistanceHit << "  " << maxDistanceHit
                                                << "].");
      }
      else
      {
        smtkInfoMacro(
          this->log(), "No primitives were adjacent to " << assoc->name() << " within tolerance.");
      }
    }

#ifdef SMTK_ENABLE_PARAVIEW_SUPPORT
    auto* app = pqApplicationCore::instance();
    auto* behavior = app ? pqSMTKBehavior::instance() : nullptr;
    auto* wrapper = behavior ? behavior->builtinOrActiveWrapper() : nullptr;
    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,
        "adjacency feature",
        selectedValue,
        smtk::view::SelectionAction::UNFILTERED_REPLACE,
        /* bitwise */ true,
        /* notify */ false);
    }
    else
#endif
    {
      auto face = resource->addFace();
      face.setName("selected by adjacency");
      smtk::model::Face(std::dynamic_pointer_cast<smtk::model::Entity>(assoc))
        .owningModel()
        .addCell(face);
      auto fcomp = face.entityRecord();
      session->addStorage(fcomp->id(), geom);
      session->setPrimary(*fcomp, false);
      created->appendValue(fcomp);
      geomMarker.markModified(fcomp);
    }
  }

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

const char* AdjacencyFeature::xmlDescription() const
{
  return AdjacencyFeature_xml;
}

}
}
}
