// Copyright(C) 1999-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 <exception>
#include <map>
#include <memory>
#include <string>
#include <vector>

#include "add_to_log.h"
#define FMT_DEPRECATED_OSTREAM
#include "fmt/ostream.h"
#include "format_time.h"
#include "hwm.h"
#include "time_stamp.h"

#include <Ionit_Initializer.h>
#include <Ioss_SmartAssert.h>
#include <Ioss_SubSystem.h>

#include <cgns/Iocgns_Utils.h>

#include "CP_SystemInterface.h"
#include "CP_Version.h"

#ifdef SEACAS_HAVE_MPI
#include <mpi.h>
#endif

unsigned int debug_level = 0;

namespace {
  std::string tsFormat = "[{:%H:%M:%S}] ";

  using GlobalZgcMap   = std::map<std::pair<std::string, std::string>, Ioss::ZoneConnectivity>;
  using GlobalBcMap    = std::map<std::pair<std::string, std::string>, Ioss::BoundaryCondition>;
  using GlobalBlockMap = std::map<const std::string, const Ioss::StructuredBlock *>;
  using GlobalIJKMap   = std::map<const std::string, Ioss::IJK_t>;
  using PartVector     = std::vector<std::unique_ptr<Ioss::Region>>;

  GlobalZgcMap generate_global_zgc(const PartVector &part_mesh);
  GlobalBcMap  generate_global_bc(const PartVector &part_mesh);

  void   info_structuredblock(const Ioss::Region &region);
  void   resolve_offsets(const PartVector &part_mesh, GlobalBlockMap &all_blocks);
  void   update_global_ijk(const PartVector &part_mesh, GlobalIJKMap &global_block);
  void   transfer_nodal_field(const Ioss::StructuredBlock *sb, const std::vector<double> &input,
                              std::vector<double> &output);
  void   transfer_cell_field(const Ioss::StructuredBlock *sb, const std::vector<double> &input,
                             std::vector<double> &output);
  void   transfer_nodal_coordinates(const PartVector &part_mesh, Ioss::Region &output_region);
  double transfer_step(const PartVector &part_mesh, Ioss::Region &output_region, int istep);
  void   union_zgc_range(Ioss::ZoneConnectivity &zgc_i, const Ioss::ZoneConnectivity &zgc_j);
  void   union_bc_range(Ioss::IJK_t &g_beg, Ioss::IJK_t &g_end, const Ioss::IJK_t &l_beg,
                        const Ioss::IJK_t &l_end, const Ioss::IJK_t &offset);

  int get_constant_face(const Ioss::IJK_t &beg, const Ioss::IJK_t &end)
  {
    for (int i = 0; i < 3; i++) {
      if (beg[i] == end[i]) {
        return (beg[i] == 1) ? i : i + 3;
      }
    }
    return 7;
  }

  bool is_field_valid(const Cpup::StringVector &variable_list, const std::string &field_name)
  {
    if (variable_list.empty() ||
        (variable_list.size() == 1 && Ioss::Utils::str_equal(variable_list[0], "all") == 0)) {
      return true;
    }

    // At this point, the variable_list contains one or more entries
    // of fields that should be output on combined file.  Run through
    // list and see if `field_name` is in the list.
    for (const auto &valid : variable_list) {
      if (Ioss::Utils::str_equal(valid, field_name) == 0) {
        return true;
      }
    }
    return false;
  }

  int verify_timestep_count(const PartVector &part_mesh)
  {
    int num_time_steps = part_mesh[0]->get_property("state_count").get_int();

    bool differ = false;
    for (const auto &part : part_mesh) {
      int nts = part->get_property("state_count").get_int();
      if (nts != num_time_steps) {
        differ = true;
      }
      num_time_steps = num_time_steps < nts ? num_time_steps : nts;
    }
    if (differ) {
      fmt::print(stderr,
                 "\nWARNING: The number of time steps is not the same on all input databases.\n"
                 "         Using minimum count of {}\n\n",
                 num_time_steps);
    }
    else {
      fmt::print(stderr, "\nNumber of time steps on input databases = {}\n\n", num_time_steps);
    }
    return num_time_steps;
  }

} // namespace

