/*
  This file is part of CDO. CDO is a collection of Operators to manipulate and analyse Climate model Data.

  Author: Uwe Schulzweida

*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <iostream>
#include <vector>
#include <utility>

#include <sys/stat.h>

#include <unistd.h>  // sysconf
#include <cstring>
#include <cstdlib>
#include <csignal>

#include <cdi.h>

#include "cpp_lib.h"
#include "cdo_timer.h"
#include "cdo_getopt.h"
#include "cdo_settings.h"
#include "cdo_rlimit.h"
#include "mpim_grid.h"
#include "griddes.h"
#include "cdo_default_values.h"
#include "param_conversion.h"
#include "progress.h"
#include "module_info.h"
#include "percentiles.h"
#include "util_wildcards.h"
#include "util_string.h"
#include "process_int.h"
#include "processManager.h"
#include "cdo_options.h"
#include "commandline.h"
#include "mpmo_color.h"
#include "cdo_output.h"
#include "cdo_features.h"
#include "cdo_zaxis.h"
#include "table.h"
#include "datetime.h"
#include "grid_cellsearch.h"
#include "cdo_pthread.h"
#include "institution.h"
#include "parser.h"
#include "factory.h"

static ProcessManager g_processManager;

void
cdo_exit(std::string msg = "")
{
  (void) msg;
  g_processManager.kill_processes();
  exit(EXIT_FAILURE);
}

static int CDO_numThreads = 0;
static int CDO_Rusage = 0;
static bool applyDryRun = false;

#ifdef HIRLAM_EXTENSIONS
extern "C" void streamGrbDefDataScanningMode(int scanmode);
#endif

void set_pointsearch_method(const std::string &methodStr);

static void
cdo_display_syntax_help(const std::string &help, FILE *p_target)
{
  set_text_color(p_target, BRIGHT, BLUE);
  std::string pad = CLIOptions::pad_size_terminal('=');
  fprintf(p_target, "%s", pad.c_str());
  reset_text_color(p_target);
  fprintf(p_target, "%s", help.c_str());
  set_text_color(p_target, BRIGHT, BLUE);
  pad = CLIOptions::pad_size_terminal('=');
  fprintf(p_target, "%s", pad.c_str());
  reset_text_color(p_target);
}

static void
print_category(const std::string &p_category, FILE *p_target)
{
  const auto options = CLIOptions::print_options_help(p_category);
  if (!options.empty())
    {
      const auto pad = CLIOptions::pad_size_terminal('=', p_category);
      fprintf(p_target, "%s", pad.c_str());
      set_text_color(p_target, BLUE);
      fprintf(p_target, "%s", options.c_str());
      reset_text_color(p_target);
    }
}

static void
cdo_usage(FILE *target)
{
  auto pad = CLIOptions::pad_size_terminal('-');
  fprintf(target, "%s", pad.c_str());
  fprintf(target, "  Usage : cdo  [Options]  Operator1  [-Operator2  [-OperatorN]]\n");
  pad = CLIOptions::pad_size_terminal('-');
  fprintf(target, "%s\n", pad.c_str());

  print_category("Info", target);
  print_category("Output", target);
  print_category("Multi Threading", target);
  print_category("Search Methods", target);
  print_category("Format Specific", target);
  print_category("CGRIBEX", target);
  print_category("Numeric", target);
  print_category("History", target);
  print_category("Compression", target);
  print_category("Hirlam Extensions", target);
  print_category("Options", target);
  print_category("Help", target);

  pad = CLIOptions::pad_size_terminal('=', "Environment Variables");
  fprintf(target, "%s\n", pad.c_str());
  set_text_color(target, BLUE);
  fprintf(target, "%s", CLIOptions::print_envvar_help().c_str());
  reset_text_color(target);
  fprintf(target, "\n");
  /*
  pad = CLIOptions::pad_size_terminal('=', "Syntax Features");
  fprintf(target, "%s", pad.c_str());
  fprintf(target, "%s\n", "    Apply");
  fprintf(target, "%s\n", Parser::apply_help.c_str());
  pad = CLIOptions::pad_size_terminal('-');
  fprintf(target, "%s", pad.c_str());
  fprintf(target, "%s\n", "    Subgroups");
  fprintf(target, "%s\n", Parser::subgroup_help.c_str());
  */
  pad = CLIOptions::pad_size_terminal('=');
  fprintf(target, "%s\n", pad.c_str());
  fprintf(target, "    CDO version %s, Copyright (C) 2002-2024 MPI für Meteorologie\n", VERSION);
  fprintf(target, "    This is free software and comes with ABSOLUTELY NO WARRANTY\n");
  fprintf(target, "    Report bugs to <https://mpimet.mpg.de/cdo>\n\n");
  pad = CLIOptions::pad_size_terminal('=');
  fprintf(target, "%s", pad.c_str());
}

