//=============================================================================
// 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/operators/ReconstructSurface.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/geometry/queries/BoundingBox.h"

#include "smtk/operation/MarkGeometry.h"

#include "smtk/resource/Component.h"

#include <vtkDataArray.h>
#include <vtkDataObject.h>
#include <vtkDataSetSurfaceFilter.h>
#include <vtkExtractSurface.h>
#include <vtkPointData.h>
#include <vtkPolyData.h>
#include <vtkPolyDataNormals.h>
#include <vtkSignedDistance.h>

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

ReconstructSurface::Result ReconstructSurface::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 reconstruct
  smtk::model::EntityPtr input = this->parameters()->associations()->valueAs<smtk::model::Entity>();

  // Access the dimensions of the original voxel grid
  smtk::attribute::IntItemPtr dimensionsItem = this->parameters()->findInt("dimensions");
  std::array<int, 3> dimensions = {
    dimensionsItem->value(0), dimensionsItem->value(1), dimensionsItem->value(2)
  };

  // Access the radius of influence for each point in the original surface
  smtk::attribute::DoubleItemPtr radiusItem = this->parameters()->findDouble("radius");
  double radius = radiusItem->value();

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

  // If the geometric data does not have point normals...
  if (!inputPD->GetPointData()->GetNormals())
  {
    //...compute them.
    vtkNew<vtkPolyDataNormals> genNormals;
    genNormals->SetInputDataObject(inputPD);
    genNormals->ConsistencyOn();
    genNormals->SplittingOff();
    genNormals->Update();
    inputPD = genNormals->GetOutput();
  }

  // Access the input's bounds (using SMTK to exploit  caching if possible)
  std::array<double, 6> boundingBox;
  if (input->resource()->queries().contains<smtk::geometry::BoundingBox>())
  {
    boundingBox = input->resource()->queries().get<smtk::geometry::BoundingBox>()(input);
  }
  else
  {
    inputPD->GetBounds(boundingBox.data());
  }

  // Extend the bounding box by 5% along all axes to avoid clipping
  for (int i = 0; i < 3; i++)
  {
    double length = boundingBox[2 * i + 1] - boundingBox[2 * i];
    boundingBox[2 * i] -= length * .05;
    boundingBox[2 * i + 1] += length * .05;
  }

  // Compute an implicit signed distance image data around the surface
  vtkNew<vtkSignedDistance> signedDistance;
  signedDistance->SetDimensions(dimensions.data());
  signedDistance->SetBounds(boundingBox.data());
  signedDistance->SetRadius(radius);
  signedDistance->SetInputDataObject(inputPD);

  // Using the implicit signed distance image data, extract the surface
  vtkNew<vtkExtractSurface> extractSurface;
  extractSurface->SetInputConnection(signedDistance->GetOutputPort());
  extractSurface->SetRadius(radius);
  extractSurface->SetComputeNormals(false);
  extractSurface->Update();

  vtkPolyData* outputPD = extractSurface->GetOutput();

  // 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() + " (reconstructed)");
  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>(ReconstructSurface::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* ReconstructSurface::xmlDescription() const
{
  return ReconstructSurface_xml;
}
}
}
}