template <typename INT> void cpup(Cpup::SystemInterface &interFace, INT dummy);

int main(int argc, char *argv[])
{
#ifdef SEACAS_HAVE_MPI
  MPI_Init(&argc, &argv);
#endif

  try {
    Cpup::SystemInterface::show_version();
    Ioss::Init::Initializer io;

    Cpup::SystemInterface interFace;
    bool                  ok = interFace.parse_options(argc, argv);

    debug_level = interFace.debug();

    if (!ok) {
      fmt::print(stderr, "\nERROR: Problems parsing command line arguments.\n\n");
      exit(EXIT_FAILURE);
    }

    int    error = 0;
    double begin = Ioss::Utils::timer();
    cpup(interFace, static_cast<int64_t>(0));
    double end = Ioss::Utils::timer();

    fmt::print(stderr,
               "\nTotal Execution Time = {:.2f} seconds, Maximum memory = {} MiBytes.\n******* "
               "END *******\n\n",
               end - begin,
               fmt::group_digits((get_hwm_memory_info() + 1024 * 1024 - 1) / (1024 * 1024)));

    add_to_log(argv[0], end - begin);

#ifdef SEACAS_HAVE_MPI
    MPI_Finalize();
#endif

    return (error);
  }
  catch (std::exception &e) {
    fmt::print(stderr, "ERROR: Standard exception: {}\n", e.what());
  }
}

