//
// One header file Commandline Parse for C++11 2017-01-29.01 GPL
// https://github.com/trueroad/cmdlineparse/
//
// Copyright (C) 2016, 2017 Masamichi Hosoda
//
// One header file Commandline Parse for C++11 is free software:
// you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// One header file Commandline Parse for C++11 is distributed
// in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with One header file Commandline Parse for C++11.
// If not, see <http://www.gnu.org/licenses/>.
//

#ifndef INCLUDE_GUARD_CMDLINEPARSE_HH
#define INCLUDE_GUARD_CMDLINEPARSE_HH

#include <vector>
#include <map>
#include <string>
#include <iostream>
#include <sstream>
#include <functional>

// gettext macros
#ifndef _

#define _(DUMMY) DUMMY
#define CMDLINEPARSE_HH_DEFINE_DUMMY_GETTEXT

#endif

namespace cmdlineparse
{

  enum class arg_mode
    {
      no_argument = 0,
      required_argument = 1,
      optional_argument = 2
    };

  enum class abort_reason
    {
      option_handler = -1,
      no_abort = 0,
      error_extra_arg,
      error_no_arg,
      error_ambiguous_option,
      error_unknown_option,
      error_no_arg_short,
      error_unknown_option_short,
    };

  class parser
  {
  public:
    // Add an option handler
    bool add_handler (char short_name, const std::string &long_name,
                      arg_mode has_arg,
                      std::function<bool(const std::string&)> option_handler,
                      const std::string &description = "",
                      const std::string &typestr = "",
                      const std::string &header = "",
                      const std::string &group = "");

    // Add a flag option
    bool add_flag (char short_name, const std::string &long_name,
                   bool *flag,
                   const std::string &description = "",
                   const std::string &group = "");

    // Add a string option
    bool add_string (char short_name, const std::string &long_name,
                     std::string *var, const std::string &defval,
                     const std::string &description = "",
                     const std::string &typestr = "STRING",
                     const std::string &group = "");

    // Add help description
    void add_description (char short_name, const std::string &long_name,
                          arg_mode has_arg,
                          const std::string &description,
                          const std::string &typestr = "",
                          const std::string &header = "",
                          const std::string &group = "");

    // Add default handler
    bool add_default_help (void);
    bool add_default_version (void);
    bool add_default (void)
    {
      if (!add_default_help ())
        return false;
      return add_default_version ();
    }

    // Parse options
    bool parse (int argc, char const* const* argv, int optind = 1);

    // Build default strings
    std::string build_usage (void) const;
    std::string build_help (void) const;

    // Get version_string
    const std::string &get_version_string () const noexcept
    {
      return version_string;
    }

    // Set version_string
    void set_version_string (const std::string &s)
    {
      version_string = s;
    }

    // Get unamed_args
    const std::vector<std::string> &get_unamed_args () const noexcept
    {
      return unamed_args;
    }

    // Set usage_unamed_opts
    void set_usage_unamed_opts (const std::string &s)
    {
      usage_unamed_opts = s;
    }

    // Get abort reason
    abort_reason get_abort_reason () const noexcept
    {
      return abort;
    }

    // Get abort option
    const std::string &get_abort_option () const noexcept
    {
      return abort_option;
    }

    // Set opterr
    void set_opterr (bool flag) noexcept
    {
      opterr = flag;
    }

    // Set long_only
    void set_long_only (bool flag) noexcept
    {
      long_only = flag;
    }

    // Set continue_on_error
    void set_continue_on_error (bool flag) noexcept
    {
      continue_on_error = flag;
    }

    // Set abbreviated_long_name
    void set_abbreviated_long_name (bool flag) noexcept
    {
      abbreviated_long_name = flag;
    }

    // Constructor
    parser ();

    // Const
    const std::string h_indent {"  "};
    const std::string h_space {"   "};
    const std::string d_indent {"    "};

