// Copyright(C) 2021, 2022 National Technology & Engineering Solutions
// of Sandia, LLC (NTESS).  Under the terms of Contract DE-NA0003525 with
// NTESS, the U.S. Government retains certain rights in this software.
//
// See packages/seacas/LICENSE for details

#include <cstdlib>
#include <numeric>

#include "Cell.h"
#include "Decompose.h"
#include "Grid.h"
#include "ZE_SystemInterface.h"
#include "ZE_Version.h"
#include <Ioss_CommSet.h>
#include <Ioss_ElementBlock.h>
#include <Ioss_IOFactory.h>
#include <Ioss_NodeBlock.h>
#include <Ioss_Region.h>
#include <Ioss_SideBlock.h>
#include <Ioss_SideSet.h>
#include <Ioss_SmartAssert.h>

#include <exodusII.h>
#include <fmt/chrono.h>
#include <fmt/color.h>
#include <fmt/ostream.h>
#include <open_file_limit.h>
#include <time_stamp.h>
#include <tokenize.h>

//! \file

extern unsigned int debug_level;

#if __cplusplus == 201103L
// From: https://stackoverflow.com/questions/17902405/how-to-implement-make-unique-function-in-c11
namespace std {
  template <class T> struct _Unique_if
  {
    typedef unique_ptr<T> _Single_object;
  };

  template <class T> struct _Unique_if<T[]>
  {
    typedef unique_ptr<T[]> _Unknown_bound;
  };

  template <class T, size_t N> struct _Unique_if<T[N]>
  {
    typedef void _Known_bound;
  };

  template <class T, class... Args>
  typename _Unique_if<T>::_Single_object make_unique(Args &&...args)
  {
    return unique_ptr<T>(new T(std::forward<Args>(args)...));
  }

  template <class T> typename _Unique_if<T>::_Unknown_bound make_unique(size_t n)
  {
    typedef typename remove_extent<T>::type U;
    return unique_ptr<T>(new U[n]());
  }

  template <class T, class... Args>
  typename _Unique_if<T>::_Known_bound make_unique(Args &&...) = delete;
} // namespace std
#endif

namespace {
  std::string tsFormat = "[{:%H:%M:%S}]";
  int         axis_index(const std::string &axis_str)
  {
    char axis = axis_str[0];
    if (axis == 'x' || axis == 'i') {
      return 0;
    }
    if (axis == 'X' || axis == 'I') {
      return 1;
    }
    if (axis == 'y' || axis == 'j') {
      return 2;
    }
    if (axis == 'Y' || axis == 'J') {
      return 3;
    }
    if (axis == 'z' || axis == 'k') {
      return 4;
    }
    if (axis == 'Z' || axis == 'K') {
      return 5;
    }
    return -1;
  }

  unsigned int which_sidesets(const std::string &sideset_string)
  {
    unsigned generate = 0;
    for (const auto &c : sideset_string) {
      if (c == 'x' || c == 'i') {
        generate |= (unsigned)Flg::MIN_I;
      }
      if (c == 'y' || c == 'j') {
        generate |= (unsigned)Flg::MIN_J;
      }
      if (c == 'z' || c == 'k') {
        generate |= (unsigned)Flg::MIN_K;
      }
      if (c == 'X' || c == 'I') {
        generate |= (unsigned)Flg::MAX_I;
      }
      if (c == 'Y' || c == 'J') {
        generate |= (unsigned)Flg::MAX_J;
      }
      if (c == 'Z' || c == 'K') {
        generate |= (unsigned)Flg::MAX_K;
      }
    }
    return generate;
  }

  Ioss::PropertyManager parse_properties(SystemInterface &interFace, int int_size);

  std::shared_ptr<Ioss::Region> create_input_region(const std::string &key, std::string filename,
                                                    bool ints_32_bits);

  template <typename INT>
  std::vector<INT> generate_node_map(Grid &grid, const Cell &cell, Mode mode, INT /*dummy*/);

  void   output_summary(Ioss::Region *region, std::ostream &strm);
  size_t handle_elements(Grid &grid, int start_rank, int rank_count);
  size_t handle_nodes(Grid &grid, int start_rank, int rank_count);
  void   handle_communications(Grid &grid, int start_rank, int rank_count);
  void   handle_surfaces(Grid &grid, int start_rank, int rank_count);
  void   generate_surfaces(Grid &grid, int start_rank, int rank_count);
} // namespace

Grid::Grid(SystemInterface &interFace)
    : m_scaleFactor(interFace.scale_factor()), m_parallelSize(interFace.ranks()),
      m_rankCount(interFace.rank_count()), m_startRank(interFace.start_rank()),
      m_equivalenceNodes(interFace.equivalence_nodes()),
      m_useInternalSidesets(!interFace.ignore_internal_sidesets()),
      m_subCycle(interFace.subcycle()), m_minimizeOpenFiles(interFace.minimize_open_files()),
      m_generatedSideSets(which_sidesets(interFace.sideset_surfaces()))
{
  set_sideset_names(interFace.sideset_names());
}

void Grid::set_extent(size_t extent_i, size_t extent_j, size_t /* unused */)
{
  m_gridI = extent_i;
  m_gridJ = extent_j;
  m_grid.resize(m_gridI * m_gridJ);
}

bool Grid::initialize(size_t i, size_t j, const std::string &key)
{
  if (unit_cells().find(key) == unit_cells().end()) {
    return false;
  }
  auto &unit_cell = unit_cells()[key];
  SMART_ASSERT(unit_cell->m_region != nullptr)(i)(j)(key);

  auto &cell = get_cell(i, j);
  cell.initialize(i, j, unit_cell);
  return true;
}

