//=========================================================================
//  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/NameManager.h"
#include "smtk/session/aeva/Predicates.h"
#include "smtk/session/aeva/operators/Import.h"

#include "vtk/aeva/ext/vtkMedReader.h"

#include "smtk/session/aeva/Import_xml.h"
#include "smtk/session/aeva/Resource.h"
#include "smtk/session/aeva/Session.h"

#include "smtk/extension/vtk/io/ImportAsVTKData.h"

#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/ComponentItem.h"
#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/FileItemDefinition.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/ReferenceItem.h"
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/ResourceItem.h"
#include "smtk/attribute/StringItem.h"

#include "smtk/model/Face.h"
#include "smtk/model/Group.h"
#include "smtk/model/Model.h"
#include "smtk/model/Vertex.h"
#include "smtk/model/Volume.h"

#include "smtk/operation/MarkGeometry.h"

#include "smtk/resource/Manager.h"

#include "smtk/common/Paths.h"

#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h"

#include "itkImage.h"
#include "itkImageFileReader.h"
#include "itkImageIOBase.h"
#include "itkImageToVTKImageFilter.h"
#include "itkObjectFactoryBase.h"

#include "pqApplicationCore.h"

#include "vtkCell3D.h"
#include "vtkCellData.h"
#include "vtkDataArray.h"
#include "vtkExodusIIReader.h"
#include "vtkFieldData.h"
#include "vtkGenerateGlobalIds.h"
#include "vtkGeometryFilter.h"
#include "vtkImageData.h"
#include "vtkInformation.h"
#include "vtkIntArray.h"
#include "vtkLookupTable.h"
#include "vtkMultiBlockDataSet.h"
#include "vtkMultiThreshold.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkSmartPointer.h"
#include "vtkStringArray.h"
#include "vtkUnstructuredGrid.h"
#include "vtkXMLImageDataReader.h"
#include "vtkXMLPImageDataReader.h"

SMTK_THIRDPARTY_PRE_INCLUDE
#include "boost/filesystem.hpp"
SMTK_THIRDPARTY_POST_INCLUDE

#ifdef ERROR
#undef ERROR
#endif

#include <sstream>

using CellEntity = smtk::model::CellEntity;
using CellEntities = smtk::model::CellEntities;

namespace smtk
{
namespace session
{
namespace aeva
{
namespace
{

// Object types
std::array<int, 14> exoObjectTypes{
  vtkExodusIIReader::ObjectType::EDGE_BLOCK,
  vtkExodusIIReader::ObjectType::FACE_BLOCK,
  vtkExodusIIReader::ObjectType::ELEM_BLOCK,
  vtkExodusIIReader::ObjectType::NODE_SET,
  vtkExodusIIReader::ObjectType::EDGE_SET,
  vtkExodusIIReader::ObjectType::FACE_SET,
  vtkExodusIIReader::ObjectType::SIDE_SET,
  vtkExodusIIReader::ObjectType::ELEM_SET,
  vtkExodusIIReader::ObjectType::NODE_MAP,
  vtkExodusIIReader::ObjectType::EDGE_MAP,
  vtkExodusIIReader::ObjectType::FACE_MAP,
  vtkExodusIIReader::ObjectType::ELEM_MAP,
  vtkExodusIIReader::ObjectType::GLOBAL,
  vtkExodusIIReader::ObjectType::NODAL,
};

// Connectivity types
// These correspond to the ordered list of top-level blocks of the Exodus reader's output.
std::array<int, 8> exoConnTypes{ vtkExodusIIReader::ELEM_BLOCK_ELEM_CONN,
  vtkExodusIIReader::FACE_BLOCK_CONN,
  vtkExodusIIReader::EDGE_BLOCK_CONN,
  vtkExodusIIReader::ELEM_SET_CONN,
  vtkExodusIIReader::SIDE_SET_CONN,
  vtkExodusIIReader::FACE_SET_CONN,
  vtkExodusIIReader::EDGE_SET_CONN,
  vtkExodusIIReader::NODE_SET_CONN };
// Map the above connectivity types to exodus object types.
std::array<int, 8> exoConnTypeToObjectType{ 2, 1, 0, 7, 6, 5, 4, 3 };
// Names for the block and set types.
std::array<std::string, 8> exoConnTypeNames{ // Blocks
  "element block",
  "face block",
  "edge block",
  // Sets
  "element set",
  "side set",
  "face set",
  "edge set",
  "node set"
};

struct BlockGlobals
{
  int ObjectType;
  int ObjectId;
  vtkIdType StartCellId;
  vtkIdType StartPointId;
  std::map<vtkIdType, vtkIdType> CellIdMap; // From exodus ID to aeva ID.

  struct CellLessThan
  {
    bool operator()(const BlockGlobals& a, const BlockGlobals& b) const
    {
      return a.StartCellId < b.StartCellId;
    }
  };

