/***************************************************************************
 *
 * $Id: cmdopt.cpp 348342 2005-11-23 02:03:26Z sebor $
 *
 ************************************************************************
 *
 * Copyright (c) 1994-2005 Quovadx,  Inc., acting through its  Rogue Wave
 * Software division. Licensed under the Apache License, Version 2.0 (the
 * "License");  you may  not use this file except  in compliance with the
 * License.    You    may   obtain   a   copy   of    the   License    at
 * http://www.apache.org/licenses/LICENSE-2.0.    Unless   required    by
 * applicable law  or agreed to  in writing,  software  distributed under
 * the License is distributed on an "AS IS" BASIS,  WITHOUT WARRANTIES OR
 * CONDITIONS OF  ANY KIND, either  express or implied.  See  the License
 * for the specific language governing permissions  and limitations under
 * the License.
 * 
 **************************************************************************/

// expand _TEST_EXPORT macros
#define _RWSTD_TEST_SRC

#include <cmdopt.h>

#include <assert.h>   // for assert
#include <errno.h>    // for errno
#include <stdarg.h>   // for va_arg, ...
#include <stdio.h>    // for fprintf
#include <stdlib.h>   // for atexit, free, malloc
#include <string.h>   // for memcpy, strcpy, strcmp, ...

#ifndef EINVAL
#  define EINVAL   22   /* e.g., HP-UX, Linux, Solaris */
#endif   // EINVAL

/**************************************************************************/

typedef int (optcallback_t)(int, char*[]);

struct cmdopts_t
{
    char           loptbuf_ [32];   // buffer for long option name
    optcallback_t *callback_;       // function to call to process option

    // counter to increment for each occurrence of an option
    // or to set to the numeric argument (when specified)
    int           *pcntr_;

    char          *lopt_;           // long option name
    char           sopt_;           // short option name

    size_t         maxcalls_;       // how many times option can be invoked
    size_t         ncalls_;         // how many times it has been invoked

    unsigned       arg_ : 1;        // option takes an argument?
    unsigned       inv_ : 1;        // callback invocation inverted
    unsigned       envseen_ : 1;    // environment option already processed
};


// total number of registered options
static size_t ncmdopts;

// number of default (always defined) options
static size_t ndefopts;
static cmdopts_t cmdoptbuf [32];
static cmdopts_t *cmdopts = cmdoptbuf;
static size_t optbufsize = sizeof cmdoptbuf / sizeof *cmdoptbuf;

/**************************************************************************/

static int
rw_print_help (int argc, char *argv[])
{
    if (1 == argc && argv && 0 == argv [0]) {
        static const char helpstr[] = {
            "Without an argument, prints this help to stdout and exits with\n"
            "a status of 0 without further processing.\n"
            "With the optional argument prints help on the option with that\n"
            "name, if one exists, and exits with a status of 0. If an option\n"
            "with the specified name does not exist, prints an error message\n"
            "to stderr and exits with a status of 1.\n"
            "The leading underscores in the name of an option are optional.\n"
        };

        argv [0] = _RWSTD_CONST_CAST (char*, helpstr);

        return 0;
    }

    // the option name to get help on, if any
    const char* opthelp = 1 < argc ? argv [1] : 0;

    // remove the optional one or two leading underscores
    if (opthelp && '-' == opthelp [0] && opthelp [1])
        opthelp += 1 + ('-' == opthelp [1]);

    if (0 == opthelp)
        printf ("OPTIONS\n");

    // set to a non-zero when the specified option is found
    int option_found = 0;

    for (size_t i = 0; i != ncmdopts; ++i) {

        // get a pointer to the name of the long option, if any
        const char* const lopt =
            cmdopts [i].lopt_ ? cmdopts [i].lopt_ : cmdopts [i].loptbuf_;

        if (opthelp && *opthelp) {

            if (   cmdopts [i].sopt_ == opthelp [0] && '\0' == opthelp [1]
                || *lopt && 0 == strcmp (lopt + 1, opthelp)) {

                // remember that we found the option whose (short
                // or long) name we're to give help on; after printing
                // the help text on the option keep looping in case
                // there is another option and callback with the same
                // name (unlikely but possible)
                option_found = 1;
            }
            else {
                // the option doesn't match, continue searching
                continue;
            }
        }

        printf ("     ");

        if (cmdopts [i].sopt_) {
            printf ("-%c", cmdopts [i].sopt_);

            if (lopt)
                printf (" | ");
        }

        const char *pfx = "";
        const char *sfx = pfx;

        if (lopt) {
            printf ("-%s", lopt);
            if (   cmdopts [i].arg_
                && '=' != lopt [strlen (lopt) - 1]) {
                pfx = " [ ";
                sfx = " ]";
            }
        }

        printf ("%s%s%s", pfx, cmdopts [i].arg_ ? "<arg>" : "", sfx);

        if (_RWSTD_SIZE_MAX == cmdopts [i].maxcalls_)
            printf (" (each occurrence evaluated)\n");
        else if (1 < cmdopts [i].maxcalls_)
            printf (" (at most %u occurrences evaluated)\n",
                    unsigned (cmdopts [i].maxcalls_));
        else
            printf (" (only the first occurrence evaluated)\n");

        // invoke callback with the "--help" option
        if (cmdopts [i].callback_) {

            char* help [2] = { 0, 0 };

            cmdopts [i].callback_ (1, help);

            for (const char *line = help [0]; line; ) {

                const char* const nl = strchr (line, '\n');
                const int len = nl ? int (nl - line) : int (strlen (line));

                printf ("       %.*s\n", len, line);

                line = nl;
                if (nl)
                    ++line;
            }
        }
    }

    if (opthelp && !option_found) {
        fprintf (stderr, "Unknown option \"%s\".\n", opthelp);
        exit (1);
    }

    exit (0);

    return 0;
}