void Grid::add_unit_cell(const std::string &key, const std::string &unit_filename, bool ints32bit)
{
  static size_t open_files = open_file_limit();
  if (!minimize_open_files(Minimize::UNIT) && unit_cells().size() >= open_files) {
    // Just hit the limit...  Close all previous unit_cell files and set the minimize_open_files
    // behavior to UNIT.
    for (auto &unit_cell : m_unitCells) {
      unit_cell.second->m_region->get_database()->closeDatabase();
    }
    fmt::print(stderr, fmt::fg(fmt::color::yellow),
               "\nWARNING: Number of unit cells exceeds open file limit. Setting "
               "'mimimize_open_file' mode to UNIT.\n\n");
    m_minimizeOpenFiles = Minimize((unsigned)m_minimizeOpenFiles | (unsigned)Minimize::UNIT);
  }

  if (unit_cells().find(key) != unit_cells().end()) {
    fmt::print(stderr, fmt::fg(fmt::color::red),
               "\nERROR: There is a duplicate `unit cell` ({}) in the lattice dictionary.\n\n",
               key);
    exit(EXIT_FAILURE);
  }

  auto region = create_input_region(key, unit_filename, ints32bit);

  if (region == nullptr) {
    fmt::print(stderr, fmt::fg(fmt::color::red),
               "\nERROR: Unable to open the database '{}' associated with the unit cell '{}'.\n\n",
               unit_filename, key);
    exit(EXIT_FAILURE);
  }

  unit_cells().emplace(key, std::make_shared<UnitCell>(region));
  if (minimize_open_files(Minimize::UNIT)) {
    unit_cells()[key]->m_region->get_database()->closeDatabase();
  }

  if (debug_level & 2) {
    m_pu.progress(fmt::format("\tCreated Unit Cell {}", key));
  }
}

void Grid::set_coordinate_offsets()
{
  // All unit_cells have same X, Y (and Z) extent.  Only need X and Y
  auto x_range = get_cell(0, 0).get_coordinate_range(Axis::X);
  auto y_range = get_cell(0, 0).get_coordinate_range(Axis::Y);
  auto delta_x = x_range.second - x_range.first;
  auto delta_y = y_range.second - y_range.first;

  for (size_t j = 0; j < JJ(); j++) {
    for (size_t i = 0; i < II(); i++) {
      auto &cell  = get_cell(i, j);
      cell.m_offX = delta_x * (double)i;
      cell.m_offY = delta_y * (double)j;

      if (debug_level & 2) {
        util().progress(
            fmt::format("\tCell({}, {}) X = {}, Y = {}", i, j, cell.m_offX, cell.m_offY));
      }
    }
  }
}

void Grid::create_output_regions(SystemInterface &interFace)
{
  if (debug_level & 2) {
    util().progress(__func__);
  }
  m_outputRegions.resize(interFace.ranks());

  int                   int_size   = interFace.ints32bit() ? 4 : 8;
  Ioss::PropertyManager properties = parse_properties(interFace, int_size);
  if (parallel_size() == 1) {
    properties.add(Ioss::Property("OMIT_EXODUS_NUM_MAPS", 1));
  }
  properties.add(Ioss::Property("MINIMAL_NEMESIS_DATA", 1));

  if (debug_level & 2) {
    properties.add(Ioss::Property("ENABLE_TRACING", 1));
  }

  for (int i = m_startRank; i < m_startRank + m_rankCount; i++) {
    // Define the output database(s)...
    if (m_parallelSize > 1) {
      properties.add(Ioss::Property("processor_count", parallel_size()));
      properties.add(Ioss::Property("my_processor", i));
    }
    Ioss::DatabaseIO *dbo =
        Ioss::IOFactory::create("exodus", interFace.outputName_, Ioss::WRITE_RESTART,
                                Ioss::ParallelUtils::comm_self(), properties);
    if (dbo == nullptr || !dbo->ok(true)) {
      std::exit(EXIT_FAILURE);
    }
    m_outputRegions[i] =
        std::unique_ptr<Ioss::Region>(new Ioss::Region(dbo, "zellij_output_region"));
    output_region(i)->begin_mode(Ioss::STATE_DEFINE_MODEL);
    output_region(i)->property_add(Ioss::Property("code_name", qainfo[0]));
    output_region(i)->property_add(Ioss::Property("code_version", qainfo[2]));

    if (interFace.minimize_open_files() == Minimize::OUTPUT ||
        interFace.minimize_open_files() == Minimize::ALL) {
      output_region(i)->get_database()->closeDatabase();
    }
  }
}

void Grid::set_sideset_names(const std::string &names)
{
  // 'names' is a string of the form "Axis:name,Axis:name, ..."
  // For example "x:left,y:top,X:right,Y:bottom"
  // Parse the list and only update the names specified in the list...

  if (names.empty()) {
    return;
  }

  auto tokens = Ioss::tokenize(names, ",");
  for (auto &token : tokens) {
    auto axis  = token.substr(0, 1);
    bool valid = axis.find_first_not_of("ijkIJKxyzXYZ") == std::string::npos;
    if (!valid) {
      fmt::print(stderr, fmt::fg(fmt::color::red),
                 "\nERROR: Invalid axis '{}' specified for sideset name.  Valid is one of "
                 "'ijkIJKxyzXYZ'.\n\n",
                 axis);
      exit(EXIT_FAILURE);
    }

    // Now get the name...
    auto ss_name = token.substr(2);

    // Update the name in the list of generated sideset names...
    auto index = axis_index(axis);
    SMART_ASSERT(index >= 0)(axis)(index);
    generated_surface_names[index] = ss_name;
  }
}

void Grid::decompose(const std::string &method)
{
  if (debug_level & 2) {
    util().progress(__func__);
  }
  decompose_grid(*this, m_parallelSize, method);

  categorize_processor_boundaries();
}

void Grid::categorize_processor_boundaries()
{
  if (debug_level & 2) {
    util().progress(__func__);
  }

  // Now iterate the cells and tell each cell the rank of all neighboring cells...
  // boundary with its "left" or "lower" neighboring cell.

  for (size_t j = 0; j < JJ(); j++) {
    for (size_t i = 0; i < II(); i++) {
      auto &cell = get_cell(i, j);
      if (i > 0) {
        auto &left = get_cell(i - 1, j);
        cell.set_rank(Loc::L, left.rank(Loc::C));
        if (j > 0) {
          auto &BL = get_cell(i - 1, j - 1);
          cell.set_rank(Loc::BL, BL.rank(Loc::C));
        }
        if (j < JJ() - 1) {
          auto &TL = get_cell(i - 1, j + 1);
          cell.set_rank(Loc::TL, TL.rank(Loc::C));
        }
      }
      if (i < II() - 1) {
        auto &right = get_cell(i + 1, j);
        cell.set_rank(Loc::R, right.rank(Loc::C));
        if (j > 0) {
          auto &BR = get_cell(i + 1, j - 1);
          cell.set_rank(Loc::BR, BR.rank(Loc::C));
        }
        if (j < JJ() - 1) {
          auto &TR = get_cell(i + 1, j + 1);
          cell.set_rank(Loc::TR, TR.rank(Loc::C));
        }
      }
      if (j > 0) {
        auto &B = get_cell(i, j - 1);
        cell.set_rank(Loc::B, B.rank(Loc::C));
      }
      if (j < JJ() - 1) {
        auto &T = get_cell(i, j + 1);
        cell.set_rank(Loc::T, T.rank(Loc::C));
      }
    }
  }

  if (debug_level & 32) {
    auto width = Ioss::Utils::number_width(parallel_size());
    for (size_t j = 0; j < JJ(); j++) {
      for (size_t i = 0; i < II(); i++) {
        const auto &cell  = get_cell(i, j);
        auto        left  = cell.processor_boundary(Loc::L) ? '<' : ' ';
        auto        below = cell.processor_boundary(Loc::B) ? '^' : ' ';
        fmt::print(" {0}{1:{3}}{2}", left, cell.rank(Loc::C), below, width);
      }
      fmt::print("\n");
    }
  }
}