  struct PointLessThan
  {
    bool operator()(const BlockGlobals& a, const BlockGlobals& b) const
    {
      return a.StartPointId < b.StartPointId;
    }
  };
};

void generatePrimaryIds(vtkDataSet* block,
  long& pointIdOffset,
  long& cellIdOffset,
  std::set<BlockGlobals, BlockGlobals::PointLessThan>& pointGlobals,
  std::set<BlockGlobals, BlockGlobals::CellLessThan>& cellGlobals,
  int objectId,
  int objectType)
{
  if (!block)
  {
    return;
  }

  auto* pgids = vtkIdTypeArray::SafeDownCast(block->GetPointData()->GetGlobalIds());
  auto* cgids = vtkIdTypeArray::SafeDownCast(block->GetCellData()->GetGlobalIds());

  vtkIdType numPoints = block->GetNumberOfElements(vtkDataObject::POINT);
  vtkIdType numCells = block->GetNumberOfElements(vtkDataObject::CELL);
  vtkIdType startPointId = pgids->GetValueRange()[0] - 1 + static_cast<vtkIdType>(pointIdOffset);
  BlockGlobals info{ objectId, objectType, cellIdOffset, startPointId };

  // Rewrite point globals to integers (NB: once aeva switches to vtkIdType-globals,
  // we will still need to rewrite IDs to include globalPointOffset).
  vtkNew<vtkIntArray> smallGlobals;
  smallGlobals->SetNumberOfValues(numPoints);
  for (vtkIdType ii = 0; ii < numPoints; ++ii)
  {
    smallGlobals->SetValue(ii, pgids->GetValue(ii) - 1 + pointIdOffset);
  }
  smallGlobals->SetName("global ids");
  block->GetPointData()->SetGlobalIds(smallGlobals);

  // Create cell globals
  vtkNew<vtkIntArray> consecutiveGlobals;
  consecutiveGlobals->SetName("global ids");
  consecutiveGlobals->SetNumberOfValues(numCells);
  for (vtkIdType ii = 0; ii < numCells; ++ii)
  {
    consecutiveGlobals->SetValue(ii, cellIdOffset + ii);
    info.CellIdMap[cgids->GetValue(ii)] = cellIdOffset + ii;
  }
  block->GetCellData()->SetGlobalIds(consecutiveGlobals);

  // Add to index
  pointGlobals.insert(info);
  cellGlobals.insert(info);

  // Update offset
  cellIdOffset += numCells;
  // NB: Do not update pointIdOffset since points
  // are shared across all objects in the same file.
}

void generateSecondaryIds(vtkDataSet* block, long& pointIdOffset, long& cellIdOffset)
{
  if (!block)
  {
    return;
  }

  auto* pgids = vtkIdTypeArray::SafeDownCast(block->GetPointData()->GetGlobalIds());
  // auto* cgids = vtkIdTypeArray::SafeDownCast(block->GetCellData()->GetGlobalIds());

  vtkIdType numPoints = block->GetNumberOfElements(vtkDataObject::POINT);
  vtkIdType numCells = block->GetNumberOfElements(vtkDataObject::CELL);

  // Rewrite point globals to integers (NB: once aeva switches to vtkIdType-globals,
  // we will still need to rewrite IDs to include globalPointOffset).
  vtkNew<vtkIntArray> smallGlobals;
  smallGlobals->SetNumberOfValues(numPoints);
  for (vtkIdType ii = 0; ii < numPoints; ++ii)
  {
    smallGlobals->SetValue(ii, pgids->GetValue(ii) - 1 + pointIdOffset);
  }
  smallGlobals->SetName("global ids");
  block->GetPointData()->SetGlobalIds(smallGlobals);

  // Rewrite global cell IDs to reference our contiguous versions.
  // TODO: Find ID on surface of element block if it exists?
  vtkNew<vtkIntArray> consecutiveGlobals;
  consecutiveGlobals->SetName("global ids");
  consecutiveGlobals->SetNumberOfValues(numCells);
  for (vtkIdType ii = 0; ii < numCells; ++ii)
  {
    consecutiveGlobals->SetValue(ii, cellIdOffset + ii);
    // info.CellIdMap[cgids->GetValue(ii)] = cellIdOffset + ii;
  }
  block->GetCellData()->SetGlobalIds(consecutiveGlobals);

  // Add to index
  // BlockGlobals info{objectId, objectType, startCellId, startPointId};
  // pointGlobals.insert(info);
  // cellGlobals.insert(info);

  // Update offset
  // TODO: This should not be needed for non-primary geometry, but
  //       Exodus side sets need not lie on the surface of element
  //       blocks (sides can be taken of any element), so we cannot
  //       rely on the face-cell of a volumetric element block to
  //       have all the cell IDs we need.
  cellIdOffset += numCells;
  // NB: Do not update pointIdOffset since points
  // are shared across all objects in the same file.
}

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

smtk::model::EntityRef createCellForData(smtk::model::EntityRef& parent,
  vtkSmartPointer<vtkDataObject> const& data,
  smtk::attribute::ComponentItem::Ptr& 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);

    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;

    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;
}
} // anonymous namespace

Import::Result Import::operateInternal()
{
  smtk::session::aeva::Resource::Ptr resource = nullptr;
  smtk::session::aeva::Session::Ptr session = nullptr;
  m_result = this->createResult(Import::Outcome::FAILED);

  this->prepareResourceAndSession(m_result, resource, session);

  smtk::attribute::FileItem::Ptr filenameItem = this->parameters()->findFile("filename");
  std::string filename = filenameItem->value(0);

  std::string potentialName = smtk::common::Paths::stem(filename);
  if (resource->name().empty() && !potentialName.empty())
  {
    resource->setName(potentialName);
  }

  auto itkExtensions = Import::supportedITKFileFormats();
  std::string ext = smtk::common::Paths::extension(filename);
  if (ext == ".med")
  {
    return this->importMedMesh(resource);
  }
  if (ext == ".vti")
  {
    return this->importVTKImage(resource);
  }
  if (itkExtensions.find(ext) != itkExtensions.end())
  {
    return this->importITKImage(resource);
  }
  if (ext == ".ex2" || ext == ".exii" || ext == ".exo" || ext == ".e" || ext == ".gen" ||
    ext == ".g")
  {
    return this->importExodusMesh(resource);
  }
  return this->importVTKMesh(resource);
}

Import::Specification Import::createSpecification()
{
  Specification spec = this->smtk::operation::XMLOperation::createSpecification();
  auto importDef = spec->findDefinition("import");

  std::vector<smtk::attribute::FileItemDefinition::Ptr> fileItemDefinitions;
  auto fileItemDefinitionFilter = [](smtk::attribute::FileItemDefinition::Ptr const& ptr) {
    return ptr->name() == "filename";
  };
  importDef->filterItemDefinitions(fileItemDefinitions, fileItemDefinitionFilter);

  assert(fileItemDefinitions.size() == 1);

  std::stringstream fileFilters;
  // Add fixed list of explicitly-supported import formats:
  fileFilters << "Nearly Raw Raster Data (*.nrrd *.nrrd.gz);; "
              << "Neuroimaging InFormatics Technology Initiative Files (*.nifti *.nia *.nii "
                 "*.nii.gz *.hdr *.img *.img.gz);; "
              << "VTK Image Data (*.vti);; "
              << "Salome Med Mesh (*.med);; "
              << "Exodus II Datasets (*.e *.exo *.ex2);; "
              << "Genesis files (*.gen);; ";

  // Add any other extensions ITK supports that we don't know about:
  auto itkExtensions = Import::supportedITKFileFormats();
  // Subtract extensions we just advertised:
  itkExtensions.erase(".nrrd");
  itkExtensions.erase(".nrrd.gz");
  itkExtensions.erase(".nifti");
  itkExtensions.erase(".nia");
  itkExtensions.erase(".nii");
  itkExtensions.erase(".nii.gz");
  itkExtensions.erase(".hdr");
  itkExtensions.erase(".img");
  itkExtensions.erase(".img.gz");
  fileFilters << "ITK Image Data (";
  bool iefirst = true;
  for (const auto& ext : itkExtensions)
  {
    fileFilters << (iefirst ? "*" : " *") << ext;
    iefirst = false;
  }
  fileFilters << ");;";

  // Add extensions SMTK knows VTK can do.
  auto vtkFormats = Import::supportedVTKFileFormats();
  for (const auto& format : vtkFormats)
  {
    std::set<std::string> exts = format.second;
    exts.erase(".med");
    exts.erase(".exo");
    exts.erase(".ex2");
    exts.erase(".exii");
    exts.erase(".e");
    if (exts.empty())
    {
      continue;
    }
    fileFilters << format.first << " (";
    bool vefirst = true;
    for (const auto& ext : exts)
    {
      fileFilters << (vefirst ? "*." : " *.") << ext;
      vefirst = false;
    }
    fileFilters << ");;";
  }

  fileFilters << "All files (*.*)";
  fileItemDefinitions[0]->setFileFilters(fileFilters.str());
  return spec;
}

const char* Import::xmlDescription() const
{
  return Import_xml;
}

vtkSmartPointer<vtkImageData> Import::readVTKImage(const std::string& filename)
{
  vtkSmartPointer<vtkImageData> image;
  // For now, load a surrogate VTK image
  vtkNew<vtkXMLImageDataReader> reader;
  reader->SetFileName(filename.c_str());
  reader->Update();
  image = reader->GetOutput();
  if (image)
  {
    auto* scalars = image->GetPointData()->GetScalars();
    if (scalars)
    {
      auto* lkup = scalars->GetLookupTable();
      if (!lkup)
      {
        vtkNew<vtkLookupTable> greyscale;
        greyscale->SetSaturationRange(0, 0);
        greyscale->SetValueRange(0, 1);
        greyscale->Build();
        scalars->SetLookupTable(greyscale);
        lkup = greyscale;
      }
      lkup->SetTableRange(scalars->GetRange());
    }
  }
  return image;
}

Import::Result Import::importVTKImage(const Resource::Ptr& resource)
{
  auto const& session = resource->session();
  smtk::attribute::FileItem::Ptr filenameItem = this->parameters()->findFile("filename");
  smtk::attribute::StringItem::Ptr labelItem = this->parameters()->findString("label map");
  std::string filename = filenameItem->value();

  vtkSmartPointer<vtkImageData> image = Import::readVTKImage(filename);
  if (!image)
  {
    smtkInfoMacro(this->log(), "Unable to read image from: " << filename << ".");
    return m_result;
  }

  auto model = resource->addModel(3, 3, filename);
  auto modelComp = model.component();
  modelComp->properties().get<std::string>()["aeva_datatype"] = "image";
  modelComp->properties().get<std::string>()["import_filename"] = filename;
  auto volume = resource->addVolume();
  model.addCell(volume);
  auto createdItems = m_result->findComponent("created");
  createdItems->appendValue(modelComp);
  createdItems->appendValue(volume.component());

  session->addStorage(volume.entity(), image);
  if (image->GetPointData()->GetScalars())
  {
    volume.setName(image->GetPointData()->GetScalars()->GetName());
  }
  operation::MarkGeometry(resource).markModified(volume.component());

  m_result->findInt("outcome")->setValue(0, static_cast<int>(Import::Outcome::SUCCEEDED));
  return m_result;
}

Import::Result Import::importITKImage(const Resource::Ptr& resource)
{
  auto const& session = resource->session();
  smtk::attribute::FileItem::Ptr filenameItem = this->parameters()->findFile("filename");
  smtk::attribute::StringItem::Ptr labelItem = this->parameters()->findString("label map");
  std::string filename = filenameItem->value();

  vtkSmartPointer<vtkImageData> image;

  // ITK code
  constexpr unsigned int Dimension = 3;

  using PixelType = unsigned short;
  using ImageType = itk::Image<PixelType, Dimension>;

  using ReaderType = itk::ImageFileReader<ImageType>;
  ReaderType::Pointer reader = ReaderType::New();
  if (!reader)
  {
    smtkInfoMacro(this->log(), "Unable to create reader for \"" << filename << "\".");
    return m_result;
  }
  reader->SetFileName(filename);
  reader->Update();

  ImageType::Pointer img = reader->GetOutput();
  if (!img)
  {
    smtkInfoMacro(this->log(), "Unable to read image from: " << filename << ".");
    return m_result;
  }

  // Hook into smtk resource after the image has been successfully loaded.
  // TODO use preserved UUIDS, if it exists, and call insertModel() insertVolume()
  auto model = resource->addModel(3, 3, filename);
  auto volume = resource->addVolume();
  model.addCell(volume);
  // tag the model with its aeva datatype, so Export can filter against it.
  // TODO maybe should be a typed enum.
  model.component()->properties().get<std::string>()["aeva_datatype"] = "image";
  model.component()->properties().get<std::string>()["import_filename"] = filename;
  auto createdItems = m_result->findComponent("created");
  createdItems->appendValue(model.component());
  createdItems->appendValue(volume.component());

  // Extract data from ITK image
  auto region = img->GetLargestPossibleRegion();
  auto size = region.GetSize();
  auto spacing = img->GetSpacing();
  auto origin = img->GetOrigin();
  auto directions = img->GetDirection();
  auto directions_vnl = directions.GetVnlMatrix();

  // Set up image importer
  vtkSmartPointer<vtkImageImport> importer = vtkSmartPointer<vtkImageImport>::New();
  importer->SetWholeExtent(0,
    static_cast<int>(size[0]) - 1,
    0,
    static_cast<int>(size[1]) - 1,
    0,
    static_cast<int>(size[2]) - 1);
  importer->SetDataExtentToWholeExtent();
  importer->SetDataDirection(directions_vnl.data_block());
  importer->SetDataSpacing(spacing[0], spacing[1], spacing[2]);
  importer->SetDataOrigin(origin[0], origin[1], origin[2]);
  importer->SetDataScalarType(VTK_UNSIGNED_SHORT);
  importer->SetNumberOfScalarComponents(1);
  importer->SetImportVoidPointer(img->GetBufferPointer());
  importer->Update();

  image = vtkSmartPointer<vtkImageData>::New();
  image->DeepCopy(importer->GetOutput());
  auto* scalars = image->GetPointData()->GetScalars();
  if (scalars)
  {
    auto* lkup = scalars->GetLookupTable();
    if (!lkup)
    {
      vtkNew<vtkLookupTable> greyscale;
      greyscale->SetSaturationRange(0, 0);
      greyscale->SetValueRange(0, 1);
      greyscale->Build();
      scalars->SetLookupTable(greyscale);
      lkup = greyscale;
    }
    lkup->SetTableRange(scalars->GetRange());
  }

  session->addStorage(volume.entity(), image);
  if (image->GetPointData()->GetScalars())
  {
    volume.setName(image->GetPointData()->GetScalars()->GetName());
  }
  operation::MarkGeometry(resource).markModified(volume.component());

  m_result->findInt("outcome")->setValue(0, static_cast<int>(Import::Outcome::SUCCEEDED));
  return m_result;
}

Import::Result Import::importMedMesh(const Resource::Ptr& resource)
{
  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;

  auto const& session = resource->session();
  smtk::attribute::FileItem::Ptr filenameItem = this->parameters()->findFile("filename");
  smtk::attribute::StringItem::Ptr labelItem = this->parameters()->findString("label map");
  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 );
  }

