/*
 * Simple commandline argument processing
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2002-2008 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
/*
Description
===========
Structures and functions for simple commandline argument processing,
option argument handling (short and long forms supported).

Output function for printing out short on-line help for said options
and program commandline usage.


Format for typical commandline:

$ program -a -b -c --long-d -e argument-for-e --long-f="argument for f"

where -a, -b and -c are short options without required arguments;
--long-d is a long option without argument; -e is short option with
argument and finally --long-f is long option with argument.


Introduction
============
Handling of commandline options in th_args_process() has various
options, which effect how an option is handled. Also the error
situations (unknown option/no required argument) can be handled
in different ways.

Typically th_args_process() is called as follows:

BOOL optionHandlerCallback(int optionNumber, char *optionArgument, char *optionName)
{
  if (option is OK)
    return TRUE;
  else
    return FALSE;
}

BOOL nonoptionHandlerCallback(char *argumentString)
{
  // return value same as in optionHandlerCallback
}


int main(int argc, char *argv[])
{
  if (th_args_process(argc, argv, optList, optListN,
      optionHandlerCallback, nonoptionHandlerCallback)) {
    ... arguments OK ...
  } else {
    ... arguments invalid or required arguments missing ...
  }
}


NOTICE!
-------
The return value from handler callbacks affects the return value of
th_args_process(). Additionally, a failure in callback (returns FALSE)
effects the argument processing if bailOut argument of th_args_process()
is TRUE!

If bailOut is TRUE, any error/failure in argument processing (including
callbacks) immediately stops the argument processing and FALSE is
returned from th_args_process().

If bailOut is FALSE, most errors are "ignored", but FALSE is still returned
if any errors occured.


NOTICE #2!
----------
A small temporary buffer of N*sizeof(BOOL) (where N is number of
options in optList[]) is allocated for processing required options.
If this allocation fails, the program is immediately exited with
code 128.


Examples
========
Example of different options, in a fictional optionlist struct:

optarg_t optList[] = {
  // Option without arguments
  { 0, '?', "help",      "Show this help",                    OPT_NONE },

  // Option with a required argument
  { 1, 'o', "output",    "Output file name",                  OPT_ARGREQ },
  
  // Option with optional argument
  { 2, 'f', "foobar",    "Use foobar, with optional string",  OPT_ARGOPT },

  // This option is required to be given, though without other flags
  // it may not make much sense.
  { 4, 'S', "stupid",     "You must give this option",        OPT_REQUIRED },

  // The flags can be combined with OR operator: this option is both
  // required to be specified, and also requires argument (the filename)
  { 5, 'i', "input",     "Input file name",                   OPT_REQUIRED | OPT_ARGREQ },


  // Option with only long form
  { 0, 0,   "only-long", "Long option",                       OPT_NONE },

  // Option with only short form
  { 0, 's', NULL,        "Short option",                      OPT_NONE },

};

const int optListN = (sizeof(optList) / sizeof(optarg_t));


*/
#ifndef TH_EXTERNAL
#include "th_util.h"
#include "th_args.h"
#include "th_string.h"
#endif


/* Check if option requires an argument
 */
static BOOL th_args_check_arg(const optarg_t *o, const char *optArg)
{
    if ((o->flags & OPT_ARGMASK) == OPT_ARGREQ && optArg == NULL)
    {
        if (o->optShort != 0 && o->optLong != NULL)
        {
            THERR("Option '-%c ARG' (--%s=ARG) requires an argument!\n",
                  o->optShort, o->optLong);
        }
        else if (o->optShort != 0)
        {
            THERR("Option '-%c ARG' requires an argument!\n", o->optShort);
        }
        else if (o->optLong != NULL)
        {
            THERR("Option --%s=ARG requires an argument!\n", o->optLong);
        }

        return FALSE;
    }
    else
        return TRUE;
}


/* Handle short options
 */
static BOOL th_args_process_short(char *currArg, int *newArgIndex,
    BOOL *wasGiven, int argc, char *argv[],
    optarg_t optList[], int optListN,
    BOOL (*handleOpt)(int, char *, char *))
{
    char *tmpArg = currArg, *optArg;
    int optN;
    BOOL isFound;

    // Short options can be combined: -a -b -c == -abc
    while (*tmpArg)
    {
        for (optN = 0, isFound = FALSE; (optN < optListN) && !isFound; optN++)
            if (*tmpArg == optList[optN].optShort)
            {
                // Get possible option argument, if needed
                if ((optList[optN].flags & OPT_ARGMASK) != 0
                    && (++(*newArgIndex) < argc))
                    optArg = argv[*newArgIndex];
                else
                    optArg = NULL;

                // Check if option argument is required
                if (!th_args_check_arg(&optList[optN], optArg))
                    return FALSE;
                else
                {
                    char tmpStr[2] = { 0, 0 };

                    // Option was given succesfully, try to handle it
                    wasGiven[optN] = TRUE;

                    tmpStr[0] = *tmpArg;

                    if (!handleOpt(optList[optN].id, optArg, tmpStr))
                        return FALSE;
                }

                isFound = TRUE;
            }

        if (!isFound)
        {
            THERR("Unknown short option '%c' in argument '-%s'\n",
                  *tmpArg, currArg);
            return FALSE;
        }

        tmpArg++;
    }

    return TRUE;
}


/* Handle long options
 */