template <typename INT> void cpup(Cpup::SystemInterface &interFace, INT /*dummy*/)
{
  auto width = Ioss::Utils::number_width(interFace.processor_count(), false);

  PartVector part_mesh(interFace.processor_count());
  for (int p = 0; p < interFace.processor_count(); p++) {
    std::string inp_file = interFace.basename() + "." + interFace.cgns_suffix();
    auto        filename = Ioss::Utils::decode_filename(inp_file, p, interFace.processor_count());

    if (debug_level & 1) {
      fmt::print(stderr, "{} Processor rank {:{}}, file {}\n", time_stamp(tsFormat), p, width,
                 filename);
    }
    Ioss::DatabaseIO *dbi = Ioss::IOFactory::create("cgns", filename, Ioss::READ_RESTART,
                                                    Ioss::ParallelUtils::comm_world());
    if (dbi == nullptr || !dbi->ok(true)) {
      std::exit(EXIT_FAILURE);
    }

    dbi->set_field_separator(1);

    // NOTE: region owns database pointer at this time...
    std::string name = "CPUP_" + std::to_string(p + 1);
    part_mesh[p]     = std::unique_ptr<Ioss::Region>(new Ioss::Region(dbi, name));

    if (part_mesh[p]->mesh_type() != Ioss::MeshType::STRUCTURED) {
      part_mesh[p]->output_summary(std::cerr);
      fmt::print(stderr,
                 "\nERROR: Only UNSTRUCTURED CGNS file joining is supported at this time.\n");
      exit(EXIT_FAILURE);
    }

    if (debug_level & 2) {
      part_mesh[p]->output_summary(std::cerr);
      fmt::print(stderr, "\n");
    }
  }

  // Each processor may have a different set of zones.  This routine
  // will sync the information such that at end, there is a consistent
  // set of structuredBlocks defined with the correct local and
  // global, i,j,k ranges and offsets.

  GlobalBlockMap all_blocks;
  GlobalIJKMap   global_block;
  for (const auto &part : part_mesh) {
    auto &blocks = part->get_structured_blocks();
    for (const auto &block : blocks) {
      auto &name       = block->name();
      all_blocks[name] = block;

      // Build map of unique blocks in the mesh.
      auto name_proc = Iocgns::Utils::decompose_name(name, true);
      global_block[name_proc.first];

      // Set the zgc 'from_decomp' property...
      for (const auto &zgc : block->m_zoneConnectivity) {
        auto zgc_name_proc = Iocgns::Utils::decompose_name(zgc.m_donorName, true);
        if (zgc_name_proc.first == name_proc.first) {
          zgc.m_fromDecomp = true;
        }
      }
    }
  }

  // Resolve the offsets for each split block based on the decomp-ZGC
  // with the same block on other processors
  resolve_offsets(part_mesh, all_blocks);

  // Offsets are now correct. Need to calculate and update the global size of each unique block...
  update_global_ijk(part_mesh, global_block);

  // Need a consistent set of Boundary Conditions for each zone unioned across the proc-local-zones
  GlobalBcMap global_bc = generate_global_bc(part_mesh);

  // Need a consistent set of ZGC for each zone unioned across the proc-local-zones...
  // Skip the ZGC that are "from_decomp"
  GlobalZgcMap global_zgc = generate_global_zgc(part_mesh);

  // Create output file...
  Ioss::PropertyManager properties{};
  properties.add(Ioss::Property("FLUSH_INTERVAL", 0));
  Ioss::DatabaseIO *dbo =
      Ioss::IOFactory::create("cgns", interFace.output_filename(), Ioss::WRITE_RESTART,
                              Ioss::ParallelUtils::comm_world(), properties);
  if (dbo == nullptr || !dbo->ok(true)) {
    std::exit(EXIT_FAILURE);
  }

  // NOTE: 'output_region' owns 'dbo' pointer at this time
  Ioss::Region output_region(dbo, "cpup_output_region");
  output_region.property_add(Ioss::Property("code_name", qainfo[0]));
  output_region.property_add(Ioss::Property("code_version", qainfo[2]));

  output_region.begin_mode(Ioss::STATE_DEFINE_MODEL);

  // KLUGE: Remove this...
  // Doesn't affect the output model at all, so not a big issue, ...
  auto *nb = new Ioss::NodeBlock(dbo, "nodeblock_1", 1, 3);
  output_region.add(nb);

  // Create the output structured blocks...
  for (auto &block_range : global_block) {
    auto &block_name = block_range.first;
    auto  block      = new Ioss::StructuredBlock(dbo, block_name, 3, block_range.second);
    output_region.add(block);

    // Add BC to the block...
    for (const auto &bc_map : global_bc) {
      if (bc_map.first.first == block_name) {
        block->m_boundaryConditions.push_back(bc_map.second);
      }
    }

    // Add ZGC to the block...
    for (const auto &zgc_map : global_zgc) {
      if (zgc_map.first.first == block_name) {
        block->m_zoneConnectivity.push_back(zgc_map.second);
      }
    }
  }

  // Copy the sidesets and assemblies from the proc-0 input file to the output file...
  auto       &part  = part_mesh[0];
  const auto &ssets = part->get_sidesets();
  for (const auto &sset : ssets) {
    auto oss = new Ioss::SideSet(*sset);
    output_region.add(oss);
  }

  const auto &assems = part->get_assemblies();
  for (const auto &assem : assems) {
    auto oass = new Ioss::Assembly(*assem);
    output_region.add(oass);
  }

  if (debug_level & 4) {
    info_structuredblock(output_region);
  }

  output_region.end_mode(Ioss::STATE_DEFINE_MODEL);

  output_region.begin_mode(Ioss::STATE_MODEL);
  transfer_nodal_coordinates(part_mesh, output_region);
  output_region.end_mode(Ioss::STATE_MODEL);

  // ******* Transient Data...
  output_region.begin_mode(Ioss::STATE_DEFINE_TRANSIENT);

  // ... Iterate the output_region structured blocks,
  //     .. Find corresponding structured blocks on part meshes.
  //        .. Add each valid block and node_block field
  const auto &variable_list = interFace.var_names();
  if (!(variable_list.size() == 1 && Ioss::Utils::str_equal(variable_list[0], "none") == 0)) {
    auto &blocks = output_region.get_structured_blocks();
    for (const auto &block : blocks) {
      int64_t num_cell = block->get_property("cell_count").get_int();
      int64_t num_node = block->get_property("node_count").get_int();

      auto &onb = block->get_node_block();

      // Find all corresponding blocks on the input part meshes...
      for (const auto &prt : part_mesh) {
        auto &pblocks = prt->get_structured_blocks();
        for (const auto &pblock : pblocks) {
          auto &name      = pblock->name();
          auto  name_proc = Iocgns::Utils::decompose_name(name, true);
          if (name_proc.first == block->name()) {
            Ioss::NameList fields = pblock->field_describe(Ioss::Field::TRANSIENT);

            for (const auto &field_name : fields) {
              if (is_field_valid(variable_list, field_name)) {
                Ioss::Field field = pblock->get_field(field_name);
                if (!block->field_exists(field_name)) {
                  // If the field does not already exist, add it to the output block...
                  field.reset_count(num_cell);
                  block->field_add(field);
                }
              }
            }

            // Now transfer the fields on the embedded node block...
            auto pnb = pblock->get_node_block();
            fields.clear();
            pnb.field_describe(Ioss::Field::TRANSIENT, &fields);
            for (const auto &field_name : fields) {
              if (is_field_valid(variable_list, field_name)) {
                Ioss::Field field = pblock->get_field(field_name);
                if (!onb.field_exists(field_name)) {
                  field.reset_count(num_node);
                  // If the field does not already exist, add it to the output block...
                  onb.field_add(field);
                }
              }
            }

            break; // Should be only a single instance of each block on a part mesh.
          }
        }
      }
    }
  }

  output_region.end_mode(Ioss::STATE_DEFINE_TRANSIENT);

  output_region.begin_mode(Ioss::STATE_TRANSIENT);

  int num_time_steps = verify_timestep_count(part_mesh);

  // Determine if user wants a subset of timesteps transferred to the output file.
  int ts_min  = interFace.step_min();
  int ts_max  = interFace.step_max();
  int ts_step = interFace.step_interval();

  if (ts_min == -1 && ts_max == -1) {
    ts_min = num_time_steps;
    ts_max = num_time_steps;
  }

  // Time steps for output file
  int time_step_out = 0;

  ts_max = ts_max < num_time_steps ? ts_max : num_time_steps;
  if (ts_min <= ts_max) {
    fmt::print(stderr, "\tTransferring step {} to step {} by {}\n", ts_min, ts_max, ts_step);
  }

  // Determine how many steps will be written...
  int output_steps = (ts_max - ts_min) / ts_step + 1;

  double start_time = Ioss::Utils::timer();
  double cur_time   = start_time;
  for (int time_step = ts_min; time_step <= ts_max; time_step += ts_step) {
    time_step_out++;
    double time_val = transfer_step(part_mesh, output_region, time_step);

    double time_per_step       = Ioss::Utils::timer() - cur_time;
    cur_time                   = Ioss::Utils::timer();
    double elapsed             = cur_time - start_time;
    double avg_time_per_step   = elapsed / time_step_out;
    double percentage_done     = (time_step_out * 100.0) / output_steps;
    double estimated_remaining = avg_time_per_step * (output_steps - time_step_out);
    if (debug_level & 1) {
      fmt::print(stderr,
                 "{} \tWrote step {:6}, time {:8.4e}\t[{:5.1f}%, Elapsed={}, \tETA={}, \tTPS={}]\n",
                 time_stamp(tsFormat), fmt::group_digits(time_step), time_val, percentage_done,
                 format_time(elapsed), format_time(estimated_remaining),
                 format_time(time_per_step));
    }
    else {
      fmt::print(stderr,
                 "\tWrote step {:6}, time {:8.4e}\t[{:5.1f}%, Elapsed={}, ETA={}, TPS={}]       \r",
                 fmt::group_digits(time_step), time_val, percentage_done, format_time(elapsed),
                 format_time(estimated_remaining), format_time(time_per_step));
    }
  }
  output_region.end_mode(Ioss::STATE_TRANSIENT);

  fmt::print(stderr, "\n\n********************* OUTPUT DATABASE ********************\n");
  output_region.output_summary(std::cerr);
}