  private:
    // Internal functions
    void add_short (char short_name, arg_mode has_arg,
                    std::function<bool(const std::string&)> option_handler);
    void add_long (const std::string &long_name, arg_mode has_arg,
                    std::function<bool(const std::string&)> option_handler);
    bool find_unique_long_name (const std::string &long_name,
                                std::pair<arg_mode,
                                std::function<bool(const std::string&)>>
                                *target,
                                bool *ambiguous);
    bool parse_long_name (std::vector<std::string>::const_iterator *it);
    bool parse_short_name (std::vector<std::string>::const_iterator *it);

    // Error handlers
    std::function<bool(const std::string&,
                       const std::string&)> error_extra_arg
    {
      [this](const std::string &long_name, const std::string &)->bool
        {
          if (opterr)
            std::cerr << argvs[0]
                      << _(": option doesn't take an argument -- ")
                      << long_name << std::endl;
          return continue_on_error;
        }
    };
    std::function<bool(const std::string&)> error_no_arg
    {
      [this](const std::string &long_name)->bool
        {
          if (opterr)
            std::cerr << argvs[0]
                      << _(": option requires an argument -- ")
                      << long_name << std::endl;
          return continue_on_error;
        }
    };
    std::function<bool(const std::string&)> error_ambiguous_option
    {
      [this](const std::string &optchars)->bool
        {
          if (opterr)
            std::cerr << argvs[0]
                      << _(": ambiguous option -- ")
                      << optchars << std::endl;
          return continue_on_error;
        }
    };
    std::function<bool(const std::string&)> error_unknown_option
    {
      [this](const std::string &optchars)->bool
        {
          if (opterr)
            std::cerr << argvs[0]
                      << _(": unknown option -- ")
                      << optchars << std::endl;
          return continue_on_error;
        }
    };
    std::function<bool(char)> error_no_arg_short
    {
      [this](char optchar)->bool
        {
          if (opterr)
            std::cerr << argvs[0]
                      << _(": option requires an argument -- ")
                      << optchar << std::endl;
          return continue_on_error;
        }
    };
    std::function<bool(char)> error_unknown_option_short
    {
      [this](char optchar)->bool
        {
          if (opterr)
            std::cerr << argvs[0]
                      << _(": unknown option -- ")
                      << optchar << std::endl;
          return continue_on_error;
        }
    };

    // Help strings
    std::string version_string;
    std::string usage_unamed_opts;

    // Flags
    bool opterr {true};
    bool continue_on_error {false};
    bool long_only {false};
    bool abbreviated_long_name {true};

    // Abort reason
    abort_reason abort {abort_reason::no_abort};
    std::string abort_option;

    // Arguments
    std::vector<std::string> argvs;
    std::vector<std::string> unamed_args;

    // Maps
    std::map<char,
             std::pair<arg_mode,
                       std::function <bool(const std::string&)>>> short_map;
    std::map<std::string,
             std::pair<arg_mode,
                       std::function <bool(const std::string&)>>> long_map;
    std::map<std::string, std::vector<std::string>> help_map;
  };

  inline
  parser::parser ()
  {
#ifdef PACKAGE_STRING
    // Build version_string
    std::stringstream ss;
    ss << PACKAGE_STRING << std::endl;

#ifdef PACKAGE_COPYRIGHT
    ss << PACKAGE_COPYRIGHT << std::endl;
#ifdef PACKAGE_LICENSE
    ss << PACKAGE_LICENSE << std::endl;
#endif // PACKAGE_LICENSE
#endif // PACKAGE_COPYRIGHT

#ifdef PACKAGE_URL
    ss << std::endl << PACKAGE_URL << std::endl;
#endif // PACKAGE_URL

    version_string = ss.str ();
#endif // PACKAGE_STRING
  }

  inline void
  parser::add_short (char short_name, arg_mode has_arg,
                     std::function<bool(const std::string&)> option_handler)
  {
    if (short_name)
      {
        short_map[short_name] =
          std::pair<arg_mode, std::function <bool(const std::string&)>>
          (has_arg, option_handler);
      }
  }

