/*
Autor: Paweł Wysocki
Opis:
Program demonstruje możliwości biblioteki Boost.Program_Options do przetwarzania
opcji konfiguracyjnych z linii poleceń oraz plików konfiguracyjnych w języku
C++. Przykład zawiera definiowanie różnych rodzajów opcji (zwykłych,
pozycyjnych, ukrytych), parsowanie opcji z różnych źródeł (cli, plik
konfiguracyjny), oraz użycie niestandardowych walidatorów i parserów.

Zastosowano mechanizm walidacji typu własnego oraz własny parser flag w
stylu `-fno-...`, co rozszerza standardowe możliwości biblioteki Boost.

Skrócony przewodnik użycia programu:
1. Uruchamianie programu:
   - Program akceptuje wiele opcji, np. `--config-file`, `--verbose`, `--speed`,
     `--input-words`.
   - Argumenty pozycyjne są traktowane jako `--input-words`.

2. Obsługa pliku konfiguracyjnego:
   - Opcje zdefiniowane jako część `config-file` mogą być wczytane z pliku
`.cfg`, np. `compiler.optimization.level`.

3. Własny typ `CustomClass`:
   - Opcja `--obj` przyjmuje wartość w formacie `int,string`, która jest
walidowana i konwertowana do struktury.

4. Niestandardowy parser flag:
   - Flagi w stylu `-fturbo-optimization` lub `-fno-turbo-optimization` są
automatycznie mapowane na wartości logiczne.

Materiały i odnośniki:
Boost.Program_Options -
https://www.boost.org/doc/libs/release/doc/html/program_options.html

Kompilacja: g++ boostpo_showcase.cpp -o po_showcase -lboost_program_options

*/

#include <boost/program_options.hpp>
#include <fstream>
#include <iostream>

// Skrót dla przestrzeni nazw Boost.ProgramOptions
namespace po = boost::program_options;

/* ============ Własny walidator dla typu złożonego =============
 *  Biblioteka Boost.ProgramOptions pozwala na tworzenie własnych walidatorów
 * dla opcji. Wymaganiem jest, żeby zarówno obiekt, jak i walidator były
 * zdefiniowane w tej samej przestrzeni nazw.
 */
namespace customNamespace {
// W tym przykładzie definiujemy klasę CustomClass, która będzie używana jako
// typ opcji.
class CustomClass {
 public:
  int a;          // Pierwszy element to liczba całkowita
  std::string b;  // Drugi element to ciąg znaków
};
// Funkcja walidująca, która sprawdza poprawność formatu opcji.
void validate(
    boost::any &v,  // Parametry wyjściowy, dowolnego typu (tutaj CustomClass)
    const std::vector<std::string>
        &values,    // Lista wartości przkazanych opcji
                    // (zazwyczaj tylko jedna wartość, ale jest wektorem,
                    // ponieważ opcja może być użyta wielokrotnie)
    CustomClass *,  // Wskaźnik do typu, który walidujemy
                    // (używany przez Boost.ProgramOptions do rozróżnienia
                    // przeciążonych funkcji walidujących)
    int) {  // Indeks opcji, która jest walidowana (zazwyczaj ignorowany przez
            // użytkownika)

  // Sprawdzenie, czy opcja została już wcześniej zdefiniowana
  po::validators::check_first_occurrence(v);
  // Pobranie pojedynczej wartości z wektora wartości
  const std::string &s = po::validators::get_single_string(values);
  std::istringstream iss(s);
  std::string token;
  CustomClass result;

  // Oczekujemy formatu: "liczba,ciąg"

  // Pobranie pierwszej części (liczba całkowita)
  if (std::getline(iss, token, ',')) {
    result.a = boost::lexical_cast<int>(token);
  } else {  // Boost.ProgramOptions posiada wygodne typy wyjątków, które można
            // wykorzystać do zgłaszania błędów walidacji
    throw po::validation_error(po::validation_error::invalid_option_value,
                               "obj", s);
  }

  // Pobranie drugiej części (ciąg znaków)
  if (std::getline(iss, token)) {
    result.b = token;
  } else {
    throw po::validation_error(po::validation_error::invalid_option_value,
                               "obj", s);
  }

  // Ustawienie wyniku walidacji w boost::any
  v = boost::any(result);
}
}  // namespace customNamespace

