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

  Author: Uwe Schulzweida

*/

#include "cdo_getopt.h"
#include "util_string.h"

#include <cstdio>
#include <sstream>
#include <cstdlib>

#include <vector>
#include <map>

#define CLIOP_DBG false

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>

int
get_width()
{
  int terminal_width = 120;
  struct stat statbuf;
  fstat(2, &statbuf);
  if (S_ISCHR(statbuf.st_mode))
    {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
      CONSOLE_SCREEN_BUFFER_INFO csbi;
      int columns, rows;

      GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
      columns = csbi.srWindow.Right - csbi.srWindow.Left + 1;
#elif __APPLE__
#include <TargetConditionals.h>
#if TARGET_OS_MACCATALYST
      // Mac's Catalyst (ports iOS API into Mac, like UIKit).
#elif TARGET_OS_MAC
      // Other kinds of Apple platforms
#else
#error "Unknown Apple platform"
#endif
#elif __linux__ || __unix__ || defined(_POSIX_VERSION)
      struct winsize w;
      ioctl(STDERR_FILENO, TIOCGWINSZ, &w);
      terminal_width = w.ws_col;
#endif
    }
  return terminal_width;
}

std::string
CLIOptions::pad_size_terminal(char padWith, const std::string &sourround)
{
  int terminal_width = get_width();

  std::string line;
  if (sourround.empty()) { line = std::string(terminal_width, padWith); }
  else
    {
      int front_pad = 3;
      int num_spaces = 2;
      int space_left = terminal_width - front_pad - num_spaces - sourround.size();
      if (space_left > terminal_width) { space_left = 1; }
      if (space_left < 0) { space_left = 0; }

      line = std::string(front_pad, padWith);
      line += " " + sourround + " ";
      line += std::string(space_left, padWith);
    }
  return line + "\n";
}

std::vector<std::shared_ptr<cdo_option_2>> CLIOptions::optionRegistry;
std::map<std::string, std::shared_ptr<cdo_option_2>> CLIOptions::optionMap;

std::vector<std::shared_ptr<cdo_option_2>> CLIOptions::envvarRegistry;
std::map<std::string, std::shared_ptr<cdo_option_2>> CLIOptions::envvarMap;

const int CLIOptions::EXIT_REQUESTED = -2;
const int CLIOptions::ABORT_REQUESTED = -1;
bool CLIOptions::print_settings = false;
bool CLIOptions::print_envvars = false;
const int CLIOptions::padding = 46;

int
CLIOptions::parse(std::vector<std::string> p_argv)
{
  int retval = p_argv.size();
  for (size_t i = 1, n = p_argv.size(); i < n; ++i)
    {
      Debug(CLIOP_DBG, "Checking: %s", p_argv[i]);
      const std::map<std::string, std::shared_ptr<cdo_option_2>>::iterator &it = optionMap.find(p_argv[i]);
      if (it == optionMap.end())
        {
          std::string arg = p_argv[i];
          bool isLongFrom = arg.size() >= 2 && arg[0] == '-' && arg[1] == '-';
          bool isShortForm = arg.size() == 2 && arg[0] == '-';
          if (isLongFrom || isShortForm) { cdo_abort("Option %s not found", p_argv[i]); }
          retval = i;
          break;
        }
      if (it->second->hasArgument)
        {
          auto &argument = it->second->argument;
          i++;

          bool out_of_bounds = i >= p_argv.size();

          auto is_new_option = [&]() {
            /*
             * extracted into labmda for a balance of readability and preventing the seqfault that is caused
             * by checking for this when we are out of bounds. By delaying execution we prevent the seqfault
             */
            bool is_not_negative_number = !std::get<0>(tokenize_comma_seperated_int_list(p_argv[i]));
            bool has_minus_in_first_pos = (p_argv[i][0] == '-');
            return has_minus_in_first_pos && is_not_negative_number;
          };

          if (out_of_bounds || is_new_option())
            {
              if (argument.default_value.size() > 0) { argument.value = argument.default_value; }
              else if (!argument.is_optional)
                {
                  argument.on_empty_argument(p_argv[i - 1]);
                  retval = EXIT_REQUESTED;
                  break;
                }
            }
          else { argument.value = p_argv[i]; }
        }

      Debug(CLIOP_DBG, "executing option %s", (*it).first);
      it->second->execute();

      if (it->second->abortOnExecute)
        {
          retval = EXIT_REQUESTED;
          break;
        }
      if (i >= p_argv.size() - 1)
        {
          // TODO: missing err msg
          retval = ABORT_REQUESTED;
          break;
        }
    }
  if (print_settings) print_registry(optionRegistry);
  if (print_envvars) print_registry(envvarRegistry);

  return retval;
}

void
CLIOptions::get_env_vars()
{
  for (auto &setting_ptr : envvarRegistry)
    {
      const char *envVarValue = getenv(setting_ptr->name.c_str());
      if (envVarValue)
        {
          if (!setting_ptr->hasArgument || *envVarValue)
            {
              Debug(CLIOP_DBG, "Executing envvar %s", setting_ptr->name);
              setting_ptr->argument.value = envVarValue ? std::string(envVarValue) : std::string();
              setting_ptr->execute();
            }
        }
    }
}