void Grid::generate_sidesets()
{
  if (m_generatedSideSets != 0) {
    for (auto &unit_cell : m_unitCells) {
      unit_cell.second->generate_boundary_faces(m_generatedSideSets);
    }
  }
}

void Grid::internal_process()
{
  if (debug_level & 2) {
    util().progress(__func__);
  }
  auto node_count    = handle_nodes(*this, m_startRank, m_rankCount);
  auto element_count = handle_elements(*this, m_startRank, m_rankCount);
  handle_communications(*this, m_startRank, m_rankCount);
  if (m_useInternalSidesets) {
    handle_surfaces(*this, m_startRank, m_rankCount);
  }
  generate_surfaces(*this, m_startRank, m_rankCount);

  for (int i = m_startRank; i < m_startRank + m_rankCount; i++) {
    output_region(i)->end_mode(Ioss::STATE_DEFINE_MODEL);
    if (debug_level & 64) {
      output_summary(output_region(i), std::cerr);
    }
  }
  if (util().parallel_rank() == 0) {
    fmt::print("                {} Nodes; {} Elements.\n", fmt::group_digits(node_count),
               fmt::group_digits(element_count));
  }
}

template void Grid::process(SystemInterface &, int64_t);
template void Grid::process(SystemInterface &, int);

template <typename INT> void Grid::process(SystemInterface &interFace, INT /* dummy */)
{
  bool subcycle   = m_subCycle;
  int  start_rank = m_startRank;
  int  rank_count = m_rankCount;
  int  end_rank   = subcycle ? m_parallelSize : m_startRank + m_rankCount;

  if (end_rank > m_parallelSize) {
    end_rank    = m_parallelSize;
    m_rankCount = m_parallelSize - m_startRank;
  }

  for (int begin = start_rank; begin < end_rank; begin += rank_count) {
    m_startRank = begin;
    if (m_startRank + m_rankCount > m_parallelSize) {
      m_rankCount = m_parallelSize - m_startRank;
    }
    if (debug_level & 1) {
      fmt::print(stderr, "{} Processing Ranks {} to {}\n", time_stamp(tsFormat), begin,
                 begin + rank_count - 1);
    }

    create_output_regions(interFace);

    internal_process();
    if (debug_level & 1) {
      fmt::print(stderr, "{} Lattice Processing Finalized\n", time_stamp(tsFormat));
    }

    output_model(INT(0));
    if (debug_level & 1) {
      fmt::print(stderr, "{} Model Output\n", time_stamp(tsFormat));
    }
  }
}

template void Grid::output_model(int64_t);
template void Grid::output_model(int);

// Write the output database(s) for all ranks...
template <typename INT> void Grid::output_model(INT /*dummy*/)
{
  if (debug_level & 2) {
    util().progress(__func__);
  }
  // Coordinates do not depend on order of operations, so can do these
  // a rank at a time which can be more efficient once we exceed
  // maximum number of open files...  Also easier to parallelize...
  for (int r = m_startRank; r < m_startRank + m_rankCount; r++) {
    for (size_t j = 0; j < JJ(); j++) {
      for (size_t i = 0; i < II(); i++) {
        auto &cell = get_cell(i, j);
        if (cell.rank(Loc::C) == r) {
          output_nodal_coordinates(cell);
        }
      }
    }
    if (minimize_open_files(Minimize::OUTPUT)) {
      output_region(r)->get_database()->closeDatabase();
    }
  }
  if (debug_level & 2) {
    util().progress("\tEnd Nodal Coordinate Output");
  }

  // Surfaces do not depend on order of operations, so can do these
  // a rank at a time which can be more efficient once we exceed
  // maximum number of open files...  Also easier to parallelize...
  for (int r = m_startRank; r < m_startRank + m_rankCount; r++) {
    for (size_t j = 0; j < JJ(); j++) {
      for (size_t i = 0; i < II(); i++) {
        auto &cell = get_cell(i, j);
        if (cell.rank(Loc::C) == r) {
          if (m_useInternalSidesets) {
            output_surfaces(cell, INT(0));
          }
          output_generated_surfaces(cell, INT(0));
        }
      }
    }
    if (minimize_open_files(Minimize::OUTPUT)) {
      output_region(r)->get_database()->closeDatabase();
    }
  }
  if (debug_level & 2) {
    util().progress("\tEnd Surface Output");
  }

  // All the rest of these depend on progressing through the cells in
  // correct order so that can pass information correctly from cell to
  // cell.  Need to figure out how to eliminate this ordering so can
  // parallelize and not worry about open file count.

  for (size_t j = 0; j < JJ(); j++) {
    for (size_t i = 0; i < II(); i++) {
      auto &cell     = get_cell(i, j);
      auto  node_map = generate_node_map(*this, cell, Mode::PROCESSOR, INT(0));
      output_block_connectivity(cell, node_map);
      if (parallel_size() > 1) {
        output_nodal_communication_map(cell, node_map);
      }
    }
  }
  if (debug_level & 2) {
    util().progress("\tEnd Nodal Communication Map Output");
  }

  if (parallel_size() > 1) {
    for (size_t j = 0; j < JJ(); j++) {
      for (size_t i = 0; i < II(); i++) {
        auto &cell = get_cell(i, j);
        output_node_map(cell, INT(0));
        output_element_map(cell, INT(0));
      }
    }
    if (debug_level & 2) {
      util().progress("\tEnd Node/Element Map Output");
    }
  }
}