  for (size_t j = 0; j < filenameItem->numberOfValues(); j++)
  {
    vtkSmartPointer<vtkMedReader> reader = vtkSmartPointer<vtkMedReader>::New();
    std::string filename = filenameItem->value(j);
    reader->SetFileName(filename.c_str());
    reader->Update();
    vtkSmartPointer<vtkMultiBlockDataSet> output =
      vtkMultiBlockDataSet::SafeDownCast(reader->GetOutput());
    if (!output)
    {
      smtkErrorMacro(this->log(), "Unable to read .med file from: " << filename << ".");
      return m_result;
    }
    // For every mesh within the file
    for (vtkIdType k = 0; k < output->GetNumberOfBlocks(); k++)
    {
      vtkSmartPointer<vtkMultiBlockDataSet> meshOutput =
        vtkMultiBlockDataSet::SafeDownCast(output->GetBlock(k));
      if (!meshOutput)
      {
        smtkErrorMacro(
          this->log(), "Unexpected format from .med file reader, from: " << filename << ".");
        return m_result;
      }
      vtkSmartPointer<vtkMultiBlockDataSet> geomOutput =
        vtkMultiBlockDataSet::SafeDownCast(meshOutput->GetBlock(0));
      vtkSmartPointer<vtkMultiBlockDataSet> groupOutput =
        vtkMultiBlockDataSet::SafeDownCast(meshOutput->GetBlock(1));
      // OK for there to be no groups.
      if (!geomOutput || !geomOutput->GetNumberOfBlocks() || !groupOutput)
      {
        smtkErrorMacro(
          this->log(), "Unexpected format from .med file reader, from: " << filename << ".");
        return m_result;
      }

      // If there is no name, use file name
      const char* meshNamecStr = output->GetMetaData(k)->Get(vtkCompositeDataSet::NAME());
      const std::string meshNameStr =
        meshNamecStr ? std::string(meshNamecStr) : smtk::common::Paths::stem(filename);
      auto model = resource->addModel(3, 3, meshNameStr);
      auto modelComp = model.component();
      modelComp->properties().get<std::string>()["aeva_datatype"] = "poly";
      modelComp->properties().get<std::string>()["import_filename"] = filename;
      auto createdItems = m_result->findComponent("created");
      createdItems->appendValue(modelComp);
      CellEntities cells;
      // maintain a list of volumes and the global Ids they contain,
      // so surfaces can be matched to them.
      std::vector<std::pair<CellEntity, std::set<int> > > volumeIds;

      // loop through once and collect global ids from volumes.
      for (vtkIdType i = 0; i < geomOutput->GetNumberOfBlocks(); ++i)
      {
        const char* val = geomOutput->GetMetaData(i)->Get(vtkCompositeDataSet::NAME());
        std::string name = val ? std::string(val) : std::string();
        vtkUnstructuredGrid* mesh = vtkUnstructuredGrid::SafeDownCast(geomOutput->GetBlock(i));
        if (!mesh || !mesh->GetNumberOfCells())
        {
          continue;
        }
        CellEntity cell;
        vtkUnstructuredGrid* data = nullptr;
        auto cellType = mesh->GetCellType(0);
        if (cellType == VTK_TETRA || cellType == VTK_QUADRATIC_TETRA)
        {
          cell = resource->addVolume();
          Session::setPrimary(*cell.component(), true);
          data = mesh;
          size_t index = volumeIds.size();
          if (name.empty())
          {
            if (nameManager)
            {
              name = meshNameStr + " " + nameManager->nameForObject(*cell.component());
            }
            else
            {
              name = meshNameStr + " volume " + std::to_string(index);
            }
          }
          cell.setName(name);
          // record all used volume IDs, and apply ID offset so they are unique
          Session::offsetGlobalIds(data, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
          std::set<int> idSet;
          vtkIntArray* volNums = vtkIntArray::SafeDownCast(data->GetPointData()->GetGlobalIds());
          for (int i = 0; i < volNums->GetNumberOfValues(); ++i)
          {
            idSet.insert(volNums->GetValue(i));
          }
          volumeIds.emplace_back(cell, idSet);
          // smtkInfoMacro(this->log(), "vol pt ids " << volNums->GetNumberOfValues()
          //                                          << " min: " << int(volNums->GetRange()[0])
          //                                          << " max: " << int(volNums->GetRange()[1]));
          // volNums = vtkIntArray::SafeDownCast(data->GetCellData()->GetGlobalIds());
          // smtkInfoMacro(this->log(), "vol cl ids " << volNums->GetNumberOfValues()
          //                                          << " min: " << int(volNums->GetRange()[0])
          //                                          << " max: " << int(volNums->GetRange()[1]));
        }
      }
      // Loop through again, and connect surfaces to volumes using global ids.
      size_t volNum = 0;
      for (vtkIdType i = 0; i < geomOutput->GetNumberOfBlocks(); ++i)
      {
        const char* val = geomOutput->GetMetaData(i)->Get(vtkCompositeDataSet::NAME());
        std::string name = val ? std::string(val) : std::string();
        vtkUnstructuredGrid* mesh = vtkUnstructuredGrid::SafeDownCast(geomOutput->GetBlock(i));
        if (mesh == nullptr)
        {
          continue;
        }
        CellEntity cell;
        vtkUnstructuredGrid* data = nullptr;
        auto cellType = mesh->GetCellType(0);
        if (cellType == VTK_TETRA || cellType == VTK_QUADRATIC_TETRA)
        {
          // already handled above, retrieve.
          data = mesh;
          cell = volumeIds[volNum].first;
          ++volNum;
        }
        else if (cellType == VTK_TRIANGLE || cellType == VTK_QUADRATIC_TRIANGLE)
        {
          cell = resource->addFace();
          Session::setPrimary(*cell.component(), true);
          data = mesh;
          Session::offsetGlobalIds(data, pointIdOffset, cellIdOffset, maxPointId, maxCellId);

          size_t index = volumeIds.size();
          CellEntity vol;
          vtkIntArray* srfNums = vtkIntArray::SafeDownCast(data->GetPointData()->GetGlobalIds());
          for (size_t j = 0; j < volumeIds.size(); ++j)
          {
            auto& idSet = volumeIds[j].second;
            bool belongs = true;
            for (int i = 0; i < srfNums->GetNumberOfValues(); ++i)
            {
              if (idSet.count(srfNums->GetValue(i)) == 0)
              {
                // found a surface point ID not belonging to this volume, skip
                belongs = false;
                break;
              }
            }
            if (belongs)
            {
              // all surf IDs belong to the volume, too.
              vol = volumeIds[j].first;
              index = j;
              break;
            }
          }
          if (!vol.isValid())
          {
            // empty volume bounded by this valid surface
            vol = resource->addVolume();
            Session::setPrimary(*vol.component(), true);
            index = volumeIds.size();
            if (nameManager)
            {
              vol.setName(meshNameStr + " " + nameManager->nameForObject(*vol.component()));
            }
            else
            {
              vol.setName(meshNameStr + " volume " + std::to_string(index));
            }
            volumeIds.emplace_back(vol, std::set<int>());
          }
          if (name.empty())
          {
            if (nameManager)
            {
              name = meshNameStr + " " + nameManager->nameForObject(*cell.component());
            }
            else
            {
              name = meshNameStr + " surface " + std::to_string(index);
            }
          }
          cell.setName(name);
          // Connect to each other, abd add the volume to the model.
          vol.addRawRelation(cell);
          cell.addRawRelation(vol);
          model.addCell(vol);
          // smtkInfoMacro(this->log(), "srf pt ids " << srfNums->GetNumberOfValues()
          //                                          << " min: " << int(srfNums->GetRange()[0])
          //                                          << " max: " << int(srfNums->GetRange()[1]));
          // srfNums = vtkIntArray::SafeDownCast(data->GetCellData()->GetGlobalIds());
          // smtkInfoMacro(this->log(), "srf cl ids " << srfNums->GetNumberOfValues()
          //                                          << " min: " << int(srfNums->GetRange()[0])
          //                                          << " max: " << int(srfNums->GetRange()[1]));
        }
        else
        {
          smtkWarningMacro(this->log(),
            "Unhandled med cell type, not triangle or tetra: " << mesh->GetCellType(0));
        }

        if (data)
        {
          createdItems->appendValue(cell.component());
          session->addStorage(cell.entity(), data);
          operation::MarkGeometry(resource).markModified(cell.component());
        }
      }
      // handle the groups/side-sets, which are pieces of the geometry output.
      for (vtkIdType i = 0; i < groupOutput->GetNumberOfBlocks(); ++i)
      {
        const char* val = groupOutput->GetMetaData(i)->Get(vtkCompositeDataSet::NAME());
        std::string name = val ? std::string(val) : std::string();
        vtkUnstructuredGrid* mesh = vtkUnstructuredGrid::SafeDownCast(groupOutput->GetBlock(i));
        if (mesh == nullptr || mesh->GetNumberOfCells() == 0)
        {
          continue;
        }
        CellEntity cell;
        vtkDataObject* data = nullptr;
        auto cellType = mesh->GetCellType(0);
        if (vtkCell3D::SafeDownCast(mesh->GetCell(0)) != nullptr ||
          cellType == VTK_QUADRATIC_TETRA || cellType == VTK_QUADRATIC_HEXAHEDRON ||
          cellType == VTK_QUADRATIC_PYRAMID || cellType == VTK_QUADRATIC_WEDGE)
        {
          cell = resource->addVolume();
          data = mesh;
        }
        else if (cellType == VTK_TRIANGLE || cellType == VTK_LINE || cellType == VTK_QUAD ||
          cellType == VTK_QUADRATIC_TRIANGLE || cellType == VTK_QUADRATIC_EDGE ||
          cellType == VTK_QUADRATIC_QUAD)
        {
          cell = resource->addFace();
          data = mesh;
        }
        else if (cellType == VTK_VERTEX)
        {
          cell = resource->addVertex();
          data = mesh;
        }
        else
        {
          smtkWarningMacro(this->log(), "Unhandled med cell type: " << mesh->GetCellType(0));
        }

        if (data)
        {
          Session::offsetGlobalIds(mesh, pointIdOffset, cellIdOffset, maxPointId, maxCellId);
          model.addCell(cell);
          // mark the element as being a side-set.
          Session::setPrimary(*cell.component(), false);
          cell.setName(name);

          createdItems->appendValue(cell.component());
          session->addStorage(cell.entity(), data);
          operation::MarkGeometry(resource).markModified(cell.component());
          // DEBUG
          // vtkIntArray* srfNums = vtkIntArray::SafeDownCast(
          //   vtkUnstructuredGrid::SafeDownCast(data)->GetCellData()->GetGlobalIds());
          // if (srfNums)
          // {
          //   smtkInfoMacro(this->log(), "srf id checked "
          //       << name << ": " << srfNums->GetNumberOfValues() << " min: "
          //       << int(srfNums->GetRange()[0]) << " max: " << int(srfNums->GetRange()[1]));
          // }
          // else
          // {
          //   smtkInfoMacro(this->log(), "missing ids " << name);
          // }
        }
      }
      // update the ID offsets per-file
      pointIdOffset = maxPointId + 1;
      cellIdOffset = maxCellId + 1;
    }
  }
  // update the resource ID offsets.
  resource->properties().get<long>()["global point id offset"] = pointIdOffset;
  resource->properties().get<long>()["global cell id offset"] = cellIdOffset;

  m_result->findInt("outcome")->setValue(0, static_cast<int>(Import::Outcome::SUCCEEDED));
  return m_result;
}

Import::Result Import::importExodusMesh(const smtk::session::aeva::Resource::Ptr& resource)
{
  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;

  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 filenameItem = this->parameters()->findFile("filename");

  for (size_t j = 0; j < filenameItem->numberOfValues(); j++)
  {
    std::string filename = filenameItem->value(j);
    std::string fileStem = smtk::common::Paths::stem(filename);
    auto reader = vtkSmartPointer<vtkExodusIIReader>::New();
    reader->SetFileName(filename.c_str());
    // reader->SetSqueezePoints(false);
    reader->UpdateInformation();
    for (int ii = 0; ii < exoConnTypes.size(); ++ii)
    {
      // Ask for all data of all types, and for all arrays stored on that data.
      int numberOfObjects = reader->GetNumberOfObjects(exoObjectTypes[exoConnTypeToObjectType[ii]]);
      for (int jj = 0; jj < numberOfObjects; ++jj)
      {
        reader->SetObjectStatus(exoObjectTypes[exoConnTypeToObjectType[ii]], jj, 1);
      }
      reader->SetAllArrayStatus(exoObjectTypes[exoConnTypeToObjectType[ii]], 1);
    }
    reader
      ->GenerateGlobalElementIdArrayOn(); // aeva requires consecutive numbers within each entity.
    reader->GenerateGlobalNodeIdArrayOn();
    reader->Update();
    auto* output = reader->GetOutput();
    if (!output || output->GetNumberOfElements(vtkDataObject::CELL) == 0)
    {
      smtkErrorMacro(
        this->log(), "Unable to read Exodus-II file \"" << filename << "\" or it was empty.");
      return m_result;
    }

    auto model = resource->addModel(3, 3, fileStem);
    auto modelComp = model.component();
    modelComp->properties().get<std::string>()["aeva_datatype"] = "poly";
    modelComp->properties().get<std::string>()["import_filename"] = filename;
    auto createdItems = m_result->findComponent("created");
    createdItems->appendValue(modelComp);

    std::set<BlockGlobals, BlockGlobals::CellLessThan> cellGlobals;
    std::set<BlockGlobals, BlockGlobals::PointLessThan> pointGlobals;
    // For every block, create primary geometry.
    // For every set, create secondary geometry.
    // The first 3 entries of the output are blocks:
    for (int ii = 0; ii < static_cast<int>(exoConnTypes.size()); ++ii)
    {
      auto* blockTypeData = vtkMultiBlockDataSet::SafeDownCast(output->GetBlock(ii));
      if (!blockTypeData)
      {
        continue;
      } // This should never happen.
      for (vtkIdType jj = 0; jj < blockTypeData->GetNumberOfBlocks(); ++jj)
      {
        auto* block = vtkDataSet::SafeDownCast(blockTypeData->GetBlock(jj));
        if (!block)
        {
          continue;
        } // Empty blocks are not an error.

        int dim = EstimateParametricDimension(block);
        auto uuid = resource->addCellOfDimension(dim);
        smtk::model::CellEntity cellEnt(resource, uuid);
        auto cellComp = cellEnt.component();
        bool isPrimary =
          ii < 3; // exoConnTypeToObjectType[ii] < vtkExodusIIReader::ObjectType::NODE_SET;
        if (isPrimary)
        {
          generatePrimaryIds(block,
            pointIdOffset,
            cellIdOffset,
            pointGlobals,
            cellGlobals,
            ii,
            exoConnTypeToObjectType[ii]);
        }
        else
        {
          generateSecondaryIds(block, pointIdOffset, cellIdOffset);
        }
        resource->session()->addStorage(uuid, block);
        resource->session()->setPrimary(*cellComp, isPrimary);
        model.addCell(cellEnt);

        // If there is no name, use file name
        std::string exoBlockName = blockTypeData->GetMetaData(jj)->Get(vtkCompositeDataSet::NAME());
        std::ostringstream defaultName;
        if (nameManager)
        {
          defaultName << nameManager->nameForObject(*cellComp) << " ";
        }
        defaultName << exoConnTypeNames[ii] << " " << jj << " " << fileStem;
        const std::string meshNameStr =
          !exoBlockName.empty() && exoBlockName.find("Unnamed") == std::string::npos
          ? exoBlockName
          : defaultName.str();
        cellEnt.setName(meshNameStr);

        // Deal with volumetric meshes
        if (isPrimary)
        {
          if (dim == 3)
          {
            // Add a face cell representing the boundary.
            auto face = resource->addFace();
            vtkNew<vtkGeometryFilter> bdy;
            bdy->MergingOff();
            bdy->PassThroughCellIdsOn();
            bdy->PassThroughPointIdsOn();
            bdy->SetInputDataObject(0, block);
            bdy->Update();

            vtkPolyData* poly = bdy->GetOutput();
            auto* origCellIds =
              vtkIdTypeArray::SafeDownCast(poly->GetCellData()->GetArray("vtkOriginalCellIds"));
            auto* origPointIds =
              vtkIdTypeArray::SafeDownCast(poly->GetPointData()->GetArray("vtkOriginalPointIds"));
            if (origCellIds)
            {
              vtkNew<vtkIntArray> properCellIds;
              properCellIds->DeepCopy(origCellIds);
              properCellIds->SetName("global ids");
              poly->GetCellData()->RemoveArray(origCellIds->GetName());
              poly->GetCellData()->SetGlobalIds(properCellIds);
            }
            if (origPointIds)
            {
              vtkNew<vtkIntArray> properPointIds;
              properPointIds->DeepCopy(origPointIds);
              properPointIds->SetName("global ids");
              poly->GetPointData()->RemoveArray(origPointIds->GetName());
              poly->GetPointData()->SetGlobalIds(properPointIds);
            }
            auto faceComp = face.component();
            resource->session()->addStorage(face.entity(), poly);
            resource->session()->setPrimary(*faceComp, isPrimary);
            cellEnt.addRawRelation(face);
            face.addRawRelation(cellEnt);
            std::ostringstream faceName;
            if (nameManager)
            {
              faceName << nameManager->nameForObject(*faceComp) << " ";
            }
            faceName << exoConnTypeNames[ii] << " " << jj;
            face.setName(faceName.str());
          }
        }

        createdItems->appendValue(cellComp);
        smtk::operation::MarkGeometry().markModified(cellComp);
      }
    }
  }

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

  m_result->findInt("outcome")->setValue(0, static_cast<int>(Import::Outcome::SUCCEEDED));
  return m_result;
}

Import::Result Import::importVTKMesh(const smtk::session::aeva::Resource::Ptr& resource)
{
  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");
  }

