//
// tardy - a tar post-processor
// Copyright (C) 1991-1999, 2002, 2004, 2008, 2009, 2011 Peter Miller
//
// 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 <libtardy/ac/ctype.h>
#include <libtardy/ac/stddef.h>
#include <libtardy/ac/stdlib.h>
#include <libtardy/ac/string.h>
#include <libexplain/output.h>
#include <libexplain/program_name.h>

#include <libtardy/arglex.h>
#include <libtardy/rcstring.h>
#include <libtardy/roff.h>
#include <libtardy/trace.h>


arglex::~arglex()
{
}


static arglex::table_t table[] =
{
    { "-",        arglex::token_stdio   },
    { "-Help",    arglex::token_help    },
    { "-VERSion", arglex::token_version },
    { "-TRace",   arglex::token_trace   },
};


arglex::arglex(int ac, char **av, const table_t *tp) :
    token(token_eoln),
    argc(ac - 1),
    argv(av + 1),
    utable(tp),
    partial(0),
    pushback(0)
{
    explain_program_name_set(av[0]);
    explain_option_hanging_indent_set(4);
}


bool
arglex::compare(const char *formal, const char *actual)
{
    trace(("arglex_compare(formal = \"%s\", actual = \"%s\")\n{\n",
        formal, actual));
    bool result = false;
    for (;;)
    {
        trace_string(formal);
        trace_string(actual);
        char ac = *actual++;
        if (isupper(ac))
            ac = tolower(ac);
        char fc = *formal++;
        switch (fc)
        {
        case 0:
            result = !ac;
            goto done;

        case '_':
            if (ac == '-')
                break;
            // fall through...

        case 'a': case 'b': case 'c': case 'd': case 'e':
        case 'f': case 'g': case 'h': case 'i': case 'j':
        case 'k': case 'l': case 'm': case 'n': case 'o':
        case 'p': case 'q': case 'r': case 's': case 't':
        case 'u': case 'v': case 'w': case 'x': case 'y':
        case 'z':
            //
            // optional characters
            //
            if (ac == fc && compare(formal, actual))
            {
                result = true;
                goto done;
            }

            //
            // skip forward to next
            // mandatory character, or after '_'
            //
            while (islower(*formal))
                ++formal;
            if (*formal == '_')
            {
                ++formal;
                if (ac == '_' || ac == '-')
                    ++actual;
            }
            --actual;
            break;

        case '*':
            //
            // This is a hack, it should really
            // check for a match match the stuff after
            // the '*', too, a la glob.
            //
            if (!ac)
            {
                result = false;
                goto done;
            }
            partial = actual - 1;
            result = true;
            goto done;

        case '\\':
            if (actual[-1] != *formal++)
            {
                result = false;
                goto done;
            }
            break;

        case 'A': case 'B': case 'C': case 'D': case 'E':
        case 'F': case 'G': case 'H': case 'I': case 'J':
        case 'K': case 'L': case 'M': case 'N': case 'O':
        case 'P': case 'Q': case 'R': case 'S': case 'T':
        case 'U': case 'V': case 'W': case 'X': case 'Y':
        case 'Z':
            fc = tolower(fc);
            // fall through...

        default:
            //
            // mandatory characters
            //
            if (fc != ac)
            {
                result = false;
                goto done;
            }
            break;
        }
    }
    done:
    trace(("return %s;\n}\n", (result ? "true" : "false")));
    return result;
}


bool
arglex::is_a_number(const char *s)
{
    char *ep = 0;
    long n = strtol(s, &ep, 0);
    if (ep == s || *ep)
        return false;
    value.number = n;
    return true;
}