/**************************************************************************/

static int
rw_set_ignenv (int argc, char *argv[])
{
    if (1 == argc && argv && 0 == argv [0]) {
        static const char helpstr[] = {
            "Prevents options specified in the RWSTD_TESTOPTS environment\n"
            "variable from taking effect.\n"
            "Unless this option is specified, the RWSTD_TESTOPTS environment\n"
            "variable is processed as if its value were specified on the \n"
            "command line.\n"
            "For example, setting the value of the variable to the string\n"
            "\"--verbose --no-wchar\" and invoking this program with no\n"
            "command line arguments will have the same effect as invoking\n"
            "it with the two arguments on the command line.\n"
        };

        argv [0] = _RWSTD_CONST_CAST (char*, helpstr);

        return 0;
    }

    return 0;
}

extern "C" {

static void
rw_clear_opts ()
{
    // reset all options, deallocating dynamically allocated storage

    for (size_t i = 0; i != ncmdopts; ++i) {

        // free any storage allocated for the option name
        free (cmdopts [i].lopt_);
    }

    if (cmdopts != cmdoptbuf) {
        // free the storage allocated for all the options
        free (cmdopts);
    }

    // reset the options pointer to point at the statically
    // allocated buffer and the count back to 0
    ncmdopts   = 0;
    cmdopts    = cmdoptbuf;
    optbufsize = sizeof cmdoptbuf / sizeof *cmdoptbuf;
}

}

static void
rw_set_myopts ()
{
    static int cleanup_handler_registered;

    if (0 == cleanup_handler_registered) {
        atexit (rw_clear_opts);
        cleanup_handler_registered = 1;
    }

    if (0 != ncmdopts)
        return;

    static int recursive;

    if (recursive)
        return;

    ++recursive;

    rw_setopts ("|-help: "   // argument optional
                "|-ignenv ",
                rw_print_help,
                rw_set_ignenv);

    ndefopts = ncmdopts;

    recursive = 0;
}

/**************************************************************************/

//////////////////////////////////////////////////////////////////////
// syntax of the option description string:
//
// opts ::= opt [ ':' | '=' | '#' ] [ @N | @* | '!' ] [ opts ]
// opt  ::= <sopt> [ '|' <lopt>]
//      ::= '|' <lopt>
// sopt ::= char
// lopt ::= char char*
// char ::= A-Z a-z _ 0-9