  smtk::attribute::FileItem::Ptr filenameItem = this->parameters()->findFile("filename");
  smtk::extension::vtk::io::ImportAsVTKData vtkImporter;
  auto createdItems = m_result->findComponent("created");
  operation::MarkGeometry marker(resource);
  for (std::size_t ii = 0; ii < filenameItem->numberOfValues(); ++ii)
  {
    std::string filename = filenameItem->value(ii);
    auto data = vtkImporter(filename);
    if (data)
    {
      // Remove any point/cell normals.
      auto* cdata = data->GetAttributes(vtkDataObject::CELL);
      auto* pdata = data->GetAttributes(vtkDataObject::POINT);
      if (cdata)
      {
        cdata->SetAttribute(nullptr, vtkDataSetAttributes::NORMALS);
      }
      if (pdata)
      {
        pdata->SetAttribute(nullptr, vtkDataSetAttributes::NORMALS);
      }

      auto stem = smtk::common::Paths::stem(filename);
      auto model = resource->addModel(3, 3, stem);
      auto modelComp = model.component();
      modelComp->properties().get<std::string>()["aeva_datatype"] = "poly";
      modelComp->properties().get<std::string>()["filename"] = filename;
      modelComp->properties().get<std::string>()["import_filename"] = filename;
      createdItems->appendValue(modelComp);
      int count = 0;
      auto cell = createCellForData(model,
        data,
        createdItems,
        count,
        marker,
        pointIdOffset,
        cellIdOffset,
        maxPointId,
        maxCellId);
      // Volume cells should have surface faces:
      if (cell.isVolume())
      {
        vtkNew<vtkGeometryFilter> boundaryFilter;
        // share points (and ids) with volume.
        boundaryFilter->MergingOff();
        boundaryFilter->SetInputDataObject(0, data);
        boundaryFilter->Update();
        // faces have global ids passed through from volume cells. Move to pedigree ids,
        // then new global ids are generated and they are offset from the volume cells.
        auto* geom = vtkDataSet::SafeDownCast(boundaryFilter->GetOutputDataObject(0));
        auto* pedigreeIds = geom->GetCellData()->GetGlobalIds();
        // take ownership (incr reference count) so it's not deleted when we clear global ids.
        pedigreeIds->Register(nullptr);
        geom->GetCellData()->SetGlobalIds(nullptr);
        geom->GetCellData()->SetPedigreeIds(pedigreeIds);
        pedigreeIds->FastDelete();
        cellIdOffset = maxCellId + 1;

        auto bdy = createCellForData(cell,
          geom,
          createdItems,
          count,
          marker,
          pointIdOffset,
          cellIdOffset,
          maxPointId,
          maxCellId);
      }
      // update the ID offsets per-file
      pointIdOffset = maxPointId + 1;
      cellIdOffset = maxCellId + 1;
    }
  }

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