void Grid::output_nodal_coordinates(const Cell &cell)
{
  int rank = cell.rank(Loc::C);

  auto               *nb = cell.region()->get_node_blocks()[0];
  std::vector<double> coord_x;
  std::vector<double> coord_y;
  std::vector<double> coord_z;

  nb->get_field_data("mesh_model_coordinates_x", coord_x);
  nb->get_field_data("mesh_model_coordinates_y", coord_y);
  nb->get_field_data("mesh_model_coordinates_z", coord_z);

  // Apply coordinate offsets to all nodes...
  if (cell.m_offX != 0.0) {
    std::for_each(coord_x.begin(), coord_x.end(), [&cell](double &d) { d += cell.m_offX; });
  }
  if (cell.m_offY != 0.0) {
    std::for_each(coord_y.begin(), coord_y.end(), [&cell](double &d) { d += cell.m_offY; });
  }

  // If there is a scale factor specified, apply to all nodes...
  if (m_scaleFactor != 1.0) {
    double scale = m_scaleFactor;
    std::for_each(coord_x.begin(), coord_x.end(), [scale](double &d) { d *= scale; });
    std::for_each(coord_y.begin(), coord_y.end(), [scale](double &d) { d *= scale; });
    std::for_each(coord_z.begin(), coord_z.end(), [scale](double &d) { d *= scale; });
  }

  // Filter coordinates down to only "new nodes"...
  if (m_equivalenceNodes && (cell.has_neighbor_i() || cell.has_neighbor_j())) {
    auto   mode              = parallel_size() > 1 ? Mode::PROCESSOR : Mode::GLOBAL;
    auto   categorized_nodes = cell.categorize_nodes(mode);
    size_t nn                = 0;
    for (size_t n = 0; n < categorized_nodes.size(); n++) {
      if (categorized_nodes[n] == 0) {
        coord_x[nn] = coord_x[n];
        coord_y[nn] = coord_y[n];
        coord_z[nn] = coord_z[n];
        nn++;
      }
    }
  }

  int  exoid = output_region(rank)->get_database()->get_file_pointer();
  auto start = cell.m_localNodeIdOffset + 1;
  auto count = cell.added_node_count(Mode::PROCESSOR, m_equivalenceNodes);
  ex_put_partial_coord(exoid, start, count, coord_x.data(), coord_y.data(), coord_z.data());

  if (minimize_open_files(Minimize::UNIT)) {
    cell.region()->get_database()->closeDatabase();
  }
}

template <typename INT> void Grid::output_generated_surfaces(Cell &cell, INT /*dummy*/)
{
  auto generated_ijk = get_generated_sidesets();
  if (generated_ijk == 0) {
    return;
  }

  int rank = cell.rank(Loc::C);

  std::array<int, 6> boundary_rank{
      cell.rank(Loc::L), cell.rank(Loc::R), cell.rank(Loc::B), cell.rank(Loc::T), -1, -1};
  std::array<enum Flg, 6> boundary_flag{Flg::MIN_I, Flg::MAX_I, Flg::MIN_J,
                                        Flg::MAX_J, Flg::MIN_K, Flg::MAX_K};

  int exoid = output_region(rank)->get_database()->get_file_pointer();

  for (int face = 0; face < 6; face++) {
    if ((generated_ijk & (unsigned)boundary_flag[face]) && boundary_rank[face] == -1) {
      // Find surface on output mesh...
      auto *osurf = output_region(rank)->get_sideset(generated_surface_names[face]);
      SMART_ASSERT(osurf != nullptr);
      auto &oblocks = osurf->get_side_blocks();
      SMART_ASSERT(oblocks.size() == 1)(oblocks.size());

      auto            &boundary = cell.unit()->boundary_blocks[face];
      auto             count    = boundary.size();
      std::vector<INT> elements;
      std::vector<INT> faces;
      elements.reserve(count);
      faces.reserve(count);

      for (auto &block_faces : boundary.m_faces) {
        auto &block_name = block_faces.first;
        auto &bnd_faces  = block_faces.second;

        // This is the offset within this element block -- i.e., the 'element_offsetth' element in
        // this block.
        size_t element_offset = cell.m_localElementIdOffset[block_name];
        // This is the offset of the elements in this element block within all of the elements in
        // the output file.
        size_t global_offset = output_region(rank)->get_element_block(block_name)->get_offset();

        for (auto &bface : bnd_faces) {
          elements.push_back(bface / 10 + element_offset + global_offset);
          faces.push_back(bface % 10 + 1);
        }
      }

      auto id    = osurf->get_property("id").get_int();
      auto start = cell.m_localSurfaceOffset[generated_surface_names[face]];
      ex_put_partial_set(exoid, EX_SIDE_SET, id, start + 1, count, elements.data(), faces.data());
    }
  }
}

template <typename INT> void Grid::output_surfaces(Cell &cell, INT /*dummy*/)
{
  int rank  = cell.rank(Loc::C);
  int exoid = output_region(rank)->get_database()->get_file_pointer();

  // Get the surfaces on this cell...
  const auto &surfaces = cell.region()->get_sidesets();
  for (const auto *surface : surfaces) {

    // Find corresponding surface on output mesh...
    auto *osurf = output_region(rank)->get_sideset(surface->name());
    SMART_ASSERT(osurf != nullptr);
    const auto &oblocks = osurf->get_side_blocks();

    std::vector<INT> elements;
    std::vector<INT> faces;
    elements.reserve(oblocks[0]->entity_count());
    faces.reserve(oblocks[0]->entity_count());

    const auto &blocks = surface->get_side_blocks();
    for (const auto *block : blocks) {
      // Get the element/face pairs for the SideBlock in this surface...
      std::vector<INT> element_side;
      block->get_field_data("element_side_raw", element_side);

      // Now separate the element and the face into different vectors and
      // update the element number to match the elements in this cell...
      const auto *parent = block->parent_element_block();
      SMART_ASSERT(parent != nullptr);

      // This is the offset of the elements in the unit cell parent element block
      size_t unit_block_offset = parent->get_offset();

      // This is the offset within this element block -- i.e., the 'element_offsetth' element in
      // this block.
      size_t element_offset = cell.m_localElementIdOffset[parent->name()];
      // This is the offset of the elements in this element block within all of the elements in the
      // output file.
      size_t global_offset = output_region(rank)->get_element_block(parent->name())->get_offset();

      size_t entity_count = block->entity_count();
      for (size_t i = 0; i < entity_count; i++) {
        auto element        = element_side[2 * i + 0];
        auto output_element = element - unit_block_offset + element_offset + global_offset;
        elements.push_back(output_element);
        faces.push_back(element_side[2 * i + 1]);
      }
    }

    auto id    = osurf->get_property("id").get_int();
    auto start = cell.m_localSurfaceOffset[osurf->name()];
    auto count = elements.size();
    ex_put_partial_set(exoid, EX_SIDE_SET, id, start + 1, count, elements.data(), faces.data());
  }

  if (minimize_open_files(Minimize::UNIT)) {
    cell.region()->get_database()->closeDatabase();
  }
}