_TEST_EXPORT int
rw_vsetopts (const char *opts, va_list va)
{
    if (0 == opts) {

        rw_clear_opts ();
        return 0;
    }

    rw_set_myopts ();

    const char *next = opts;

    for ( ; ; ++ncmdopts) {

        while (' ' == *next)
            ++next;

        if ('\0' == *next) {
            break;
        }

        if (ncmdopts == optbufsize) {

            const size_t newbufsize = 2 * ncmdopts + 1;
            
            cmdopts_t* const newopts =
                (cmdopts_t*)malloc (newbufsize * sizeof (cmdopts_t));

            if (0 == newopts) {
                fprintf (stderr, "%s%d: failed to allocate memory\n",
                         __FILE__, __LINE__);
                abort ();
            }

            memcpy (newopts, cmdopts, ncmdopts * sizeof (cmdopts_t));

            if (cmdopts != cmdoptbuf)
                free (cmdopts);

            cmdopts    = newopts;
            optbufsize = newbufsize;
        }

        // clear the next option info
        memset (cmdopts + ncmdopts, 0, sizeof *cmdopts);

        if ('|' != *next)
            cmdopts [ncmdopts].sopt_ = *next++;

        if ('|' == *next) {
            const char* end = strpbrk (++next, "|@:=*!# ");
            if (0 == end)
                end = next + strlen (next);

            // copy the option name up to but not including the delimiter
            // (except when the delimiter is the equals sign ('='), which
            // becomes the last character of the option name
            const size_t optlen = size_t (end - next) + ('=' == *end);

            char *lopt = 0;

            if (optlen < sizeof cmdopts [ncmdopts].loptbuf_)
                lopt = cmdopts [ncmdopts].loptbuf_;
            else {
                lopt = (char*)malloc (optlen + 1);
                cmdopts [ncmdopts].lopt_ = lopt;
            }

            memcpy (lopt, next, optlen);
            lopt [optlen] = '\0';

            next = end;
        }

        // only the first occurrence of each command line option
        // causes an invocation of the callback, all subsequent
        // ones will be ignored by default
        cmdopts [ncmdopts].maxcalls_ = 1;

        int arg_is_callback = true;

        if ('#' == *next) {
            // insead of a pointer to a callback, the argument
            // is a pointer to an int counter that is to be
            // incremented for each occurrence of the option
            // during processing; when the option is immediately
            // followed by the equals sign ('=') and a numeric
            // argument the value of the argument will be stored
            arg_is_callback = false;
            ++next;
            
            // an unlimited number of occurrences of the option
            // are allowed and will be counted
            cmdopts [ncmdopts].maxcalls_ = _RWSTD_SIZE_MAX;
        }
        else if (':' == *next || '=' == *next) {
            // ':' : argument optional
            // '=' : argument required
            cmdopts [ncmdopts].arg_ = true;
            ++next;
        }

        if ('@' == *next) {

            ++next;

            // at most how many occurrences of an option can be processed?
            if ('*' == *next) {
                // unlimited
                cmdopts [ncmdopts].maxcalls_ = _RWSTD_SIZE_MAX;
                ++next;
            }
            else {
                // at most this many
                char *end;
                cmdopts [ncmdopts].maxcalls_ = strtoul (next, &end, 10);
                next = end;
            }
        }
        else if ('!' == *next) {
            cmdopts [ncmdopts].inv_ = true;
            ++next;
        }

        if (arg_is_callback) {
            // retrieve the callback and verify it's not null
            // (null callback is permitted in the special case when
            // the short option is '-', i.e., when setting up or
            // resetting an "unknown option" handler)
            cmdopts [ncmdopts].callback_ = va_arg (va, optcallback_t*);
        }
        else {
            // retrieve the address of the int counter where to keep
            // track of the number of occurrences of the option, or
            // where to store the value of the numeric argument of
            // the option
            cmdopts [ncmdopts].pcntr_ = va_arg (va, int*);
        }

        if (   '-' != cmdopts [ncmdopts].sopt_
            && 0 == cmdopts [ncmdopts].callback_
            && 0 == cmdopts [ncmdopts].pcntr_) {

            // get a pointer to the long option name
            const char* const lopt = cmdopts [ncmdopts].lopt_
                ? cmdopts [ncmdopts].lopt_ : cmdopts [ncmdopts].loptbuf_;

            if (*lopt)
                fprintf (stderr, "null handler for option -%s\n", lopt);
            else
                fprintf (stderr, "null handler for option -%c\n",
                         cmdopts [ncmdopts].sopt_);
                
            abort ();
        }
    }

    return int (ncmdopts - ndefopts);
}

/**************************************************************************/

_TEST_EXPORT int
rw_setopts (const char *opts, ...)
{
    va_list va;
    va_start (va, opts);
    const int result = rw_vsetopts (opts, va);
    va_end (va);
    return result;
}

