#ifndef GETOPT_CPP_H_
#define GETOPT_CPP_H_

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <getopt.h>
#include <iomanip>
#include <string.h>

class GetOpt {
 public:
  class Value {
   protected:
    bool enabled;
    std::string value;
   public:
    char id;
    int long_id;
    int has_arg;
    Value(char id, int long_id, int has_arg)
        : enabled(false),
          id(id),
          long_id(long_id),
          has_arg(has_arg) {
    }

    operator bool() const {
      return enabled;
    }

    template<typename T>
    T GetAs() const {
      T result;
      std::istringstream iss(value);
      iss >> result;
      return result;
    }

    template<typename T>
    operator T() const {
      return GetAs<T>();
    }

    void Apply() {
      enabled = true;
    }

    void Apply(const char* value) {
      Apply();
      if (value) {
        this->value = value;
      }
    }
  };
  typedef std::vector<Value> ValueList;
  typedef std::vector<Value*> ValuePointerList;

 protected:
  ValuePointerList values;
  ValueList non_option_values;
  std::vector<option> long_options;
  std::ostringstream help_stream;
  std::string short_opt;

 public:
  void AddHelpLine(char id, std::string long_option, std::string metavar,
                   std::string description) {
    std::string short_full = "   ";
    std::string long_full;

    if (id) {
      short_full = "-?,";
      short_full[1] = id;
    }
    if (long_option.size()) {
      long_full = "--" + long_option + (metavar.size() ? "=" + metavar : "");
    }
    std::string options_list = short_full + long_full + " ";
    help_stream << "  " << std::left << std::setw(30) << options_list
                << std::setw(0) << " " << description << "\n";
  }

  Value& Add(char id, std::string long_option, std::string metavar,
             std::string description) {
    AddHelpLine(id, long_option, metavar, description);
    if (id) {
      short_opt.push_back(id);
      short_opt += metavar.size() ? ":" : "";
    }

    // allocated memory via strdup() will be freed in destructor
    long_options.push_back(
        option { strdup(long_option.c_str()), (
            metavar.size() ? required_argument : no_argument), 0, id });

    // allocated memory via new will be deleted in destructor
    values.push_back(
        new Value(id, long_options.size() - 1, long_options.back().has_arg));
    return *values.back();
  }

  int Run(int argc, char* argv[]) {
    int result = 0;
    long_options.push_back(option { 0, 0, 0, 0 });  // add empty option

    while (1) {
      int c, option_index = 0;
      c = getopt_long(argc, argv, short_opt.c_str(), &long_options.front(),
                      &option_index);
      if (c == -1) {
        // getopt finished
        break;
      }

      if (c == '?') {
        // getopt error
        result = 1;
        break;
      }

      for (Value* value : values) {
        if ((c && value->id == c) || (!c && value->long_id == option_index)) {
          if (value->has_arg) {
            value->Apply(optarg);
          } else {
            value->Apply();
          }
        }
      }
    }

    if (optind < argc) {
      while (optind < argc) {
        non_option_values.push_back(Value(-1, -1, required_argument));
        Value& value = non_option_values.back();
        value.Apply(argv[optind]);
        optind++;
      }
    }

    return result;
  }

  const ValueList& GetNonOptionValues() const {
    return non_option_values;
  }

  std::string GetHelp() const {
    return help_stream.str();
  }

  ~GetOpt() {

    for (Value* v : values) {
      delete v;  // delete all allocated values
    }

    for (option& o : long_options) {
      free((void*) o.name);  // delete all allocated names
    }

  }
};

#endif /* GETOPT_CPP_H_ */