namespace {
  GlobalZgcMap generate_global_zgc(const PartVector &part_mesh)
  {
    GlobalZgcMap global_zgc;
    for (const auto &part : part_mesh) {
      auto &blocks = part->get_structured_blocks();
      for (const auto &block : blocks) {
        auto name_proc = Iocgns::Utils::decompose_name(block->name(), true);

        for (const auto &zgc : block->m_zoneConnectivity) {
          if (!zgc.m_fromDecomp) {
            auto &gzgc = global_zgc[std::make_pair(name_proc.first, zgc.m_connectionName)];
            if (gzgc.m_connectionName.empty()) {
              // First time this ZGC has been found.  Copy from the per-proc instance and update...
              gzgc.m_connectionName = zgc.m_connectionName;
              gzgc.m_donorName      = Iocgns::Utils::decompose_name(zgc.m_donorName, true).first;
              gzgc.m_transform      = zgc.m_transform;
            }

            // Create a temporary zgc; adjust its ranges to "global" by adding the offsets
            // and then union if with the global instance...
            Ioss::IJK_t own_off = block->get_ijk_offset();

            auto tmp_zgc{zgc};
            tmp_zgc.m_ownerRangeBeg[0] += own_off[0];
            tmp_zgc.m_ownerRangeBeg[1] += own_off[1];
            tmp_zgc.m_ownerRangeBeg[2] += own_off[2];
            tmp_zgc.m_ownerRangeEnd[0] += own_off[0];
            tmp_zgc.m_ownerRangeEnd[1] += own_off[1];
            tmp_zgc.m_ownerRangeEnd[2] += own_off[2];

            // Now find the donor block...
            auto  donor       = Iocgns::Utils::decompose_name(zgc.m_donorName, true);
            auto *donor_block = part_mesh[donor.second]->get_structured_block(zgc.m_donorName);
            SMART_ASSERT(donor_block != nullptr);
            Ioss::IJK_t don_off = donor_block->get_ijk_offset();

            tmp_zgc.m_donorRangeBeg[0] += don_off[0];
            tmp_zgc.m_donorRangeBeg[1] += don_off[1];
            tmp_zgc.m_donorRangeBeg[2] += don_off[2];
            tmp_zgc.m_donorRangeEnd[0] += don_off[0];
            tmp_zgc.m_donorRangeEnd[1] += don_off[1];
            tmp_zgc.m_donorRangeEnd[2] += don_off[2];

            union_zgc_range(gzgc, tmp_zgc);
          }
        }
      }
    }
    return global_zgc;
  }