static void
cdo_init_is_tty()
{
  struct stat statbuf;
  fstat(0, &statbuf);
  if (S_ISCHR(statbuf.st_mode)) { cdo::stdinIsTerminal = true; }
  fstat(1, &statbuf);
  if (S_ISCHR(statbuf.st_mode)) { cdo::stdoutIsTerminal = true; }
  fstat(2, &statbuf);
  if (S_ISCHR(statbuf.st_mode)) { cdo::stderrIsTerminal = true; }
}

static void
get_env_vars()
{
  CLIOptions::envvar("CDO_TEST")
      ->add_effect([&](const std::string &envstr) { Options::test = parameter_to_bool(envstr); })
      ->describe_argument("true|false")
      ->add_default("false")
      ->add_help("'true' test new features [default: false].");

  CLIOptions::envvar("CDO_CORESIZE")
      ->add_effect([&](const std::string &envstr) { Options::coresize = parameter_to_long(envstr); })
      ->describe_argument("max. core dump size")
      ->add_help("The largest size (in bytes) core file that may be created.");

  CLIOptions::envvar("CDO_DOWNLOAD_PATH")
      ->add_effect([&](const std::string &downloadPath) { DownloadPath = downloadPath; })
      ->describe_argument("path")
      ->add_help("Path where CDO can store downloads.");

  CLIOptions::envvar("CDO_ICON_GRIDS")
      ->add_effect([&](const std::string &iconGrid) { IconGrids = iconGrid; })
      ->describe_argument("path")
      ->add_help("Root directory of the installed ICON grids (e.g. /pool/data/ICON).");

  CLIOptions::envvar("CDO_DISABLE_HISTORY")
      ->add_effect([&](const std::string &envstr) {
        if (parameter_to_bool(envstr) == true)
          {
            Options::CDO_Reset_History = true;
            Options::CDO_Append_History = false;
          }
      })
      ->describe_argument("true|false")
      ->add_help("'true' disables history attribute.");

  CLIOptions::envvar("CDO_RESET_HISTORY")
      ->add_effect([&](const std::string &envstr) { Options::CDO_Reset_History = parameter_to_bool(envstr); })
      ->describe_argument("true|false")
      ->add_default("false")
      ->add_help("'true' resets the global history attribute [default: false].");

  CLIOptions::envvar("CDO_HISTORY_INFO")
      ->add_effect([&](const std::string &envstr) { Options::CDO_Append_History = parameter_to_bool(envstr); })
      ->describe_argument("true|false")
      ->add_default("true")
      ->add_help("'false' don't write information to the global history attribute [default: true].");

  CLIOptions::envvar("CDO_FILE_SUFFIX")
      ->add_effect([&](const std::string &envstr) {
        if (envstr.size()) cdo::FileSuffix = envstr;
      })
      ->describe_argument("suffix")
      ->add_help("Default filename suffix.");

  CLIOptions::envvar("CDO_DISABLE_FILE_SUFFIX")
      ->add_effect([&](const std::string &envstr) {
        if (parameter_to_bool(envstr)) cdo::FileSuffix = "NULL";
      })
      ->describe_argument("true|false")
      ->add_help("'true' disables file suffix.");

  CLIOptions::envvar("CDO_VERSION_INFO")
      ->add_effect([&](const std::string &envstr) { Options::VersionInfo = parameter_to_bool(envstr); })
      ->describe_argument("true|false")
      ->add_default("true")
      ->add_help("'false' disables the global NetCDF attribute CDO [default: true].");
}

static const char *
get_progname(char *string)
{
#ifdef _WIN32
  //  progname = strrchr(string, '\\');
  char *progname = " cdo";
#else
  char *progname = strrchr(string, '/');
#endif

  return (progname == nullptr) ? string : ++progname;
}

#ifdef HAVE_H5DONT_ATEXIT
extern "C" void H5dont_atexit(void);
#endif

static void
print_operator_attributes(const std::string &argument)
{
  ModListOptions local_modListOpt;

  local_modListOpt.parse_request(argument);

  operator_print_list(local_modListOpt);
}

static void
cdo_print_debug_info()
{
  fprintf(stderr, "stdinIsTerminal:   %d\n", cdo::stdinIsTerminal);
  fprintf(stderr, "stdoutIsTerminal:  %d\n", cdo::stdoutIsTerminal);
  fprintf(stderr, "stderrIsTerminal:  %d\n", cdo::stderrIsTerminal);
  cdo::features::print_system_info();
  print_pthread_info();
}

