#include "opt_parser.h"

#include <getopt.h>

#include <cassert>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

#include "log.h"

using namespace std;

#if defined(_WIN32) && !defined(__GNUC__)

enum { no_argument = 0, required_argument, optional_argument };

int optind = 0;
const char* optarg = NULL;

struct option {
  const char* name;
  int has_arg;
  int* flag;
  int val;
};

int getopt_long_only(int argc, char* const argv[], const char* short_opts, const option* long_opts,
                     int* index) {
  if (short_opts && *short_opts != '\0') {
    AG_LOG(ERROR, "short options have not been implemented yet!");
    return -1;
  }

  optind = optind == 0 ? 1 : optind;
  optarg = NULL;

  if (optind >= argc) return -1;

  const char* a = argv[optind];
  if (a[0] != '-' || a[1] != '-') return -1;

  a += 2;

  int i = 0;

  for (; long_opts[i].name != NULL; ++i) {
    if (!strcmp(long_opts[i].name, a)) {
      ++optind;
      *index = i;
      return 0;
    }
  }

  return '?';
}

#endif

// bool opt_parser::insert_short_arg(internal_opt arg, char short_arg) {
//   if (!isalpha(short_arg)) {
//     AG_LOG(ERROR, "short parameters should be alphabetic!");
//     return false;
//   }
//
//   if (short_args_.count(short_arg) > 0) {
//     AG_LOG(ERROR, "short parameter[%c] has been occupied!", short_arg);
//     return false;
//   }
//
//   short_args_[short_arg] = arg;
//   return true;
// }
opt_parser::opt_parser() : sequence_(0) {}
bool opt_parser::insert_long_opt(internal_opt& opt, const char* long_opt) {
  if (!long_opt) {
    AG_LOG(ERROR, "A full parameter should be supplied!");
    return false;
  }

  if (long_opts_.count(long_opt) > 0) {
    AG_LOG(ERROR, "{%s} has been occupied yet!", long_opt);
    return false;
  }
  if (opt.help == NULL || !strlen(opt.help)) opt.help = "NA";

  long_opts_[long_opt] = opt;
  return true;
}

// bool opt_parser::add_short_arg(bool *store, char short_arg) {
//   *store = false;
//
//   internal_opt arg = {kBool, {store}};
//   return insert_short_arg(arg, short_arg);
// }
//
// bool opt_parser::add_short_arg(int *store, char short_arg) {
//   internal_opt arg;
//   arg.type = kInt;
//   arg.int_ptr = store;
//
//   return insert_short_arg(arg, short_arg);
// }
//
// bool opt_parser::add_short_arg(string *store, char short_arg) {
//   internal_opt arg;
//   arg.type = kString;
//   arg.string_ptr = store;
//
//   return insert_short_arg(arg, short_arg);
// }