  inline void
  parser::add_long (const std::string &long_name, arg_mode has_arg,
                    std::function<bool(const std::string&)> option_handler)
  {
    if (!long_name.empty ())
      {
        long_map[long_name] =
          std::pair<arg_mode, std::function <bool(const std::string&)>>
          (has_arg, option_handler);
      }
  }

  inline void
  parser::add_description (char short_name, const std::string &long_name,
                           arg_mode has_arg,
                           const std::string &description,
                           const std::string &typestr,
                           const std::string &header,
                           const std::string &group)
  {
    std::stringstream ss;

    if (short_name || !long_name.empty () || !header.empty ())
      {
        ss << h_indent;
        if (short_name)
          {
            ss << "-" << short_name;
            if (long_name.empty () && !typestr.empty ())
              ss << typestr;
          }
        if (!long_name.empty ())
          {
            if (short_name)
              ss << ", ";
            ss << "--" << long_name;
            if (!typestr.empty ())
              {
                switch (has_arg)
                  {
                  case arg_mode::no_argument:
                    // Nothing to do
                    break;
                  case arg_mode::required_argument:
                    ss << "=" << typestr;
                    break;
                  case arg_mode::optional_argument:
                    ss << "[=" << typestr << "]";
                    break;
                  }
              }
          }
        if (!header.empty ())
          ss << header;
        ss << std::endl;
      }

    if (!description.empty ())
      ss << description << std::endl;

    help_map[group].push_back (ss.str ());
  }

  inline bool
  parser::add_handler (char short_name, const std::string &long_name,
                       arg_mode has_arg,
                       std::function<bool(const std::string&)> option_handler,
                       const std::string &description,
                       const std::string &typestr,
                       const std::string &header,
                       const std::string &group)
  {
    if (short_name && short_map.find (short_name) != short_map.end ())
      {
        // The short name is already registerd.
        return false;
      }
    if (!long_name.empty() && long_map.find (long_name) != long_map.end ())
      {
        // The long name is already registerd.
        return false;
      }

    add_short (short_name, has_arg, option_handler);
    add_long (long_name, has_arg, option_handler);

    if (!description.empty () || !typestr.empty ())
      add_description (short_name, long_name, has_arg,
                       description, typestr, header, group);

    return true;
  }

  inline bool
  parser::add_flag (char short_name, const std::string &long_name,
                    bool *flag,
                    const std::string &description,
                    const std::string &group)
  {
    *flag = false;
    return add_handler (short_name, long_name, arg_mode::no_argument,
                        [flag](const std::string &)->bool
                        {
                          *flag = true;
                          return true;
                        },
                        description, "", "", group);
  }

  inline bool
  parser::add_string (char short_name, const std::string &long_name,
                      std::string *var, const std::string &defval,
                      const std::string &description,
                      const std::string &typestr,
                      const std::string &group)
  {
    *var = defval;
    std::string header;

    if(!defval.empty ())
      {
        // Three spaces for separator (same as h_space)
        header = _("   (default=") + defval + ")";
      }
    return add_handler (short_name, long_name, arg_mode::required_argument,
                        [var](const std::string &optarg)->bool
                        {
                          *var = optarg;
                          return true;
                        },
                        description, typestr, header, group);
  }

  inline bool
  parser::add_default_help (void)
  {
    return add_handler ('h', "help", arg_mode::no_argument,
                        [this](const std::string &)->bool
                        {
                          std::cout << build_help ();
                          return false;
                        },
                        // Four spaces for indent (same as d_indent)
                        _("    Print help and exit"));
  }

  inline bool
  parser::add_default_version (void)
  {
    return add_handler ('V', "version", arg_mode::no_argument,
                        [this](const std::string &)->bool
                        {
                          std::cout << version_string;
                          return false;
                        },
                        // Four spaces for indent (same as d_indent)
                        _("    Print version and exit"));
  }

