/* argp.pk - Argp like interface for Poke programs.  */

/* Copyright (C) 2020, 2021, 2022, 2023 Jose E. Marchesi */

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

type Argp_Option_Handler = (string)void;

type Argp_Option =
  struct
  {
    string name;
    string long_name;
    string summary;
    string arg_name;
    int arg_required;
    Argp_Option_Handler handler;
  };

fun argp_do_help = (string program, string summary,
                    Argp_Option[] opts) void:
{
  printf "Usage: %s [OPTION]...\n", program;

  if (summary != "")
     printf "%s\n", summary;

  if (opts'length > 0)
   {
     print "\nOptions:\n";

     for (opt in opts)
       printf ("  %s%s%s%s%s\n",
               opt.name != "" ? "-" + opt.name : "",
               opt.name != "" && opt.long_name != "" ? ", " : "    ",
               opt.long_name != "" ? "--" + opt.long_name : "",
               opt.arg_required ?
                  (opt.arg_name == "" ? "=ARG\t" : "=" + opt.arg_name + "\t") : "\t\t",
               opt.summary);
   }

  exit (0);
}

fun argp_do_version = (string program, string version) void:
{
  printf "%s%s\n", program, version != "" ? " " + version : "";
  exit (0);
}

fun argp_parse = (string program,
                  string version = "",
                  string summary = "",
                  Argp_Option[] opts = Argp_Option[](),
                  string[] argv = string[](),
                  int allow_unknown = 0) string[]:
{
  var rest = string[]();
  var found_dashdash = 0;

  /* Add some default options.  Note that user-provided options with
     the same name will take precedence.  */
  opts += [Argp_Option {
               long_name = "help",
               handler = lambda (string val) void: { argp_do_help (program,
                                                                   summary,
                                                                   opts); },
               summary = "print a help message and exit"
           },
           Argp_Option {
               long_name = "version",
               handler = lambda (string val) void: { argp_do_version (program,
                                                                      version); },
               summary = "show version and exit"
           },
          ];

  fun getopt = (string arg) Argp_Option:
  {
    for (opt in opts)
    {
      if ((arg[0] == '-' && arg[1:] == opt.long_name)
          || (arg == opt.name))
        return opt;
    }

    if (allow_unknown)
      rest += ["-" + arg];
    else
      raise Exception {code = EC_generic,
                       name = "unknown option -" + arg};
  }

  for (var i = 0; i < argv'length; i++)
  {
    var arg = argv[i];

    if (arg == "--" && !found_dashdash)
     {
       found_dashdash = 1;
       continue;
     }

    if (found_dashdash || arg[0] != '-' || arg'length == 1)
      rest += [arg];
    else
    {
      if (arg[1] == '-')
        {
          /* This is one long option.  It may have an argument.  */
          var eqidx = strchr (arg, '=');
          var optname = arg[1:eqidx];
          var optarg_p = eqidx < arg'length;
          var optarg = eqidx < (arg'length - 1) ? arg[eqidx+1:] : "";
          var opt = getopt (optname);

          if (optarg_p && !opt.arg_required)
             raise Exception {code = EC_inval,
                              name = "option " + arg + " doesn't require an argument"};

          if (opt.arg_required)
             {
               if (!optarg_p)
                  raise Exception {code = EC_inval,
                                   name = "option " + arg + " requires an argument"};
                opt.handler (optarg);
              }
          else
            opt.handler ("");
        }
      else
        {
          /* This is one or more short options.  */
          for (short_arg in arg[1:])
            {
              var opt = getopt (short_arg as string);

               if (opt.arg_required)
               {
                 if (i + 1 == argv'length || argv[i + 1][0] == '-')
                   raise Exception {code = EC_inval,
                                    name = "option " + arg + " requires an argument"};
                 opt.handler (argv[(i++) + 1]);
               }
               else
               opt.handler ("");
            }
        }
    }
  }
  return rest;
}