template <typename INT>
void Grid::output_block_connectivity(Cell &cell, const std::vector<INT> &node_map)
{
  int rank = cell.rank(Loc::C);
  if (rank >= m_startRank && rank < m_startRank + m_rankCount) {
    int exoid = output_region(rank)->get_database()->get_file_pointer();

    const auto      &blocks = cell.region()->get_element_blocks();
    std::vector<INT> connect;
    for (const auto *block : blocks) {
      block->get_field_data("connectivity_raw", connect);
      for (size_t k = 0; k < connect.size(); k++) {
        connect[k] = node_map[connect[k]];
      }
      auto start = cell.m_localElementIdOffset[block->name()] + 1;
      auto count = block->entity_count();
      auto id    = block->get_property("id").get_int();
      if (debug_level & 8) {
        fmt::print(stderr, "Rank: {}, Cell({}, {}), Block {}, id {}, start {}, count {}\n", rank,
                   cell.m_i, cell.m_j, block->name(), id, start, count);
      }
      ex_put_partial_conn(exoid, EX_ELEM_BLOCK, id, start, count, connect.data(), nullptr, nullptr);
    }

    if (debug_level & 2) {
      util().progress(fmt::format("Generated Node Map / Output Connectivity for Cell({}, {})",
                                  cell.m_i, cell.m_j));
    }
    if (minimize_open_files(Minimize::UNIT)) {
      cell.region()->get_database()->closeDatabase();
    }
    if (minimize_open_files(Minimize::OUTPUT)) {
      output_region(rank)->get_database()->closeDatabase();
    }
  }
}

template <typename INT>
void Grid::output_nodal_communication_map(Cell &cell, const std::vector<INT> &node_map)
{
  // The `node_map` has the processor-local node ids for all nodes on this cell.
  // Note that the `node_map` starts at index 1 in the vector...
  // Need to check the boundaries of the cell and determine which boundaries are on a different rank
  // and output the nodes and processors to the communication map.
  int rank = cell.rank(Loc::C);
  if (rank >= m_startRank && rank < m_startRank + m_rankCount) {
    std::vector<INT> nodes;
    std::vector<INT> procs;
    cell.populate_node_communication_map(node_map, nodes, procs);

    int exoid = output_region(rank)->get_database()->get_file_pointer();

    auto start = cell.m_communicationNodeOffset + 1;
    auto count = cell.m_communicationNodeCount;

    ex_put_partial_node_cmap(exoid, 1, start, count, nodes.data(), procs.data(), rank);

    if (minimize_open_files(Minimize::OUTPUT)) {
      output_region(rank)->get_database()->closeDatabase();
    }

    if (debug_level & 32) {
      fmt::print(stderr, "Rank: {}, Cell({}, {}), Node Comm Map: start {}, count {}\n", rank,
                 cell.m_i, cell.m_j, start, count);
    }

    if (debug_level & 2) {
      util().progress(
          fmt::format("Output Nodal Communication Map for Cell({}, {})", cell.m_i, cell.m_j));
    }
  }
}

template <typename INT> void Grid::output_node_map(const Cell &cell, INT /*dummy*/)
{
  int rank = cell.rank(Loc::C);

  auto start = cell.m_localNodeIdOffset + 1;
  auto count = cell.added_node_count(Mode::PROCESSOR, m_equivalenceNodes);

  if (parallel_size() == 1) {
    auto             gid = cell.m_globalNodeIdOffset + 1;
    std::vector<INT> map(count);
    std::iota(map.begin(), map.end(), gid);
    int exoid = output_region(rank)->get_database()->get_file_pointer();
    ex_put_partial_id_map(exoid, EX_NODE_MAP, start, count, map.data());
  }
  else {
    auto map = generate_node_map(*this, cell, Mode::GLOBAL, INT(0));

    if (rank >= m_startRank && rank < m_startRank + m_rankCount) {

      // Filter nodes down to only "new nodes"...
      if (m_equivalenceNodes && (cell.has_neighbor_i() || cell.has_neighbor_j())) {
        auto   mode              = Mode::PROCESSOR;
        auto   categorized_nodes = cell.categorize_nodes(mode);
        size_t nn                = 0;
        for (size_t n = 0; n < categorized_nodes.size(); n++) {
          if (categorized_nodes[n] == 0) {
            map[nn + 1] = map[n + 1];
            nn++;
          }
        }
      }
      if (debug_level & 8) {
        fmt::print("Cell({}, {}), start {}, count {}\n", cell.m_i, cell.m_j, start, count);
      }
      int exoid = output_region(rank)->get_database()->get_file_pointer();
      ex_put_partial_id_map(exoid, EX_NODE_MAP, start, count, &map[1]);
      if (minimize_open_files(Minimize::OUTPUT)) {
        output_region(rank)->get_database()->closeDatabase();
      }
    }
  }

  if (debug_level & 2) {
    util().progress(fmt::format("Generated Node Map for Rank {}, Cell({}, {}): start {}, count "
                                "{}\n",
                                rank, cell.m_i, cell.m_j, start, count));
  }
}

template <typename INT> void Grid::output_element_map(Cell &cell, INT /*dummy*/)
{
  int rank = cell.rank(Loc::C);
  if (rank >= m_startRank && rank < m_startRank + m_rankCount) {
    int exoid = output_region(rank)->get_database()->get_file_pointer();

    const auto &output_blocks = output_region(rank)->get_element_blocks();

    // This is the element block offset for the "single output file"
    // for the block being output For example, if the total mesh has
    // 3 blocks, with 100, 200, 100 elements, then the element block
    // offset would be 0, 100, 300 (Ioss::ElementBlock::get_offset())
    size_t global_id_offset = 0;

    for (const auto *output_element_block : output_blocks) {
      auto *block = cell.region()->get_element_block(output_element_block->name());
      if (block != nullptr) {

        auto             gid = cell.m_globalElementIdOffset[block->name()] + 1 + global_id_offset;
        auto             element_count = block->entity_count();
        std::vector<INT> map(element_count);

        std::iota(map.begin(), map.end(), gid);

        auto output_block_offset = output_element_block->get_offset();

        // This cells element block ids start this far into the portion of the map for this
        // element block
        auto local_offset = cell.m_localElementIdOffset[block->name()];

        auto start = output_block_offset + local_offset + 1;
        ex_put_partial_id_map(exoid, EX_ELEM_MAP, start, element_count, map.data());

        if (debug_level & 8) {
          fmt::print("Rank {}: Cell({}, {}), Block {}, start {}, element_count {}, gid {}\n", rank,
                     cell.m_i, cell.m_j, block->name(), start, element_count, gid);
        }
      }
      // If we were outputting a single file, then this element
      // block in that file would have this many elements.
      auto global_block_element_count =
          output_element_block->get_property("global_entity_count").get_int();
      global_id_offset += global_block_element_count;
    }
    if (minimize_open_files(Minimize::OUTPUT)) {
      output_region(rank)->get_database()->closeDatabase();
    }
  }
}