  inline bool
  parser::find_unique_long_name (const std::string &long_name,
                                 std::pair<arg_mode,
                                 std::function<bool(const std::string&)>>
                                 *target,
                                 bool *ambiguous)
  {
    if (long_map.find (long_name) != long_map.end ())
      {
        // Long option name found
        *target = long_map[long_name];
        return true;
      }
    if (abbreviated_long_name)
      {
        // Search abbreviated long option name
        for (decltype (long_map.cbegin()) it =
               long_map.upper_bound (long_name);
             it != long_map.cend ();
             ++it)
          {
            if (it->first.substr (0, long_name.size ()) == long_name)
              {
                auto next_it = it;
                ++next_it;
                if (next_it != long_map.cend () &&
                    next_it->first.substr (0, long_name.size ()) == long_name)
                  {
                    // Failed: ambiguous option
                    *ambiguous = true;
                    return false;
                  }
                // Unique abbreviated long option name fount
                *target = it->second;
                return true;
              }
          }
      }
    // Failed: unknown long option name
    *ambiguous = false;
    return false;
  }

  inline bool
  parser::parse_long_name (std::vector<std::string>::const_iterator *it)
  {
    size_t optchars_pos = (*it)->at(1) == '-' ?
      2: // Double hyphen (--long_name) style
      1; // Single hyphen (-long_name) style
    auto delimiter_pos = (*it)->find ('=');
    std::string long_name = (*it)->substr (optchars_pos,
                                           delimiter_pos - optchars_pos);
    std::string optarg;

    if (delimiter_pos != std::string::npos)
      {
        // Option characters have an option argument
        // (something like --long_name=optarg)
        optarg = (*it)->substr (delimiter_pos + 1, std::string::npos);
      }

    std::pair<arg_mode, std::function<bool(const std::string&)>> target;
    bool ambiguous;
    if (find_unique_long_name (long_name, &target, &ambiguous))
      {
        // Long option name found
        switch (target.first)
          {
          case arg_mode::no_argument:
            if (delimiter_pos != std::string::npos)
              {
                if (!error_extra_arg (long_name, optarg))
                  {
                    abort = abort_reason::error_extra_arg;
                    abort_option = long_name;
                    return false;
                  }
                return true;
              }
            break;
          case arg_mode::required_argument:
            if (delimiter_pos == std::string::npos)
              {
                if ((*it + 1) != argvs.cend ())
                  {
                    // Next argv-element is an option argument
                    // (something like --long_name optarg)
                    optarg = *(++(*it));
                  }
                else
                  {
                    if (!error_no_arg (long_name))
                      {
                        abort = abort_reason::error_no_arg;
                        abort_option = long_name;
                        return false;
                      }
                    return true;
                  }
              }
            break;
          case arg_mode::optional_argument:
            // Nothing to do
            break;
          }
        // Call option handler
        if (!target.second (optarg))
          {
            abort = abort_reason::option_handler;
            abort_option = long_name;
            return false;
          }
        return true;
      }

    // Long option name did not find
    if (optchars_pos == 1)
      {
        // Fallback to short option name
        return parse_short_name (it);
      }
    if (ambiguous)
      {
        if (!error_ambiguous_option ((*it)->substr (optchars_pos,
                                                    std::string::npos)))
          {
            abort = abort_reason::error_ambiguous_option;
            abort_option = long_name;
            return false;
          }
        return true;
      }
    if (!error_unknown_option ((*it)->substr (optchars_pos,
                                              std::string::npos)))
      {
        abort = abort_reason::error_unknown_option;
        abort_option = long_name;
        return false;
      }
    return true;
  }

