// 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 "ED_SystemInterface.h" // for SystemInterface, etc
#include "edge_block.h"         // for Edge_Block
#include "exoII_read.h"
#include "exo_block.h"  // for Exo_Block
#include "exodusII.h"   // for ex_init_params, ex_opts, etc
#include "face_block.h" // for Face_Block
#include "fmt/ostream.h"
#include "node_set.h"     // for Node_Set
#include "side_set.h"     // for Side_Set
#include "smart_assert.h" // for SMART_ASSERT, Assert, etc
#include "stringx.h"      // for chop_whitespace
#include "util.h"         // for free_name_array, etc
#include <algorithm>      // for copy
#include <cstdint>        // for int64_t
#include <cstdio>         // for fclose, FILE, fopen
#include <cstdlib>        // for exit
#include <cstring>        // for strlen
#include <iostream>
#include <set>    // for set
#include <string> // for string, char_traits, etc
#include <vector> // for vector

namespace {
  void read_vars(int file_id, EXOTYPE flag, const char *type, int num_vars,
                 std::vector<std::string> &varlist);
} // namespace

template <typename INT> ExoII_Read<INT>::ExoII_Read() = default;

template <typename INT> ExoII_Read<INT>::ExoII_Read(const std::string &fname) : file_name(fname) {}

template <typename INT> ExoII_Read<INT>::~ExoII_Read()
{
  try {
    SMART_ASSERT(Check_State());

    if (file_id >= 0) {
      std::string err = Close_File();
      if (!err.empty()) {
        Error(fmt::format("ExoII_Read destructor(): closing file: \"{}\"\n", err));
      }
    }

    delete[] eblocks;
    delete[] nsets;
    delete[] ssets;
    delete[] nodes;
    delete[] times;
    delete[] edge_blocks;
    delete[] face_blocks;

    if (results) {
      for (unsigned i = 0; i < nodal_vars.size(); ++i) {
        delete[] results[i];
      }
      delete[] results;
    }
    delete[] global_vals;
    delete[] global_vals2;
    delete[] node_map;
    delete[] elmt_map;
    delete[] elmt_order;
  }
  catch (...) {
  }
}

template <typename INT> std::string ExoII_Read<INT>::Close_File()
{
  SMART_ASSERT(Check_State());

  if (file_id < 0) {
    return "exodiff: ERROR: File is not open!";
  }
  int err = ex_close(file_id);

  if (err < 0) {
    Error(fmt::format("ExoII_Read::Close_File(): {}: Unable to close file!  Aborting...\n", err));
  }
  if (err > 0) {
    return fmt::format("WARNING: {} issued upon close", err);
  }

  file_id = -1;

  return "";
}

template <typename INT> double ExoII_Read<INT>::Time(int time_num) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(time_num > 0 && time_num <= num_times)(time_num)(num_times);
  return times[time_num - 1];
}

template <typename INT> const std::string &ExoII_Read<INT>::Global_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < global_vars.size());
  return global_vars[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::Nodal_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < nodal_vars.size());
  return nodal_vars[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::Element_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < elmt_vars.size());
  return elmt_vars[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::Element_Att_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < elmt_atts.size());
  return elmt_atts[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::NS_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < ns_vars.size());
  return ns_vars[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::SS_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < ss_vars.size());
  return ss_vars[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::EB_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < eb_vars.size());
  return eb_vars[index];
}

template <typename INT> const std::string &ExoII_Read<INT>::FB_Var_Name(int index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(index >= 0 && (unsigned)index < fb_vars.size());
  return fb_vars[index];
}

template <typename INT>
Exo_Block<INT> *ExoII_Read<INT>::Get_Element_Block_by_Index(size_t block_index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(block_index < num_elmt_blocks);
  return &eblocks[block_index];
}

template <typename INT> Exo_Block<INT> *ExoII_Read<INT>::Get_Element_Block_by_Id(size_t id) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_elmt_blocks; i++) {
    if (eblocks[i].Id() == id) {
      return &eblocks[i];
    }
  }
  return nullptr;
}

template <typename INT>
Exo_Block<INT> *ExoII_Read<INT>::Get_Element_Block_by_Name(const std::string &name) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_elmt_blocks; i++) {
    if (eblocks[i].Name() == name) {
      return &eblocks[i];
    }
  }
  return nullptr;
}

template <typename INT>
Exo_Entity *ExoII_Read<INT>::Get_Entity_by_Index(EXOTYPE type, size_t block_index) const
{
  SMART_ASSERT(Check_State());

  switch (type) {
  case EX_ELEM_BLOCK: SMART_ASSERT(block_index < num_elmt_blocks); return &eblocks[block_index];
  case EX_NODE_SET: SMART_ASSERT(block_index < num_node_sets); return &nsets[block_index];
  case EX_SIDE_SET: SMART_ASSERT(block_index < num_side_sets); return &ssets[block_index];
  case EX_EDGE_BLOCK: SMART_ASSERT(block_index < num_edge_blocks); return &edge_blocks[block_index];
  case EX_FACE_BLOCK: SMART_ASSERT(block_index < num_face_blocks); return &face_blocks[block_index];
  default: return nullptr;
  }
}