/**************************************************************************/

_TEST_EXPORT int
rw_runopts (int argc, char *argv[])
{
    rw_set_myopts ();

    static int recursive = false;

    // ignore options set in the environment?
    int ignenv = recursive;

    // return status
    int status = 0;

    // number of options processed
    int nopts = 0;

    // index of registered option whose callback should be invoked
    // for command line options that do not match any other
    size_t not_found_inx = _RWSTD_SIZE_MAX;

    // iterate over the command line arguments until a callback
    // returns a non-zero value or until all options have been
    // successfully processed
    for (int i = 0; i < argc && argv [i] && 0 == status; ++i) {

        if (0 == strcmp ("--ignore-environment", argv [i])) {
            // ignore options set in the environment
            ignenv = true;
            continue;
        }

        if (0 == strcmp ("--", argv [i])) {
            // "--" terminates options, everything
            // after it is treated as an argument
            break;
        }

        // the name of the option without the leading dash
        const char* const optname = argv [i] + 1;

        // look for the first equals sign
        const char* const eq = strchr (optname, '=');

        // compute the length of the option including the equals sign (if any)
        const size_t optlen = eq ? size_t (eq - optname + 1) : strlen (optname);

        int found = false;

        // look up each command line option (i.e., a string that starts
        // with a dash ('-')) and invoke the callback associated with it
        for (size_t j = 0; j != ncmdopts; ++j) {

            if ('-' == cmdopts [j].sopt_)
                not_found_inx = j;

            if ('-' == argv [i][0]) {

                const size_t cmplen =
                    eq && cmdopts [j].pcntr_ ? optlen - 1 : optlen;

                // get a pointer to the (possibly empty) name
                // of the long option
                const char* const lopt = cmdopts [j].lopt_ ? 
                    cmdopts [j].lopt_ : cmdopts [j].loptbuf_;

                // try to match the long option first, and only if it
                // doesn't match try the short single-character option
                if (   cmplen == strlen (lopt)
                    && 0 == memcmp (optname, lopt, cmplen)
                    || cmdopts [j].sopt_
                    && optname [0] == cmdopts [j].sopt_
                    && (1 == optlen || cmdopts [j].arg_)) {

                    // matching option has been found
                    found = true;

                    // ignore the option if invoked recursively (by processing
                    // options set in the environment) and the option has
                    // already been seen (this prevents duplicate processing
                    // of options that are set both on the command line and
                    // in the environment and allows option with an argument
                    // set on the command line to override those set in the
                    // environment)

                    if (cmdopts [j].ncalls_ && recursive)
                        continue;

                    // if the option has been evaluated the maximum number
                    // of times, avoid evaluating it and continue processing
                    if (cmdopts [j].maxcalls_ <= cmdopts [j].ncalls_)
                        continue;

                    if (cmdopts [j].callback_) {
                        if (!cmdopts [j].inv_) {
                            // when the command line argument matched
                            // the option,  invoke the callback function
                            status = cmdopts [j].callback_ (argc - i, argv + i);
                        }
                    }
                    else if (eq) {
                        assert (0 != cmdopts [j].pcntr_);

                        // obtain the numeric argument
                        char *end = 0;
                        const long optval = strtol (eq + 1, &end, 0);

                        if (end && '\0' != *end) {
                            fprintf (stderr, "expected numeric argument: %s\n",
                                     optname);
                            ignenv = true;
                            errno  = EINVAL;
                            status = 1;
                        }

#if _RWSTD_INT_SIZE < _RWSTD_LONG_SIZE

                        else if (   optval < _RWSTD_INT_MIN
                                 || _RWSTD_INT_MAX < optval) {
                            fprintf (stderr, "numeric argument %ld out of range"
                                     " [%d, %d]: %s\n", optval,
                                     _RWSTD_INT_MIN, _RWSTD_INT_MAX, optname);
                            ignenv = true;
                            errno  = EINVAL;
                            status = 1;
                        }

#endif   // _RWSTD_INT_SIZE < _RWSTD_LONG_SIZE

                        else {
                            *cmdopts [j].pcntr_ = optval;
                        }
                    }
                    else {
                        assert (0 != cmdopts [j].pcntr_);
                        ++*cmdopts [j].pcntr_;
                    }

                    ++cmdopts [j].ncalls_;

                    if (recursive)
                        cmdopts [j].envseen_ = true;

                    ++nopts;

                    if (status) {
                        // when the status returned from the last callback
                        // is non-0 stop further processing (including
                        // any options set in the environment) and return
                        // status to the caller
                        ignenv = true;
                        break;
                    }
                }
            }
        }

        if (!found && '-' == argv [i][0]) {

            // invoke the appropriate error handler for an option
            // that was not found
            if (_RWSTD_SIZE_MAX != not_found_inx) {

                // invoke the error handler set up through rw_setopts()
                // and let the handler decide whether to go on processing
                // other options or whether to abort
                status = cmdopts [not_found_inx].callback_ (argc - i, argv + i);
                if (status) {
                    // no further processing done
                    ignenv = true;
                    break;
                }
            }
            else {
                // print an error message to stderr when no error
                // handler has been set up
                fprintf (stderr, "unknown option: %s\n", argv [i]);
                ignenv = true;
                errno  = EINVAL;
                status = 1;
                break;
            }
        }
    }

    if (!ignenv) {
        // process options from the environment
        const char* const envar = getenv ("RWSTD_TESTOPTS");
        if (envar) {
            recursive = true;
            rw_runopts (envar);
            recursive = false;
        }
    }

    // invoke any inverted callbacks or bump their user-specified counters,
    // and reset internal counters indicating if/how many times each option
    // has been processed
    for (size_t j = 0; j != ncmdopts; ++j) {

        if (cmdopts [j].inv_ && 0 == cmdopts [j].ncalls_ && 0 == status) {

            if (cmdopts [j].callback_)
                status = cmdopts [j].callback_ (0, 0);
            else {
                assert (0 != cmdopts [j].pcntr_);
                ++*cmdopts [j].pcntr_;
            }
        }

        cmdopts [j].ncalls_  = 0;
        cmdopts [j].envseen_ = false;
    }

    return status;
}

