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

#include "smtk/operation/MarkGeometry.h"

#include "vtkCellData.h"
#include "vtkCellType.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkDoubleArray.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkUnstructuredGrid.h"
#include "vtkVector.h"

#include "smtk/session/aeva/NetgenSurfaceRemesher_xml.h"

#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4067) // unexpected tokens following preprocessor directive
#endif
namespace nglib
{
#include "nglib.h"
}
#ifdef _MSC_VER
#pragma warning(pop)
#endif

// 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
{

NetgenSurfaceRemesher::Result NetgenSurfaceRemesher::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);

  // Access the input surface to remesh
  smtk::model::EntityPtr input = this->parameters()->associations()->valueAs<smtk::model::Entity>();

  // Access the mesh size
  smtk::attribute::DoubleItemPtr meshSizeItem =
    this->parameters()->findDouble("max global mesh size");
  double meshSize = meshSizeItem->value();

  // Access the mesh size
  smtk::attribute::IntItemPtr optStepItem = this->parameters()->findInt("optimize step");
  int optStep = optStepItem->value();

  vtkSmartPointer<vtkDataObject> inputData;
  if (!(inputData = Operation::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;
  }

  vtkSmartPointer<vtkPolyData> outputPD = vtkPolyData::New();

  nglib::Ng_Init();
  try
  {
    auto* mesh = nglib::Ng_NewMesh();
    auto* stl_geom = nglib::Ng_STL_NewGeometry();
    nglib::Ng_Result ng_res;
    nglib::Ng_Meshing_Parameters mp;
    mp.maxh = meshSize;
    mp.optsteps_2d = optStep;

    vtkIdType numberOfPoints;
    vtkIdType const* connectivity;
    vtkVector3d coords;

    // Add surface triangles
    auto* polys = inputPD->GetPolys();
    auto* points = inputPD->GetPoints();
    for (polys->InitTraversal(); polys->GetNextCell(numberOfPoints, connectivity);)
    {
      if (numberOfPoints != 3)
      {
        smtkErrorMacro(smtk::io::Logger::instance(), "Non-triangular surface element detected.");
        return result;
      }
      double p1[3];
      double p2[3];
      double p3[3];
      points->GetPoint(connectivity[0], p1);
      points->GetPoint(connectivity[1], p2);
      points->GetPoint(connectivity[2], p3);

      nglib::Ng_STL_AddTriangle(stl_geom, p1, p2, p3);
    }

    ng_res = nglib::Ng_STL_InitSTLGeometry(stl_geom);
    if (ng_res != nglib::NG_OK)
    {
      smtkErrorMacro(
        smtk::io::Logger::instance(), "Error Initialising the STL Geometry....Aborting!!");
      return result;
    }

    ng_res = nglib::Ng_STL_MakeEdges(stl_geom, mesh, &mp);
    if (ng_res != nglib::NG_OK)
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "Error in Edge Meshing....Aborting!!");
      return result;
    }

    ng_res = nglib::Ng_STL_GenerateSurfaceMesh(stl_geom, mesh, &mp);
    if (ng_res != nglib::NG_OK)
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "Error in Surface Meshing....Aborting!!");
      return result;
    }

    points = outputPD->GetPoints();
    if (!points)
    {
      points = vtkPoints::New();
      outputPD->SetPoints(points);
    }
    numberOfPoints = nglib::Ng_GetNP(mesh);
    points->SetNumberOfPoints(numberOfPoints);
    for (int pp = 0; pp < numberOfPoints; ++pp)
    {
      nglib::Ng_GetPoint(mesh, pp + 1, coords.GetData());
      points->SetPoint(pp, coords.GetData());
    }

    std::array<int, 8> ngconn; // Current max number of points per surface element is 8
    std::array<vtkIdType, 8> vtkconn;

    int cellType;
    int nconn;
    int numberOfElements = nglib::Ng_GetNSE(mesh);
    outputPD->Allocate(numberOfElements);

    for (int se = 0; se < numberOfElements; ++se)
    {
      auto elementType = nglib::Ng_GetSurfaceElement(mesh, se + 1, ngconn.data());
      switch (elementType)
      {
        case nglib::NG_TRIG:
          nconn = 3;
          cellType = VTK_TRIANGLE;
          break;
        case nglib::NG_QUAD:
          nconn = 4;
          cellType = VTK_QUAD;
          break;
        case nglib::NG_TRIG6:
          nconn = 6;
          cellType = VTK_QUADRATIC_TRIANGLE;
          break;
        case nglib::NG_QUAD6:
          nconn = 8;
          cellType = VTK_QUADRATIC_QUAD;
          break;
        default:
          nconn = 0;
          cellType = VTK_EMPTY_CELL;
          smtkErrorMacro(smtk::io::Logger::instance(),
            "Unknown output surface element type (" << elementType << ").");
          break;
      }
      for (int ii = 0; ii < nconn; ++ii)
      {
        vtkconn[ii] = static_cast<vtkIdType>(ngconn[ii] - 1);
      }
      outputPD->InsertNextCell(cellType, nconn, vtkconn.data());
    }
  }
  catch (...)
  {
    smtkErrorMacro(smtk::io::Logger::instance(), "Netgen surface remeshing failed.");
    return result;
  }
  nglib::Ng_Exit();

  smtkInfoMacro(smtk::io::Logger::instance(),
    "Number of points in remeshed surface: " << outputPD->GetNumberOfPoints());
  smtkInfoMacro(smtk::io::Logger::instance(),
    "Number of elements in remeshed surface: " << outputPD->GetNumberOfCells());

  // 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");
    }

    Session::offsetGlobalIds(outputPD, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
    pointIdOffset = maxPointId + 1;
    cellIdOffset = maxCellId + 1;

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

  // Construct a new model face for the reconstructed face
  smtk::model::CellEntity face = resource->addFace();
  face.setName(input->name() + " (netgen remeshed)");
  input->owningModel()->referenceAs<smtk::model::Model>().addCell(face);

  // Assign the geometric data for the new face
  session->addStorage(face.entity(), outputPD);

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

  // Mark the new components to update their representative geometry
  smtk::operation::MarkGeometry markGeometry(resource);
  markGeometry.markModified(face.component());

  // Add the new face to the operation result's "created" item
  smtk::attribute::ComponentItem::Ptr created = result->findComponent("created");
  created->appendValue(face.component());

  return result;
}

const char* NetgenSurfaceRemesher::xmlDescription() const
{
  return NetgenSurfaceRemesher_xml;
}

}
}
}