static std::string
predefined_tables(int p_padding)
{
  const char *name;
  constexpr int id_padding = 4;
  int padding = p_padding + id_padding;
  int numTables = tableInqNumber();
  std::string tables{ "Predefined tables: " };
  for (int id = 0; id < numTables; id++)
    {
      if (id % 7 == 6) tables += std::string("\n") + std::string(padding, ' ');
      if ((name = tableInqNamePtr(id))) tables += std::string(name);
      if (id < numTables - 1) tables += ",";
    }
  return tables;
}

static void
create_options_from_envvars()
{
  CLIOptions::option_from_envvar("CDO_VERSION_INFO");
  CLIOptions::option_from_envvar("CDO_DISABLE_FILE_SUFFIX");
  CLIOptions::option_from_envvar("CDO_FILE_SUFFIX");
  CLIOptions::option_from_envvar("CDO_DISABLE_HISTORY")->set_category("History");
  CLIOptions::option_from_envvar("CDO_HISTORY_INFO")->set_category("History");
  CLIOptions::option_from_envvar("CDO_RESET_HISTORY")->set_category("History");
  CLIOptions::option_from_envvar("CDO_DOWNLOAD_PATH");
  CLIOptions::option_from_envvar("CDO_ICON_GRIDS");
  CLIOptions::option_from_envvar("CDO_TEST");
}