bool opt_parser::add_long_opt(const char* long_opt, bool* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kBool;
  arg.bool_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, int32_t* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kInt32;
  arg.int32_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, uint32_t* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kUInt32;
  arg.uint32_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, int64_t* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kInt64;
  arg.int64_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, uint64_t* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kUInt64;
  arg.uint64_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, double* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kDouble;
  arg.double_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, string* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kString;
  arg.string_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, ipv4* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kIPv4;
  arg.ipv4_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::add_long_opt(const char* long_opt, mac_addr* store, const char* help,
                              opt_parser::opt_type optParamType) {
  internal_opt arg;
  arg.type = kMacAddr;
  arg.addr_ptr = store;
  arg.help = help;
  arg.optType = optParamType;
  arg.seq = ++sequence_;
  return insert_long_opt(arg, long_opt);
}

bool opt_parser::parse_ipv4(const char* arg, ipv4* ip) {
  uint8_t(&a)[4] = ip->repr;
  if (sscanf(arg, "%hhu.%hhu.%hhu.%hhu", &a[0], &a[1], &a[2], &a[3]) != 4) {
    AG_LOG(ERROR, "Illegal IP Format: %s", arg);
    return false;
  }

  return true;
}

bool opt_parser::parse_mac_addr(const char* arg, mac_addr* addr) {
  uint8_t(&b)[6] = addr->addr_bytes;
  if (sscanf(arg, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) != 6) {
    AG_LOG(ERROR, "Illegal ethernet physical address: %s", arg);
    return false;
  }

  return true;
}

bool opt_parser::fill_arg(const char* opt_name, const internal_opt& opt, const char* opt_arg) {
  if (opt_arg == NULL) {
    AG_LOG(ERROR, "No argument available for %s", opt_name);
    return false;
  }

  char* end_ptr = NULL;

  switch (opt.type) {
    case kBool: {
      unsigned long n = strtoul(opt_arg, &end_ptr, 10);
      if (*end_ptr != '\0') {
        AG_LOG(ERROR, "Invalid integer argument: %s", opt_arg);
        return false;
      }
      *opt.bool_ptr = n ? true : false;
    } break;

    case kInt32: {
      long n = strtol(opt_arg, &end_ptr, 10);
      if (*end_ptr != '\0') {
        AG_LOG(ERROR, "Invalid integer argument: %s", opt_arg);
        return false;
      }
      *opt.int32_ptr = int32_t(n);
      break;
    }
    case kUInt32: {
      unsigned long n = strtoul(opt_arg, &end_ptr, 10);
      if (*end_ptr != '\0') {
        AG_LOG(ERROR, "Invalid integer argument: %s", opt_arg);
        return false;
      }
      *opt.uint32_ptr = uint32_t(n);
      break;
    }
    case kInt64: {
      long long n = strtoll(opt_arg, &end_ptr, 10);
      if (*end_ptr != '\0') {
        AG_LOG(ERROR, "Invalid integer argument: %s", opt_arg);
        return false;
      }
      *opt.int64_ptr = int64_t(n);
      break;
    }
    case kUInt64: {
      unsigned long long n = strtoull(opt_arg, &end_ptr, 10);
      if (*end_ptr != '\0') {
        AG_LOG(ERROR, "Invalid integer argument: %s", opt_arg);
        return false;
      }
      *opt.uint64_ptr = uint64_t(n);
      break;
    }
    case kDouble: {
      double n = strtod(opt_arg, &end_ptr);
      if (*end_ptr != '\0') {
        AG_LOG(ERROR, "Invalid double argument: %s", opt_arg);
        return false;
      }
      *opt.double_ptr = n;
      break;
    }
    case kString: {
      *opt.string_ptr = opt_arg;
      break;
    }
    case kIPv4: {
      if (!parse_ipv4(opt_arg, opt.ipv4_ptr)) return false;
      break;
    }
    case kMacAddr: {
      if (!parse_mac_addr(opt_arg, opt.addr_ptr)) return false;
      break;
    }
    default:
      assert(false);
      break;
  }

  return true;
}

void opt_parser::save_to_exopts() const {
  typedef unordered_map<const char*, internal_opt>::const_iterator It;
  for (It iter = long_opts_.begin(); iter != long_opts_.end(); ++iter) {
    ex_internal_opt ex_opt;
    ex_opt.long_opt = iter->first;
    ex_opt.stru_opt = iter->second;
    int key = iter->second.seq;
    ex_long_opts_[key] = ex_opt;
  }
}
bool opt_parser::parse_opts(int argc, char* const argv[]) {
  //  vector<char> options;
  //  options.reserve(short_args_.size() + 1);
  //
  //  unordered_map<char, internal_opt>::const_iterator it;
  //  for (it = short_args_.begin(); it != short_args_.end(); ++it) {
  //    options.push_back(it->first);
  //    if (it->second.type != kBool)
  //      options.push_back(':');
  //  }
  //
  //  options.push_back('\0');

  vector<option> long_opt;
  long_opt.reserve(long_opts_.size() + 1);

  unordered_map<const char*, internal_opt>::const_iterator f;
  for (f = long_opts_.begin(); f != long_opts_.end(); ++f) {
    option arg = {f->first, f->second.optType == opt_argu ? optional_argument : required_argument,
                  0, 0};
    long_opt.push_back(arg);
  }

  option end_indicator = {NULL, 0, NULL, 0};
  long_opt.push_back(end_indicator);
  int index, ret;

  // NOTE(liuyong): MUST initialize this variable first!
  optind = 1;
  optarg = NULL;

  while ((ret = getopt_long_only(argc, argv, "", &long_opt[0], &index)) == 0 ||
         (ret > 0 && ret != '?')) {
    if (ret == 0) {  // handle long options
      const option& opt = long_opt[index];
      const internal_opt& a = long_opts_[opt.name];
      const char* arg = optarg;
      if (!arg && optind < argc) {
        arg = argv[optind];
        ++optind;
      }

      if (!fill_arg(opt.name, a, arg)) {
        optind = 0;
        return false;
      }
    } /* else if (short_args_.count(ret) > 0) { // handle short options
       const internal_opt &a = short_args_[ret];
       if (!fill_arg(a, optarg))
         return false;
     } */
  }

  index = optind;
  optind = 0;

  if (index < argc) {
    AG_LOG(ERROR, "Unrecognized option argument: %s", argv[index]);
    return false;
  }

  if (ret != -1) {
    AG_LOG(ERROR, "Unregconized option argument %s", argv[index - 1]);
    return false;
  }

  return true;
}

void opt_parser::clear() {
  //  short_args_.clear();
  long_opts_.clear();
  ex_long_opts_.clear();
}

void opt_parser::print_usage(const char* exec_file, ostream& sout) const {
  sout << "Usage: \n  " << exec_file << " ";
  size_t fileLength = strlen(exec_file);
  save_to_exopts();
  typedef std::map<int, ex_internal_opt>::const_iterator It;
  for (It f = ex_long_opts_.begin(); f != ex_long_opts_.end(); ++f) {
    sout << "--" << f->second.long_opt << " ";
    const internal_opt& opt = f->second.stru_opt;
    const char* opt_arg = NULL;

    switch (opt.type) {
      case kBool:
        continue;
      case kInt32:
        opt_arg = "INTEGER32";
        break;
      case kUInt32:
        opt_arg = "UINTEGER32";
        break;
      case kInt64:
        opt_arg = "INTEGER64";
        break;
      case kUInt64:
        opt_arg = "UINTEGER64";
        break;
      case kDouble:
        opt_arg = "DOUBLE";
        break;
      case kString:
        opt_arg = "STRING";
        break;
      case kIPv4:
        opt_arg = "ddd.ddd.ddd.ddd";
        break;
      case kMacAddr:
        opt_arg = "xx:xx:xx:xx:xx:xx";
        break;
      default:
        assert(false);
        break;
    }

    sout << opt_arg << " ";
  }

  sout << endl;

  for (It f = ex_long_opts_.begin(); f != ex_long_opts_.end(); ++f) {
    sout << "   ";
    for (size_t i = 0; i < fileLength; i++) sout << " ";
    sout << "--" << f->second.long_opt << "     (" << f->second.stru_opt.help << ")" << endl;
  }
}