  GlobalBcMap generate_global_bc(const PartVector &part_mesh)
  {
    GlobalBcMap global_bc;
    for (const auto &part : part_mesh) {
      auto &blocks = part->get_structured_blocks();
      for (const auto &block : blocks) {
        Ioss::IJK_t offset    = block->get_ijk_offset();
        auto        name_proc = Iocgns::Utils::decompose_name(block->name(), true);
        auto       &sb_bc     = block->m_boundaryConditions;
        for (const auto &bc : sb_bc) {
          auto &gbc = global_bc[std::make_pair(name_proc.first, bc.m_bcName)];
          if (gbc.m_bcName.empty()) {
            gbc.m_bcName  = bc.m_bcName;
            gbc.m_famName = bc.m_famName;
          }
          union_bc_range(gbc.m_rangeBeg, gbc.m_rangeEnd, bc.m_rangeBeg, bc.m_rangeEnd, offset);
          SMART_ASSERT(gbc.which_face() == bc.which_face());
        }
      }
    }
    return global_bc;
  }

  double transfer_step(const PartVector &part_mesh, Ioss::Region &output_region, int istep)
  {
    double time  = part_mesh[0]->get_state_time(istep);
    int    ostep = output_region.add_state(time);

    output_region.begin_state(ostep);

    for (const auto &part : part_mesh) {
      part->begin_state(istep);
    }

    const auto &blocks = output_region.get_structured_blocks();
    for (const auto &block : blocks) {
      int64_t             num_cell = block->get_property("cell_count").get_int();
      std::vector<double> output(num_cell);
      std::vector<double> input;

      Ioss::NameList fields = block->field_describe(Ioss::Field::TRANSIENT);

      // Not sure if this is the best ordering of loops, but it minimizes the
      // amount of data gathered at one time at the cost of multiple iterations
      // through the block-finding loop...
      for (const auto &field_name : fields) {

        // Find all corresponding blocks on the input part meshes...
        for (const auto &part : part_mesh) {
          const auto &pblocks = part->get_structured_blocks();
          for (const auto &pblock : pblocks) {
            auto &name      = pblock->name();
            auto  name_proc = Iocgns::Utils::decompose_name(name, true);
            if (name_proc.first == block->name()) {
              if (pblock->field_exists(field_name)) {
                pblock->get_field_data(field_name, input);
                transfer_cell_field(pblock, input, output);
              }
              break; // Should be only a single instance of each block on a part mesh.
            }
          }
        }
        block->put_field_data(field_name, output);
      }
    }

    // Now do the fields on the embedded node block...
    for (const auto &block : blocks) {
      int64_t num_node = block->get_property("node_count").get_int();
      auto   &onb      = block->get_node_block();

      std::vector<double> output(num_node);
      std::vector<double> input;

      Ioss::NameList fields = onb.field_describe(Ioss::Field::TRANSIENT);

      // Not sure if this is the best ordering of loops, but it minimizes the
      // amount of data gathered at one time at the cost of multiple iterations
      // through the block-finding loop...
      for (const auto &field_name : fields) {

        // Find all corresponding blocks on the input part meshes...
        for (const auto &part : part_mesh) {
          auto &pblocks = part->get_structured_blocks();
          for (const auto &pblock : pblocks) {
            auto &name      = pblock->name();
            auto  name_proc = Iocgns::Utils::decompose_name(name, true);
            if (name_proc.first == block->name()) {
              auto &inb = pblock->get_node_block();
              if (inb.field_exists(field_name)) {
                inb.get_field_data(field_name, input);
                transfer_nodal_field(pblock, input, output);
              }
              break; // Should be only a single instance of each block on a part mesh.
            }
          }
        }
        onb.put_field_data(field_name, output);
      }
    }

    output_region.end_state(ostep);
    for (const auto &part : part_mesh) {
      part->end_state(istep);
    }
    return time;
  }

