//=============================================================================
// 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/session/aeva/operators/VolumeMesher.h"
#include "smtk/io/Logger.h"

#include "smtk/model/Face.h"
#include "smtk/model/Model.h"
#include "smtk/model/Volume.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"

#include "vtkDataObject.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkPolyData.h"
#include "vtkUnstructuredGrid.h"

#include "vtk/meshing/netgen/vtkNetGenVolume.h"

#include "smtk/session/aeva/VolumeMesher_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
{
VolumeMesher::Result VolumeMesher::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 mesh
  smtk::model::EntityPtr input = this->parameters()->associations()->valueAs<smtk::model::Entity>();

  // Access the maximum global mesh size allowed
  smtk::attribute::DoubleItemPtr maxGlobalMeshSizeItem =
    this->parameters()->findDouble("max global mesh size");
  double maxGlobalMeshSize = maxGlobalMeshSizeItem->value();

  // Access the mesh density
  smtk::attribute::DoubleItemPtr meshDensityItem = this->parameters()->findDouble("mesh density");
  double meshDensity = meshDensityItem->value();

  // Access the order of the generated elements
  smtk::attribute::IntItemPtr elementOrderItem = this->parameters()->findInt("element order");

  int elementOrder = elementOrderItem->value();

  // Access the number of optimize steps to use for 3-D mesh optimization
  smtk::attribute::IntItemPtr optimizeStepNumberItem =
    this->parameters()->findInt("optimize step number");
  int optimizeStepNumber = optimizeStepNumberItem->value();

  // Get input data
  vtkSmartPointer<vtkDataObject> inputData;
  if (!(inputData = VolumeMesher::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;
  }

  // Generate volume mesh using vtk Netgen filter with specified parameters
  vtkNew<vtkNetGenVolume> mesher;
  mesher->SetMaxGlobalMeshSize(maxGlobalMeshSize);
  mesher->SetMeshDensity(meshDensity);
  mesher->SetSecondOrder(elementOrder - 1);
  mesher->SetNumberOfOptimizeSteps(optimizeStepNumber);
  mesher->SetInputDataObject(inputPD);
  mesher->Update();
  vtkUnstructuredGrid* outputPD = mesher->GetOutput();

  if (!outputPD || !outputPD->GetNumberOfCells())
  {
    smtkErrorMacro(smtk::io::Logger::instance(), "Could not generate volume mesh.");
    return result;
  }

  // Ensure that the points and cells of our new surface contain unique global ids.
  long pointIdOffset = 0;
  long maxPointId = 0;
  long cellIdOffset = 0;
  long maxCellId = 0;
  if (resource->properties().contains<long>("global point id offset"))
  {
    pointIdOffset = resource->properties().at<long>("global point id offset");
  }
  if (resource->properties().contains<long>("global cell id offset"))
  {
    cellIdOffset = resource->properties().at<long>("global cell id offset");
  }

  // Update the ID offset
  Session::offsetGlobalIds(outputPD, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
  cellIdOffset = maxCellId + 1;

  auto created = result->findComponent("created");
  smtk::operation::MarkGeometry markGeometry(resource);

  // Add volume mesh to resource
  size_t volumeNum = 1;
  std::string volumeName = "volume " + std::to_string(volumeNum) + " (" + input->name() + ")";
  while (!resource->findEntitiesByProperty("name", volumeName).empty())
  {
    volumeNum++;
    volumeName = "volume " + std::to_string(volumeNum) + " (" + input->name() + ")";
  }

  auto volumeMesh = resource->addVolume();
  volumeMesh.setName(volumeName);
  input->owningModel()->referenceAs<smtk::model::Model>().addCell(volumeMesh);
  auto vcomp = volumeMesh.entityRecord();
  session->addStorage(vcomp->id(), outputPD);
  session->setPrimary(*vcomp, true);
  created->appendValue(vcomp);
  markGeometry.markModified(vcomp);

  //  Generate the boundary surface of the volume mesh
  vtkNew<vtkDataSetSurfaceFilter> extractSurface;
  extractSurface->SetInputDataObject(outputPD);
  extractSurface->Update();
  auto* surf = extractSurface->GetOutput();

  // Update the cell ID offset, but leave global point IDs alone - they pass through.
  Session::offsetGlobalIds(surf, -1, cellIdOffset, maxPointId, maxCellId);
  pointIdOffset = maxPointId + 1;
  cellIdOffset = maxCellId + 1;

  // Add boundary
  auto boundarySurface = resource->addFace();
  std::string prefix = "boundary of ";
  boundarySurface.setName(prefix + volumeMesh.name());
  smtk::model::EntityRef(volumeMesh).addRawRelation(boundarySurface);
  smtk::model::EntityRef(boundarySurface).addRawRelation(volumeMesh);
  auto fcomp = boundarySurface.entityRecord();
  session->addStorage(fcomp->id(), surf);
  session->setPrimary(*fcomp, true);
  created->appendValue(fcomp);
  markGeometry.markModified(fcomp);

  resource->properties().get<long>()["global point id offset"] = pointIdOffset;
  resource->properties().get<long>()["global cell id offset"] = cellIdOffset;

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

  return result;
}

const char* VolumeMesher::xmlDescription() const
{
  return VolumeMesher_xml;
}

}
}
}
