//=============================================================================
// 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/Session.h"
#include "smtk/session/aeva/operators/LinearToQuadratic.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/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 "vtkGenerateGlobalIds.h"
#include "vtkLinearToQuadraticCellsFilter.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkUnstructuredGrid.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <algorithm>
#include <cmath>

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

  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();
  const std::vector<vtkIdType> linearElements = {
    VTK_LINE, VTK_TRIANGLE, VTK_QUAD, VTK_TETRA, VTK_HEXAHEDRON, VTK_WEDGE, VTK_PYRAMID
  };

  auto isLinearElement = [&linearElements](const vtkIdType& type) {
    return std::find(linearElements.begin(), linearElements.end(), type) == linearElements.end();
  };

  for (const auto& assoc : *assocs)
  {
    auto data = session->findStorage(assoc->id());
    if (!data)
    {
      return false;
    }
    vtkSmartPointer<vtkUnstructuredGrid> linMesh(vtkUnstructuredGrid::SafeDownCast(data));
    if (!linMesh)
    {
      return false;
    }

    if (linMesh->GetNumberOfCells() == 0)
    {
      continue;
    }
    if (isLinearElement(linMesh->GetCellType(0)))
    {
      return false;
    }
  }
  return true;
}

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

  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");
    // smtkInfoMacro(this->log(), "Found pt offset " << pointIdOffset );
  }
  if (resource->properties().contains<long>("global cell id offset"))
  {
    cellIdOffset = resource->properties().at<long>("global cell id offset");
    // smtkInfoMacro(this->log(), "Found cell offset " << cellIdOffset );
  }

  auto assocs = this->parameters()->associations();
  auto created = result->findComponent("created");
  smtk::operation::MarkGeometry geomMarker(resource);

  for (const auto& assoc : *assocs)
  {
    auto data = session->findStorage(assoc->id());
    if (!data)
    {
      continue;
    }
    vtkSmartPointer<vtkUnstructuredGrid> linMesh(vtkUnstructuredGrid::SafeDownCast(data));
    if (!linMesh)
    {
      smtkErrorMacro(this->log(), "Could not convert to vtkPolyData.");
      continue;
    }
    vtkNew<vtkLinearToQuadraticCellsFilter> promote;
    promote->SetInputDataObject(linMesh);
    promote->Update();
    vtkNew<vtkUnstructuredGrid> geom;
    geom->ShallowCopy(promote->GetOutput());

    Session::offsetGlobalIds(geom, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
    // update the ID offsets per-mesh
    pointIdOffset = maxPointId + 1;
    cellIdOffset = maxCellId + 1;

    std::vector<vtkIdType> volumeType = { VTK_TETRA, VTK_HEXAHEDRON, VTK_WEDGE, VTK_PYRAMID };
    bool isVolume =
      std::find(volumeType.begin(), volumeType.end(), linMesh->GetCellType(0)) != volumeType.end();

    if (isVolume)
    {
      auto volume = resource->addVolume();
      std::string name = assoc->name();
      volume.setName(name.empty() ? "promoted" : (name + " (quadratic)"));
      smtk::model::Volume(std::dynamic_pointer_cast<smtk::model::Entity>(assoc))
        .owningModel()
        .addCell(volume);
      auto vcomp = volume.entityRecord();
      session->addStorage(vcomp->id(), geom);
      created->appendValue(vcomp);
      geomMarker.markModified(vcomp);

      vtkNew<vtkDataSetSurfaceFilter> extractSurface;
      extractSurface->PassThroughCellIdsOn();
      extractSurface->SetInputDataObject(geom);
      extractSurface->Update();
      vtkNew<vtkUnstructuredGrid> surf;
      surf->ShallowCopy(extractSurface->GetOutput());
      Session::offsetGlobalIds(surf, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
      // update the ID offsets per-mesh
      pointIdOffset = maxPointId + 1;
      cellIdOffset = maxCellId + 1;

      auto face = resource->addFace();
      std::string prefix = "boundary of ";
      face.setName(prefix + volume.name());
      smtk::model::EntityRef(volume).addRawRelation(face);
      smtk::model::EntityRef(face).addRawRelation(volume);
      auto fcomp = face.entityRecord();
      session->addStorage(fcomp->id(), surf);
      created->appendValue(fcomp);
      geomMarker.markModified(fcomp);
    }
    else
    {
      auto face = resource->addFace();
      std::string name = assoc->name();
      face.setName(name.empty() ? "promoted" : (name + " (quadratic)"));
      smtk::model::Face(std::dynamic_pointer_cast<smtk::model::Entity>(assoc))
        .owningModel()
        .addCell(face);
      auto fcomp = face.entityRecord();
      session->addStorage(fcomp->id(), geom);
      created->appendValue(fcomp);
      geomMarker.markModified(fcomp);
    }
  }

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

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

const char* LinearToQuadratic::xmlDescription() const
{
  return LinearToQuadratic_xml;
}

}
}
}
