// Copyright(C) 1999-2025 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 "CJ_SystemInterface.h"
#include "CJ_Version.h"  // for qainfo
#include "SL_tokenize.h" // for tokenize
#include <algorithm>     // for sort, transform
#include <cctype>        // for tolower
#include <copyright.h>
#include <cstddef> // for size_t
#include <cstdlib> // for exit, EXIT_SUCCESS, etc
#include <fmt/format.h>
#include <term_width.h>
#include <utility> // for pair, make_pair
#include <vector>  // for vector

namespace {
  void parse_variable_names(const char *tokens, StringIdVector *variable_list);
} // namespace

Excn::SystemInterface::SystemInterface() { enroll_options(); }

void Excn::SystemInterface::enroll_options()
{
  options_.usage("[options] list_of_files_to_join");

  options_.enroll("help", GetLongOption::NoValue, "Print this summary and exit", nullptr);

  options_.enroll("version", GetLongOption::NoValue, "Print version and exit", nullptr);

  options_.enroll("output", GetLongOption::MandatoryValue, "Name of output file to create",
                  "conjoin-out.e", nullptr, true);

  options_.enroll("alive_value", GetLongOption::MandatoryValue,
                  "Value (1 or 0) to indicate that an element is alive, default = 0", "0");

  options_.enroll("combine_status_variables", GetLongOption::MandatoryValue,
                  "The conjoin elem_status variable will be combined\n"
                  "\t\twith the specified status variable ($val) existing on the mesh.\n"
                  "\t\tBoth variables must have the same value (1 or 0) for 'alive'.\n"
                  "\t\t\tIf 1 is alive, then the combined variable is the min of the two values.\n"
                  "\t\t\tIf 0 is alive, then the combined variable is the max of the two values.\n"
                  "\t\tUse the 'alive_value' option to set conjoin's alive value",
                  "");
  options_.enroll("element_status_variable", GetLongOption::MandatoryValue,
                  "Name to use as element existence status variable;\n"
                  "\t\tmust not exist on input files. If NONE, then not created.\n"
                  "\t\tWill only be output if mesh topology changes between parts.\n"
                  "\t\tDefault = elem_status",
                  "elem_status");

  options_.enroll("nodal_status_variable", GetLongOption::MandatoryValue,
                  "Name to use as nodal status variable;\n\t\tmust not exist on input files.\n"
                  "\t\tWill only be output if mesh topology changes between parts.\n"
                  "\t\tIf NONE, then not created. Default = node_status",
                  "node_status");

  options_.enroll("force_status_variable", GetLongOption::NoValue,
                  "Output the node and element status variables even if they are not needed\n"
                  "\t\tdue to no changes in mesh topology",
                  nullptr, nullptr, true);

  options_.enroll("netcdf4", GetLongOption::NoValue,
                  "Create output database using the HDF5-based "
                  "netcdf which allows for up to 2.1 GB "
                  "nodes and elements",
                  nullptr);

  options_.enroll("64-bit", GetLongOption::NoValue,
                  "True if forcing the use of 64-bit integers for the output file", nullptr);

  options_.enroll("zlib", GetLongOption::NoValue,
                  "Use the Zlib / libz compression method if compression is enabled (default) "
                  "[exodus only, enables netcdf-4].",
                  nullptr);

  options_.enroll("szip", GetLongOption::NoValue,
                  "Use SZip compression. [exodus only, enables netcdf-4]", nullptr);
  options_.enroll("zstd", GetLongOption::NoValue,
                  "Use Zstd compression. [exodus only, enables netcdf-4, experimental]", nullptr);
  options_.enroll("bzip2", GetLongOption::NoValue,
                  "Use Bzip2 compression. [exodus only, enables netcdf-4, experimental]", nullptr);

  options_.enroll("compress", GetLongOption::MandatoryValue,
                  "Specify the compression level to be used.  Values depend on algorithm:\n"
                  "\t\tzlib/bzip2:  0..9\t\tszip:  even, 4..32\t\tzstd:  -131072..22",
                  nullptr);

  options_.enroll("quantize_nsd", GetLongOption::MandatoryValue,
                  "Use the lossy quantize compression method.\n"
                  "\t\tValue specifies number of digits to retain (1..15) [exodus only]",
                  nullptr, nullptr, true);

  options_.enroll("sort_times", GetLongOption::NoValue,
                  "Sort the input files on the minimum timestep time in the file.\n"
                  "\t\tDefault is to process files in the order they appear on the command line.",
                  nullptr);

  options_.enroll("ignore_coordinate_check", GetLongOption::NoValue,
                  "Do not use nodal coordinates to determine if node in part 1 same as node in\n"
                  "\t\tother parts; use ids only.\n"
                  "\t\tUse only if you know that the ids are consistent for all parts",
                  nullptr);

  options_.enroll("omit_nodesets", GetLongOption::NoValue,
                  "Don't transfer nodesets to output file.", nullptr);

  options_.enroll("omit_sidesets", GetLongOption::NoValue,
                  "Don't transfer sidesets to output file.", nullptr, nullptr, true);

  options_.enroll("gvar", GetLongOption::MandatoryValue,
                  "Comma-separated list of global variables to be joined or ALL or NONE.", nullptr);

  options_.enroll(
      "evar", GetLongOption::MandatoryValue,
      "Comma-separated list of element variables to be joined or ALL or NONE.\n"
      "\t\tVariables can be limited to certain blocks by appending a\n"
      "\t\tcolon followed by the block id.  E.g. -evar sigxx:10:20\n"
      "\t\tIf the first word is `OMIT`, then the list specifies variables to omit, not include.",
      nullptr);

  options_.enroll(
      "nvar", GetLongOption::MandatoryValue,
      "Comma-separated list of nodal variables to be joined or ALL, NONE, OMIT.\n"
      "\t\tIf the first word is `OMIT`, then the list specifies variables to omit, not include.",
      nullptr);

  options_.enroll(
      "nsetvar", GetLongOption::MandatoryValue,
      "Comma-separated list of nodeset variables to be joined or ALL, NONE, OMIT.\n"
      "\t\tIf the first word is `OMIT`, then the list specifies variables to omit, not include.",
      nullptr);

  options_.enroll(
      "ssetvar", GetLongOption::MandatoryValue,
      "Comma-separated list of sideset variables to be joined or ALL, NONE, OMIT.\n"
      "\t\tIf the first word is `OMIT`, then the list specifies variables to omit, not include.",
      nullptr);

  options_.enroll("use_all_times", GetLongOption::NoValue,
                  "All times on all databases will be used.\n"
                  "\t\tThe output database may have non-monotonically increasing times.",
                  nullptr, nullptr, true);
  options_.enroll(
      "interpart_minimum_time_delta", GetLongOption::MandatoryValue,
      "If the time delta between the maximum time on one\n\t\tdatabase and the minimum time on "
      "the next database is less than this value, the\n\t\ttime will not be retained in the output "
      "file",
      "0");

  options_.enroll("debug", GetLongOption::MandatoryValue,
                  "debug level (values are or'd)\n"
                  "\t\t  1 = timing information.\n"
                  "\t\t  4 = Verbose Element block information.\n"
                  "\t\t  8 = Check consistent nodal coordinates between parts.\n"
                  "\t\t 16 = Verbose Sideset information.\n"
                  "\t\t 32 = Verbose Nodeset information.\n"
                  "\t\t 64 = put exodus library into verbose mode.\n"
                  "\t\t128 = Check consistent global field values between parts.",
                  "0");

  options_.enroll("width", GetLongOption::MandatoryValue, "Width of output screen, default = 80",
                  nullptr);

  options_.enroll("copyright", GetLongOption::NoValue, "Show copyright and license data.", nullptr);
}