static BOOL th_args_process_long(char *currArg, int *newArgIndex,
    BOOL *wasGiven, int argc, char *argv[],
    optarg_t optList[], int optListN,
    BOOL (*handleOpt)(int, char *, char *))
{
    int optN, optLen, i;
    char *optArg;

    (void) argc;
    (void) argv;
    (void) newArgIndex;

    // Long option
    for (optN = -1, optLen = i = 0; i < optListN && optN < 0; i++)
        if (optList[i].optLong)
        {
            optLen = strlen(optList[i].optLong);
            if (strcmp(currArg, optList[i].optLong) == 0)
                optN = i;
        }

    // Get possible option argument, if needed
    if (optN >= 0)
    {
        if ((optList[optN].flags & OPT_ARGMASK) != 0)
        {
            if (currArg[optLen] == '=')
                optArg = &currArg[optLen + 1];
            else
                optArg = NULL;
        }
        else
            optArg = NULL;

        // Check if option argument is required
        if (!th_args_check_arg(&optList[optN], optArg))
            return FALSE;
        else
        {
            // Option was given succesfully, try to handle it
            wasGiven[optN] = TRUE;
            if (!handleOpt(optList[optN].id, optArg, currArg))
                return FALSE;
        }
    }
    else
    {
        THERR("Unknown long option '--%s'\n", currArg);
        return FALSE;
    }

    return TRUE;
}


/* Process arguments, handling short and long options by
 * calling the given callback functions.
 */
BOOL th_args_process(int argc, char *argv[],
                     optarg_t optList[], int optListN,
                     BOOL(*handleOpt) (int, char *, char *),
                     BOOL(*handleNonOption) (char *), BOOL bailOut)
{
    BOOL endOptions, optionsOK;
    int argIndex, newArgIndex, i;
    char *currArg;
    BOOL *wasGiven;

    // Allocate wasGiven
    wasGiven = (BOOL *) th_calloc(optListN, sizeof(BOOL));
    if (!wasGiven)
    {
        THERR("FATAL ERROR! Could not allocate wasGiven in th_args_process()!\n");
        exit(128);
    }

    // Parse arguments
    argIndex = 1;
    optionsOK = TRUE;
    endOptions = FALSE;
    while (argIndex < argc)
    {
        currArg = argv[argIndex];
        if ((currArg[0] == '-') && !endOptions)
        {
            newArgIndex = argIndex;
            currArg++;
            if (*currArg == '-')
            {
                // Check for "--", which ends the options-list
                currArg++;
                if (*currArg == 0)
                {
                    endOptions = TRUE;
                    continue;
                }

                // Long options
                if (!th_args_process_long(currArg, &newArgIndex,
                                          wasGiven, argc, argv, optList,
                                          optListN, handleOpt))
                    optionsOK = FALSE;
            }
            else
            {
                // Short options
                if (!th_args_process_short(currArg, &newArgIndex,
                                           wasGiven, argc, argv, optList,
                                           optListN, handleOpt))
                    optionsOK = FALSE;
            }

            argIndex = newArgIndex;
        }
        else
        {
            // Was not option argument
            if (handleNonOption == NULL
                || (handleNonOption != NULL && !handleNonOption(currArg)))
            {
                THERR("Invalid argument '%s'\n", currArg);
                optionsOK = FALSE;
            }
        }

        // Check if we bail out on invalid argument
        if (!optionsOK && bailOut)
        {
            th_free(wasGiven);
            return FALSE;
        }

        argIndex++;
    }

    // Check wasGiven by isRequired
    for (i = 0; i < optListN; i++)
        if ((optList[i].flags & OPT_REQUIRED) != 0 && !wasGiven[i])
        {
            THERR("Option -%s (--%s) is required.\n",
                  optList[i].optShort, optList[i].optLong);

            optionsOK = FALSE;
            if (bailOut)
                break;
        }

    th_free(wasGiven);
    return optionsOK;
}


/* Print help for commandline arguments/options
 */
void th_args_help(FILE *outFile, optarg_t optList[], int optListN)
{
    int i, nrequired;

    for (i = nrequired = 0; i < optListN; i++)
    {
        optarg_t *o = &optList[i];

        // Print short option
        if (o->optShort != 0)
            fprintf(outFile, "  -%c,  ", o->optShort);
        else
            fprintf(outFile, "       ");

        // Print long option
        if (o->optLong)
        {
            char tmpStr[64], *p;

            if ((o->flags & OPT_ARGMASK) == OPT_ARGOPT)
            {
                snprintf(tmpStr, sizeof(tmpStr), "%s[=ARG]",
                         optList[i].optLong);
                p = tmpStr;
            }
            else if ((o->flags & OPT_ARGMASK) == OPT_ARGREQ)
            {
                snprintf(tmpStr, sizeof(tmpStr), "%s=ARG",
                         optList[i].optLong);
                p = tmpStr;
            }
            else
                p = o->optLong;

            fprintf(outFile, "--%-15s", p);
        }
        else
            fprintf(outFile, "                 ");

        fprintf(outFile, "  %s.", optList[i].desc);

        if (o->flags & OPT_REQUIRED)
        {
            fprintf(outFile, " [*]\n");
            nrequired++;
        }
        else
            fprintf(outFile, "\n");
    }

    if (nrequired > 0)
        fprintf(outFile, "(Options marked with [*] are required)\n");
}