void Grid::handle_file_count()
{
  // If the user has specified a 'minimize_open_files' behavior on the command line,
  // honor that mode here.  Note that we have already processed the unit cells and
  // if the unit cell count exceeds the open file limit, then that code has already
  // automatically set the minimize_open_files to include UNIT...
  if (m_minimizeOpenFiles == Minimize::ALL) {
    return;
  }

  size_t open_files = open_file_limit();
  if (util().parallel_rank() == 0) {
    fmt::print("\n Maximum Open File Count = {}\n", open_file_limit());
  }

  auto unit_cell_size = unit_cells().size();
  if (minimize_open_files(Minimize::UNIT)) {
    unit_cell_size = 1; // There will only be a single unit cell file open at a time.
  }

  if (unit_cell_size + m_rankCount > open_files) {
    // Too many files, need to close some of them after accessing...
    // We have already checked the unit_cell() size earlier.
    // If user has specified 'minimize_open_files == OUTPUT', then honor that...
    if (minimize_open_files(Minimize::OUTPUT)) {
      return;
    }

    // Have decision on whether to keep all unit_files open
    // and reduce open output files further, or to close
    // unit files and have more output files open...
    int output_open = open_files - (int)unit_cell_size;
    if (output_open < (int)(.2 * m_rankCount)) {
      // Close the unit files.  Prefer to have output files open instead.
      m_minimizeOpenFiles = Minimize((unsigned)m_minimizeOpenFiles | (unsigned)Minimize::UNIT);
      unit_cell_size      = 1;
    }

    // Set the rank count to the number of files left after subtracting off 'unit_cell_size'
    // and set the 'subcycle' mode.
    auto max_rank_count = open_files - unit_cell_size;
    if (max_rank_count < (size_t)m_rankCount) {
      m_rankCount = max_rank_count;
    }
    m_subCycle = true;
  }

  if (util().parallel_rank() == 0 && m_minimizeOpenFiles != Minimize::NONE) {
    std::array<std::string, 4> smode{"NONE", "UNIT", "OUTPUT", "ALL"};
    fmt::print(" Setting `minimize_open_files` mode to {}.\n", smode[(int)m_minimizeOpenFiles]);
  }
}

namespace {
  size_t handle_elements(Grid &grid, int start_rank, int rank_count)
  {
    // Not all unit cells have the same element blocks and the output
    // grid will contain the union of the element blocks on each unit
    // cell...
    //
    // While finalizing the cells, we will create this union for use in
    // the output region.  Would be quicker to do iteration on unit_cell
    // map which is smaller, but for now do it here and see if becomes
    // bottleneck.

    std::map<std::string, std::unique_ptr<Ioss::ElementBlock>> output_element_blocks;
    std::vector<std::map<std::string, size_t>> element_block_elem_count(grid.parallel_size());
    std::map<std::string, size_t>              global_element_block_elem_count;

    for (size_t j = 0; j < grid.JJ(); j++) {
      for (size_t i = 0; i < grid.II(); i++) {
        auto &cell = grid.get_cell(i, j);
        auto  rank = cell.rank(Loc::C);

        const auto &element_blocks = cell.region()->get_element_blocks();
        for (const auto *block : element_blocks) {
          auto &blk                         = block->name();
          cell.m_globalElementIdOffset[blk] = global_element_block_elem_count[blk];
          cell.m_localElementIdOffset[blk]  = element_block_elem_count[rank][blk];

          element_block_elem_count[rank][blk] += block->entity_count();
          global_element_block_elem_count[blk] += block->entity_count();

          if (debug_level & 8) {
            fmt::print("rank, i, j, blk, loffset, goffset, block_count: {}: {} {} {} {} {} {}\n",
                       rank, i, j, blk, cell.m_localElementIdOffset[blk],
                       cell.m_globalElementIdOffset[blk], element_block_elem_count[rank][blk]);
          }

          // Create output element block if does not exist yet...
          if (output_element_blocks.find(blk) == output_element_blocks.end()) {
            output_element_blocks.emplace(
                blk, std::make_unique<Ioss::ElementBlock>(Ioss::ElementBlock(*block)));
          }
        }
      }
    }

    // Calculate values needed to set the "global_entity_count" property on the output element
    // blocks.
    size_t                               global_element_count = 0;
    std::map<const std::string, int64_t> global_block_element_count;
    for (auto &blk : output_element_blocks) {
      for (int rank = 0; rank < grid.parallel_size(); rank++) {
        auto *block = blk.second.get();
        global_block_element_count[block->name()] += element_block_elem_count[rank][block->name()];
        global_element_count += element_block_elem_count[rank][block->name()];
      }
    }

    // Define the element blocks in the output database...
    for (int rank = start_rank; rank < start_rank + rank_count; rank++) {
      for (auto &blk : output_element_blocks) {
        auto *block = new Ioss::ElementBlock(*blk.second.get());
        block->property_update("entity_count", element_block_elem_count[rank][block->name()]);
        block->property_update("global_entity_count", global_block_element_count[block->name()]);
        grid.output_region(rank)->property_add(
            Ioss::Property("global_element_count", (int64_t)global_element_count));
        grid.output_region(rank)->add(block);
        if (debug_level & 8) {
          fmt::print("rank, blk, element_count: {}: {} {}\n", rank, block->name(),
                     block->entity_count());
        }
      }
    }
    return global_element_count;
  }

