/*
* (C) 2015,2017 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#ifndef BOTAN_CLI_ARGPARSE_H_
#define BOTAN_CLI_ARGPARSE_H_

#include <string>
#include <map>
#include <set>
#include <vector>
#include <botan/parsing.h>
#include "cli_exceptions.h"

namespace Botan_CLI {

class Argument_Parser final
   {
   public:
      Argument_Parser(const std::string& spec,
                      const std::vector<std::string>& extra_flags = {},
                      const std::vector<std::string>& extra_opts = {});

      void parse_args(const std::vector<std::string>& params);

      bool flag_set(const std::string& flag) const;

      bool has_arg(const std::string& opt_name) const;
      std::string get_arg(const std::string& option) const;

      std::string get_arg_or(const std::string& option, const std::string& otherwise) const;

      size_t get_arg_sz(const std::string& option) const;

      std::vector<std::string> get_arg_list(const std::string& what) const;

   private:
      // set in constructor
      std::vector<std::string> m_spec_args;
      std::set<std::string> m_spec_flags;
      std::map<std::string, std::string> m_spec_opts;
      std::string m_spec_rest;

      // set in parse_args()
      std::map<std::string, std::string> m_user_args;
      std::set<std::string> m_user_flags;
      std::vector<std::string> m_user_rest;
   };

bool Argument_Parser::flag_set(const std::string& flag_name) const
   {
   return m_user_flags.count(flag_name) > 0;
   }

bool Argument_Parser::has_arg(const std::string& opt_name) const
   {
   return m_user_args.count(opt_name) > 0;
   }

std::string Argument_Parser::get_arg(const std::string& opt_name) const
   {
   auto i = m_user_args.find(opt_name);
   if(i == m_user_args.end())
      {
      // this shouldn't occur unless you passed the wrong thing to get_arg
      throw CLI_Error("Unknown option " + opt_name + " used (program bug)");
      }
   return i->second;
   }

std::string Argument_Parser::get_arg_or(const std::string& opt_name, const std::string& otherwise) const
   {
   auto i = m_user_args.find(opt_name);
   if(i == m_user_args.end() || i->second.empty())
      {
      return otherwise;
      }
   return i->second;
   }

size_t Argument_Parser::get_arg_sz(const std::string& opt_name) const
   {
   const std::string s = get_arg(opt_name);

   try
      {
      return static_cast<size_t>(std::stoul(s));
      }
   catch(std::exception&)
      {
      throw CLI_Usage_Error("Invalid integer value '" + s + "' for option " + opt_name);
      }
   }

std::vector<std::string> Argument_Parser::get_arg_list(const std::string& what) const
   {
   if(what == m_spec_rest)
      return m_user_rest;

   return Botan::split_on(get_arg(what), ',');
   }

void Argument_Parser::parse_args(const std::vector<std::string>& params)
   {
   std::vector<std::string> args;
   for(auto const& param : params)
      {
      if(param.find("--") == 0)
         {
         // option
         const auto eq = param.find('=');

         if(eq == std::string::npos)
            {
            const std::string opt_name = param.substr(2, std::string::npos);

            if(m_spec_flags.count(opt_name) == 0)
               {
               if(m_spec_opts.count(opt_name))
                  {
                  throw CLI_Usage_Error("Invalid usage of option --" + opt_name +
                                        " without value");
                  }
               else
                  {
                  throw CLI_Usage_Error("Unknown flag --" + opt_name);
                  }
               }
            m_user_flags.insert(opt_name);
            }
         else
            {
            const std::string opt_name = param.substr(2, eq - 2);
            const std::string opt_val = param.substr(eq + 1, std::string::npos);

            if(m_spec_opts.count(opt_name) == 0)
               {
               throw CLI_Usage_Error("Unknown option --" + opt_name);
               }

            m_user_args.insert(std::make_pair(opt_name, opt_val));
            }
         }
      else
         {
         // argument
         args.push_back(param);
         }
      }

   if(flag_set("help"))
      return;

   if(args.size() < m_spec_args.size())
      {
      // not enough arguments
      throw CLI_Usage_Error("Invalid argument count, got " +
                            std::to_string(args.size()) +
                            " expected " +
                            std::to_string(m_spec_args.size()));
      }

   bool seen_stdin_flag = false;
   size_t arg_i = 0;
   for(auto const& arg : m_spec_args)
      {
      m_user_args.insert(std::make_pair(arg, args[arg_i]));

      if(args[arg_i] == "-")
         {
         if(seen_stdin_flag)
            {
            throw CLI_Usage_Error("Cannot specify '-' (stdin) more than once");
            }
         seen_stdin_flag = true;
         }

      ++arg_i;
      }

   if(m_spec_rest.empty())
      {
      if(arg_i != args.size())
         {
         throw CLI_Usage_Error("Too many arguments");
         }
      }
   else
      {
      m_user_rest.assign(args.begin() + arg_i, args.end());
      }

   // Now insert any defaults for options not supplied by the user
   for(auto const& opt : m_spec_opts)
      {
      if(m_user_args.count(opt.first) == 0)
         {
         m_user_args.insert(opt);
         }
      }
   }

Argument_Parser::Argument_Parser(const std::string& spec,
                                 const std::vector<std::string>& extra_flags,
                                 const std::vector<std::string>& extra_opts)
   {
   class CLI_Error_Invalid_Spec final : public CLI_Error
      {
      public:
         explicit CLI_Error_Invalid_Spec(const std::string& bad_spec)
            : CLI_Error("Invalid command spec '" + bad_spec + "'") {}
      };

   const std::vector<std::string> parts = Botan::split_on(spec, ' ');

   if(parts.size() == 0)
      {
      throw CLI_Error_Invalid_Spec(spec);
      }

   for(size_t i = 1; i != parts.size(); ++i)
      {
      const std::string s = parts[i];

      if(s.empty()) // ?!? (shouldn't happen)
         {
         throw CLI_Error_Invalid_Spec(spec);
         }

      if(s.size() > 2 && s[0] == '-' && s[1] == '-')
         {
         // option or flag

         auto eq = s.find('=');

         if(eq == std::string::npos)
            {
            m_spec_flags.insert(s.substr(2, std::string::npos));
            }
         else
            {
            m_spec_opts.insert(std::make_pair(s.substr(2, eq - 2), s.substr(eq + 1, std::string::npos)));
            }
         }
      else if(s[0] == '*')
         {
         // rest argument
         if(m_spec_rest.empty() && s.size() > 2)
            {
            m_spec_rest = s.substr(1, std::string::npos);
            }
         else
            {
            throw CLI_Error_Invalid_Spec(spec);
            }
         }
      else
         {
         // named argument
         if(!m_spec_rest.empty()) // rest arg wasn't last
            {
            throw CLI_Error_Invalid_Spec(spec);
            }

         m_spec_args.push_back(s);
         }
      }

   for(std::string flag : extra_flags)
      m_spec_flags.insert(flag);
   for(std::string opt : extra_opts)
      m_spec_opts.insert(std::make_pair(opt, ""));
   }


}

#endif
