//
// 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