  void resolve_offsets(const PartVector &part_mesh, GlobalBlockMap &all_blocks)
  {
    bool change_made;
    do {
      change_made = false;
      for (const auto &part : part_mesh) {
        auto &blocks = part->get_structured_blocks();
        for (const auto &block : blocks) {
          for (const auto &zgc : block->m_zoneConnectivity) {
            if (zgc.is_from_decomp()) {
              auto plane = get_constant_face(zgc.m_ownerRangeBeg, zgc.m_ownerRangeEnd);
              if (plane < 3) {
                // This zone connects to another zone "below" it.
                // Find the connecting zone and adjust the correct offset
                auto donor     = all_blocks[zgc.m_donorName];
                auto offset    = donor->get_ijk_offset();
                auto range     = donor->get_ijk_local();
                auto my_offset = block->get_ijk_offset();
                if (my_offset[plane] != range[plane] + offset[plane]) {
                  block->set_ijk_offset(plane, range[plane] + offset[plane]);
                  change_made = true;
                }
              }
            }
          }
        }
      }
    } while (change_made);
  }

  void update_global_ijk(const PartVector &part_mesh, GlobalIJKMap &global_block)
  {
    for (const auto &part : part_mesh) {
      auto &blocks = part->get_structured_blocks();
      for (const auto &block : blocks) {
        auto  ijk_o      = block->get_ijk_offset();
        auto  ijk_g      = block->get_ijk_global();
        auto  name_proc  = Iocgns::Utils::decompose_name(block->name(), true);
        auto &cur_global = global_block[name_proc.first];
        cur_global[0]    = std::max(ijk_o[0] + ijk_g[0], cur_global[0]);
        cur_global[1]    = std::max(ijk_o[1] + ijk_g[1], cur_global[1]);
        cur_global[2]    = std::max(ijk_o[2] + ijk_g[2], cur_global[2]);
      }
    }

    for (const auto &part : part_mesh) {
      auto &blocks = part->get_structured_blocks();
      for (const auto &block : blocks) {
        auto  name_proc  = Iocgns::Utils::decompose_name(block->name(), true);
        auto &cur_global = global_block[name_proc.first];
        block->set_ijk_global(cur_global);
      }
      if (debug_level & 4) {
        info_structuredblock(*part);
      }
    }
  }

