//=========================================================================
//  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/Predicates.h"
#include "smtk/attribute/ComponentItem.h"
#include "smtk/model/CellEntity.h"
#include "smtk/model/EntityRef.h"
#include "smtk/operation/MarkGeometry.h"

#include "smtk/io/Logger.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/session/aeva/NameManager.h"
#include "smtk/session/aeva/Resource.h"

#include "vtkCellData.h"
#include "vtkCellType.h"
#include "vtkCompositeDataIterator.h"
#include "vtkCompositeDataSet.h"
#include "vtkDataArray.h"
#include "vtkDataSet.h"
#include "vtkDataSetAttributes.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkIdList.h"
#include "vtkMath.h"
#include "vtkMultiBlockDataSet.h"
#include "vtkPolyData.h"
#include "vtkPolyDataNormals.h"
#include "vtkUnstructuredGrid.h"

#include <algorithm>

#ifdef ERROR
#undef ERROR
#endif

namespace smtk
{
namespace session
{
namespace aeva
{

vtkIdType NumberOfCells(const vtkSmartPointer<vtkDataObject>& data)
{
  auto* dset = vtkDataSet::SafeDownCast(data);
  if (dset)
  {
    return dset->GetNumberOfCells();
  }
  auto* comp = vtkCompositeDataSet::SafeDownCast(data);
  if (comp)
  {
    return comp->GetNumberOfCells();
  }
  return -1;
}

int DimensionFromCellType(int cellType)
{
  switch (cellType)
  {
    case VTK_VERTEX: // fall through
    case VTK_POLY_VERTEX:
      return 0;
    case VTK_LINE:             // fall through
    case VTK_POLY_LINE:        // fall through
    case VTK_QUADRATIC_EDGE:   // fall through
    case VTK_CUBIC_LINE:       // fall through
    case VTK_LAGRANGE_CURVE:   // fall through
    case VTK_PARAMETRIC_CURVE: // fall through
    case VTK_HIGHER_ORDER_EDGE:
      return 1;
    case VTK_TRIANGLE:                // fall through
    case VTK_TRIANGLE_STRIP:          // fall through
    case VTK_POLYGON:                 // fall through
    case VTK_PIXEL:                   // fall through
    case VTK_QUAD:                    // fall through
    case VTK_QUADRATIC_TRIANGLE:      // fall through
    case VTK_QUADRATIC_QUAD:          // fall through
    case VTK_QUADRATIC_POLYGON:       // fall through
    case VTK_BIQUADRATIC_QUAD:        // fall through
    case VTK_QUADRATIC_LINEAR_QUAD:   // fall through
    case VTK_BIQUADRATIC_TRIANGLE:    // fall through
    case VTK_LAGRANGE_TRIANGLE:       // fall through
    case VTK_LAGRANGE_QUADRILATERAL:  // fall through
    case VTK_PARAMETRIC_SURFACE:      // fall through
    case VTK_PARAMETRIC_TRI_SURFACE:  // fall through
    case VTK_PARAMETRIC_QUAD_SURFACE: // fall through
    case VTK_HIGHER_ORDER_TRIANGLE:   // fall through
    case VTK_HIGHER_ORDER_QUAD:       // fall through
    case VTK_HIGHER_ORDER_POLYGON:
      return 2;
    case VTK_TETRA:                            // fall through
    case VTK_VOXEL:                            // fall through
    case VTK_HEXAHEDRON:                       // fall through
    case VTK_WEDGE:                            // fall through
    case VTK_PYRAMID:                          // fall through
    case VTK_PENTAGONAL_PRISM:                 // fall through
    case VTK_HEXAGONAL_PRISM:                  // fall through
    case VTK_QUADRATIC_TETRA:                  // fall through
    case VTK_QUADRATIC_HEXAHEDRON:             // fall through
    case VTK_QUADRATIC_WEDGE:                  // fall through
    case VTK_QUADRATIC_PYRAMID:                // fall through
    case VTK_TRIQUADRATIC_HEXAHEDRON:          // fall through
    case VTK_QUADRATIC_LINEAR_WEDGE:           // fall through
    case VTK_BIQUADRATIC_QUADRATIC_WEDGE:      // fall through
    case VTK_BIQUADRATIC_QUADRATIC_HEXAHEDRON: // fall through
    case VTK_CONVEX_POINT_SET:                 // fall through
    case VTK_POLYHEDRON:                       // fall through
    case VTK_PARAMETRIC_TETRA_REGION:          // fall through
    case VTK_PARAMETRIC_HEX_REGION:            // fall through
    case VTK_HIGHER_ORDER_TETRAHEDRON:         // fall through
    case VTK_HIGHER_ORDER_WEDGE:               // fall through
    case VTK_HIGHER_ORDER_PYRAMID:             // fall through
    case VTK_HIGHER_ORDER_HEXAHEDRON:          // fall through
    case VTK_LAGRANGE_TETRAHEDRON:             // fall through
    case VTK_LAGRANGE_HEXAHEDRON:              // fall through
    case VTK_LAGRANGE_WEDGE:                   // fall through
    case VTK_LAGRANGE_PYRAMID:
      return 3;
    case VTK_EMPTY_CELL: // fall through
    default:
      break;
  }
  return -1;
}

int EstimateParametricDimension(const vtkSmartPointer<vtkDataObject>& data)
{
  int dimension = -1;
  if (!data)
  {
    return dimension;
  }
  auto* dset = vtkDataSet::SafeDownCast(data);
  if (dset)
  {
    vtkIdType nc = dset->GetNumberOfCells();
    auto ns = static_cast<double>(vtkMath::CeilLog2(nc));
    auto skip = static_cast<vtkIdType>(nc < 10000 ? 1 : (nc > 10e7 ? nc / 1.0e5 : nc / ns));
    for (vtkIdType ii = 0; ii < nc; ii += skip)
    {
      dimension = std::max(dimension, DimensionFromCellType(dset->GetCellType(ii)));
    }
    return dimension;
  }
  auto* cdata = vtkCompositeDataSet::SafeDownCast(data);
  if (cdata)
  {
    auto* it = cdata->NewIterator();
    it->SkipEmptyNodesOn();
    for (it->InitTraversal(); !it->IsDoneWithTraversal(); it->GoToNextItem())
    {
      dimension = std::max(dimension, EstimateParametricDimension(it->GetCurrentDataObject()));
    }
    it->Delete();
    return dimension;
  }

  return -1;
}

vtkDataObject::AttributeTypes EntityType(const vtkSmartPointer<vtkDataObject>& data)
{
  switch (EstimateParametricDimension(data))
  {
    case 0:
      return vtkDataObject::POINT;
    case 1:
    case 2:
    case 3:
      return vtkDataObject::CELL;
    default:
      break;
  }
  return vtkDataObject::NUMBER_OF_ATTRIBUTE_TYPES;
}

EdgeNeighborLambda EdgeNeighbors(const vtkSmartPointer<vtkDataObject>& data)
{
  EdgeNeighborLambda lambda = [](vtkIdType cellId, const EdgeNeighborVisitor& visitor) {
    (void)cellId;
    (void)visitor;
  };
  auto* ugrid = vtkUnstructuredGrid::SafeDownCast(data);
  auto* pdata = vtkPolyData::SafeDownCast(data);
  thread_local vtkNew<vtkIdList> neighbors;
  thread_local vtkNew<vtkIdList> pointsOfCell;
  if (ugrid)
  {
    ugrid->BuildLinks(); // Do now as otherwise lambda will not be threadsafe
    thread_local vtkNew<vtkIdList> edgePoints;
    edgePoints->SetNumberOfIds(2);
    lambda = [ugrid](vtkIdType cellId, const EdgeNeighborVisitor& visitor) {
      if (!visitor)
      {
        return;
      }
      if (DimensionFromCellType(ugrid->GetCellType(cellId)) == 2)
      {
        ugrid->GetCellPoints(cellId, pointsOfCell);
        vtkIdType np = pointsOfCell->GetNumberOfIds();
        for (vtkIdType ii = 0; ii < np; ++ii)
        {
          edgePoints->SetId(0, pointsOfCell->GetId(ii));
          edgePoints->SetId(1, pointsOfCell->GetId((ii + 1) % np));
          ugrid->GetCellNeighbors(cellId, edgePoints, neighbors);
          vtkIdType nn = neighbors->GetNumberOfIds();
          for (vtkIdType jj = 0; jj < nn; ++jj)
          {
            if (visitor(neighbors->GetId(jj)) == Break)
            {
              return;
            }
          }
        }
      }
    };
  }
  else if (pdata)
  {
    pdata->BuildLinks(); // Do now as otherwise lambda will not be threadsafe
    lambda = [pdata](vtkIdType cellId, const EdgeNeighborVisitor& visitor) {
      if (!visitor)
      {
        return;
      }
      if (DimensionFromCellType(pdata->GetCellType(cellId)) == 2)
      {
        pdata->GetCellPoints(cellId, pointsOfCell);
        vtkIdType np = pointsOfCell->GetNumberOfIds();
        vtkIdType p0 = pointsOfCell->GetId(np - 1);
        vtkIdType p1;
        for (vtkIdType ii = 0; ii < np; ++ii)
        {
          p1 = pointsOfCell->GetId(ii);
          pdata->GetCellEdgeNeighbors(cellId, p0, p1, neighbors);
          vtkIdType nn = neighbors->GetNumberOfIds();
          for (vtkIdType jj = 0; jj < nn; ++jj)
          {
            if (visitor(neighbors->GetId(jj)) == Break)
            {
              return;
            }
          }
          p0 = p1;
        }
      }
    };
  }
  return lambda;
}

void SurfaceWithNormals(const vtkSmartPointer<vtkDataObject>& data,
  vtkSmartPointer<vtkPolyData>& surf,
  vtkSmartPointer<vtkDataArray>& norm)
{
  surf = vtkPolyData::SafeDownCast(data);
  if (!surf)
  {
    vtkNew<vtkDataSetSurfaceFilter> extractSurface;
    extractSurface->PassThroughCellIdsOn();
    extractSurface->SetInputDataObject(data);
    extractSurface->Update();
    surf = extractSurface->GetOutput();
  }
  if (!surf)
  {
    smtkErrorMacro(smtk::io::Logger::instance(), "Could not generate surface.");
    return;
  }
  if (!surf->GetCellData()->GetNormals())
  {
    vtkNew<vtkPolyDataNormals> genNormals;
    genNormals->SetInputDataObject(surf);
    genNormals->ConsistencyOn();
    genNormals->SplittingOff();
    genNormals->ComputeCellNormalsOn();
    genNormals->Update();
    surf = genNormals->GetOutput();
  }
  norm = surf->GetCellData()->GetNormals();
}

namespace
{

void relateToParent(const smtk::model::EntityRef& parent, const smtk::model::EntityRef& child)
{
  if (parent.isModel())
  {
    if (child.isCellEntity())
    {
      smtk::model::Model(parent).addCell(smtk::model::CellEntity(child));
    }
    else if (child.isGroup())
    {
      smtk::model::Model(parent).addGroup(smtk::model::Group(child));
    }
    else
    {
      smtkErrorMacro(smtk::io::Logger::instance(),
        "Unknown child type " << child.flagSummary() << ". Did not add to model.");
    }
  }
  else if (parent.isGroup())
  {
    smtk::model::Group(parent).addEntity(child);
  }
  else if (parent.isCellEntity())
  {
    smtk::model::EntityRef(parent).addRawRelation(child);
    smtk::model::EntityRef(child).addRawRelation(parent);
  }
}

} // anonymous namespace

smtk::model::EntityRef CreateCellForData(smtk::model::EntityRef& parent,
  vtkSmartPointer<vtkDataObject> const& data,
  smtk::attribute::ComponentItemPtr& created,
  int& count,
  smtk::operation::MarkGeometry& marker,
  long pointIdOffset,
  long cellIdOffset,
  long& maxPointId,
  long& maxCellId)
{
  smtk::model::CellEntity result;
  if (!parent.isValid() || !data)
  {
    return result;
  }
  auto resource = std::dynamic_pointer_cast<smtk::session::aeva::Resource>(parent.resource());
  auto const& session = resource->session();
  if (auto* dataset = vtkDataSet::SafeDownCast(data))
  {
    vtkIdType numberOfCells = dataset->GetNumberOfCells();
    vtkIdType numberOfPoints = dataset->GetNumberOfPoints();
    if (numberOfCells == 0 || numberOfPoints == 0)
    {
      return result;
    }
    Session::offsetGlobalIds(dataset, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
    // Assume first cell has "maximal dimension":
    auto* vcell = dataset->GetCell(0);
    auto vdim = vcell->GetCellDimension();
    auto cellComp =
      resource->insertEntityOfTypeAndDimension(smtk::model::CELL_ENTITY, vdim)->second;
    result = smtk::model::CellEntity(cellComp);
    Session::setPrimary(*cellComp, true);

#ifdef SMTK_ENABLE_PARAVIEW_SUPPORT
    auto* app = pqApplicationCore::instance();
    auto* behavior = app ? pqSMTKBehavior::instance() : nullptr;
    auto* wrapper = behavior ? behavior->builtinOrActiveWrapper() : nullptr;
    auto nameManager =
      wrapper ? wrapper->smtkManagers().get<smtk::session::aeva::NameManager::Ptr>() : nullptr;
#else
    smtk::session::aeva::NameManager* nameManager = nullptr;
#endif

    std::string name = "cell ";
    if (nameManager)
    {
      name = nameManager->nameForObject(*cellComp);
      result.setName(name);
    }
    else
    {
      if (vdim == 3)
      {
        name = "volume ";
      }
      else if (vdim == 2)
      {
        name = "surface ";
      }
      else if (vdim == 1)
      {
        name = "vertex ";
      }
      result.setName(name + std::to_string(count));
      ++count;
    }
    session->addStorage(cellComp->id(), dataset);
    if (vdim < 3)
    {
      marker.markModified(cellComp);
    }
    relateToParent(parent, result);
    created->appendValue(cellComp);
    return result;
  }
  if (auto* multiblock = vtkMultiBlockDataSet::SafeDownCast(data))
  {
    std::vector<smtk::model::CellEntity> members;
    std::string name = "group " + std::to_string(count);
    ++count;
    auto group = resource->addGroup(/* extraFlags */ 0, name);
    created->appendValue(group.component());
    for (unsigned int bb = 0; bb < multiblock->GetNumberOfBlocks(); ++bb)
    {
      auto cell = CreateCellForData(group,
        multiblock->GetBlock(bb),
        created,
        count,
        marker,
        pointIdOffset,
        cellIdOffset,
        maxPointId,
        maxCellId);
      if (cell.isValid())
      {
        members.emplace_back(cell);
      }
    }
    group.addEntities(members);
    relateToParent(parent, group);
    return group;
  }
  return result;
}

}
}
}