/**************************************************************************/

_TEST_EXPORT int
rw_runopts (const char *str)
{
    assert (0 != str);

    rw_set_myopts ();

    char buf [80];      // fixed size buffer to copy `str' into
    char *pbuf = buf;   // a modifiable copy of `str'

    size_t len = strlen (str);
    if (len >= sizeof buf) {
        // allocate if necessary
        pbuf = (char*)malloc (len + 1);
        if (!pbuf)
            return -1;
    }

    // copy `str' to modifiable buffer
    memcpy (pbuf, str, len + 1);

    char *tmp_argv_buf [32] = { 0 };   // fixed size argv buffer
    char **argv = tmp_argv_buf;        // array of arguments

    // initial size of argument array (will grow as necessary)
    size_t argv_size = sizeof tmp_argv_buf / sizeof *tmp_argv_buf;
    size_t argc = 0;   // number of arguments in array

    int in_quotes = 0;   // quoted argument being processed

    for (char *s = pbuf; *s; ++s) {
        if ('"' == *s) {
            in_quotes = !in_quotes;
            continue;
        }

        if (in_quotes)
            continue;

        // split up unquoted space-separated arguments
        if (argc == 0 || ' ' == *s) {
            if (argc > 0) 
                *s = 0;

            // skip over leading spaces
            if (argc > 0 || ' ' == *s)
                while (' ' == *++s);

            if (*s) {
                if (argc == argv_size) {
                    // grow `argv' as necessary
                    char **tmp = (char**)malloc (sizeof *tmp * argv_size * 2);
                    if (!tmp) {
                        if (argv != tmp_argv_buf)
                            free (argv);
                        return -1;
                    }

                    // copy existing elementes and zero out any new entries
                    memcpy (tmp, argv, sizeof *tmp * argv_size);
                    memset (tmp + argv_size, 0, sizeof *tmp * argv_size);

                    // free existing buffer if necessary
                    if (argv != tmp_argv_buf)
                        free (argv);

                    // reassign buffer and increase size
                    argv       = tmp;
                    argv_size *= 2;
                }

                // add argument to array
                argv [argc++] = s;
            }
        }
    }

    // process `argc' options pointed to by `argv'
    const int status = rw_runopts (int (argc), argv);

    // free buffers if necessary
    if (argv != tmp_argv_buf)
        free (argv);

    if (pbuf != buf)
        free (pbuf);

    return status;
}