void
CLIOptions::print_registry(const std::vector<std::shared_ptr<cdo_option_2>> &p_registry)
{
  for (const auto &it : p_registry)
    {
      if (it->argument.value.size() > 0) fprintf(stderr, "%s = %s\n", it->name.c_str(), it->argument.value.c_str());
    }
}

std::shared_ptr<cdo_option_2> &
CLIOptions::envvar(const std::string &p_name)
{
  if (envvarMap.find(p_name) == envvarMap.end())
    {
      envvarRegistry.push_back(std::make_shared<cdo_option_2>());
      const auto &newEnvVar = envvarRegistry.back();
      envvarMap[p_name] = newEnvVar;
      newEnvVar->name = p_name;
    }
  else { cdo_abort("Environment Variable already registered!"); }
  return envvarRegistry.back();
}

std::shared_ptr<cdo_option_2> &
CLIOptions::option(const std::string &p_name, const std::string &p_shortForm)
{
  std::string name = "--" + p_name;
  std::string shortForm = p_shortForm;
  Debug(CLIOP_DBG, "registering key: %s", name);
  if (optionMap.find(name) != optionMap.end()) { cdo_abort("option name already exists: %s", name); }

  optionRegistry.push_back(std::make_shared<cdo_option_2>());
  optionMap[name] = optionRegistry.back();
  optionMap[name]->name = name;

  if (!shortForm.empty())
    {
      if (optionMap.find(shortForm) == optionMap.end())
        {
          shortForm = "-" + p_shortForm;
          optionMap[shortForm] = optionRegistry.back();
          optionMap[name]->shortName = shortForm;
        }
      else { cdo_abort("Option short form already exists: %s", shortForm); }
    }
  return optionRegistry.back();
}

std::string
CLIOptions::print_envvar_help()
{
  std::stringstream helpString;
  for (const auto &it : envvarMap)
    {
      if (!it.second->isInternal)
        {
          auto len0 = helpString.str().size();
          helpString << std::string(4, ' ');
          helpString << it.second->name;
          if (it.second->hasArgument) helpString << " <" + it.second->argument.description + "> ";
          int spaceLeft = padding - helpString.str().size() + len0;
          if (spaceLeft < 0) spaceLeft = 0;
          for (const auto &line : it.second->description) { helpString << std::string(spaceLeft, ' ') + line + "\n"; }
        }
    }
  return helpString.str();
}

std::string
CLIOptions::print_options_help(const std::string &p_category)
{
  std::stringstream help;
  for (auto &iter : optionMap)
    {
      if (iter.first.size() != 2 && !iter.second->isInternal && iter.second->category == p_category)
        {
          auto option = iter.second;
          std::string helpString = "    ";
          if (!option->shortName.empty()) { helpString += option->shortName + ", "; }
          else { helpString += "    "; }
          helpString += option->name + " ";
          if (option->hasArgument) helpString += " <" + option->argument.description + "> ";
          if (option->hasArgument && option->argument.description.empty())
            {
              std::cerr << "error: help argument of " << option->name << " has no description!" << std::endl;
              exit(0);
            }
          int spaceLeft = padding - helpString.size();
          if (spaceLeft <= 0)
            {
              helpString += "\n";
              spaceLeft = padding;
            }
          for (const auto &line : option->description)
            {
              helpString += std::string(spaceLeft, ' ') + line + "\n";
              spaceLeft = padding;
            }
          if (option->description.empty()) helpString += "\n";
          help << helpString;
        }
    }
  return help.str();
}

#include <algorithm>

cdo_option_2 *
CLIOptions::option_from_envvar(const std::string &p_envvarName)
{
  if (envvarMap.find(p_envvarName) == envvarMap.end()) { cdo_abort("Error envvar %s does not exist", p_envvarName); }
  std::string ENVVAR_SUFFIX = "CDO_";
  std::string optionName = p_envvarName.substr(ENVVAR_SUFFIX.size(), p_envvarName.size());

  std::ranges::transform(optionName, optionName.begin(), ::tolower);

  optionName = "--" + optionName;

  if (optionMap.find(optionName) != optionMap.end())
    {
      cdo_abort("Error autogenerated name %s for envvar option %s does already exist!", optionName, p_envvarName);
    }
  optionRegistry.push_back(std::make_shared<cdo_option_2>());
  auto &newOption = optionRegistry.back();
  auto &envVar = envvarMap[p_envvarName];
  newOption->name = optionName;
  newOption->hasArgument = envVar->hasArgument;
  newOption->description = envVar->description;
  newOption->argument.description = envVar->argument.description;
  newOption->description = { "This option is generated from " + p_envvarName + " and will overwrite it.",
                             "See help of corresponding environment variable." };
  newOption->effect = envVar->effect;
  newOption->isInternal = envVar->isInternal;
  newOption->argument.default_value = envVar->argument.default_value;
  optionMap[optionName] = newOption;
  return newOption.get();
}

void
CLIOptions::print_available_options()
{
  for (const auto &iter : optionMap) { std::cerr << iter.first << std::endl; }
  std::cerr << "_---------------------------------_" << std::endl;
  for (auto &iter : optionMap)
    {
      if (iter.first.size() == 2) std::cerr << iter.second->shortName[1] << (iter.second->hasArgument ? ":" : "");
    }
}