arglex::token_t
arglex::lex(void)
{
    trace(("arglex()\n{\n"));

    const char *arg = 0;
    if (pushback)
    {
        //
        // the second half of a "-foo=bar" style argument.
        //
        arg = pushback;
        pushback = 0;
    }
    else
    {
        if (argc <= 0)
        {
            token = token_eoln;
            static char empty[1];
            arg = empty;
            goto done;
        }
        arg = argv[0];
        argc--;
        argv++;

        //
        // See if it looks like a GNU "-foo=bar" option.
        // Split it at the '=' to make it something the
        // rest of the code understands.
        //
        if (arg[0] == '-' && arg[1] != '=')
        {
            char *eqp = (char *)strchr(arg, '=');
            if (eqp)
            {
                pushback = eqp + 1;
                *eqp = 0;
            }
        }

        //
        // Turn the GNU-style leading "--"
        // into "-" if necessary.
        //
        if
        (
            arg[0] == '-'
        &&
            arg[1] == '-'
        &&
            arg[2]
        &&
            !is_a_number(arg + 1)
        )
            ++arg;
    }

    //
    // see if it is a number
    //
    if (is_a_number(arg))
    {
        token = token_number;
        goto done;
    }

    //
    // scan the tables to see what it matches
    //
    {
        const table_t *hit[20];
        int nhit = 0;
        partial = 0;
        for (const table_t *tp = table; tp < ENDOF(table); tp++)
        {
            if (compare(tp->name, arg))
                hit[nhit++] = tp;
        }
        if (utable)
        {
            for (const table_t *tp = utable; tp->name; tp++)
            {
                if (compare(tp->name, arg))
                    hit[nhit++] = tp;
            }
        }

        //
        // deal with unknown or ambiguous options
        //
        switch (nhit)
        {
        case 0:
            //
            // not found in the tables
            //
            if (*arg == '-')
                token = token_option;
            else
                token = token_string;
            break;

        case 1:
            one:
            token = hit[0]->token;
            if (partial)
                arg = partial;
            else
                arg = hit[0]->name;
            break;

        default:
            {
                //
                // not an error if they are all the same
                // e.g. due to cultural spelling differences
                // with the same abbreviation.
                //
                int j = 1;
                for (j = 1; j < nhit; ++j)
                    if (hit[0]->token != hit[j]->token)
                        break;
                if (j >= nhit)
                    goto one;

                //
                // build a list of the names
                // and complain that it is ambiguous
                //
                rcstring s(hit[0]->name);
                for (j = 1; j < nhit; ++j)
                {
                    s = s + ", " + hit[j]->name;
                }
                explain_output_error_and_die
                (
                    "option %s abmiguous (%s)",
                    rcstring(arg).quote_c().c_str(),
                    s.c_str()
                );
            }
        }
    }

    //
    // here for all exits
    //
    done:
    value.string = arg;
    trace(("return %d; /* %s */\n", token, value.string));
    trace(("}\n"));
    return token;
}


void
arglex::help_f(const char **text, size_t text_len)
{
    //
    // collect the rest of thge command line,
    // if necessary
    //
    trace(("help(text = %p, text_len = %d, usage = %p)\n{\n",
        text, text_len, usage));
    lex();
    while (token != token_eoln)
        generic_argument();

    roff().interpret(text, text_len);
    trace(("}\n"));
}


void
arglex::generic_argument(void)
{
    trace(("generic_argument()\n{\n"));
    switch (token)
    {
    default:
        bad_argument();
        // NOTREACHED

    case token_trace:
        if (lex() != token_string)
            usage();
        for (;;)
        {
            trace_enable(value.string);
            if (lex() != token_string)
                break;
        }
#ifndef DEBUG
        explain_output_error
        (
"Warning: the -TRace option is only effective when the %s program \
is compiled using the DEBUG define in the common/main.h include file.",
            explain_program_name_get()
        );
#endif
        break;
    }
    trace(("}\n"));
}


void
arglex::bad_argument(void)
{
    trace(("bad_argument()\n{\n"));
    switch (token)
    {
    case token_string:
        explain_output_error("misplaced file name (\"%s\")", value.string);
        break;

    case token_number:
        explain_output_error("misplaced number (%s)", value.string);
        break;

    case token_option:
        explain_output_error("unknown \"%s\" option", value.string);
        break;

    case token_eoln:
        explain_output_error("command line too short");
        break;

    default:
        explain_output_error("misplaced \"%s\" option", value.string);
        break;
    }
    usage();
    trace(("}\n"));
    exit(EXIT_FAILURE);
    // NOTREACHED
}