  m_result->findInt("outcome")->setValue(0, static_cast<int>(Import::Outcome::SUCCEEDED));
  return m_result;
}

std::set<std::string> Import::supportedITKFileFormats()
{
  std::set<std::string> result;

  std::list<itk::LightObject::Pointer> allobjects =
    itk::ObjectFactoryBase::CreateAllInstance("itkImageIOBase");
  for (auto& obj : allobjects)
  {
    auto* io = dynamic_cast<itk::ImageIOBase*>(obj.GetPointer());
    auto extensions = io->GetSupportedReadExtensions();
    result.insert(extensions.begin(), extensions.end());
  }
  return result;
}

std::map<std::string, std::set<std::string> > Import::supportedVTKFileFormats()
{
  std::map<std::string, std::set<std::string> > result;
  smtk::extension::vtk::io::ImportAsVTKData vtkImporter;
  auto formats = vtkImporter.fileFormats();
  for (const auto& format : formats)
  {
    std::set<std::string> extensions(format.Extensions.begin(), format.Extensions.end());
    result[format.Name] = extensions;
  }
  return result;
}

smtk::resource::ResourcePtr importResource(const std::string& filename)
{
  Import::Ptr importResource = Import::create();
  importResource->parameters()->findFile("filename")->setValue(filename);
  Import::Result result = importResource->operate();
  if (result->findInt("outcome")->value() != static_cast<int>(Import::Outcome::SUCCEEDED))
  {
    return smtk::resource::ResourcePtr();
  }
  return result->findResource("resource")->value();
}
}
}
}
