/*
    +--------------------------------------------------------------------+
    | libmemcached-awesome - C/C++ Client Library for memcached          |
    +--------------------------------------------------------------------+
    | Redistribution and use in source and binary forms, with or without |
    | modification, are permitted under the terms of the BSD license.    |
    | You should have received a copy of the license in a bundled file   |
    | named LICENSE; in case you did not receive a copy you can review   |
    | the terms online at: https://opensource.org/licenses/BSD-3-Clause  |
    +--------------------------------------------------------------------+
    | Copyright (c) 2006-2014 Brian Aker   https://datadifferential.com/ |
    | Copyright (c) 2020-2021 Michael Wallner        https://awesome.co/ |
    +--------------------------------------------------------------------+
*/

#pragma once

#include <algorithm>
#include <cstdint>
#include <climits>
#include <functional>
#include <iostream>
#include <string>
#include <vector>

#include "libmemcached/common.h"
#include "p9y/getopt.hpp"

class client_options {
public:

  struct extended_option {
    option opt;
    std::string help;
    std::function<bool(client_options &, extended_option &)> parse;
    std::function<bool(const client_options &, const extended_option &, memcached_st *)> apply;
    char *arg;
    bool set;
  };

  std::vector<extended_option> options;
  std::vector<extended_option> defaults;

  const char *prog_name;
  const char *prog_vers;
  const char *prog_desc;
  const char *prog_argp;

  client_options(const char *prg, const char *ver, const char *dsc, const char *arg = nullptr)
  : options{}
  , defaults{}
  , prog_name{prg}
  , prog_vers{ver}
  , prog_desc{dsc}
  , prog_argp{arg}
  {

    def("help", 'h', no_argument, "Print this help.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *) {
      if (ext.set) {
        opt.print_help();
        exit(EXIT_SUCCESS);
      }
      return true;
    };
    def("version", 'V', no_argument, "Print program version.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *) {
      if (ext.set) {
        opt.print_version();
        exit(EXIT_SUCCESS);
      }
      return true;
    };

    def("verbose", 'v', no_argument, "Print more informational output.")
        .parse = [](client_options &opt, extended_option &) {
      opt.unset("quiet");
      return true;
    };
    def("debug", 'd', no_argument, "Print output useful only for debugging.")
        .parse = [](client_options &opt, extended_option &) {
      opt.set("verbose");
      opt.unset("quiet");
      return true;
    };
    def("quiet", 'q', no_argument, "Print no output, not even errors.")
        .parse = [](client_options &opt, extended_option &) {
      opt.unset("verbose");
      opt.unset("debug");
      return true;
    };

    def("password", 'p', required_argument, "SASL password.");
    def("username", 'u', required_argument, "SASL username.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      if (auto username = ext.arg) {
#if !LIBMEMCACHED_WITH_SASL_SUPPORT
          (void) memc;
          if (!opt.isset("quiet")) {
            std::cerr
                << "SASL username '" << username << "' was supplied, but binary was not built with SASL support.\n";
          }
          return false;
#else
        if (memc) {
          if (MEMCACHED_SUCCESS
              != memcached_set_sasl_auth_data(memc, username, opt.argof("password"))) {
            if (!opt.isset("quiet")) {
              std::cerr << memcached_last_error_message(memc);
            }
            return false;
          }
        }
#endif
      }
      return true;
    };