/* ============ Własny parser flag (np. -fturbo-optimization) ============
 *  Boost pozwala na tworzenie własnych parserów flag, które mogą być używane do
 * przetwarzania niestandardowych opcji. W tym przypadku parser będzie
 * rozpoznawał flagi w formacie -f[no-]turbo-optimization. Jeśli flaga jest
 * rozpoznana, zwróci parę (nazwa flagi, wartość logiczna).
 */
std::pair<std::string, std::string> custom_flag_parser(const std::string &s) {
  // Definiujemy dozwolone flagi, które będą rozpoznawane przez parser.
  // Pozostałe flagi będą ignorowane.
  static const std::set<std::string> allowed_flags = {"turbo-optimization"};
  // Sprawdzamy, czy flaga zaczyna się od -f zgodnie z konwencją
  if (s.find("-f") == 0) {  
    // Określamy wartość logiczną na podstawie obecności "no-"
    bool is_negated =s.substr(2, 3) =="no-";  
    std::string flag = is_negated ? s.substr(5) : s.substr(2);
    if (allowed_flags.count(flag)) {  // Jeśli flaga jest dozwolona, zwracamy ją
                                      // wraz z wartością logiczną
      return {flag, is_negated ? "false" : "true"};
    }
  }  // Jeśli flaga nie jest dozwolona lub zgodna z konwencją, zwracamy pustą parę
  return {};
}