  void info_structuredblock(const Ioss::Region &region)
  {
    const Ioss::StructuredBlockContainer &sbs = region.get_structured_blocks();
    for (const auto &sb : sbs) {

      auto ijk_global = sb->get_ijk_global();
      fmt::print(stderr, "\n{} '{}' {}", sb->type_string(), sb->name(), fmt::join(ijk_global, "x"));

      auto ijk_offset = sb->get_ijk_offset();
      auto ijk_local  = sb->get_ijk_local();
      fmt::print(stderr, " [{}, Offset = {}] ", fmt::join(ijk_local, "x"),
                 fmt::join(ijk_offset, ", "));

      int64_t num_cell = sb->get_property("cell_count").get_int();
      int64_t num_node = sb->get_property("node_count").get_int();
      fmt::print(stderr, "  {:14} cells, {:14} nodes ", fmt::group_digits(num_cell),
                 fmt::group_digits(num_node));

      if (!sb->m_zoneConnectivity.empty()) {
        fmt::print(stderr, "\n\tConnectivity with other blocks:\n");
        for (const auto &zgc : sb->m_zoneConnectivity) {
          fmt::print(stderr, "{}\n", zgc);
        }
      }

      if (!sb->m_boundaryConditions.empty()) {
        fmt::print(stderr, "\tBoundary Conditions:\n");
        for (const auto &bc : sb->m_boundaryConditions) {
          fmt::print(stderr, "{}\n", bc);
        }
      }
    }
  }

  void transfer_nodal_field(const Ioss::StructuredBlock *sb, const std::vector<double> &input,
                            std::vector<double> &output)
  {
    // Get the IJK subset that this nodeblock covers...
    auto ijkg = sb->get_ijk_global();
    auto ijkl = sb->get_ijk_local();

    // Get the IJK offset...
    auto offset = sb->get_ijk_offset();

    SMART_ASSERT(input.size() == (size_t)(ijkl[0] + 1) * (ijkl[1] + 1) * (ijkl[2] + 1))
    (input.size())(ijkl[0])(ijkl[1])(ijkl[2]);
    SMART_ASSERT(output.size() == (size_t)(ijkg[0] + 1) * (ijkg[1] + 1) * (ijkg[2] + 1))
    (output.size())(ijkg[0])(ijkg[1])(ijkg[2]);

    size_t idx = 0;
    for (int k = offset[2]; k < offset[2] + ijkl[2] + 1; k++) {
      for (int j = offset[1]; j < offset[1] + ijkl[1] + 1; j++) {
        for (int i = offset[0]; i < offset[0] + ijkl[0] + 1; i++) {
          size_t oidx  = i + (ijkg[0] + 1) * j + (ijkg[0] + 1) * (ijkg[1] + 1) * k;
          output[oidx] = input[idx++];
        }
      }
    }
  }

  void transfer_cell_field(const Ioss::StructuredBlock *sb, const std::vector<double> &input,
                           std::vector<double> &output)
  {
    // Get the IJK subset that this structured covers...
    auto ijkg = sb->get_ijk_global();
    auto ijkl = sb->get_ijk_local();

    // Get the IJK offset...
    auto offset = sb->get_ijk_offset();

    SMART_ASSERT(input.size() == (size_t)ijkl[0] * ijkl[1] * ijkl[2])
    (input.size())(ijkl[0])(ijkl[1])(ijkl[2]);
    SMART_ASSERT(output.size() == (size_t)ijkg[0] * ijkg[1] * ijkg[2])
    (output.size())(ijkg[0])(ijkg[1])(ijkg[2]);

    size_t idx = 0;
    for (int k = offset[2]; k < offset[2] + ijkl[2]; k++) {
      for (int j = offset[1]; j < offset[1] + ijkl[1]; j++) {
        for (int i = offset[0]; i < offset[0] + ijkl[0]; i++) {
          size_t oidx  = i + ijkg[0] * j + ijkg[0] * ijkg[1] * k;
          output[oidx] = input[idx++];
        }
      }
    }
  }

