/*! @file

    @id $Id$
*/
//       1         2         3         4         5         6         7         8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
#ifndef __MRW_ARGS__
#define __MRW_ARGS__

#include <mrw/checkcxx11.hxx>
#include <mrw/iomanip.hxx>

#include <vector>
#include <map>
#include <memory>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <numeric>
#include <cstdlib> // exit()

/** @page oldcompiler Workaround for old non C++11 compilers
    ... to be documented
    @note Old compilers are automatically detected and the flag
          @ref MRW__OLD_PRE11_COMPILER is set.
    */

// Add version information for @c what and @c ident
#if defined(PACKAGE_NAME) && defined(PACKAGE_VERSION)
const std::string MRW_IDENT("$Id: " PACKAGE_NAME "-" PACKAGE_VERSION " $");
const std::string MRW_WHAT("#(@)" PACKAGE_NAME "-" PACKAGE_VERSION);
#else
# warning pass -DPACKAGE_NAME=name -DPACKAGE_VERSION=version for ident/what
#endif

namespace mrw {
  /// Cool and easy evaluation of commandline arguments in C++11.
  /** Evaluating command line arguments is extremely easy with this library:
      @begincode
        void test_func();                // bound to option --test
      
        int main(int argc, char** argv) try {
      
          // option variables
          bool flag;                      // bound to option --flag
          int r(5);                       // bound to option --repeat
          std::string txt("Hello World"); // bound to option --text
          int x(0), y(0);                 // bound to option --coord

          // bind and evaluate options
          args::parse(argc, argv, "This is an example for argument processing.",
                      {
            {"h", "help", "show this help", {args::help(), args::exit()}},
            {"v", "version", "show version", {args::version(), args::exit()}},
            {"r", "repeat", "number of repetitions",
             {args::param(r, "number")}},
            {"f", "flag", "check a flag", {args::param(flag)}},
            {"t", "text", "pass a text", {args::param(txt, "text")}},
            {"c", "coord", "add some 2D coordinates", {args::param(x, "x"),
                                                       args::param(y, "y")}},
            {"",  "test", "call test_func, no shortcut",
             {args::func(test_func)}},
          });

          // [...] now the variables are setup according to the user's choice

          return 0;
        } catch (const std::exception& x) { // error in commandline options
          std::cerr<<"**** ERROR: "<<x.what()<<std::endl;
          args::show_help(); // display help
          return 1;
        }
      @endcode
      @note This library requires C++11, but there is also a fallback
            for old compilers. But the calling syntax is much worse on
            non C++11 compliant compilers, so the use of a modern
            compiler is strongly recommended. On GNU GCC, you may have
            to add the commandline option @c -std=c++11
      @note I suggest to take this library into the next C++ standard. */
  namespace args {
    /// Parent for holding a reference to a parameter variable.
    /** This class ist used only as parent for instances of template
        parameters.  It represents one parameter that is evaluated when
        a commandline option is given. The derived classes hold a
        reference to a variable that is set in method @ref evaluate when
        the specific commandline parameter is given. */
    class abstract_parameter {
      public:
        /// Called when the user specifies the related commandline option.
        /** Method must be implemented according to the specific type in
            the child classes. */
        virtual void evaluate(char**& a, char** max) = 0;
        /// Convert the actual value to a string.
        /** Defaults to empty string, can be overwritten in child
            classes. Used by the help function to show the default
            value. */
        virtual operator std::string() {
          return std::string();
        }
        virtual std::string type() {
          return std::string();
        }
    };
    // default implementation for all types
    template<typename T> class parameter: public abstract_parameter {
      public:
        parameter(T& ref, const std::string& s = std::string()):
            _ref(ref), _type(s) {}
        virtual void evaluate(char**& a, char** max) {
          if (max<a+1)
            throw std::runtime_error("missing parameter for: "+std::string(*a));
          std::stringstream ss;
          ss<<*++a;
          ss>>_ref;
        }
        virtual operator std::string() {
          std::stringstream ss;
          ss<<_ref;
          return ss.str();
        }
        virtual std::string type() {
          return _type.size()?" <"+_type+">":"";
        }
      private:
        T& _ref;
        std::string _type;
    };
    // special case: boolean has no parameter
    template<> class parameter<bool>: public abstract_parameter {
      public:
        parameter(bool& ref, const std::string& = std::string()):
            _ref(ref=false) {
        }
        virtual void evaluate(char**&, char**) {
          _ref = true;
        }
      private:
        bool& _ref;
    };
    // special case: string cannot easily be shifted
    template<typename T> class parameter<std::basic_string<T> >:
        public abstract_parameter {
      public:
        parameter(std::basic_string<T>& ref,
                  const std::string& s = std::string()):
            _ref(ref), _type(s) {}
        virtual void evaluate(char**& a, char** max) {
          if (max<a+1)
            throw std::runtime_error("missing parameter for: "+std::string(*a));
          std::stringstream ss;
          ss<<*++a;
          _ref = std::basic_string<T>(std::istreambuf_iterator<T>(ss),
                                      std::istreambuf_iterator<T>());
        }
        virtual operator std::string() {
          std::stringstream ss;
          ss<<_ref;
          return ss.str();
        }
        virtual std::string type() {
          return _type.size()?" <"+_type+">":"";
        }
      private:
        std::basic_string<T>& _ref;
        std::string _type;
    };
    // special case: if a function is called
    template<> class parameter<void(*)()>: public abstract_parameter {
      public:
        parameter(void(*ref)(), const std::string& = std::string()):
            _ref(ref) {
        }
        virtual void evaluate(char**&, char**) {
          _ref();
        }
      private:
        void(*_ref)();
    };
    typedef std::shared_ptr<abstract_parameter> param_ptr;
    template<typename T>
        param_ptr param(T& t, const std::string& s = std::string()) {
      return param_ptr(new args::parameter<T>(t, s));
    }
    struct decl {
        typedef std::vector<param_ptr> param_list;
        decl(std::string p1, std::string p2, std::string p3, param_list p4):
            short_arg(p1), long_arg(p2), desc(p3), params(p4) {
        }
        std::string short_arg;
        std::string long_arg;
        std::string desc;
        param_list params;
    };
    typedef std::map<std::string, decl::param_list> arg_map;
    static arg_map& args() {
      static arg_map a;
      return a;
    }
    typedef std::vector<decl> list;
    static list& arg_list() {
      static list a;
      return a;
    }
    void match(const std::string& arg, char**& a, char** max) {
#ifdef MRW__OLD_PRE11_COMPILER
      arg_map::iterator it(args().find(arg));
#else
      auto it(args().find(arg));
#endif
      if (it==args().end())
        throw std::runtime_error("unknown argument: "+std::string(*a));
#ifdef MRW__OLD_PRE11_COMPILER
      for (decl::param_list::iterator it2(it->second.begin());
           it2!=it->second.end(); ++it2)
#else
      for (auto it2(it->second.begin()); it2!=it->second.end(); ++it2)
#endif
        (*it2)->evaluate(a, max);
    }
    /// Filename as passed in argv[0].
    /** Used in the help display.
        @note This function emulates a global variable using parts of a
              singleton pattern. */
    static std::string filename(const std::string& arg0 = std::string()) {
      static std::string file(arg0);
      return file;
    }
    /// Description of the program.
    /** Used in the help display.
        @note This function emulates a global variable using parts of a
              singleton pattern. */
    static std::string description(const std::string& desc = std::string()) {
      static std::string d(desc);
      return d;
    }
    /// Return values of the program.
    /** Used in the help display.
        @note This function emulates a global variable using parts of a
              singleton pattern. */
    static std::string returns(const std::string& ret = std::string()) {
      static std::string r(ret);
      return r;
    }
    /// Initialize all parameters according to the commandline options.
    /** Sets up the parser from the @ref list of parameters
        @param argc the argument count as given in C++ @c main function
        @param argv the array of arguments as given in C++ @c main function
        @param desc a string describing what the program does,
                    used in @ref show_help
        @param l    list of options and parameters to be parsed
        @param ret  documentation of the return values of the program */
    static void parse(int argc, char** argv, const std::string& desc, list l,
                      const std::string& ret = std::string()) {
      filename(argv[0]); // store filename for later use in help
      description(desc); // store program description for later use in help
      returns(ret);      // store return documentation for later use in help
      arg_list() = l;    // store options and parameters for later use in help
      // setup the argument mapping table
#ifdef MRW__OLD_PRE11_COMPILER
      for (list::iterator it(l.begin()); it!=l.end(); ++it)
#else
      for (auto it(l.begin()); it!=l.end(); ++it)
#endif
        {
          if (it->short_arg.size()==1) args()[it->short_arg] = it->params;
          if (it->long_arg.size()) args()["--"+it->long_arg] = it->params;
        }
      // parse commandline and evaluate the arguments
#ifdef MRW__OLD_PRE11_COMPILER
      for (char** a(argv+1); a<argv+argc; ++a)
#else
      for (auto a(argv+1); a<argv+argc; ++a)
#endif
        {
          std::string arg(*a);
          if (arg.size()>1&&arg[0]=='-'&&arg[1]!='-') { // short argument
#ifdef MRW__OLD_PRE11_COMPILER
            for (std::string::iterator it(arg.begin()+1); it!=arg.end(); ++it)
#else
            for (auto it(arg.begin()+1); it!=arg.end(); ++it)
#endif
              match(std::string(1, *it), a, argv+argc);
          } else { // long argument or wrong argument
            match(arg, a, argv+argc);
          }
        }
    }
    void show_help(const std::string& synopsis_txt="SYNOPSIS",
                   const std::string& description_txt="DESCRIPTION",
                   const std::string& options_txt="OPTIONS",
                   const std::string& returns_txt="RETURNS",
                   unsigned int max_line=80, unsigned int indent=2,
                   unsigned int long_indent=4,
                   unsigned int option_len=16, unsigned int param_len=21) {
      std::cout<<synopsis_txt<<std::endl<<std::endl
               <<"  "<<filename()<<" ["<<options_txt<<"]"<<std::endl<<std::endl
               <<description_txt<<std::endl<<std::endl
               <<ssplit(max_line, indent)<<description(); //! @bug
      std::cout<<std::endl<<std::endl
               <<options_txt<<std::endl;
#ifdef MRW__OLD_PRE11_COMPILER
      for (list::iterator arg(arg_list().begin()); arg!=arg_list().end(); ++arg)
#else
      for (auto arg(arg_list().begin()); arg!=arg_list().end(); ++arg)
#endif
        {
          std::string o((arg->short_arg.size()?"-"+arg->short_arg:"")+
                        (arg->short_arg.size()&&arg->long_arg.size()?", ":"")+
                        (arg->long_arg.size()?"--"+arg->long_arg:""));
#ifdef MRW__OLD_PRE11_COMPILER
          std::string a;
          for (decl::param_list::iterator p(arg->params.begin());
               p!=arg->params.end(); ++p) {
            std::string def(**p);
            a+=(*p)->type()+(def.size()?"="+def:"");
          }
#else
          std::string a(std::accumulate(arg->params.begin(), arg->params.end(),
                                        std::string(),
                                        [](const std::string& s,
                                           param_ptr p)
                                        -> std::string {
                                          std::string def(*p);
                                          return
                                            s+p->type()+(def.size()?"="+def:"");
                                        }));
#endif
        std::cout<<std::endl<<"  "
                 <<o<<std::setw(int(option_len-o.size()))<<' '
                 <<a;
        if (arg->desc.size()>max_line-indent-option_len-param_len)
          std::cout<<std::endl<<std::endl<<ssplit(max_line, long_indent)
                   <<arg->desc;
        else
          std::cout<<std::setw(int(param_len-a.size()))<<' '<<arg->desc;
        std::cout<<std::endl;
        }
      if (returns().size()) {
        std::cout<<std::endl<<returns_txt<<std::endl<<std::endl
                 <<ssplit(max_line, indent)<<returns();
        std::cout<<std::endl;
      }
    }
    /// @return version information
    std::string version_text() {
#     ifdef PACKAGE_VERSION
#       ifdef PACKAGE_NAME
          std::string v(": " PACKAGE_NAME "-" PACKAGE_VERSION);
#       else
          std::string v("-" PACKAGE_VERSION);
#       endif
#     else
#       ifdef PACKAGE_NAME
          std::string v(": " PACKAGE_NAME);
#       else
          std::string v;
#       endif
#     endif
      return filename() + v;
    }
    void show_version() {
      std::cout<<version_text()<<std::endl;
    }
    void help_no_arg() {
      show_help();
    }
    void do_exit() {
      exit(1);
    }
    param_ptr func(void(*fn)()) {
      return param_ptr(new args::parameter<void(*)()>(fn));
    }
    param_ptr help() {
      return func(args::help_no_arg);
    }
    param_ptr version() {
      return func(args::show_version);
    }
    param_ptr exit() {
      return func(args::do_exit);
    }
    /// Sets up an argument list containing help and version.
    list defaults() {
#ifdef MRW__OLD_PRE11_COMPILER
      list res;
      decl::param_list h;
      h.push_back(help());
      h.push_back(exit());
      decl::param_list v;
      v.push_back(version());
      v.push_back(exit());
      res.push_back(decl("h", "help", "show help", h));
      res.push_back(decl("v", "version", "show version", v));
      return res;
#else // New C++ standard C++11 is great:
      return list({
          {"h", "help", "show help", {help(), exit()}},
          {"v", "version", "show version", {version(), exit()}}
        });
#endif
    }
  }
}
#endif