template <typename INT> Exo_Entity *ExoII_Read<INT>::Get_Entity_by_Id(EXOTYPE type, size_t id) const
{
  SMART_ASSERT(Check_State());
  switch (type) {
  case EX_ELEM_BLOCK:
    for (size_t i = 0; i < num_elmt_blocks; i++) {
      if (eblocks[i].Id() == id) {
        return &eblocks[i];
      }
    }
    break;
  case EX_NODE_SET:
    for (size_t i = 0; i < num_node_sets; i++) {
      if (nsets[i].Id() == id) {
        return &nsets[i];
      }
    }
    break;
  case EX_SIDE_SET:
    for (size_t i = 0; i < num_side_sets; i++) {
      if (ssets[i].Id() == id) {
        return &ssets[i];
      }
    }
    break;
  case EX_EDGE_BLOCK:
    for (size_t i = 0; i < num_edge_blocks; i++) {
      if (edge_blocks[i].Id() == id) {
        return &edge_blocks[i];
      }
    }
    break;
  case EX_FACE_BLOCK:
    for (size_t i = 0; i < num_face_blocks; i++) {
      if (face_blocks[i].Id() == id) {
        return &face_blocks[i];
      }
    }
    break;
  default: return nullptr;
  }
  return nullptr;
}

template <typename INT>
Exo_Entity *ExoII_Read<INT>::Get_Entity_by_Name(EXOTYPE type, const std::string &name) const
{
  SMART_ASSERT(Check_State());
  switch (type) {
  case EX_ELEM_BLOCK:
    for (size_t i = 0; i < num_elmt_blocks; i++) {
      if (eblocks[i].Name() == name) {
        return &eblocks[i];
      }
    }
    break;
  case EX_NODE_SET:
    for (size_t i = 0; i < num_node_sets; i++) {
      if (nsets[i].Name() == name) {
        return &nsets[i];
      }
    }
    break;
  case EX_SIDE_SET:
    for (size_t i = 0; i < num_side_sets; i++) {
      if (ssets[i].Name() == name) {
        return &ssets[i];
      }
    }
    break;
  case EX_EDGE_BLOCK:
    for (size_t i = 0; i < num_edge_blocks; i++) {
      if (edge_blocks[i].Name() == name) {
        return &edge_blocks[i];
      }
    }
    break;
  case EX_FACE_BLOCK:
    for (size_t i = 0; i < num_face_blocks; i++) {
      if (face_blocks[i].Name() == name) {
        return &face_blocks[i];
      }
    }
    break;
  default: return nullptr;
  }
  return nullptr;
}

template <typename INT> Node_Set<INT> *ExoII_Read<INT>::Get_Node_Set_by_Id(size_t set_id) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_node_sets; i++) {
    if (nsets[i].Id() == set_id) {
      return &nsets[i];
    }
  }
  return nullptr;
}

template <typename INT>
Node_Set<INT> *ExoII_Read<INT>::Get_Node_Set_by_Name(const std::string &name) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_node_sets; i++) {
    if (nsets[i].Name() == name) {
      return &nsets[i];
    }
  }
  return nullptr;
}

template <typename INT> Side_Set<INT> *ExoII_Read<INT>::Get_Side_Set_by_Id(size_t set_id) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_side_sets; i++) {
    if (ssets[i].Id() == set_id) {
      return &ssets[i];
    }
  }
  return nullptr;
}

template <typename INT>
Side_Set<INT> *ExoII_Read<INT>::Get_Side_Set_by_Name(const std::string &name) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_side_sets; i++) {
    if (ssets[i].Name() == name) {
      return &ssets[i];
    }
  }
  return nullptr;
}

template <typename INT>
Edge_Block<INT> *ExoII_Read<INT>::Get_Edge_Block_by_Id(size_t block_id) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_edge_blocks; i++) {
    if (edge_blocks[i].Id() == block_id) {
      return &edge_blocks[i];
    }
  }
  return nullptr;
}

template <typename INT>
Edge_Block<INT> *ExoII_Read<INT>::Get_Edge_Block_by_Name(const std::string &name) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_edge_blocks; i++) {
    if (edge_blocks[i].Name() == name) {
      return &edge_blocks[i];
    }
  }
  return nullptr;
}

template <typename INT>
Face_Block<INT> *ExoII_Read<INT>::Get_Face_Block_by_Id(size_t block_id) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_face_blocks; i++) {
    if (face_blocks[i].Id() == block_id) {
      return &face_blocks[i];
    }
  }
  return nullptr;
}

template <typename INT>
Face_Block<INT> *ExoII_Read<INT>::Get_Face_Block_by_Name(const std::string &name) const
{
  SMART_ASSERT(Check_State());
  for (size_t i = 0; i < num_face_blocks; i++) {
    if (face_blocks[i].Name() == name) {
      return &face_blocks[i];
    }
  }
  return nullptr;
}