    def("binary", 'b', no_argument, "Use the binary memcached protocol.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      if (MEMCACHED_SUCCESS != memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, ext.set)) {
        if(!opt.isset("quiet")) {
          std::cerr << memcached_last_error_message(memc);
        }
        return false;
      }
      return true;
    };
    def("buffer", 'B', no_argument, "Buffer requests.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      if (MEMCACHED_SUCCESS != memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_BUFFER_REQUESTS, ext.set)) {
        if(!opt.isset("quiet")) {
          std::cerr << memcached_last_error_message(memc);
        }
        return false;
      }
      return true;
    };
    def("non-blocking", 'n', no_argument, "Use non-blocking connections.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      if (MEMCACHED_SUCCESS != memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_NO_BLOCK, ext.set)) {
        if(!opt.isset("quiet")) {
          std::cerr << memcached_last_error_message(memc);
        }
        return false;
      }
      return true;
    };
    def("tcp-nodelay", 'N', no_argument, "Disable Nagle's algorithm.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      if (MEMCACHED_SUCCESS != memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_TCP_NODELAY, ext.set)) {
        if(!opt.isset("quiet")) {
          std::cerr << memcached_last_error_message(memc);
        }
        return false;
      }
      return true;
    };
    def("servers", 's', required_argument, "List of servers to connect to.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      auto servers = ext.arg;
      if (!servers) {
        if (opt.isset("verbose")) {
          std::cerr << "Checking environment for a server list in MEMCACHED_SERVERS.\n";
        }
        servers = getenv("MEMCACHED_SERVERS");
        if (!servers || !*servers) {
          if (!opt.isset("quiet")) {
            std::cerr << "No servers provided.\n";
          }
          return false;
        }
      }

      auto server_list = memcached_servers_parse(servers);
      if (!server_list || !memcached_server_list_count(server_list)) {
        if (!opt.isset("quiet")) {
          std::cerr << "Invalid server list provided: '" << servers << "'\n";
        }
        if (server_list) {
          memcached_server_list_free(server_list);
        }
        return false;
      }

      if (MEMCACHED_SUCCESS != memcached_server_push(memc, server_list)) {
        if (!opt.isset("quiet")) {
          std::cerr << memcached_last_error_message(memc);
        }
        memcached_server_list_free(server_list);
        return false;
      }
      memcached_server_list_free(server_list);
      return true;
    };
    def("hash", 'H', required_argument, "Key hashing method.")
        .apply = [](const client_options &opt, const extended_option &ext, memcached_st *memc) {
      if (ext.set) {
        std::string hash_wanted{ext.arg};
        memcached_hash_t hash = MEMCACHED_HASH_DEFAULT;

        std::transform(hash_wanted.begin(), hash_wanted.end(), hash_wanted.begin(), ::toupper);

        if (opt.isset("verbose")) {
          std::cerr << "Checking for hash '" << hash_wanted << "'.\n";
        }
        for (int h = MEMCACHED_HASH_DEFAULT; h < MEMCACHED_HASH_MAX; ++h) {
          auto hash_type = static_cast<memcached_hash_t>(h);
          std::string hash_string{libmemcached_string_hash(hash_type)};

          if (hash_wanted.length() == hash_string.length()) {
            auto ci = std::equal(hash_string.begin(), hash_string.end(), hash_wanted.begin(),
                                 [](int a, int b) { return ::toupper(a) == b; });
            if (ci) {
              hash = hash_type;
              break;
            }
          }
        }
        if (hash == MEMCACHED_HASH_DEFAULT) {
          if (!opt.isset("quiet")) {
            std::cerr << "Could not find hash '" << hash_wanted << "'.\n";
          }
        }
        if (MEMCACHED_SUCCESS != memcached_behavior_set_key_hash(memc, hash)) {
          if (!opt.isset("quiet")) {
            std::cerr << memcached_last_error_message(memc);
          }
          return false;
        }
      }
      return true;
    };
  }

  extended_option &def(option opt, std::string help) {
    defaults.emplace_back(extended_option{opt, std::move(help), {}, {}, nullptr, false});
    return defaults.back();
  }

  extended_option &def(const char *name, char flag, int has_arg, const char *help) {
    return def(option{name, has_arg, nullptr, flag}, help);
  }

  extended_option &add(extended_option ext) {
    options.emplace_back(std::move(ext));
    return options.back();
  }

  extended_option &add(option opt, std::string help) {
    options.emplace_back(extended_option{opt, std::move(help), nullptr, nullptr, nullptr, false});
    return options.back();
  }

  extended_option &add(const char *name, char flag, int has_arg, const char *help) {
    return add(option{name, has_arg, nullptr, flag}, help);
  }

  extended_option &get(const std::string &name) {
    // UB if not found
    return *find(name);
  }
  extended_option &get(int c) {
    // UB if not found
    return *find(c);
  }

  const extended_option &get(const std::string &name) const {
    // UB if not found
    return *find(name);
  }
  const extended_option &get(int c) const {
    // UB if not found
    return *find(c);
  }

  bool has(const std::string &name) const {
    auto found = find(name);
    return found != options.cend();
  }
  bool has(int c) const {
    auto found = find(c);
    return found != options.cend();
  }

  bool isset(const std::string &name) const {
    return has(name) && get(name).set;
  }
  bool isset(int c) const {
    return has(c) && get(c).set;
  }

  void unset(const std::string &name) {
    set(name, false);
  }
  void unset(int c) {
    set(c, false);
  }

  void set(const std::string &name, bool set_ = true, char *optarg_ = nullptr) {
    if (has(name)) {
      auto &opt = get(name);
      opt.set = set_;
      opt.arg = optarg_;
    }
  }
  void set(int c, bool set_ = true, char *optarg_ = nullptr) {
    if (has(c)) {
      auto &opt = get(c);
      opt.set = set_;
      opt.arg = optarg_;
    }
  }

  char *argof(const std::string &name) const {
    if (has(name)) {
      return get(name).arg;
    }
    return nullptr;
  }
  char *argof(int c) const {
    if (has(c)) {
      return get(c).arg;
    }
    return nullptr;
  }

  const extended_option &operator[](const std::string &name) const {
    return get(name);
  }
  const extended_option &operator[](int c) const {
    return get(c);
  }

  void print_version() const;
  void print_help() const;

  bool parse(int argc, char *argv[], char ***argp = nullptr);
  bool apply(memcached_st *memc);

private:
  using iterator = std::vector<extended_option>::iterator;
  using const_iterator = std::vector<extended_option>::const_iterator;
  using predicate = std::function<bool(const extended_option &ext)>;

  const_iterator find(const predicate &pred) const {
    return std::find_if(options.cbegin(), options.cend(), pred);
  }
  const_iterator find(const std::string &name) const {
    return find([&name](const extended_option &ext) {
      return ext.opt.name && ext.opt.name == name;
    });
  }
  const_iterator find(int c) const {
    return find([c](const extended_option &ext) {
      return ext.opt.val == c || (c == 1 && ext.opt.val == '-');
    });
  }

  iterator find(const predicate &pred) {
    return std::find_if(options.begin(), options.end(), pred);
  }
  iterator find(const std::string &name) {
    return find([&name](const extended_option &ext) {
      return ext.opt.name && ext.opt.name == name;
    });
  }
  iterator find(int c) {
    return find([c](const extended_option &ext) {
      return ext.opt.val == c || (c == 1 && ext.opt.val == '-');
    });
  }
};