static void
setup_cli_options()
{
  CLIOptions::option("envvars")
      ->add_effect([&]() { CLIOptions::print_envvars = true; })
      ->aborts_program(true)
      ->set_category("Info")
      ->add_help("Prints the environment variables of CDO.");

  CLIOptions::option("settings")
      ->add_effect([&]() { CLIOptions::print_settings = true; })
      ->aborts_program(true)
      ->set_category("Info")
      ->add_help("Prints the settings of CDO.");

  CLIOptions::option("debug", "d")
      ->add_effect([&]() {
        unsigned cdoDebugLevel = 0;
        unsigned cdiDebugLevel = 0;
        cdo::parse_debug_arguments({ "1" }, cdoDebugLevel, cdiDebugLevel);

        cdiDebug(cdiDebugLevel);
        cdo::set_debug(cdoDebugLevel);
        cdo::features::version();
      })
      ->set_category("Output")
      ->add_help("Pring all available debug messages");

  CLIOptions::option("scoped_debug", "D")
      ->describe_argument("comma seperated scopes")
      ->set_category("Output")
      ->on_empty_argument([]() {
        std::cerr << "No debug level given please choose: " << std::endl;
        print_debug_options();
        exit(EXIT_SUCCESS);
      })
      ->add_effect([&](const std::string &argument) {
        auto [success, tokens] = tokenize_comma_seperated_int_list(argument);
        if (tokens.empty())
          {
            print_debug_options();
            exit(EXIT_SUCCESS);
          }

        unsigned cdoDebugLevel = 0;
        unsigned cdiDebugLevel = 0;
        cdo::parse_debug_arguments(tokens, cdoDebugLevel, cdiDebugLevel);

        cdiDebug(cdiDebugLevel);
        cdo::set_debug(cdoDebugLevel);

        cdo::features::version();
      })
      ->add_help("Multiple scopes simultaneously possible. Use this option without arguments to get a list of possible scopes");

  CLIOptions::option("worker")
      ->describe_argument("num")
      ->add_effect([&](const std::string &argument) { Options::numStreamWorker = parameter_to_int(argument); })
      ->set_category("Multi Threading")
      ->add_help("Number of worker to decode/decompress GRIB records.");

  CLIOptions::option("precision")
      ->describe_argument("float_digits[,double_digits]")
      ->add_effect([&](const std::string &argument) { cdo::set_digits(argument); })
      ->set_category("Numeric")
      ->add_help("Precision to use in displaying floating-point data (default: 7,15).");

  CLIOptions::option("percentile")
      ->describe_argument("method")
      ->set_category("Numeric")
      ->add_effect([&](const std::string &argument) { percentile_set_method(argument); })
      ->add_help("Methods: nrank, nist, rtype8, <NumPy method (linear|lower|higher|nearest|...)>");

  CLIOptions::option("netcdf_hdr_pad")
      ->describe_argument("nbr")
      ->add_effect([&](const std::string &argument) {
        int netcdf_hdr_pad = parameter_to_bytes(argument);
        if (netcdf_hdr_pad >= 0) cdo::netcdf_hdr_pad = netcdf_hdr_pad;
      })
      ->add_help("Pad NetCDF output header with nbr bytes.");

  CLIOptions::option("use_fftw")
      ->describe_argument("true|false")
      ->add_effect([&](const std::string &argument) { Options::Use_FFTW = (int) parameter_to_bool(argument); })
      ->add_help("Sets fftw usage.");

  CLIOptions::option("config")
      ->describe_argument("all|all-json|<specific_feature_name>")
      ->add_effect([&](const std::string &argument) { cdo::features::print_config(argument); })
      ->on_empty_argument([&]() { cdo::features::print_argument_options(); })
      ->aborts_program(true)
      ->set_category("Info")
      ->add_help("Prints all features and the enabled status.", "Use option <all> to see explicit feature names.");

  CLIOptions::option("pointsearchmethod")
      ->set_internal(true)
      ->describe_argument("<kdtree|nanoflann|spherepart|full>")
      ->set_category("Search Methods")
      ->add_effect([&](const std::string &argument) { set_pointsearch_method(argument); })
      ->add_help("Sets the point search method.");

  CLIOptions::option("gridsearchradius")
      ->describe_argument("degrees[0..180]")
      ->set_category("Search Methods")
      ->add_effect([&](const std::string &argument) {
        auto fval = radius_str_to_deg(argument);
        if (fval < 0 || fval > 180) cdo_abort("%s=%g out of bounds (0-180 deg)!", "gridsearchradius", fval);
        cdo_set_search_radius(fval);
      })
      ->add_help("Sets the grid search radius (0-180 deg).");

  CLIOptions::option("remap_weights")
      ->describe_argument("0|1")
      ->add_effect([&](const std::string &argument) {
        auto intarg = parameter_to_int(argument);
        if (intarg != 0 && intarg != 1) cdo_abort("Unsupported value for option --remap_weights %d [0/1]", intarg);
        Options::REMAP_genweights = intarg;
      })
      ->add_help("Generate remap weights (default: 1).");

  CLIOptions::option("no_remap_weights")
      ->add_effect([&]() { Options::REMAP_genweights = 0; })
      ->add_help("Switch off generation of remap weights.");

  CLIOptions::option("enableexcept")
      ->describe_argument("except")
      ->set_category("Numeric")
      ->add_effect([&](const std::string &argument) {
        auto except = cdo::evaluate_except_options(argument);
        if (except < 0) cdo_abort("option --%s: unsupported argument: %s", "enableexcept", argument);
        cdo::set_feenableexcept(except);
        if (signal(SIGFPE, cdo::signal_handler) == SIG_ERR) cdo_warning("can't catch SIGFPE!");
      })
      ->add_help("Set individual floating-point traps ", "(DIVBYZERO, INEXACT, INVALID, OVERFLOW, UNDERFLOW, ALL_EXCEPT)");

  CLIOptions::option("timestat_date")
      ->describe_argument("srcdate")
      ->add_effect([&](const std::string &argument) { set_timestat_date(argument); })
      ->add_help("Target timestamp (temporal statistics): ", "first, middle, midhigh or last source timestep.");

  CLIOptions::option("ignore_time_bounds")
      ->add_effect([&]() {
        extern bool CDO_Ignore_Time_Bounds;
        CDO_Ignore_Time_Bounds = true;
      })
      ->add_help("Ignores time bounds for time range statistics.");

  CLIOptions::option("use_time_bounds")
      ->add_effect([&]() {
        extern bool CDO_Use_Time_Bounds;
        CDO_Use_Time_Bounds = true;
      })
      ->add_help("Enables use of timebounds.");

  CLIOptions::option("cmor")->add_effect([&]() { Options::CMOR_Mode = 1; })->add_help("CMOR conform NetCDF output.");

  CLIOptions::option("reduce_dim")->add_effect([&]() { Options::CDO_Reduce_Dim = 1; })->add_help("Reduce NetCDF dimensions.");

  CLIOptions::option("float")
      ->add_effect([&]() { Options::CDO_Memtype = MemType::Float; })
      ->set_category("Numeric")
      ->add_help("Using single precision floats for data in memory.");

  CLIOptions::option("single")
      ->add_effect([&]() { Options::CDO_Memtype = MemType::Float; })
      ->set_category("Numeric")
      ->add_help("Using single precision floats for data in memory.");

  CLIOptions::option("double")
      ->add_effect([&]() { Options::CDO_Memtype = MemType::Double; })
      ->set_category("Numeric")
      ->add_help("Using double precision floats for data in memory.");

  CLIOptions::option("rusage")
      ->add_effect([&]() { CDO_Rusage = 1; })
      ->add_help("Print information about resource utilization.")
      ->set_category("Info");

  CLIOptions::option("attribs")
      ->describe_argument("arbitrary|filesOnly|onlyFirst|noOutput|obase")
      ->aborts_program(true)
      ->set_category("Info")
      ->add_effect([&](const std::string &argument) { print_operator_attributes(argument); })
      ->add_help("Lists all operators with choosen features or the attributes of given operator(s)",
                 "operator name or a combination of [arbitrary,filesOnly,onlyFirst,noOutput,obase].");

  CLIOptions::option("operators")
      ->aborts_program(true)
      ->add_effect([&]() { print_operator_attributes(std::string()); })
      ->set_category("Info")
      ->add_help("Prints list of operators.");

  CLIOptions::option("module_info")
      ->aborts_program(true)
      ->describe_argument("module name")
      ->set_category("Info")
      ->add_effect([&](const std::string &argument) {
        auto names = Factory::get_module_operator_names(argument);
        if (names.empty())
          {
            std::string errstr = "Module " + argument + " not found\n";
            std::cerr << errstr;
          }
        else
          {
            std::string info_string = "\n" + argument + ":\n";
            for (const auto &name : names) { info_string += std::string(4, ' ') + name + "\n"; }
            std::cerr << info_string + "\n";
          }
      })
      ->add_help("Prints list of operators.");

  CLIOptions::option("operators_no_output")
      ->aborts_program(true)
      ->add_effect([&]() { print_operator_attributes("noOutput"); })
      ->set_category("Info")
      ->add_help("Prints all operators which produce no output.");

  CLIOptions::option("pedantic")->add_effect([&]() { MpMO::enable_pedantic(true); })->add_help("Warnings count as errors.");

  CLIOptions::option("color", "C")
      ->describe_argument("auto|no|all")
      ->add_effect([&](const std::string &argument) { cdo::evaluate_color_options(argument); })
      ->set_category("Output")
      ->add_help("Set behaviour of colorized output messages.");

  CLIOptions::option("eccodes")
      ->add_effect([&]() { cdiDefGlobal("ECCODES_GRIB1", true); })
      ->set_category("Format Specific")
      ->add_help("Use ecCodes to decode/encode GRIB1 messages.");

  CLIOptions::option("format", "f")
      ->describe_argument("grb1|grb2|nc1|nc2|nc4|nc4c|nc5|nczarr|srv|ext|ieg")
      ->add_effect([&](const std::string &argument) { cdo::set_default_filetype(argument); })
      ->add_help("Format of the output file.");

  CLIOptions::option("help", "h")
      ->describe_argument("operator")
      ->add_effect([&](const std::string &operator_name) { cdo_print_help(operator_name); })
      ->on_empty_argument([]() { cdo_usage(stdout); })
      ->aborts_program(true)
      ->set_category("Help")
      ->add_help("Shows either help information for the given operator or the usage of CDO.");

  CLIOptions::option("history")
      ->add_effect([&]() { Options::CDO_Append_History = true; })
      ->set_category("History")
      ->add_help("Do append to NetCDF \"history\" global attribute.");

  CLIOptions::option("no_history")
      ->add_effect([&]() { Options::CDO_Append_History = false; })
      ->set_category("History")
      ->add_help("Do not append to NetCDF \"history\" global attribute.");

  CLIOptions::option("version", "V")
      ->add_effect([&]() { cdo::features::version(); })
      ->aborts_program(true)
      ->set_category("Info")
      ->add_help("Print the version number.");

  CLIOptions::option("dryrun", "A")->add_effect([&]() { applyDryRun = true; })->add_help("Dry run that shows processed CDO call.");

  CLIOptions::option("absolute_taxis", "a")
      ->add_effect([&]() {
        if (CdoDefault::TaxisType == TAXIS_RELATIVE)
          cdo_abort("option --%s: can't be combined with option --%s", "absolute_taxis (-a)", "relative_taxis (-r)");
        CdoDefault::TaxisType = TAXIS_ABSOLUTE;
      })
      ->add_help("Generate an absolute time axis.");

  CLIOptions::option("force")->add_effect([&]() { Options::force = true; })->add_help("Forcing a CDO process.");

  CLIOptions::option("fast")
      ->set_internal(true)
      ->add_effect([&]() { Options::fast = true; })
      ->add_help("If available, use a faster method even if it requires more memory.");

  // clang-format off
  CLIOptions::option("default_datatype", "b")
      ->describe_argument("nbits")
      ->set_category("Numeric")
      ->add_effect([&](const std::string &argument) { cdo::set_default_datatype(argument); })
      ->add_help("Set the number of bits for the output precision",
                 "    I8|I16|I32|F32|F64     for nc1,nc2,nc4,nc4c,nc5,nczarr;",
                 "    U8|U16|U32             for nc4,nc4c,nc5;",
                 "    F32|F64                for grb2,srv,ext,ieg;",
                 "    P1 - P24               for grb1,grb2");
  // clang-format on

  CLIOptions::option("check_data_range", "c")
      ->add_effect([&]() { Options::CheckDatarange = true; })
      ->add_help("Enables checks for data overflow.");

  CLIOptions::option("grid", "g")
      ->describe_argument("grid")
      ->add_effect([&](const std::string &argument) { cdo_set_grids(argument); })
      ->add_help("Set default grid name or file. Available grids: ",
                 "F<XXX>, t<RES>, tl<RES>, r<NX>x<NY>, global_<DXY>, zonal_<DY>, gme<NI>, lon=<LON>/lat=<LAT>, hpz<ZOOM>");

  CLIOptions::option("institution", "i")
      ->describe_argument("institute_name")
      ->add_effect([&](const std::string &argument) { define_institution(argument); })
      ->add_help("Sets institution name.");

  CLIOptions::option("chunktype", "k")
      ->describe_argument("auto|grid|lines")

      ->set_category("Format Specific")
      ->add_effect([&](const std::string &argument) { cdo::set_chunktype(argument); })
      ->add_help("NetCDF4 chunk type: auto, grid or lines.");

  CLIOptions::option("chunksize")
      ->describe_argument("size")
      ->set_category("Format Specific")
      ->add_effect([&](const std::string &argument) {
        int chunkSize = parameter_to_bytes(argument);
        if (chunkSize >= 0) Options::cdoChunkSize = chunkSize;
      })
      ->add_help("NetCDF4 chunk size.");

  CLIOptions::option("lock_io", "L")->add_effect([&]() { Threading::cdoLockIO = true; })->add_help("Lock IO (sequential access).");

  CLIOptions::option("zaxis", "l")
      ->describe_argument("zaxis")
      ->add_effect([&](const std::string &argument) { cdo_set_zaxes(argument); })
      ->add_help("Set default zaxis name or file.");

  CLIOptions::option("set_missval", "m")
      ->describe_argument("missval")
      ->add_effect([&](const std::string &argument) { cdiDefMissval(std::stof(argument)); })
      ->add_help("Set the missing value of non NetCDF files (default: " + get_scientific(cdiInqMissval()) + ").");

  CLIOptions::option("has_missval", "M")
      ->add_effect([&]() { cdiDefGlobal("HAVE_MISSVAL", true); })
      ->add_help("Set HAS_MISSVAL to true.");

  CLIOptions::option("varnames", "n")
      ->set_internal(true)
      ->describe_argument("<varname| file>")
      ->add_effect([&](const std::string &argument) { Options::cdoVarnames = split_string(argument, ","); })
      ->add_help("Set default varnames or file.");

  CLIOptions::option("overwrite", "O")
      ->add_effect([&]() { Options::cdoOverwriteMode = true; })
      ->add_help("Overwrite existing output file, if checked.");

  CLIOptions::option("num_threads", "P")
      ->describe_argument("nthreads")
      ->add_effect([&](const std::string &argument) { CDO_numThreads = parameter_to_int(argument); })
      ->set_category("Multi Threading")
      ->add_help("Set number of OpenMP threads.");

  CLIOptions::option("parrallel_read", "p")
      ->set_internal(true)
      ->add_effect([&]() {
        Options::CDO_Parallel_Read = true;
        Options::CDO_task = true;
      })
      ->set_category("Multi Threading")
      ->add_help("Enables parallel read.");

  CLIOptions::option("sortname", "Q")
      ->add_effect([&]() { cdiDefGlobal("SORTNAME", true); })
      ->set_category("Format Specific")
      ->add_help("Alphanumeric sorting of NetCDF parameter names.");

  CLIOptions::option("seed")
      ->describe_argument("seed")
      ->set_category("Numeric")
      ->add_effect([&](const std::string &argument) {
        int intarg = parameter_to_int(argument);
        if (intarg < 0) cdo_abort("Unsupported value for option --seed %d [>=0]", intarg);
        Options::Random_Seed = intarg;
      })
      ->add_help("Seed for a new sequence of pseudo-random numbers. <seed> must be >= 0");

  CLIOptions::option("regular", "R")
      ->add_effect([&]() {
        Options::cdoRegulargrid = true;
        cdiDefGlobal("REGULARGRID", true);
      })
      ->set_category("CGRIBEX")
      ->add_help("Convert GRIB1 data from global reduced to regular Gaussian grid (cgribex only).");

  CLIOptions::option("relative_taxis", "r")
      ->add_effect([&]() {
        if (CdoDefault::TaxisType == TAXIS_ABSOLUTE)
          cdo_abort("option --%s: can't be combined with option --%s", "relative_taxis (-r)", "absolute_taxis (-a)");
        CdoDefault::TaxisType = TAXIS_RELATIVE;
      })
      ->add_help("Generate a relative time axis.");

  CLIOptions::option("cdo_diagnostic", "S")
      ->add_effect([&]() { Options::cdoDiag = true; })
      ->add_help("Create an extra output stream for the module TIMSTAT. This stream",
                 "contains the number of non missing values for each output period.");

  CLIOptions::option("silent", "s")
      ->add_effect([&]() {
        Options::silentMode = true;
        MpMO::enable_silent_mode(Options::silentMode);
      })
      ->set_category("Output")
      ->add_help("Silent mode.");

  CLIOptions::option("timer", "T")->add_effect([&]() { Options::Timer = true; })->add_help("Enable timer.");

  CLIOptions::option("table", "t")
      ->describe_argument("codetab")
      ->set_category("CGRIBEX")
      ->add_effect([&](const std::string &argument) { CdoDefault::TableID = cdo::define_table(argument); })
      ->add_help("Set GRIB1 default parameter code table name or file (cgribex only).", predefined_tables(CLIOptions::padding));

  CLIOptions::option("interactive", "u")
      ->add_effect([&]() { Options::cdoInteractive = true; })
      ->add_help("Enable CDO interactive mode.");

  CLIOptions::option("verbose", "v")
      ->add_effect([&]() {
        Options::cdoVerbose = true;
        MpMO::enable_verbose(true);
        CLIOptions::print_envvars = true;
        gridEnableVerbose(Options::cdoVerbose);
      })
      ->add_help("Print extra details for some operators.");

  CLIOptions::option("disable_warnings", "w")
      ->add_effect([&]() {  // disable warning messages
        MpMO::enable_warnings(false);
        extern int _Verbose;  // CDI Warnings
        _Verbose = 0;
      })
      ->set_category("Output")
      ->add_help("Disable warning messages.");

  CLIOptions::option("par_io", "X")
      ->set_internal(true)
      ->add_effect([&]() {
        Options::cdoParIO = true;  // multi threaded I/O
      })
      ->add_help("Enables multithreaded I/O.")
      ->set_category("Multi Threading");

  CLIOptions::option("shuffle")
      ->add_effect([&]() { Options::cdoShuffle = true; })
      ->set_category("Compression")
      ->add_help("Specify shuffling of variable data bytes before compression (NetCDF)");

  CLIOptions::option("compress", "Z")
      ->add_effect([&]() { Options::cdoCompress = true; })
      ->set_category("Compression")
      ->add_help("Enables compression. Default = SZIP");

  CLIOptions::option("filter", "F")
      ->describe_argument("filterspec")
      ->add_effect([&](const std::string &argument) { cdo::set_filterspec(argument); })
      ->set_category("Compression")
      ->add_help("NetCDF4 filter specification");

  CLIOptions::option("compression_type", "z")
      ->describe_argument("aec|jpeg|zip[_1-9]|zstd[1-19]")
      ->set_category("Compression")
      ->add_effect([&](const std::string &argument) { cdo::set_compression_type(argument); })
      ->add_help("aec         AEC compression of GRIB2 records", "jpeg        JPEG compression of GRIB2 records",
                 "zip[_1-9]   Deflate compression of NetCDF4 variables", "zstd[_1-19] Zstandard compression of NetCDF4 variables");

  CLIOptions::option("nsb")
      ->set_internal(true)
      ->describe_argument("1-23")
      ->add_effect([&](const std::string &argument) { Options::nsb = parameter_to_int(argument); })
      ->set_category("Numeric")
      ->add_help("Number of significant bits used for bit-rounding.");

  CLIOptions::option("show_available_options")
      ->set_internal(true)
      ->aborts_program(true)
      ->set_category("Info")
      ->add_effect([&]() { CLIOptions::print_available_options(); })
      ->add_help("Shows all available optins and prints all shortforms, only internal use for testing.");

  CLIOptions::option("argument_groups")
      ->aborts_program(true)
      ->add_help("Explanation and Examples for subgrouping operators with [ ] syntax")
      ->add_effect([&]() { cdo_display_syntax_help(Parser::subgroup_help, stderr); })
      ->set_category("Help");

  CLIOptions::option("apply")
      ->aborts_program(true)
      ->add_help("Explanation and Examples for -apply syntax")
      ->add_effect([&]() { cdo_display_syntax_help(Parser::apply_help, stderr); })
      ->set_category("Help");

  CLIOptions::option("sortparam")->add_effect([]() { cdiDefGlobal("SORTPARAM", true); });

#ifdef HIRLAM_EXTENSIONS
  CLIOptions::option("Dkext")
      ->describe_argument("debLev")
      ->set_category("Hirlam Extension")
      ->add_effect([&](const std::string &argument) {
        auto extDebugVal = parameter_to_int(argument);
        if (extDebugVal > 0)
          {
            extern int cdiDebugExt;
            cdoDebugExt = extDebugVal;
            cdiDebugExt = extDebugVal;
          }
      })
      ->add_help("Setting debugLevel for extensions.");

  CLIOptions::option("outputGribDataScanningMode")
      ->describe_argument("mode")
      ->set_category("Hirlam Extension")
      ->add_effect([&](const std::string &argument) {
        auto scanningModeValue = parameter_to_int(argument);
        if (cdoDebugExt) printf("scanningModeValue=%d\n", scanningModeValue);

        if ((scanningModeValue == 0) || (scanningModeValue == 64) || (scanningModeValue == 96))
          {
            streamGrbDefDataScanningMode(scanningModeValue);  // -1: not used; allowed modes: <0,
                                                              // 64, 96>; Default is 64
          }
        else
          {
            cdo_warning("Warning: %d not in allowed modes: <0, 64, 96>; Using default: 64\n", scanningModeValue);
            streamGrbDefDataScanningMode(64);
          }
      })
      ->add_help("Setting grib scanning mode for data in output file <0, 64, 96>.", "Default is 64");
#endif  // HIRLAM_EXTENSIONS
}