bool Excn::SystemInterface::parse_options(int argc, char **argv)
{
  int option_index = options_.parse(argc, argv);
  if (option_index < 1) {
    return false;
  }

  // Get options from environment variable also...
  char *options = getenv("CONJOIN_OPTIONS");
  if (options != nullptr) {
    fmt::print(
        "\nThe following options were specified via the CONJOIN_OPTIONS environment variable:\n"
        "\t{}\n\n",
        options);
    options_.parse(options, GetLongOption::basename(*argv));
  }

  if (options_.retrieve("help") != nullptr) {
    options_.usage();
    fmt::print("\n\tCan also set options via CONJOIN_OPTIONS environment variable.\n"
               "\n\tDocumentation: "
               "https://sandialabs.github.io/seacas-docs/sphinx/html/index.html#conjoin\n"
               "\n\t->->-> Send email to sierra-help@sandia.gov for conjoin support.<-<-<-\n");
    exit(EXIT_SUCCESS);
  }

  if (options_.retrieve("version") != nullptr) {
    // Version is printed up front, just exit...
    exit(0);
  }

  debugLevel_ = options_.get_option_value("debug", debugLevel_);

  {
    const char *temp = options_.retrieve("alive_value");
    if (temp != nullptr) {
      int value = std::stoi(temp);
      if (value == 1 || value == 0) {
        aliveValue_ = value;
      }
      else {
        fmt::print(stderr,
                   "\nERROR: Invalid value specified for node and element status."
                   "\nValid values are '1' or '0'.  Found '{}'\n",
                   value);
        exit(EXIT_FAILURE);
      }
    }
  }

  interpartMinimumTimeDelta_ =
      options_.get_option_value("interpart_minimum_time_delta", interpartMinimumTimeDelta_);
  elementStatusVariable_ =
      options_.get_option_value("element_status_variable", elementStatusVariable_);
  nodalStatusVariable_ = options_.get_option_value("nodal_status_variable", nodalStatusVariable_);
  meshCombineStatusVariable_ =
      options_.get_option_value("combine_status_variables", meshCombineStatusVariable_);

  screenWidth_ = options_.get_option_value("width", term_width());
  outputName_  = options_.get_option_value("output", outputName_);

  {
    const char *temp = options_.retrieve("gvar");
    if (temp != nullptr) {
      parse_variable_names(temp, &globalVarNames_);
    }
  }

  {
    const char *temp = options_.retrieve("nvar");
    if (temp != nullptr) {
      parse_variable_names(temp, &nodeVarNames_);
    }
  }

  {
    const char *temp = options_.retrieve("evar");
    if (temp != nullptr) {
      parse_variable_names(temp, &elemVarNames_);
    }
  }

  {
    const char *temp = options_.retrieve("nsetvar");
    if (temp != nullptr) {
      parse_variable_names(temp, &nsetVarNames_);
    }
  }

  {
    const char *temp = options_.retrieve("ssetvar");
    if (temp != nullptr) {
      parse_variable_names(temp, &ssetVarNames_);
    }
  }

  useNetcdf4_          = options_.retrieve("netcdf4") != nullptr;
  sortTimes_           = options_.retrieve("sort_times") != nullptr;
  ints64Bit_           = options_.retrieve("64-bit") != nullptr;
  ignoreCoordinates_   = options_.retrieve("ignore_coordinate_check") != nullptr;
  omitNodesets_        = options_.retrieve("omit_nodesets") != nullptr;
  omitSidesets_        = options_.retrieve("omit_sidesets") != nullptr;
  useAllTimes_         = options_.retrieve("use_all_times") != nullptr;
  forceStatusVariable_ = options_.retrieve("force_status_variable") != nullptr;

  zlib_ = (options_.retrieve("zlib") != nullptr);
  szip_ = (options_.retrieve("szip") != nullptr);
  zstd_ = (options_.retrieve("zstd") != nullptr);
  bz2_  = (options_.retrieve("bzip2") != nullptr);

  if (szip_ + zlib_ + zstd_ + bz2_ > 1) {
    fmt::print(stderr,
               "ERROR: Only one of 'szip' or 'zlib' or 'zstd' or 'bzip2' can be specified.\n");
  }

  {
    const char *temp = options_.retrieve("compress");
    if (temp != nullptr) {
      compressionLevel_ = std::stoi(temp);
      if (!szip_ && !zlib_ && !zstd_ && !bz2_) {
        zlib_ = true;
      }

      if (zlib_ || bz2_) {
        if (compressionLevel_ < 0 || compressionLevel_ > 9) {
          fmt::print(stderr,
                     "ERROR: Bad compression level {}, valid value is between 0 and 9 inclusive "
                     "for gzip/zlib/bzip2 compression.\n",
                     compressionLevel_);
          return false;
        }
      }
      else if (szip_) {
        if (compressionLevel_ % 2 != 0) {
          fmt::print(
              stderr,
              "ERROR: Bad compression level {}. Must be an even value for szip compression.\n",
              compressionLevel_);
          return false;
        }
        if (compressionLevel_ < 4 || compressionLevel_ > 32) {
          fmt::print(stderr,
                     "ERROR: Bad compression level {}, valid value is between 4 and 32 inclusive "
                     "for szip compression.\n",
                     compressionLevel_);
          return false;
        }
      }
    }
  }

  {
    const char *temp = options_.retrieve("quantize_nsd");
    if (temp != nullptr) {
      quantizeNSD_ = std::stoi(temp);
      if (!szip_ && !zlib_ && !zstd_ && !bz2_) {
        zlib_ = true;
      }
    }
  }

  if (options_.retrieve("copyright") != nullptr) {
    fmt::print("{}", copyright("2009-2021"));
    exit(EXIT_SUCCESS);
  }

  // Parse remaining options as directory paths.
  if (option_index < argc) {
    while (option_index < argc) {
      inputFiles_.emplace_back(argv[option_index++]);
    }
  }
  else {
    fmt::print(stderr, "\nERROR: no files specified\n\n");
    return false;
  }
  return true;
}