  void transfer_nodal_coordinates(const PartVector &part_mesh, Ioss::Region &output_region)
  {
    // This implementation results in having to iterate over the part
    // mesh 3 times -- once for each coordinate axis, but minimizes
    // the memory requirements.  Will monitor whether this results in
    // excessive computation time, but does permit larger models to be
    // processed.

    std::array<std::string, 3> fields{"mesh_model_coordinates_x", "mesh_model_coordinates_y",
                                      "mesh_model_coordinates_z"};

    const auto &blocks = output_region.get_structured_blocks();
    for (const auto &block : blocks) {
      // Get size of node_block...
      auto               &onb       = block->get_node_block();
      size_t              num_coord = onb.entity_count();
      std::vector<double> coord(num_coord);
      for (int dim = 0; dim < 3; dim++) {

        // Find all corresponding blocks on the input part meshes...
        for (const auto &part : part_mesh) {
          auto &pblocks = part->get_structured_blocks();
          for (const auto &pblock : pblocks) {
            auto &name      = pblock->name();
            auto  name_proc = Iocgns::Utils::decompose_name(name, true);
            if (name_proc.first == block->name()) {
              std::vector<double> lcoord;
              pblock->get_field_data(fields[dim], lcoord);
              transfer_nodal_field(pblock, lcoord, coord);
              break; // Should be only a single instance of each block on a part mesh.
            }
          }
        }
        block->put_field_data(fields[dim], coord);
      }
    }
  }

  void union_bc_range(Ioss::IJK_t &g_beg, Ioss::IJK_t &g_end, const Ioss::IJK_t &l_beg,
                      const Ioss::IJK_t &l_end, const Ioss::IJK_t &offset)
  {
    for (int i = 0; i < 3; i++) {
      g_beg[i] = g_beg[i] == 0 ? l_beg[i] + offset[i] : std::min(g_beg[i], l_beg[i] + offset[i]);
      g_end[i] = std::max(g_end[i], l_end[i] + offset[i]);
    }
  }

  void union_zgc_range(Ioss::ZoneConnectivity &zgc_i, const Ioss::ZoneConnectivity &zgc_j)
  {
    assert(zgc_i.m_transform == zgc_j.m_transform);
    if (zgc_i.m_ownerRangeBeg[0] == 0 && zgc_i.m_ownerRangeBeg[1] == 0 &&
        zgc_i.m_ownerRangeBeg[2] == 0) {
      // This is a newly-created zgc that hasn't been unioned with anything yet.
      zgc_i.m_ownerRangeBeg = zgc_j.m_ownerRangeBeg;
      zgc_i.m_ownerRangeEnd = zgc_j.m_ownerRangeEnd;
      zgc_i.m_donorRangeBeg = zgc_j.m_donorRangeBeg;
      zgc_i.m_donorRangeEnd = zgc_j.m_donorRangeEnd;
    }
    else {
      for (int i = 0; i < 3; i++) {
        if (zgc_i.m_ownerRangeBeg[i] <= zgc_i.m_ownerRangeEnd[i]) {
          zgc_i.m_ownerRangeBeg[i] = std::min(zgc_i.m_ownerRangeBeg[i], zgc_j.m_ownerRangeBeg[i]);
          zgc_i.m_ownerRangeEnd[i] = std::max(zgc_i.m_ownerRangeEnd[i], zgc_j.m_ownerRangeEnd[i]);
        }
        else {
          zgc_i.m_ownerRangeBeg[i] = std::max(zgc_i.m_ownerRangeBeg[i], zgc_j.m_ownerRangeBeg[i]);
          zgc_i.m_ownerRangeEnd[i] = std::min(zgc_i.m_ownerRangeEnd[i], zgc_j.m_ownerRangeEnd[i]);
        }

        if (zgc_i.m_donorRangeBeg[i] <= zgc_i.m_donorRangeEnd[i]) {
          zgc_i.m_donorRangeBeg[i] = std::min(zgc_i.m_donorRangeBeg[i], zgc_j.m_donorRangeBeg[i]);
          zgc_i.m_donorRangeEnd[i] = std::max(zgc_i.m_donorRangeEnd[i], zgc_j.m_donorRangeEnd[i]);
        }
        else {
          zgc_i.m_donorRangeBeg[i] = std::max(zgc_i.m_donorRangeBeg[i], zgc_j.m_donorRangeBeg[i]);
          zgc_i.m_donorRangeEnd[i] = std::min(zgc_i.m_donorRangeEnd[i], zgc_j.m_donorRangeEnd[i]);
        }
      }
    }
  }
} // namespace