  size_t handle_nodes(Grid &grid, int start_rank, int rank_count)
  {
    size_t              global_node_count = 0;
    std::vector<size_t> local_node_count(grid.parallel_size());

    for (size_t j = 0; j < grid.JJ(); j++) {
      for (size_t i = 0; i < grid.II(); i++) {
        auto &cell                = grid.get_cell(i, j);
        auto  rank                = cell.rank(Loc::C);
        cell.m_globalNodeIdOffset = global_node_count;
        cell.m_localNodeIdOffset  = local_node_count[rank];
        SMART_ASSERT(cell.region() != nullptr)(i)(j);

        auto new_global_nodes    = cell.added_node_count(Mode::GLOBAL, grid.equivalence_nodes());
        auto new_processor_nodes = cell.added_node_count(Mode::PROCESSOR, grid.equivalence_nodes());
        if (debug_level & 8) {
          fmt::print("rank: i, j, node_offset, added_nodes: {}: {} {} {} {} {}\n", rank, i, j,
                     local_node_count[rank], new_global_nodes, new_processor_nodes);
        }
        local_node_count[rank] += new_processor_nodes;
        global_node_count += new_global_nodes;
      }
    }
    // Define the output database node block...
    for (int i = start_rank; i < start_rank + rank_count; i++) {
      std::string block_name        = "nodeblock_1";
      int         spatial_dimension = 3;
      auto        block = new Ioss::NodeBlock(grid.output_region(i)->get_database(), block_name,
                                              local_node_count[i], spatial_dimension);
      block->property_add(Ioss::Property("id", 1));
      grid.output_region(i)->add(block);
      grid.output_region(i)->property_add(
          Ioss::Property("global_node_count", (int64_t)global_node_count));
    }
    return global_node_count;
  }

  void handle_communications(Grid &grid, int start_rank, int rank_count)
  {
    // Need to determine the number of nodes on the processor
    // boundaries for each output database and create a CommSet...
    std::vector<size_t> bnode(grid.parallel_size());
    for (size_t j = 0; j < grid.JJ(); j++) {
      for (size_t i = 0; i < grid.II(); i++) {
        auto &cell                     = grid.get_cell(i, j);
        auto  rank                     = cell.rank(Loc::C);
        cell.m_communicationNodeOffset = bnode[rank];
        auto bnd                       = cell.processor_boundary_node_count();
        bnode[rank] += bnd;
        if (debug_level & 32) {
          fmt::print("rank: {}, Cell({}, {}): Boundary Count = {}, Total = {}\n", rank, i, j, bnd,
                     bnode[rank]);
        }
      }
    }

    // Now add communication sets to all output databases...
    for (int rank = start_rank; rank < start_rank + rank_count; rank++) {
      auto *cs = new Ioss::CommSet(grid.output_region(rank)->get_database(), "commset_node", "node",
                                   bnode[rank]);
      grid.output_region(rank)->add(cs);
    }
  }

  void handle_surfaces(Grid &grid, int start_rank, int rank_count)
  {
    // Not all unit cells have the same surfaces and the output
    // grid will contain the union of the surfaces on each unit
    // cell that has a surface on the boundary of the output mesh.
    //
    // While finalizing the cells, we will create this union for use in
    // the output region.  Would be quicker to do iteration on unit_cell
    // map which is smaller, but for now do it here and see if becomes
    // bottleneck.
    std::map<std::string, std::unique_ptr<Ioss::SideSet>> output_surfaces;
    std::map<std::string, size_t>                         global_surface_face_count;

    // Per rank...
    std::vector<std::map<std::string, size_t>> surface_face_count(grid.parallel_size());
    std::vector<std::map<std::string, size_t>> local_surface_offset(grid.parallel_size());

    for (size_t j = 0; j < grid.JJ(); j++) {
      for (size_t i = 0; i < grid.II(); i++) {
        auto &cell = grid.get_cell(i, j);
        auto  rank = cell.rank(Loc::C);

        const auto &surfaces = cell.region()->get_sidesets();
        for (const auto *surface : surfaces) {
          auto &surf                      = surface->name();
          cell.m_localSurfaceOffset[surf] = local_surface_offset[rank][surf];

          const auto &blocks = surface->get_side_blocks();
          for (const auto *blk : blocks) {
            surface_face_count[rank][surf] += blk->entity_count();
            global_surface_face_count[surf] += blk->entity_count();
            local_surface_offset[rank][surf] += blk->entity_count();
          }

          // Create output surface if does not exist yet...
          if (output_surfaces.find(surf) == output_surfaces.end()) {
            output_surfaces.emplace(surf, std::make_unique<Ioss::SideSet>(Ioss::SideSet(*surface)));
          }
        }
      }
    }

    // Define the surfaces in the output database...
    for (int rank = start_rank; rank < start_rank + rank_count; rank++) {
      for (auto &surf : output_surfaces) {
        auto *surface =
            new Ioss::SideSet(grid.output_region(rank)->get_database(), surf.second->name());
        auto *block =
            new Ioss::SideBlock(grid.output_region(rank)->get_database(), surf.second->name(),
                                "quad4", "hex8", surface_face_count[rank][surface->name()]);
        surface->add(block);
        block->property_update("global_entity_count", global_surface_face_count[surface->name()]);
        surface->property_update("global_entity_count", global_surface_face_count[surface->name()]);
        block->property_update("distribution_factor_count", 0);
        grid.output_region(rank)->add(surface);
      }
    }
  }

  void generate_surfaces(Grid &grid, int start_rank, int rank_count)
  {
    // The user may have requested that sidesets on one or more
    // of the models boundary surfaces (min i,j,k and/or max i,j,k)
    // be automatically generated (they don't exist on the input
    // unit cells).  Handle those sidesets here.

    auto generated_ijk = grid.get_generated_sidesets();
    if (generated_ijk == 0) {
      return;
    }

    std::array<size_t, 6> global_surface_face_count{};

    // Per rank...
    std::vector<std::array<size_t, 6>> surface_face_count(grid.parallel_size());
    std::vector<std::array<size_t, 6>> local_surface_offset(grid.parallel_size());

    std::array<enum Flg, 6> boundary_flag{Flg::MIN_I, Flg::MAX_I, Flg::MIN_J,
                                          Flg::MAX_J, Flg::MIN_K, Flg::MAX_K};

    for (size_t j = 0; j < grid.JJ(); j++) {
      for (size_t i = 0; i < grid.II(); i++) {
        auto              &cell = grid.get_cell(i, j);
        auto               rank = cell.rank(Loc::C);
        std::array<int, 6> boundary_rank{
            cell.rank(Loc::L), cell.rank(Loc::R), cell.rank(Loc::B), cell.rank(Loc::T), -1, -1};

        for (int face = 0; face < 6; face++) {
          if ((generated_ijk & (unsigned)boundary_flag[face]) && boundary_rank[face] == -1) {
            cell.m_localSurfaceOffset[grid.generated_surface_names[face]] =
                local_surface_offset[rank][face];

            auto count = cell.unit()->boundary_blocks[face].size();
            global_surface_face_count[face] += count;
            surface_face_count[rank][face] += count;
            local_surface_offset[rank][face] += count;
          }
        }
      }
    }

    // Define the surfaces in the output database...
    for (int rank = start_rank; rank < start_rank + rank_count; rank++) {
      for (size_t i = 0; i < 6; i++) {
        if (global_surface_face_count[i] > 0) {
          auto *surface = new Ioss::SideSet(grid.output_region(rank)->get_database(),
                                            grid.generated_surface_names[i]);
          auto *block   = new Ioss::SideBlock(grid.output_region(rank)->get_database(),
                                              grid.generated_surface_names[i], "quad4", "hex8",
                                              surface_face_count[rank][i]);
          surface->property_update("global_entity_count", global_surface_face_count[i]);
          block->property_update("global_entity_count", global_surface_face_count[i]);
          block->property_update("distribution_factor_count", 0);
          surface->add(block);
          grid.output_region(rank)->add(surface);
        }
      }
    }
  }

