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

#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 "vtkAppendPolyData.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 "vtkUnstructuredGrid.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>

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

vtkSmartPointer<vtkDataObject> AllPrimitivesFeature::CellsOfVertices(
  const smtk::resource::PersistentObjectPtr& assoc,
  const vtkSmartPointer<vtkDataObject>& data)
{
  vtkNew<vtkUnstructuredGrid> result;
  auto* comp = dynamic_cast<smtk::resource::Component*>(assoc.get());
  if (!comp)
  {
    smtkWarningMacro(
      smtk::io::Logger::instance(), "Object \"" << assoc->name() << "\" was not a component.");
    return result;
  }
  vtkIntArray* dataPointIds;
  if (!data ||
    !(dataPointIds =
        vtkIntArray::SafeDownCast(data->GetAttributes(vtkDataObject::POINT)->GetGlobalIds())))
  {
    smtkWarningMacro(smtk::io::Logger::instance(),
      "CellsOfVertices passed null data or data with no global point IDs.");
    return result;
  }
  auto assocResource = std::dynamic_pointer_cast<smtk::session::aeva::Resource>(comp->resource());
  if (!assocResource)
  {
    smtkWarningMacro(
      smtk::io::Logger::instance(), "CellsOfVertices passed object with no resource.");
    return result;
  }
  std::set<vtkIdType> globalPointIds(dataPointIds->Begin(), dataPointIds->End());
  vtkNew<vtkIntArray> resultCellIds;
  vtkNew<vtkIntArray> resultPointIds;
  vtkNew<vtkPoints> resultPoints;
  resultCellIds->SetName("global ids");
  resultPointIds->SetName("global ids");
  result->SetPoints(resultPoints);
  result->GetAttributes(vtkDataObject::CELL)->SetGlobalIds(resultCellIds);
  result->GetAttributes(vtkDataObject::POINT)->SetGlobalIds(resultPointIds);
  std::map<int, vtkIdType> pointMap;
  auto assocSession = assocResource->session();
  assocSession->visitPrimaryDataById(assocResource,
    globalPointIds,
    vtkVisitationRule::CELLS_CONNECTED_TO_POINTS,
    [&result, &resultPoints, &resultCellIds, &resultPointIds, &pointMap](
      vtkIdType /* globalPointId */,
      vtkIdType globalCellId,
      vtkDataObject* primaryData,
      int cellType,
      vtkIdType numberOfPoints,
      const vtkIdType* connectivity) -> vtkVisitation {
      // Map points of cell from primary to result:
      std::vector<vtkIdType> connOut;
      connOut.reserve(numberOfPoints);
      auto* primaryDataSet = vtkDataSet::SafeDownCast(primaryData);
      if (!primaryDataSet)
      {
        return vtkVisitation::CONTINUE;
      }
      auto* primaryPointIds =
        vtkIntArray::SafeDownCast(primaryData->GetAttributes(vtkDataObject::POINT)->GetGlobalIds());
      if (!primaryPointIds)
      {
        return vtkVisitation::CONTINUE;
      }
      for (vtkIdType ii = 0; ii < numberOfPoints; ++ii)
      {
        int primaryId = primaryPointIds->GetValue(connectivity[ii]);
        auto pmit = pointMap.find(primaryId);
        if (pmit == pointMap.end())
        {
          vtkIdType resultPoint =
            resultPoints->InsertNextPoint(primaryDataSet->GetPoint(connectivity[ii]));
          pointMap[primaryId] = resultPoint;
          connOut.push_back(resultPoint);
          resultPointIds->InsertNextValue(primaryId);
        }
        else
        {
          connOut.push_back(pmit->second);
        }
      }
      // Insert primary cell into result:
      result->InsertNextCell(cellType, numberOfPoints, &connOut[0]);
      resultCellIds->InsertNextValue(globalCellId);
      return vtkVisitation::CONTINUE;
    });
  return result;
}

AllPrimitivesFeature::Result AllPrimitivesFeature::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 assocs = this->parameters()->associations();
  auto created = result->findComponent("created");
  smtk::operation::MarkGeometry geomMarker(resource);
  std::vector<vtkSmartPointer<vtkPolyData> > pieces;
  smtk::model::Model anOwningModel;
  for (const auto& assoc : *assocs)
  {
    anOwningModel =
      smtk::model::Face(std::dynamic_pointer_cast<smtk::model::Entity>(assoc)).owningModel();
    auto data = Operation::storage(assoc);
    if (!data)
    {
      continue;
    }
    vtkSmartPointer<vtkPolyData> surf(vtkPolyData::SafeDownCast(data));
    if (EstimateParametricDimension(data) == 0)
    {
      surf = nullptr;
      data = AllPrimitivesFeature::CellsOfVertices(assoc, data);
    }
    // TODO: We should not force the selection to be a surface here:
    if (!surf)
    {
      vtkNew<vtkDataSetSurfaceFilter> extractSurface;
      extractSurface->PassThroughCellIdsOn();
      extractSurface->SetInputDataObject(data);
      extractSurface->Update();
      surf = extractSurface->GetOutput();
    }
    if (!surf)
    {
      smtkErrorMacro(this->log(), "Could not generate surface of " << assoc->name() << ".");
      continue;
    }
    if (!surf->GetPointData()->GetNormals())
    {
      vtkNew<vtkPolyDataNormals> genNormals;
      genNormals->SetInputDataObject(surf);
      genNormals->SplittingOff();
      genNormals->ConsistencyOn();
      genNormals->SplittingOff();
      genNormals->ComputePointNormalsOn(); // Without this, rendering is super-flaky.
      genNormals->Update();
      surf = genNormals->GetOutput();
    }
    vtkNew<vtkPolyData> geom;
    geom->ShallowCopy(surf);
    pieces.emplace_back(geom);
  }

  if (!pieces.empty())
  {
    vtkNew<vtkAppendPolyData> append;
    for (const auto& piece : pieces)
    {
      append->AddInputData(piece.GetPointer());
    }
    append->Update();
    vtkNew<vtkPolyData> geom;
    geom->ShallowCopy(append->GetOutput());
    geom->GetPointData()->RemoveArray("Normals");
    geom->GetCellData()->RemoveArray("Normals");

#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,
        "all primitives feature",
        selectedValue,
        smtk::view::SelectionAction::UNFILTERED_REPLACE,
        /* bitwise */ true,
        /* notify */ false);
    }
    else
#endif
    {
      auto face = resource->addFace();
      face.setName("all boundary primitives");
      anOwningModel.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>(AllPrimitivesFeature::Outcome::SUCCEEDED));
  }
  else
  {
    smtkErrorMacro(this->log(), "No output.");
  }

  return result;
}

const char* AllPrimitivesFeature::xmlDescription() const
{
  return AllPrimitivesFeature_xml;
}

}
}
}