template <typename INT>
std::string ExoII_Read<INT>::Load_Element_Block_Description(size_t block_index) const
{
  SMART_ASSERT(Check_State());
  if (!Open()) {
    return "exodiff: ERROR:  Must open file before loading blocks!";
  }
  SMART_ASSERT(block_index < num_elmt_blocks);

  eblocks[block_index].Load_Connectivity();
  //  eblocks[idx].Load_Connectivity();
  //  eblocks[idx].Load_Attributes();

  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Load_Element_Block_Descriptions() const
{
  SMART_ASSERT(Check_State());
  if (!Open()) {
    return "exodiff: ERROR:  Must open file before loading blocks!";
  }
  for (size_t b = 0; b < num_elmt_blocks; ++b) {
    eblocks[b].Load_Connectivity();
  }

  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Free_Element_Block(size_t block_index) const
{
  SMART_ASSERT(Check_State());

  SMART_ASSERT(block_index < num_elmt_blocks);

  eblocks[block_index].Free_Connectivity();
  eblocks[block_index].Free_Attributes();

  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Free_Element_Blocks() const
{
  SMART_ASSERT(Check_State());

  for (size_t b = 0; b < num_elmt_blocks; ++b) {
    eblocks[b].Free_Connectivity();
    eblocks[b].Free_Attributes();
  }

  return "";
}

template <typename INT> size_t ExoII_Read<INT>::Block_Id(size_t block_index) const
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(block_index < num_elmt_blocks);
  return eblocks[block_index].Id();
}

template <typename INT> std::string ExoII_Read<INT>::Load_Node_Map()
{
  SMART_ASSERT(Check_State());

  if (!Open()) {
    return "WARNING:  File not open!";
  }
  delete[] node_map;
  node_map = nullptr;

  if (num_nodes == 0) {
    return "WARNING:  There are no nodes!";
  }
  node_map = new INT[num_nodes];
  SMART_ASSERT(node_map != nullptr);

  ex_opts(0); // Temporarily turn off error reporting in case map isn't stored.
  int err = ex_get_id_map(file_id, EX_NODE_MAP, node_map);
  ex_opts(EX_VERBOSE);

  if (err < 0) {
    Error(fmt::format("Unable to load node map; Exodus error = {}.  Aborting...\n", err));
  }
  else if (err > 0) {
    return "WARNING: Default node map being used.";
  }
  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Free_Node_Map()
{
  SMART_ASSERT(Check_State());

  delete[] node_map;
  node_map = nullptr;

  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Load_Element_Map()
{
  SMART_ASSERT(Check_State());

  if (!Open()) {
    return "WARNING:  File not open!";
  }
  delete[] elmt_map;
  elmt_map = nullptr;

  if (num_elmts == 0) {
    return "WARNING:  There are no elements!";
  }
  elmt_map = new INT[num_elmts];
  SMART_ASSERT(elmt_map != nullptr);

  ex_opts(0); // Temporarily turn off error reporting in case map isn't stored.
  int err = ex_get_id_map(file_id, EX_ELEM_MAP, elmt_map);
  ex_opts(EX_VERBOSE);

  if (err < 0) {
    Error(fmt::format("Unable to load element map; Exodus error = {}.  Aborting...\n", err));
  }
  else if (err > 0) {
    return "WARNING: Default element map being used.";
  }
  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Free_Element_Map()
{
  SMART_ASSERT(Check_State());

  delete[] elmt_map;
  elmt_map = nullptr;

  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Load_Nodal_Coordinates()
{
  SMART_ASSERT(Check_State());

  if (!Open()) {
    return "WARNING:  File not open!";
  }
  if (num_nodes) {
    size_t count = num_nodes * dimension;
    nodes        = new double[count];
    SMART_ASSERT(nodes != nullptr);
    double *x = nodes, *y = nodes, *z = nodes;
    if (dimension > 1) {
      y = nodes + num_nodes;
    }
    if (dimension > 2) {
      z = nodes + (2 * num_nodes);
    }

    int err = ex_get_coord(file_id, x, y, z);
    if (err < 0) {
      Error("Failed to get nodal coordinates!  Aborting...\n");
    }
    else if (err > 0) {
      delete[] nodes;
      nodes = nullptr;
      return fmt::format("exodiff: WARNING:  "
                         "Exodus issued warning \"{}\" on call to ex_get_coord()!"
                         "  I'm not going to keep what it gave me for coordinates.",
                         err);
    }
  }
  else {
    return "WARNING:  There are no nodes!";
  }
  return "";
}

template <typename INT> void ExoII_Read<INT>::Free_Nodal_Coordinates()
{
  SMART_ASSERT(Check_State());
  delete[] nodes;
  nodes = nullptr;
}

template <typename INT>
std::string ExoII_Read<INT>::Load_Nodal_Results(int time_step_num, int var_index)
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(time_step_num > 0 && time_step_num <= num_times);
  SMART_ASSERT(var_index >= 0 && (unsigned)var_index < nodal_vars.size());

  if (!Open()) {
    return "WARNING:  File not open!";
  }
  if (cur_time != time_step_num) {
    for (unsigned i = 0; i < nodal_vars.size(); ++i) {
      delete[] results[i];
      results[i] = nullptr;
    }
    cur_time = time_step_num;
  }

  if (num_nodes) {
    results[var_index] = new double[num_nodes];

    int err =
        ex_get_var(file_id, cur_time, EX_NODAL, var_index + 1, 0, num_nodes, results[var_index]);
    if (err < 0) {
      Error("ExoII_Read::Load_Nodal_Results(): Failed to get "
            "nodal variable values!  Aborting...\n");
    }
    else if (err > 0) {
      delete[] results[var_index];
      results[var_index] = nullptr;
      return fmt::format("ExoII_Read::Load_Nodal_Results(): WARNING:  "
                         "Exodus issued warning \"{}\" on call to ex_get_var()!"
                         "  I'm not going to keep what it gave me for values.",
                         err);
    }
  }
  else {
    return "WARNING:  There are no nodes!";
  }
  return "";
}

template <typename INT>
const double *ExoII_Read<INT>::Get_Nodal_Results(int t1, int t2, double proportion,
                                                 int var_index) const // Interpolated results.
{
  static double *st_results  = nullptr;
  static double *st_results2 = nullptr;

  SMART_ASSERT(Check_State());
  SMART_ASSERT(t1 > 0 && t1 <= num_times);
  SMART_ASSERT(t2 > 0 && t2 <= num_times);
  SMART_ASSERT(var_index >= 0 && (unsigned)var_index < nodal_vars.size());

  if (!Open()) {
    return nullptr;
  }

  if (st_results == nullptr) {
    st_results = new double[num_nodes];
  }

  int err = ex_get_var(file_id, t1, EX_NODAL, var_index + 1, 0, num_nodes, st_results);
  if (err < 0) {
    Error("ExoII_Read::Get_Nodal_Results(): Failed to get "
          "nodal variable values!  Aborting...\n");
  }

  if (t1 != t2) {
    if (st_results2 == nullptr) {
      st_results2 = new double[num_nodes];
    }

    err = ex_get_var(file_id, t2, EX_NODAL, var_index + 1, 0, num_nodes, st_results2);
    if (err < 0) {
      Error("ExoII_Read::Load_Nodal_Results(): Failed to get "
            "nodal variable values!  Aborting...\n");
    }

    // Interpolate the values...
    for (size_t i = 0; i < num_nodes; i++) {
      st_results[i] = (1.0 - proportion) * st_results[i] + proportion * st_results2[i];
    }
  }
  return st_results;
}

template <typename INT> void ExoII_Read<INT>::Free_Nodal_Results()
{
  SMART_ASSERT(Check_State());
  if (results) {
    for (unsigned i = 0; i < nodal_vars.size(); ++i) {
      delete[] results[i];
      results[i] = nullptr;
    }
  }
}

template <typename INT> void ExoII_Read<INT>::Free_Nodal_Results(int var_index)
{
  SMART_ASSERT(Check_State());
  if (results) {
    if (results[var_index]) {
      delete[] results[var_index];
      results[var_index] = nullptr;
    }
  }
}

template <typename INT> const double *ExoII_Read<INT>::Get_Nodal_Results(int var_index) const
{
  SMART_ASSERT(Check_State());
  if (cur_time == 0) {
    return nullptr;
  }
  SMART_ASSERT(var_index >= 0 && (unsigned)var_index < nodal_vars.size());

  return results[var_index];
}

template <typename INT> std::string ExoII_Read<INT>::Load_Global_Results(int time_step_num)
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(time_step_num > 0 && time_step_num <= num_times);

  if (!Open()) {
    return "WARNING:  File not open!";
  }
  if (global_vars.empty()) {
    return "WARNING:  No global variables! (doing nothing)";
  }

  if (global_vals == nullptr) {
    global_vals = new double[global_vars.size()];
    SMART_ASSERT(global_vals != nullptr);
  }

  for (unsigned j = 0; j < global_vars.size(); ++j) {
    global_vals[j] = 0.0;
  }

  int err = ex_get_var(file_id, time_step_num, EX_GLOBAL, 1, 1, global_vars.size(), global_vals);

  if (err < 0) {
    Error("ExoII_Read::Load_Global_Results(): Failed to get "
          "global variable values!  Aborting...\n");
  }
  else if (err > 0) {
    return fmt::format("ExoII_Read::Load_Global_Results(): WARNING:  "
                       "Exodus issued warning \"{}\" on call to ex_get_glob_vars()!",
                       err);
  }
  return "";
}

template <typename INT>
std::string ExoII_Read<INT>::Load_Global_Results(int t1, int t2, double proportion)
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(t1 > 0 && t1 <= num_times);
  SMART_ASSERT(t2 > 0 && t2 <= num_times);

  if (!Open()) {
    return "WARNING:  File not open!";
  }
  if (global_vars.empty()) {
    return "WARNING:  No global variables! (doing nothing)";
  }
  if (global_vals == nullptr) {
    global_vals = new double[global_vars.size()];
    SMART_ASSERT(global_vals != nullptr);
  }

  if (t2 != t1 && (global_vals2 == nullptr)) {
    global_vals2 = new double[global_vars.size()];
    SMART_ASSERT(global_vals2 != nullptr);
  }

  for (unsigned j = 0; j < global_vars.size(); ++j) {
    global_vals[j] = 0.0;
  }

  int err = ex_get_var(file_id, t1, EX_GLOBAL, 1, 1, global_vars.size(), global_vals);

  if (err < 0) {
    Error("ExoII_Read::Load_Global_Results(): Failed to get "
          "global variable values!  Aborting...\n");
  }

  if (t2 != t1) {
    err = ex_get_var(file_id, t2, EX_GLOBAL, 1, 1, global_vars.size(), global_vals2);
    if (err < 0) {
      Error("ExoII_Read::Load_Global_Results(): Failed to get "
            "global variable values!  Aborting...\n");
    }

    // Do the interpolation...
    for (size_t j = 0; j < global_vars.size(); j++) {
      global_vals[j] = (1.0 - proportion) * global_vals[j] + proportion * global_vals2[j];
    }
  }
  return "";
}

template <typename INT>
Side_Set<INT> *ExoII_Read<INT>::Get_Side_Set_by_Index(size_t side_set_index) const
{
  SMART_ASSERT(Check_State());

  if (side_set_index >= num_side_sets) {
    return nullptr;
  }

  return &ssets[side_set_index];
}

template <typename INT>
Node_Set<INT> *ExoII_Read<INT>::Get_Node_Set_by_Index(size_t set_index) const
{
  SMART_ASSERT(Check_State());

  if (set_index >= num_node_sets) {
    return nullptr;
  }

  return &nsets[set_index];
}

template <typename INT>
Edge_Block<INT> *ExoII_Read<INT>::Get_Edge_Block_by_Index(size_t edge_block_index) const
{
  SMART_ASSERT(Check_State());

  if (edge_block_index >= num_edge_blocks) {
    return nullptr;
  }

  return &edge_blocks[edge_block_index];
}

template <typename INT>
Face_Block<INT> *ExoII_Read<INT>::Get_Face_Block_by_Index(size_t face_block_index) const
{
  SMART_ASSERT(Check_State());

  if (face_block_index >= num_face_blocks) {
    return nullptr;
  }

  return &face_blocks[face_block_index];
}

// **********************  Misc functions  *************************** //

// This function converts an Exodus global element number (1-offset) into
// its block index (0-offset) and block element index (0-offset).
template <typename INT>
std::pair<int, size_t> ExoII_Read<INT>::Global_to_Block_Local(size_t global_elmt_num) const
{
  SMART_ASSERT(Check_State());

  if (!Open()) {
    Error("exodiff: ERROR:  File not open!");
  }
  if (global_elmt_num < 1 || global_elmt_num > num_elmts) {
    Error(fmt::format("exodiff: ERROR:  global_elmt_num = {} is out of bounds [1, {}]!",
                      fmt::group_digits(global_elmt_num), fmt::group_digits(num_elmts)));
  }

  int block_index = 0;

  size_t total = 0;
  while (total + eblocks[block_index].Size() < global_elmt_num) {
    total += eblocks[block_index++].Size();
  }

  return std::make_pair(block_index, global_elmt_num - total - 1);
}

template <typename INT> int ExoII_Read<INT>::Check_State() const
{
  SMART_ASSERT(file_id >= -1);
  SMART_ASSERT(db_version >= 0.0);
  SMART_ASSERT(api_version >= 0.0);
  SMART_ASSERT(io_word_size == 0 || io_word_size == 4 || io_word_size == 8);

  SMART_ASSERT(!(file_id >= 0 && io_word_size == 0));
  SMART_ASSERT(!(file_id >= 0 && file_name == ""));

  SMART_ASSERT(!(num_elmt_blocks > 0 && !eblocks));
  SMART_ASSERT(!(num_node_sets > 0 && !nsets));
  SMART_ASSERT(!(num_side_sets > 0 && !ssets));

  SMART_ASSERT(!(num_nodes == 0 && nodes));

  SMART_ASSERT(num_times >= 0);
  SMART_ASSERT(!(num_times > 0 && !times));

  SMART_ASSERT(cur_time >= 0 && cur_time <= num_times);
  SMART_ASSERT(!(!nodal_vars.empty() && !results));
  SMART_ASSERT(!(nodal_vars.empty() && results));

  return 1;
}

template <typename INT> std::string ExoII_Read<INT>::File_Name(const char *fname)
{
  SMART_ASSERT(Check_State());

  if (Open()) {
    return "exodiff: ERROR: File is already open!";
  }
  if ((fname == nullptr) || std::strlen(fname) == 0) {
    return "exodiff: ERROR: File name is empty!";
  }
  file_name = fname;

  return "";
}

template <typename INT> std::string ExoII_Read<INT>::Open_File(const char *fname)
{
  SMART_ASSERT(Check_State());

  if (Open()) {
    return "exodiff: ERROR: File already open!";
  }
  if ((fname != nullptr) && std::strlen(fname) > 0) {
    file_name = fname;
  }
  else if (file_name == "") {
    return "No file name to open!";
  }
  int   ws = 0, comp_ws = 8;
  float dumb = 0.0;
  int   mode = EX_READ;
  if (sizeof(INT) == 8) {
    mode |= EX_ALL_INT64_API;
  }
  auto old_opt = ex_opts(EX_VERBOSE);
  int  err     = ex_open(file_name.c_str(), mode, &comp_ws, &ws, &dumb);
  ex_opts(old_opt);
  if (err < 0) {
    std::ostringstream oss;
    fmt::print(oss, "Couldn't open file \"{}\".", file_name);

    // ExodusII library could not open file.  See if a file (exodusII
    // or not) exists with the specified name.
    FILE *fid = fopen(file_name.c_str(), "r");
    if (fid != nullptr) {
      fmt::print(oss, " File exists, but library could not open.");
      fclose(fid);
    }
    else {
      fmt::print(oss, " File does not exist.");
    }
    return oss.str();
  }

  file_id      = err;
  io_word_size = ws;

  Get_Init_Data();

  return "";
}

template <typename INT> void ExoII_Read<INT>::Get_Init_Data()
{
  SMART_ASSERT(Check_State());
  SMART_ASSERT(file_id >= 0);

  // Determine max size of entity and variable names on the database
  int name_length = ex_inquire_int(file_id, EX_INQ_DB_MAX_USED_NAME_LENGTH);
  ex_set_max_name_length(file_id, name_length);

  ex_init_params info{};
  info.title[0] = '\0';

  int err = ex_get_init_ext(file_id, &info);
  if (err < 0) {
    Error(fmt::format("Failed to get init data!"
                      " Error number = {}.  Aborting...\n",
                      err));
  }

  dimension       = info.num_dim;
  num_nodes       = info.num_nodes;
  num_elmts       = info.num_elem;
  num_faces       = info.num_face;
  num_edges       = info.num_edge;
  num_elmt_blocks = info.num_elem_blk;
  num_node_sets   = info.num_node_sets;
  num_side_sets   = info.num_side_sets;
  num_edge_blocks = info.num_edge_blk;
  num_face_blocks = info.num_face_blk;
  title           = info.title;

  if (err > 0 && !interFace.quiet_flag) {
    fmt::print(stderr, "exodiff: WARNING: was issued, number = {}\n", err);
  }
  if (dimension < 1 || dimension > 3) {
    Error(fmt::format("Init data appears corrupt:\n"
                      "         dimension = {}\n"
                      "         num_nodes = {}\n"
                      "         num_elmts = {}\n"
                      "         num_elmt_blocks = {}\n"
                      "         num_node_sets = {}\n"
                      "         num_side_sets = {}\n"
                      "         num_edge_blocks = {}\n"
                      "         num_face_blocks = {}\n"
                      " ... Aborting...\n",
                      dimension, fmt::group_digits(num_nodes), fmt::group_digits(num_elmts),
                      num_elmt_blocks, num_node_sets, num_edge_blocks, num_face_blocks,
                      num_side_sets));
  }

  int num_qa   = ex_inquire_int(file_id, EX_INQ_QA);
  int num_info = ex_inquire_int(file_id, EX_INQ_INFO);

  if (num_qa < 0 || num_info < 0) {
    Error(fmt::format("inquire data appears corrupt:\n"
                      "         num_qa   = {}\n"
                      "         num_info = {}\n"
                      " ... Aborting...\n",
                      num_qa, num_info));
  }

  //                   Coordinate Names...

  char **coords = get_name_array(3, name_length);
  err           = ex_get_coord_names(file_id, coords);
  if (err < 0) {
    Error("Failed to get coordinate names!  Aborting...\n");
  }

  coord_names.clear();
  for (int i = 0; i < dimension; ++i) {
    coord_names.push_back(coords[i]);
  }
  free_name_array(coords, 3);

  //                 Element Block Data...

  if (eblocks) {
    delete[] eblocks;
  }
  eblocks = nullptr;
  if (num_elmt_blocks > 0) {
    eblocks = new Exo_Block<INT>[num_elmt_blocks];
    SMART_ASSERT(eblocks != nullptr);
    std::vector<INT> ids(num_elmt_blocks);

    err = ex_get_ids(file_id, EX_ELEM_BLOCK, ids.data());

    if (err < 0) {
      Error("Failed to get element block ids!  Aborting...\n");
    }

    size_t e_count = 0;
    for (size_t b = 0; b < num_elmt_blocks; ++b) {
      if (ids[b] <= EX_INVALID_ID) {
        fmt::print(stderr,
                   "EXODIFF  WARNING:  Element block Id "
                   "for block index {} is {} which is negative. This was returned by call to "
                   "ex_get_elem_blk_ids().\n",
                   b, ids[b]);
      }

      eblocks[b].initialize(file_id, ids[b]);
      eblocks[b].offset(e_count);
      e_count += eblocks[b].Size();
    }

    if (e_count != num_elmts && !interFace.quiet_flag) {
      fmt::print(stderr,
                 "exodiff: WARNING: Total number of elements {}"
                 " does not equal the sum of the number of elements "
                 "in each block {}\n",
                 fmt::group_digits(num_elmts), fmt::group_digits(e_count));
    }

    // Gather the attribute names (even though not all attributes are on all blocks)
    std::set<std::string> names;
    for (size_t b = 0; b < num_elmt_blocks; ++b) {
      for (int a = 0; a < eblocks[b].attr_count(); a++) {
        names.insert(eblocks[b].Get_Attribute_Name(a));
      }
    }
    elmt_atts.resize(names.size());
    std::copy(names.begin(), names.end(), elmt_atts.begin());
  }

  //                     Node & Side sets...

  if (nsets) {
    delete[] nsets;
  }
  nsets = nullptr;
  if (num_node_sets > 0) {
    nsets = new Node_Set<INT>[num_node_sets];
    SMART_ASSERT(nsets != nullptr);
    std::vector<INT> ids(num_node_sets);

    err = ex_get_ids(file_id, EX_NODE_SET, ids.data());

    if (err < 0) {
      Error("Failed to get nodeset ids!  Aborting...\n");
    }

    for (size_t nset = 0; nset < num_node_sets; ++nset) {
      if (ids[nset] <= EX_INVALID_ID) {
        fmt::print(stderr,
                   "EXODIFF  WARNING: Nodeset Id "
                   "for nodeset index {} is {}"
                   " which is negative.  This was returned by call to ex_get_ids().\n",
                   nset, ids[nset]);
      }

      nsets[nset].initialize(file_id, ids[nset]);
    }
  }

  if (ssets) {
    delete[] ssets;
  }
  ssets = nullptr;
  if (num_side_sets) {
    ssets = new Side_Set<INT>[num_side_sets];
    SMART_ASSERT(ssets != nullptr);
    std::vector<INT> ids(num_side_sets);

    err = ex_get_ids(file_id, EX_SIDE_SET, ids.data());

    if (err < 0) {
      Error("Failed to get sideset ids!  Aborting...\n");
    }

    for (size_t sset = 0; sset < num_side_sets; ++sset) {
      if (ids[sset] <= EX_INVALID_ID) {
        fmt::print(stderr,
                   "EXODIFF  WARNING: Sideset Id for sideset index {} is {}"
                   " which is negative.  This was returned by call to ex_get_ids().\n",
                   sset, ids[sset]);
      }
      ssets[sset].initialize(file_id, ids[sset]);
    }
  }

  //                     Edge & Face blocks...

  if (edge_blocks) {
    delete[] edge_blocks;
  }
  edge_blocks = nullptr;
  if (num_edge_blocks > 0) {
    edge_blocks = new Edge_Block<INT>[num_edge_blocks];
    SMART_ASSERT(edge_blocks != nullptr);
    std::vector<INT> ids(num_edge_blocks);

    err = ex_get_ids(file_id, EX_EDGE_BLOCK, ids.data());

    if (err < 0) {
      Error("Failed to get edgeblock ids!  Aborting...\n");
    }

    for (size_t edge_block = 0; edge_block < num_edge_blocks; ++edge_block) {
      if (ids[edge_block] <= EX_INVALID_ID) {
        fmt::print(stderr,
                   "EXODIFF  WARNING: Edgeblock Id "
                   "for edgeblock index {} is {}"
                   " which is negative.  This was returned by call to ex_get_ids().\n",
                   edge_block, ids[edge_block]);
      }

      edge_blocks[edge_block].initialize(file_id, ids[edge_block]);
    }
  }

  if (face_blocks) {
    delete[] face_blocks;
  }
  face_blocks = nullptr;
  if (num_face_blocks > 0) {
    face_blocks = new Face_Block<INT>[num_face_blocks];
    SMART_ASSERT(face_blocks != nullptr);
    std::vector<INT> ids(num_face_blocks);

    err = ex_get_ids(file_id, EX_FACE_BLOCK, ids.data());

    if (err < 0) {
      Error("Failed to get faceblock ids!  Aborting...\n");
    }

    for (size_t face_block = 0; face_block < num_face_blocks; ++face_block) {
      if (ids[face_block] <= EX_INVALID_ID) {
        fmt::print(stderr,
                   "EXODIFF  WARNING: Faceblock Id "
                   "for faceblock index {} is {}"
                   " which is negative.  This was returned by call to ex_get_ids().\n",
                   face_block, ids[face_block]);
      }

      face_blocks[face_block].initialize(file_id, ids[face_block]);
    }
  }

  //  **************  RESULTS info  ***************  //

  int num_global_vars, num_nodal_vars, num_elmt_vars, num_ns_vars, num_ss_vars, num_edge_vars,
      num_face_vars;

  err = ex_get_variable_param(file_id, EX_GLOBAL, &num_global_vars);
  if (err < 0) {
    Error("Failed to get number of global variables!  Aborting...\n");
  }

  err = ex_get_variable_param(file_id, EX_NODAL, &num_nodal_vars);
  if (err < 0) {
    Error("Failed to get number of nodal variables!  Aborting...\n");
  }

  err = ex_get_variable_param(file_id, EX_ELEM_BLOCK, &num_elmt_vars);
  if (err < 0) {
    Error("Failed to get number of element variables!  Aborting...\n");
  }

  err = ex_get_variable_param(file_id, EX_NODE_SET, &num_ns_vars);
  if (err < 0) {
    Error("Failed to get number of nodeset variables!  Aborting...\n");
  }

  err = ex_get_variable_param(file_id, EX_SIDE_SET, &num_ss_vars);
  if (err < 0) {
    Error("Failed to get number of sideset variables!  Aborting...\n");
  }

  err = ex_get_variable_param(file_id, EX_EDGE_BLOCK, &num_edge_vars);
  if (err < 0) {
    Error("Failed to get number of edgeblock variables!  Aborting...\n");
  }

  err = ex_get_variable_param(file_id, EX_FACE_BLOCK, &num_face_vars);
  if (err < 0) {
    Error("Failed to get number of faceblock variables!  Aborting...\n");
  }

  if (num_global_vars < 0 || num_nodal_vars < 0 || num_elmt_vars < 0 || num_ns_vars < 0 ||
      num_ss_vars < 0 || num_edge_vars < 0 || num_face_vars < 0) {
    Error(fmt::format("Data appears corrupt for"
                      " number of variables !\n"
                      "\tnum global vars  = {}\n"
                      "\tnum nodal vars   = {}\n"
                      "\tnum element vars = {}\n"
                      "\tnum nodeset vars = {}\n"
                      "\tnum sideset vars = {}\n"
                      "\tnum edgeblock vars = {}\n"
                      "\tnum faceblock vars = {}\n"
                      " ... Aborting...\n",
                      num_global_vars, num_nodal_vars, num_elmt_vars, num_ns_vars, num_ss_vars,
                      num_edge_vars, num_face_vars));
  }

  read_vars(file_id, EX_GLOBAL, "Global", num_global_vars, global_vars);
  read_vars(file_id, EX_NODAL, "Nodal", num_nodal_vars, nodal_vars);
  read_vars(file_id, EX_ELEM_BLOCK, "Element", num_elmt_vars, elmt_vars);
  read_vars(file_id, EX_NODE_SET, "Nodeset", num_ns_vars, ns_vars);
  read_vars(file_id, EX_SIDE_SET, "Sideset", num_ss_vars, ss_vars);
  read_vars(file_id, EX_EDGE_BLOCK, "Edgeblock", num_edge_vars, eb_vars);
  read_vars(file_id, EX_FACE_BLOCK, "Faceblock", num_face_vars, fb_vars);

  // Times:
  num_times = ex_inquire_int(file_id, EX_INQ_TIME);
  if (num_times < 0) {
    Error(fmt::format("Number of time steps came back negative ({})!  Aborting...\n", num_times));
  }

  if ((num_global_vars > 0 || num_nodal_vars > 0 || num_elmt_vars > 0 || num_ns_vars > 0 ||
       num_ss_vars > 0 || num_edge_vars > 0 || num_face_vars > 0) &&
      num_times == 0) {
    Warning("Consistency error -- The database contains transient variables, but no "
            "timesteps!\n");
  }

  if (num_times) {
    times = new double[num_times];
    SMART_ASSERT(times != nullptr);
    err = ex_get_all_times(file_id, times);
  }

  if (num_nodal_vars != 0) {
    if (num_times == 0) {
      Warning(fmt::format("Consistency error--The database contains {}"
                          " nodal variables, but there are no time steps defined.\n",
                          num_nodal_vars));
    }
    if (num_times) {
      results = new double *[num_nodal_vars];
      for (int i = 0; i < num_nodal_vars; ++i) {
        results[i] = nullptr;
      }
    }
  }

} // End of EXODIFF

namespace {
  void read_vars(int file_id, EXOTYPE flag, const char *type, int num_vars,
                 std::vector<std::string> &varlist)
  {
    if (num_vars != 0) {
      int    name_size = ex_inquire_int(file_id, EX_INQ_MAX_READ_NAME_LENGTH);
      char **varnames  = get_name_array(num_vars, name_size);
      int    err       = ex_get_variable_names(file_id, flag, num_vars, varnames);

      if (err < 0) {
        Error(fmt::format("Failed to get {} variable names!  Aborting...\n", type));
      }
      else if (err > 0 && !interFace.quiet_flag) {
        fmt::print(
            stderr,
            "exodiff: WARNING: Exodus issued warning \"{}\" on call to ex_get_var_names()!\n", err);
      }
      for (int vg = 0; vg < num_vars; ++vg) {
        SMART_ASSERT(varnames[vg] != nullptr);
        if (std::strlen(varnames[vg]) == 0 ||
            static_cast<int>(std::strlen(varnames[vg])) > name_size) {
          std::ostringstream out;
          fmt::print(out,
                     "exodiff: ERROR: {} variable names appear corrupt\n"
                     "                A length is 0 or greater than name_size({})\n"
                     "                Here are the names that I received from"
                     " a call to ex_get_var_names(...):\n",
                     type, name_size);
          for (int k = 1; k <= num_vars; ++k) {
            fmt::print(out, "\t\t{}) \"{}\"\n", k, varnames[k - 1]);
          }
          fmt::print(out, "                 Aborting...\n");
          Error(out);
        }

        std::string n(varnames[vg]);
        chop_whitespace(n);
        varlist.push_back(n);
      }
      free_name_array(varnames, num_vars);
    }
  }
} // namespace
template class ExoII_Read<int>;
template class ExoII_Read<int64_t>;