  template <typename INT>
  std::vector<INT> generate_node_map(Grid &grid, const Cell &cell, Mode mode, INT /*dummy*/)
  {
    // Generate a "map" from nodes in the input connectivity to the
    // output connectivity in global nodes.  If no neighbors, then
    // this would just be adding `cell.m_globalNodeIdOffset` to each
    // connectivity entry.

    std::vector<INT> map = cell.generate_node_map(mode, grid.equivalence_nodes(), INT(0));
    if (debug_level & 8) {
      fmt::print("Cell({},{}) PROCESSOR MAP: {}\n", cell.m_i, cell.m_j, fmt::join(map, " "));
    }

    // Now that we have the node map for this cell, we need to save
    // the mappings for the max_I and max_J faces and max_I-max_J edge
    // for use by later neighbors...  Check whether cell has neighbors
    // on max_I or max_J faces...
    if (grid.equivalence_nodes()) {
      if ((mode == Mode::GLOBAL ||
           (mode == Mode::PROCESSOR && cell.rank(Loc::C) == cell.rank(Loc::R))) &&
          (cell.m_i + 1 < grid.II())) {
        const auto &neighbor = grid.get_cell(cell.m_i + 1, cell.m_j);
        cell.populate_neighbor(Loc::L, map, neighbor);
      }

      if ((mode == Mode::GLOBAL ||
           (mode == Mode::PROCESSOR && cell.rank(Loc::C) == cell.rank(Loc::T))) &&
          (cell.m_j + 1 < grid.JJ())) {
        const auto &neighbor = grid.get_cell(cell.m_i, cell.m_j + 1);
        cell.populate_neighbor(Loc::B, map, neighbor);
      }

      if (mode == Mode::PROCESSOR) {
        if (cell.processor_boundary(Loc::L) && (cell.rank(Loc::TL) == cell.rank(Loc::C))) {
          const auto &tl_corner = grid.get_cell(cell.m_i - 1, cell.m_j + 1);
          cell.populate_neighbor(Loc::BR, map, tl_corner);
        }
        // Now the other "corner case"
        if (cell.processor_boundary(Loc::R) && (cell.rank(Loc::TR) == cell.rank(Loc::C))) {
          const auto &tr_corner = grid.get_cell(cell.m_i + 1, cell.m_j + 1);
          cell.populate_neighbor(Loc::BL, map, tr_corner);
        }
      }
    }
    return map;
  }

  Ioss::PropertyManager parse_properties(SystemInterface &interFace, int int_size)
  {
    Ioss::PropertyManager properties;
    if (int_size == 8) {
      properties.add(Ioss::Property("INTEGER_SIZE_DB", 8));
      properties.add(Ioss::Property("INTEGER_SIZE_API", 8));
    }

    if (interFace.use_netcdf4()) {
      properties.add(Ioss::Property("FILE_TYPE", "netcdf4"));
    }

    if (interFace.use_netcdf5()) {
      properties.add(Ioss::Property("FILE_TYPE", "netcdf5"));
    }

    if (interFace.compression_level() > 0 || interFace.szip()) {
      properties.add(Ioss::Property("FILE_TYPE", "netcdf4"));
      properties.add(Ioss::Property("COMPRESSION_LEVEL", interFace.compression_level()));
      properties.add(Ioss::Property("COMPRESSION_SHUFFLE", true));
      if (interFace.szip()) {
        properties.add(Ioss::Property("COMPRESSION_METHOD", "szip"));
      }
      else if (interFace.zlib()) {
        properties.add(Ioss::Property("COMPRESSION_METHOD", "zlib"));
      }
    }
    return properties;
  }

  std::shared_ptr<Ioss::Region> create_input_region(const std::string &key, std::string filename,
                                                    bool ints_32_bits)
  {
    // Check that 'filename' does not contain a starting/ending double quote...
    filename.erase(remove(filename.begin(), filename.end(), '\"'), filename.end());
    Ioss::DatabaseIO *dbi = Ioss::IOFactory::create("exodus", filename, Ioss::READ_RESTART,
                                                    Ioss::ParallelUtils::comm_self());
    if (dbi == nullptr || !dbi->ok(true)) {
      std::exit(EXIT_FAILURE);
    }

    dbi->set_surface_split_type(Ioss::SPLIT_BY_DONT_SPLIT);
    if (ints_32_bits) {
      dbi->set_int_byte_size_api(Ioss::USE_INT32_API);
    }
    else {
      dbi->set_int_byte_size_api(Ioss::USE_INT64_API);
    }

    // Splitting surfaces by element block makes it easier to transform the input
    // element id into the output element ids.
    dbi->set_surface_split_type(Ioss::SPLIT_BY_ELEMENT_BLOCK);

    // Generate a name for the region based on the key...
    std::string name = "Region_" + key;
    // NOTE: region owns database pointer at this time...
    return std::make_shared<Ioss::Region>(dbi, name);
  }

  void output_summary(Ioss::Region *region, std::ostream &strm)
  {
    int64_t nodes    = region->get_property("node_count").get_int();
    int64_t elements = region->get_property("element_count").get_int();

    fmt::print(strm, " Database: {}\tNodes = {} \tElements = {}\n",
               region->get_database()->get_filename(), fmt::group_digits(nodes),
               fmt::group_digits(elements));
  }

} // namespace