int main(int argc, char *argv[]) {
  // Zmienne do przechowywania wartości opcji
  std::string config_file;
  std::vector<std::string> input_words;
  int optimization_level;
  int speed = 0;

  /* ================= Definicja opcji programu =================
   * Boost pozwala na łatwe definiowanie i grupowanie opcji programu.
   * Możliwe jest także tworzenie grup opcji ukrytych oraz pozycyjnych.
   *
   * Ogólna składnia dla standardowych flag to:
   *   po::options_description nazwa_grupy("Opis grupy");
   *   nazwa_grupy.add_options()
   *       ("nazwa_opcji", [typ], opis)
   *       (...);
   *
   * Możliwa jest konfiguracja zachowań poszczególnych opcji, co jest
   * przedstawione poniżej.
   */

  // Opcje ogólne
  po::options_description general_opts("General options");
  general_opts.add_options()("help,h", "Produce help message")
      // Opcja będąca przełącznikiem logicznym (nie przypisujemy wartości, tylko
      // włączamy/wyłączamy); `default_value()` pozwala ustawić domyślną wartość
      // (tutaj `false`), która będzie używana, jeśli opcja nie zostanie podana
      ("verbose,v", po::bool_switch()->default_value(false),
       "Enable verbose output")
      // Flaga pozwalająca na podanie ścieżki do pliku konfiguracyjnego, z
      // którego będą odczytywane dodatkowe opcje; wykorzystujemy tutaj
      // referencję do zmiennej, co pozwala na łatwe i automatyczne przypisanie
      // wartości; ponownie przypisujemy wartość domyślną
      ("config-file",
       po::value<std::string>(&config_file)->default_value("boost_config.cfg"),
       "Configuration file");

  // Opcje cli, bardziej złożone
  po::options_description cli_opts("Command-line options");
  cli_opts.add_options()
      // Opcja ta przyjmuje wiele wartości (wektor); `composing()` pozwala na
      // zbieranie wartości z wielu źródeł do jednej zmiennej (np. jako wartość
      // flagi i argumenty pozycyjne / plik konfiguracyjny)
      ("input-words,i",
       po::value<std::vector<std::string>>(&input_words)->composing(),
       "Input words (also positional)")
      // Flaga, którą rozpoznajemy jako -f[no-]turbo-optimization z
      // wykorzystaniem napisanego wcześniej własnego parsera
      ("turbo-optimization", po::value<bool>(), "Enable turbo-optimization")
      // Opcja, która przyjmuje parametry potrzebne do utworzenia złożonego
      // obiektu (tutaj `CustomClass` zdefiniowany w przestrzeni nazw
      // `customNamespace`); do zbudowania obiektu wykorzystujemy napisany wyżej
      // własny walidator
      ("obj", po::value<customNamespace::CustomClass>(),
       "Custom object with custom validation (=int,string)")
      // Opcja, która przyjmuje wartość liczbową i ma domyślną wartość (0
      // przypisane przy inicjalizacji); `implicit_value(x)` oznacza, że jeśli
      // opcja zostanie podana bez wartości, to zostanie użyta wartość x
      ("speed", po::value<int>(&speed)->implicit_value(3), "Speed setting");

  // Opcje konfiguracyjne, które są przechowywane w pliku konfiguracyjnym
  po::options_description config_opts(
      "Options set in the configuration file only");
  config_opts.add_options()
      // Opcje składające się z nazw rozdzielonych kropkami pozwalają na
      // grupowanie opcji w hierarchii, co jest przydatne do organizacji
      // większych projektów; tutaj w pliku konfiguracyjnym opcję można podać
      // jako `level` w grupie `[compiler.optimization]`
      ("compiler.optimization.level",
       po::value<int>(&optimization_level)->default_value(0),
       "Optimization level")
      // Opcja przyjmująca wiele wartości (wektor) z pliku konfiguracyjnego
      // `multitoken()` umożliwia wielokrotne zdefiniowanie tej samej opcji w
      // pliku konfiguracyjnym, co pozwala na zebranie wielu wartości
      ("multi-value-option",
       po::value<std::vector<std::string>>()->multitoken(),
       "Multi-value option");

  // Opcja ukryta, która nie będzie widoczna w pomocy, ale może być
  // używana w programie; jest to przydatne do przechowywania informacji
  // wewnętrznych lub konfiguracji, które nie powinny być widoczne dla
  // użytkownika końcowego
  po::options_description hidden_opts("Hidden options");
  hidden_opts.add_options()("secret-word", po::value<std::string>(),
                            "A secret word (hidden option)");

  // Opcje pozycyjne, które pozwalają na przekazywanie argumentów
  // bezpośrednio po wartości, bez użycia flag
  po::positional_options_description pos_opts;
  // Składnia: `pos_opts.add("nazwa_opcji", liczba_wartości)`, gdzie
  // -1 oznacza, że opcja może przyjąć nieograniczoną ilość wartości
  pos_opts.add("input-words", -1);

  // Opcje można grupować w większe zestawy, co pozwala na łatwiejsze
  // zarządzanie nimi oraz lepszą organizację kodu
  po::options_description visible_opts;
  visible_opts.add(general_opts).add(cli_opts).add(config_opts);

  po::options_description input_opts;
  input_opts.add(cli_opts).add(hidden_opts);

  // Mapy przechowujące wartości opcji
  po::variables_map general_vm, cli_vm, config_vm;

  /* ============ Krok 1: Parsowanie ogólnych opcji ============
   * po::store() jest funkcją, która przyjmuje parser opcji i mapę, do której
   * zostaną zapisane wyniki parsowania.
   * po::command_line_parser() jest funkcją, która tworzy parser opcji na
   * podstawie argumentów przekazanych do programu (argc, argv).
   *
   * po::notify() jest funkcją, która informuje Boost.ProgramOptions, że
   * wszystkie opcje zostały sparsowane i wartości są gotowe do użycia.
   * Jeśli wystąpił błąd podczas parsowania, zostanie zgłoszony wyjątek.
   */

  po::parsed_options parsed_general =
      po::command_line_parser(argc, argv)  // podanie argumentów do parsera
          .options(
              general_opts)  // specyfikacja grupy opcji, które chcemy sparsować
          .allow_unregistered()  // umożliwienie parsowania opcji, które nie są
                                 // zdefiniowane w grupie (bez tego program
                                 // zgłosi błąd po napotkaniu nieznanych
                                 // wartości)
          .run();  // parsowanie opcji
  po::store(parsed_general,
            general_vm);   // zapisanie wyników parsowania do mapy `general_vm`
  po::notify(general_vm);  // powiadomienie Boost.ProgramOptions, że parsowanie
                           // zostało zakończone

  // Jeśli użytkownik podał opcję `--help`, wyświetlamy pomoc i kończymy
  // działanie programu; sprawdzamy obecność opcji za pomocą
  // `vm.count(nazwa_opcji)`
  if (general_vm.count("help")) {
    std::cout << "\nUsage: " << argv[0] << " [options] [input-words...]\n";
    std::cout << visible_opts << "\n";
    return 0;
  }

  // Zbieramy wszystkie opcje, które nie zostały rozpoznane przez parser,
  // aby moc je później wykorzystać do pozostałych flag cli
  std::vector<std::string> remaining_args =
      po::collect_unrecognized(parsed_general.options, po::include_positional);

  // Wyświetlenie informacji o poprawnie sparsowanych opcjach
  std::cout << "\nGeneral options parsed successfully.\n";
  std::cout << "[--config-file] Config file: " << config_file << "\n";
  std::cout << "[--verbose] Verbose mode: "
            << (general_vm["verbose"].as<bool>() ? "ON" : "OFF") << "\n";

  /* ============= Krok 2: Parsowanie opcji konfiguracyjnych ============
   * Otwieramy plik konfiguracyjny, jeśli został podany, a następnie parsujemy
   * jego zawartość. Jeśli plik konfiguracyjny nie istnieje lub nie można go
   * otworzyć, program zgłosi błąd i zakończy działanie.
   *
   * po::parsed_options jest strukturą, która przechowuje wyniki parsowania
   * pliku konfiguracyjnego. Możemy użyć jej do sprawdzenia, czy wystąpiły
   * błędy podczas parsowania, a także do uzyskania informacji o opcjach,
   * które nie zostały zdefiniowane w grupie.
   *
   * po::parse_config_file() jest funkcją, która odczytuje plik
   * konfiguracyjny i zwraca strukturę po::parsed_options, która zawiera
   * wyniki parsowania. Funkcja ta przyjmuje dwa argumenty: strumień wejściowy
   * (std::ifstream) oraz grupę opcji, które chcemy sparsować.
   * Trzeci argument (true) oznacza, że program nie zgłosi błędu w przypadku
   * napotkania nieznanych opcji w pliku konfiguracyjnym.
   */
  std::cout << "\nParsing config file...\n";
  std::ifstream cfg(config_file);
  if (cfg) {
    po::parsed_options parsed_cfg =
        po::parse_config_file(cfg, config_opts, true);
    po::store(parsed_cfg, config_vm);
    for (const auto &opt :
         parsed_cfg.options) {  // Robimy przegląd wszystkich odczytanych opcji
      if (opt.unregistered) {
        // Po natrafieniu opcji, która nie jest zdefiniowana w grupie,
        // wypisujemy ostrzeżenie na standardowe wyjście błędów
        std::cerr << "WARNING: Unrecognized option in config file: "
                  << opt.string_key << " = ";
        // Opcja niezdefiniowana może mieć wiele wartości
        // (zazwyczaj jednak ma jedną)
        for (const auto &v : opt.value) std::cerr << v << " ";
        std::cerr << "\n";
      }
    }
    po::notify(config_vm);
    std::cout << "\nConfig file parsed successfully.\n";
  } else {
    std::cerr << "Error: Could not open config file: " << config_file << "\n";
    return 1;
  }

  // Wyświetlenie wartości opcji konfiguracyjnych
  std::cout << "[compiler.optimization.level] Optimization level: "
            << optimization_level << "\n";  // Zmienna z domyślną wartością 0

  // W przypadku niepodania zmiennej, do której przypisujemy wartość,
  // musimy ręcznie ją odczytać; służy do tego `.as<typ>()`
  if (config_vm.count("multi-value-option")) {
    std::cout << "[multi-value-option] Multi-value option: ";
    for (const auto &value :
         config_vm["multi-value-option"].as<std::vector<std::string>>()) {
      std::cout << value << " ";
    }
    std::cout << "\n";
  }

  /* ============ Krok 3: Parsowanie opcji wiersza poleceń ============
   * `style()` pozwala na określenie dodatkowych stylów parsowania opcji, np.:
   * - `unix_style` - pozwala na używanie opcji w stylu Uniksa (np. -f,
   *   --flag=x, grupowanie krótkich flag typu -abc jako odpowiednik -a -b -c)
   * - `allow_long_disguise` - pozwala na używanie pojedynczego myślnika w
   *   długich flagach (-flaga zamiast --flaga)
   * - `allow_sticky` - pozwala na używanie opcji tzw. "sklejonych" z
   *   argumentami, np. `-irower` zamiast `-i rower`
   *
   * `extra_parser()` umożliwia dodanie własnego parsera flag, który będzie
   * używany do rozpoznawania niestandardowych opcji, takich jak
   * -f[no-]turbo-optimization.
   */
  po::parsed_options parsed_cli =
      po::command_line_parser(
          remaining_args)  // podajemy argumenty nierozpoznane w grupie ogólnej
          .options(input_opts)
          .positional(pos_opts)
          .style(po::command_line_style::unix_style |
                 po::command_line_style::allow_long_disguise |
                 po::command_line_style::allow_sticky)
          .extra_parser(custom_flag_parser)
          .allow_unregistered()
          .run();
  po::store(parsed_cli, cli_vm);
  po::notify(cli_vm);
  std::cout << "\nCommand-line options parsed successfully.\n";

  // Wyświetlenie wartości ukrytej opcji
  if (cli_vm.count("secret-word"))
    std::cout << "[--secret-word] Secret word: "
              << cli_vm["secret-word"].as<std::string>() << "\n";

  // Wyświetlenie wartości opcji parsowanej własnym parserem
  if (cli_vm.count("turbo-optimization"))
    std::cout << "[-f[no-]turbo-optimization] turbo-optimization is "
              << (cli_vm["turbo-optimization"].as<bool>() ? "enabled"
                                                          : "disabled")
              << "\n";

  // Wyświetlenie wartości opcji o złożonym typie, dla której zdefiniowaliśmy
  // własny walidator; wynik można odczytać jako stworzony obiekt
  std::cout << "[--obj] Custom object value: ";
  if (cli_vm.count("obj")) {
    auto obj = cli_vm["obj"].as<customNamespace::CustomClass>();
    std::cout << obj.a << "-" << obj.b << "\n";
  } else {
    std::cout << "not set\n";
  }

  // Wyświetlenie wartości opcji liczbowej z domyślną wartością
  std::cout << "[--speed] Speed setting: " << speed << "\n";

  // Wyświetlenie wartości opcji, które można pozyskać zarówno z flagi, jak i
  // jako wartości pozycyjne
  std::cout << "[--input-words | positional] Input words:";
  for (const auto &word : input_words) std::cout << ", " << word;
  std::cout << "\n";

  /* =========== Identyfikacja nieznanych opcji ============
   * Boost.ProgramOptions pozwala na łatwe sprawdzenie, które opcje zostały
   * rozpoznane, a które nie. Możemy to zrobić poprzez porównanie zdefiniowanych
   * opcji z tymi, które zostały przekazane w wierszu poleceń.
   *
   * `collect_unrecognized()` jest funkcją, która zbiera wszystkie opcje,
   * które nie zostały rozpoznane przez parser i zwraca je jako wektor
   * ciągów znaków.
   *
   * `exclude_positional` oznacza, że nie chcemy zbierać opcji pozycyjnych,
   * czyli tych wartości, które są przekazywane bez flagi.
   */
  std::vector<std::string> unrecognized =
      po::collect_unrecognized(parsed_cli.options, po::exclude_positional);
  if (!unrecognized.empty()) {
    std::cerr << "Unrecognized options: \n";
    for (const auto &opt : unrecognized) {
      std::cerr << " " << opt << "\n";
    }
  }
  return 0;
}
