/* args.cc - function implementations of args.hh    -*- C++ -*-
 * Copyright 2003-2010 Bas Wijnen <wijnen@debian.org>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <unistd.h>
#include <fstream>
#include "iostring.hh"
#include "debug.hh"
#include "args.hh"
#include "error.hh"

namespace shevek
{
  args::args (int &argc, char **&argv, int min_args, int max_args,
	      Glib::ustring const &description,
	      Glib::ustring const &copyright_years,
	      Glib::ustring const &copyright_email,
	      Glib::ustring const &programmer,
	      Glib::ustring const &email, char const *programname,
	      char const *packagename, char const *version)
  {
    // don't put this structure in l_setup, because it has variable length
    struct ::option longopts[4];
    l_setup (argc, argv, 0, 0, min_args, max_args, description,
	     longopts, copyright_years, copyright_email, programmer, email,
	     programname, packagename, version);
  }

  void args::l_setup (int &argc, char **&argv,
		      option *o, unsigned num_options,
		      int min_args, int max_args,
		      Glib::ustring const &description,
		      struct ::option *longopts,
		      Glib::ustring const &copyright_years,
		      Glib::ustring const &copyright_email,
		      Glib::ustring const &programmer,
		      Glib::ustring const &email,
		      char const *programname,
		      char const *packagename,
		      char const *version)
  {
    startfunc;
    int dummy; // to make sure the returnvalue is 0 for long options.
    Glib::ustring optstring = "h";
    // Every progam must have a --help argument.
    longopts[num_options].name = "help";
    longopts[num_options].has_arg = no_argument;
    longopts[num_options].flag = 0;
    longopts[num_options].val = 'h';
    // Every progam must have a --version argument.
    longopts[num_options + 1].name = "version";
    longopts[num_options + 1].has_arg = no_argument;
    longopts[num_options + 1].flag = &dummy;
    longopts[num_options + 1].val = 0;
    // Every program must have a --configfile argument.
    longopts[num_options + 2].name = "configfile";
    longopts[num_options + 2].has_arg = required_argument;
    longopts[num_options + 2].flag = &dummy;
    longopts[num_options + 2].val = 0;
    // The last element must mark the end of the array.
    ::memset (&longopts[num_options + 3], 0, sizeof (*longopts));
    for (unsigned i = 0; i < num_options; ++i)
      {
	if (o[i].m_shortopt)
	  {
	    optstring += o[i].m_shortopt;
	    if (o[i].m_have_arg != option::NO_ARG) optstring += ':';
	    if (o[i].m_have_arg == option::OPT_ARG) optstring += ':';
	  }
	longopts[i].name = o[i].m_longopt.c_str ();
	switch (o[i].m_have_arg)
	  {
	  case option::NO_ARG:
	    longopts[i].has_arg = no_argument;
	    break;
	  case option::OPT_ARG:
	    longopts[i].has_arg = optional_argument;
	    break;
	  case option::NEED_ARG:
	    longopts[i].has_arg = required_argument;
	  }
	longopts[i].flag = &dummy;
	longopts[i].val = 0;
      }
    int opt;
    std::string filename;
    while (true)
      {
	char nul = '\0';
	optarg = &nul;
	int idx;
	opt = getopt_long (argc, argv, optstring.c_str (), longopts, &idx);
	if (opt == -1)
	  break;
	if (opt == ':')
	  {
	    shevek_error ("invalid option definition");
	    return;
	  }
	if (opt == '?')
	  {
	    shevek_error ("parse error in option list (try --help)");
	    return;
	  }
	if (opt == 'h')
	  {
	    // handle --help internally
	    std::cout << programname;
	    if (Glib::ustring (programname) != packagename)
		std::cout << " from " << packagename;
	    else
		std::cout << programname;
	    std::cout << ", version " << version << "\n\n" << description
		      << "\n"
	      "Possible options are:\n"
	      "-h\t--help\t\tShow this help and exit.\n"
	      "\t--version\tShow version information and exit.\n"
	      "\t--configfile\tUse a specific config file.\n";
	    for (unsigned i = 0; i < num_options; ++i)
	      {
		if (o[i].m_shortopt != 0)
		  std::cout << "-" << o[i].m_shortopt;
		unsigned len = o[i].m_longopt.length ();
		std::cout << "\t--" << o[i].m_longopt << '\t';
		if (len < 6) std::cout << '\t';
		std::cout << o[i].m_help;
		if (o[i].m_have_default)
		  std::cout << " [" << o[i].m_default << ']';
		std::cout << '\n';
	      }
	    std::cout << "\nPlease send bug reports and other comments to <" << email << ">\n";
	    exit (0);
	  }
	if (opt == 0 && unsigned (idx) == num_options + 1)
	  {
	    // handle --version internally
	    std::cout << programname << " from " << packagename << ", version "
		      << version << "\n\n" << "Copyright (C) "
		      << copyright_years << ' ' << programmer << " <"
		      << copyright_email << ">\n\n"
		"This program is free software: you can redistribute it and/or modify\n"
		"it under the terms of the GNU General Public License as published by\n"
		"the Free Software Foundation, either version 3 of the License, or\n"
		"(at your option) any later version.\n"
		"\n"
		"This program is distributed in the hope that it will be useful,\n"
		"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
		"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
		"GNU General Public License for more details.\n"
		"\n"
		"You should have received a copy of the GNU General Public License\n"
		"along with this program.  If not, see <http://www.gnu.org/licenses/>.\n"
		"\n"
		"\nPlease send bug reports and other comments to <" << email << ">\n";
	    exit (0);
	  }
	if (opt == 0 && unsigned (idx) == num_options + 2)
	  {
	    // Handle --configfile internally.
	    filename = optarg;
	    continue;
	  }
	if (opt == 0) // long option
	  {
	    o[idx].call (false, optarg);
	    continue;
	  }
	for (unsigned i = 0; i < num_options; ++i)
	  {
	    if (opt == o[i].m_shortopt)
	      {
		o[i].call (false, optarg);
		break;
	      }
	  }
      }
    if (optind > 1)
      {
	char *tmp = argv[0];
	argv[0] = argv[optind - 1];
	argv[optind - 1] = tmp;
	argv += optind - 1;
	argc -= optind - 1;
      }
    if (argc - 1 < min_args || (max_args >= min_args && argc - 1 > max_args) )
      {
	if (min_args < max_args)
	  shevek_error (ostring ("invalid number of non-option arguments %d.  "
			   "Must be between %d and %d.", argc - 1, min_args,
			   max_args));
	else if (min_args == max_args)
	  shevek_error (ostring ("invalid number of non-option arguments %d.  "
			   "Must be %d.", argc - 1, min_args));
	else
	  shevek_error (ostring ("invalid number of non-option arguments %d.  "
			   "Must be at least %d.", argc - 1, min_args));
      }
    for (int i = 1; i < argc; ++i)
      {
	m_args.push_back (argv[i]);
      }
    // Read the configuration file.
    bool use_default_filename = false;
    if (filename.empty ())
      {
	std::vector <std::string> path (2);
	path[0] = Glib::get_user_config_dir ();
	path[1] = programname;
	filename = Glib::build_filename (path);
	use_default_filename = true;
      }
    std::ifstream file (filename.c_str ());
    if (!use_default_filename && !file)
      shevek_error (shevek::ostring ("specified configuration file not found: %s", Glib::ustring (filename)));
    std::string line;
    while (std::getline (file, line))
      {
	shevek::ristring s (line);
	std::string key;
	if (s (" #") || s (" %"))
	  continue;
	if (s (" %[^ \t=] =", key))
	  {
	    unsigned i;
	    for (i = 0; i < num_options; ++i)
	      {
		if (key == o[i].m_longopt)
		  {
		    if (o[i].m_have_arg == option::NO_ARG)
		      shevek_warning (shevek::ostring ("config file %s: option %s doesn't accept an argument", Glib::ustring (filename), Glib::ustring (key)));
		    o[i].call (o[i].m_is_used, s.rest ().c_str ());
		    break;
		  }
	      }
	    if (i >= num_options)
	      shevek_warning (shevek::ostring ("config file %s: unknown option %s", Glib::ustring (filename), Glib::ustring (key)));
	  }
	else if (s (" %s %", key))
	  {
	    unsigned i;
	    for (i = 0; i < num_options; ++i)
	      {
		if (key == o[i].m_longopt)
		  {
		    if (o[i].m_have_arg == option::NEED_ARG)
		      shevek_warning (shevek::ostring ("config file %s: option %s requires an argument", Glib::ustring (filename), Glib::ustring (key)));
		    o[i].call (o[i].m_is_used, NULL);
		    break;
		  }
	      }
	    if (i >= num_options)
	      shevek_warning (shevek::ostring ("config file %s: unknown option %s", Glib::ustring (filename), Glib::ustring (key)));
	  }
      }
  }

  unsigned args::size () const
  {
    startfunc;
    return m_args.size ();
  }

  std::string const &args::operator[] (unsigned idx) const
  {
    startfunc;
    if (idx > m_args.size () ) throw "index out of range";
    return m_args[idx];
  }

  std::vector <std::string>::const_iterator args::begin () const
  {
    startfunc;
    return m_args.begin ();
  }

  std::vector <std::string>::const_iterator args::end () const
  {
    startfunc;
    return m_args.end ();
  }

  void args::option::setup (char shortopt, Glib::ustring const &longopt,
			    Glib::ustring const &help_line, bool have_default,
			    opt_t have_arg, callback0 handle0,
			    callback1 handle1, bool *used)
  {
    startfunc;
    m_shortopt = shortopt;
    m_longopt = longopt;
    m_help = help_line;
    m_have_default = have_default;
    m_have_arg = have_arg;
    m_handle0 = handle0;
    m_handle1 = handle1;
    m_is_used = false;
    m_used = used;
    if (m_used)
	    *m_used = false;
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, callback0 handle, bool *used)
  {
    startfunc;
    setup (shortopt, longopt, help_line, false, NO_ARG, handle, callback1 (), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, callback1 handle, Glib::ustring default_val, bool *used)
  {
    startfunc;
    m_default = default_val;
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), handle, used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, callback0 handle0, callback1 handle1, bool *used)
  {
    startfunc;
    setup (shortopt, longopt, help_line, false, OPT_ARG, handle0, handle1, used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool &var, bool value, bool *used)
  {
    startfunc;
    setup (shortopt, longopt, help_line, false, NO_ARG, sigc::bind (sigc::bind (sigc::ptr_fun (&l_setbool), &var), value), callback1 (), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, Glib::ustring &var, bool *used)
  {
    startfunc;
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_set), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, std::string &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = var;
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setstd), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, unsigned long &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setulint), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, long &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setlint), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, unsigned &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setuint), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, int &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setint), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, unsigned short &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setusint), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, short &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setsint), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, float &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setfloat), &var), used);
  }

  args::option::option (char shortopt, Glib::ustring const &longopt, Glib::ustring const &help_line, bool have_default, double &var, bool *used)
  {
    startfunc;
    if (have_default)
      m_default = ostring ("%d", var);
    setup (shortopt, longopt, help_line, have_default, NEED_ARG, callback0 (), sigc::bind (sigc::ptr_fun (&l_setdfloat), &var), used);
  }

  void args::option::call (bool is_double, char const *arg)
  {
    startfunc;
    m_is_used = true;
    if (m_used)
	    *m_used = true;
    switch (m_have_arg)
      {
      case NO_ARG:
	m_handle0 (is_double);
	break;
      case NEED_ARG:
	m_handle1 (is_double, arg);
	break;
      case OPT_ARG:
	if (arg)
	  m_handle1 (is_double, arg);
	else
	  m_handle0 (is_double);
      }
  }

  void args::option::l_set (bool is_double, Glib::ustring const &arg, Glib::ustring *var)
  {
    startfunc;
    if (!is_double)
      *var = arg;
  }

  void args::option::l_setstd (bool is_double, Glib::ustring const &arg, std::string *var)
  {
    startfunc;
    if (!is_double)
      *var = arg;
  }

  void args::option::l_setlint (bool is_double, Glib::ustring const &arg, long *var)
  {
    startfunc;
    char *ep;
    if (!is_double)
      {
	*var = strtol (arg.c_str (), &ep, 0);
	if (*ep != 0) throw "invalid argument to option expecting long";
      }
  }

  void args::option::l_setulint (bool is_double, Glib::ustring const &arg, unsigned long *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	*var = strtoul (arg.c_str (), &ep, 0);
	if (*ep != 0) throw "invalid argument to option expection unsigned long";
      }
  }

  void args::option::l_setint (bool is_double, Glib::ustring const &arg, int *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	long tmp;
	tmp = strtol (arg.c_str (), &ep, 0);
	if (*ep != 0) throw "invalid argument to option expecting long";
	*var = tmp;
      }
  }

  void args::option::l_setuint (bool is_double, Glib::ustring const &arg, unsigned *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	unsigned long tmp;
	tmp = strtoul (arg.c_str (), &ep, 0);
	if (*ep != 0) throw "invalid argument to option expecting unsigned long";
	*var = tmp;
      }
  }

  void args::option::l_setsint (bool is_double, Glib::ustring const &arg, short *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	long tmp;
	tmp = strtol (arg.c_str (), &ep, 0);
	if (*ep != 0) throw "invalid argument to option expecting long";
	*var = tmp;
      }
  }

  void args::option::l_setusint (bool is_double, Glib::ustring const &arg, unsigned short *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	unsigned long tmp;
	tmp = strtoul (arg.c_str (), &ep, 0);
	if (*ep != 0) throw "invalid argument to option expecting unsigned long";
	*var = tmp;
      }
  }

  void args::option::l_setbool (bool is_double, bool val, bool *var)
  {
    startfunc;
    if (!is_double)
      *var = val;
  }

  void args::option::l_setfloat (bool is_double, Glib::ustring const &arg, float *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	*var = strtof (arg.c_str (), &ep);
	if (*ep != 0) throw "invalid argument to option expecting float";
      }
  }

  void args::option::l_setdfloat (bool is_double, Glib::ustring const &arg, double *var)
  {
    startfunc;
    if (!is_double)
      {
	char *ep;
	*var = strtod (arg.c_str (), &ep);
	if (*ep != 0) throw "invalid argument to option expecting double";
      }
  }
}