  inline bool
  parser::parse_short_name (std::vector<std::string>::const_iterator *it)
  {
    for (auto name_it = (*it)->cbegin () + 1;
         name_it != (*it)->cend ();
         ++name_it)
      {
        if (short_map.find (*name_it) != short_map.end())
          {
            // Short option name found
            auto target = short_map[*name_it];
            switch (target.first)
              {
              case arg_mode::no_argument:
                // Option characters doesn't have an option argument
                // (something like -o)
                if (!target.second (""))  // Call option handler
                  {
                    abort = abort_reason::option_handler;
                    abort_option = *name_it;
                    return false;
                  }
                break;
              case arg_mode::required_argument:
                if ((name_it + 1) != (*it)->cend ())
                  {
                    // Option characters have an option argument
                    // (something like -ooptarg)
                    std::string optarg (name_it + 1, (*it)->cend ());
                    if (!target.second (optarg))  // Call option handler
                      {
                        abort = abort_reason::option_handler;
                        abort_option = *name_it;
                        return false;
                      }
                    return true;
                  }
                else if ((*it + 1) != argvs.cend ())
                  {
                    // Next argv-element is an option argument
                    // (something like -o optarg)
                    std::string optarg = *(++(*it));
                    if (!target.second (optarg))  // Call option handler
                      {
                        abort = abort_reason::option_handler;
                        abort_option = *name_it;
                        return false;
                      }
                    return true;
                  }
                else
                  {
                    if (!error_no_arg_short (*name_it))
                      {
                        abort = abort_reason::error_no_arg_short;
                        abort_option = *name_it;
                        return false;
                      }
                  }
                break;
              case arg_mode::optional_argument:
                if ((name_it + 1) != (*it)->cend ())
                  {
                    // Option characters have an option argument
                    // (something like -ooptarg)
                    std::string optarg (name_it + 1, (*it)->cend ());
                    if (!target.second (optarg))  // Call option handler
                      {
                        abort = abort_reason::option_handler;
                        abort_option = *name_it;
                        return false;
                      }
                    return true;
                  }
                // Option characters doesn't have an option argument
                // (something like -o)
                if (!target.second (""))  // Call option handler
                  {
                    abort = abort_reason::option_handler;
                    abort_option = *name_it;
                    return false;
                  }
                break;
              }
          }
        else
          {
            if (!error_unknown_option_short (*name_it))
              {
                abort = abort_reason::error_unknown_option_short;
                abort_option = *name_it;
                return false;
              }
          }
      }
    return true;
  }

  inline bool
  parser::parse (int argc, char const* const* argv, int optind)
  {
    argvs.assign (argv, argv + argc);

    if (version_string.empty ())
      version_string = argvs[0] + "\n";

    bool all_skip = false;

    for (auto argv_it = argvs.cbegin () + optind;
         argv_it != argvs.cend ();
         ++argv_it)
      {
        // Check skip
        if (all_skip)
          {
            unamed_args.push_back (*argv_it);
            continue;
          }

        // Check "--" and "-"
        // They are not option element.
        if (*argv_it == "--")
          {
            all_skip = true;
            continue;
          }
        else if (*argv_it == "-")
          {
            unamed_args.push_back (*argv_it);
            continue;
          }

        // Check option element
        if (argv_it->substr (0, 2) == "--")
          {
            // Long option
            if (!parse_long_name (&argv_it))
              return false;
          }
        else if (argv_it->substr (0, 1) == "-")
          {
            if (long_only)
              {
                // Long option
                if (!parse_long_name (&argv_it))
                  return false;
              }
            else
              {
                // Short option
                if (!parse_short_name (&argv_it))
                  return false;
              }
          }
        else
          {
            // It is not an option element.
            unamed_args.push_back (*argv_it);
          }
      }
    return true;
  }

  inline std::string
  parser::build_usage (void) const
  {
    std::stringstream ss;

    ss << _("Usage: ") << argvs[0] << _(" [options] ");
    if (!usage_unamed_opts.empty ())
      ss << "[" << usage_unamed_opts << "] ";
    ss << "..." << std::endl;

    return ss.str ();
  }

  inline std::string
  parser::build_help (void) const
  {
    std::stringstream ss;

    ss << version_string << std::endl << build_usage () << std::endl;

    for (const auto &group: help_map)
      {
        if (!group.first.empty ())
          ss << std::endl << group.first << ":" << std::endl;
        for (const auto &description: group.second)
          {
            ss << description;
          }
      }

    return ss.str ();
  }

}

#if defined (_) && defined (CMDLINEPARSE_HH_DEFINE_DUMMY_GETTEXT)
#undef _
#undef CMDLINEPARSE_HH_DEFINE_DUMMY_GETTEXT
#endif

#endif