void Excn::SystemInterface::dump(std::ostream & /*unused*/) const {}

void Excn::SystemInterface::show_version()
{
  fmt::print(
      "{}\n"
      "\t(A code for sequentially appending Exodus databases. Supersedes conex and conex2.)\n"
      "\t(Version: {}) Modified: {}\n",
      qainfo[0], qainfo[1], qainfo[2]);
}

namespace {
  std::string LowerCase(const std::string &name)
  {
    std::string s = name;
    std::transform(s.begin(), s.end(), // source
                   s.begin(),          // destination
                   ::tolower);         // operation
    return s;
  }

  using StringVector = std::vector<std::string>;
  bool string_id_sort(const std::pair<std::string, int> &t1, const std::pair<std::string, int> &t2)
  {
    return t1.first < t2.first || (!(t2.first < t1.first) && t1.second < t2.second);
  }

  void parse_variable_names(const char *tokens, StringIdVector *variable_list)
  {
    // Break into tokens separated by ","
    if (tokens != nullptr) {
      std::string  token_string(tokens);
      StringVector var_list = SLIB::tokenize(token_string, ",");

      // At this point, var_list is either a single string, or a string
      // separated from 1 or more block ids with ":" delimiter.
      // For example, sigxx:1:10:100 would indicate that the variable
      // "sigxx" should be written only for blocks with id 1, 10, and
      // 100.  "sigxx" would indicate that the variable should be
      // written for all blocks.
      for (const auto &var : var_list) {
        StringVector name_id  = SLIB::tokenize(var, ":");
        std::string  var_name = LowerCase(name_id[0]);
        if (var_name == "omit") {
          var_name = "!omit"; // So sorts at front of list...
        }
        if (name_id.size() == 1) {
          (*variable_list).emplace_back(var_name, 0);
        }
        else {
          for (size_t i = 1; i < name_id.size(); i++) {
            // Convert string to integer...
            int id = std::stoi(name_id[i]);
            (*variable_list).emplace_back(var_name, id);
          }
        }
      }
      // Sort the list...
      std::sort(variable_list->begin(), variable_list->end(), string_id_sort);
    }
  }
} // namespace