static void
timer_report(std::vector<cdo::iTimer *> &timers)
{
  FILE *fp = stdout;
  fprintf(fp, "\nTimer report:  shift = %g\n", cdo::timerShift);
  fprintf(fp, "    Name   Calls          Min      Average          Max        Total\n");

  for (auto &timer : timers)
    {
      auto total = timer->elapsed();
      auto avg = timer->sum;
      if (timer->calls > 0) avg /= timer->calls;

      // if (timer.stat != rt_stat_undef)
      fprintf(fp, "%8s %7d %12.4g %12.4g %12.4g %12.4g\n", timer->name.c_str(), timer->calls, timer->min, avg, timer->max, total);
    }
}

int
main(int argc, char *argv[])
{
  cdo::set_exit_function(cdo_exit);
  cdo::set_context_function(process_inq_prompt);
  progress::set_context_function(process_inq_prompt);

  mpmo_color_set(Auto);

  cdo_init_is_tty();

  Options::CDO_Reduce_Dim = 0;

  // mallopt(M_MMAP_MAX, 0);

  cdo::set_command_line(argc, argv);

  cdo::progname = get_progname(argv[0]);

  get_env_vars();
  create_options_from_envvars();
  CLIOptions::get_env_vars();

  setup_cli_options();

  auto CDO_optind = CLIOptions::parse(std::vector<std::string>(argv, argv + argc));

  if (CDO_optind == CLIOptions::ABORT_REQUESTED)
    exit(EXIT_FAILURE);
  else if (CDO_optind == CLIOptions::EXIT_REQUESTED)
    exit(EXIT_SUCCESS);

  if (CDO_optind >= argc)
    {
      cdo_usage(stderr);
      fprintf(stderr, "\nNo operator given!\n\n");
      exit(EXIT_FAILURE);
    }
  else
    {
      cdo::set_cdi_options();
      cdo::set_external_proj_func();
      cdo::set_stacksize(67108864);  // 64MB
      cdo::set_coresize(Options::coresize);
      cdo::setup_openMP(CDO_numThreads);

      if (cdo::dbg()) cdo_print_debug_info();

      std::vector<std::string> new_argv(&argv[CDO_optind], argv + argc);

      new_argv = expand_wild_cards(new_argv);

      if (CdoDefault::TableID != CDI_UNDEFID) cdo_def_table_id(CdoDefault::TableID);

#ifdef HAVE_H5DONT_ATEXIT
      H5dont_atexit();  // don't call H5close on exit
#endif
#ifdef CUSTOM_MODULES
      load_custom_modules("custom_modules");
      close_library_handles();
#endif

      auto processStructure = Parser::parse(new_argv, process_inq_prompt);
      if (applyDryRun == true)
        {
          std::cerr << processStructure[0]->to_string() << std::endl;
          exit(applyDryRun ? 0 : -1);
        }

      std::vector<cdo::iTimer *> allTimers;
      auto totalTimer = cdo::iTimer("total");
      cdo::readTimer = cdo::iTimer("read");
      cdo::writeTimer = cdo::iTimer("write");
      allTimers.push_back(&totalTimer);
      allTimers.push_back(&cdo::readTimer);
      allTimers.push_back(&cdo::writeTimer);

      g_processManager.buildProcessTree(processStructure);
      totalTimer.start();
      g_processManager.run_processes();
      totalTimer.stop();
      g_processManager.clear_processes();

      if (Options::Timer) timer_report(allTimers);
    }

  if (CDO_Rusage) cdo::features::print_rusage();

  return Options::cdoExitStatus;
}
