#include "xmlrpc_config.h"  /* prereq for mallocvar.h -- defines __inline__ */

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <limits.h>

#include "bool.h"
#include "mallocvar.h"
#include "casprintf.h"
#include "getoptx.h"

#include "cmdline_parser.h"

#define MAXOPTS 100

struct optionDesc {
    const char *    name;
    enum optiontype type;
    bool            present;
    union {
        unsigned int u;
        int          i;
        const char * s;
    } value;
};



struct cmdlineParserCtl {
    struct optionDesc * optionDescArray;
    unsigned int        numOptions;
    const char **       argumentArray;
    unsigned int        numArguments;
};



static struct optionx *
createLongOptsArray(struct optionDesc * const optionDescArray,
                    unsigned int        const numOptions) {

    struct optionx * longopts; 

    MALLOCARRAY(longopts, numOptions+1);
    if (longopts != NULL) {
        unsigned int i;

        for (i = 0; i < numOptions; ++i) {
            longopts[i].name = optionDescArray[i].name;
            /* If the option takes a value, we say it is optional even
               though it never is.  That's because if we say it is
               mandatory, getopt_long_only() pretends it doesn't even
               recognize the option if the user doesn't give a value.
               We prefer to generate a meaningful error message when
               the user omits a required option value.
            */
            longopts[i].has_arg = 
                optionDescArray[i].type == OPTTYPE_FLAG ? 
                no_argument : optional_argument;
            longopts[i].flag = NULL;
            longopts[i].val = i;
        }
        longopts[numOptions].name = 0;
        longopts[numOptions].has_arg = 0;
        longopts[numOptions].flag = 0;
        longopts[numOptions].val = 0;
    }
    return longopts;
}



static void
parseOptionValue(const char *        const optarg, 
                 struct optionDesc * const optionP,
                 const char **       const errorP) {
    
    switch (optionP->type) {
    case OPTTYPE_UINT: 
    case OPTTYPE_INT: {
        if (optarg == NULL)
            casprintf(errorP, "Option requires a value");
        else if (strlen(optarg) == 0)
            casprintf(errorP, "Numeric option value is null string");
        else {
            char * tailptr;
            long const longvalue = strtol(optarg, &tailptr, 10);
            if (*tailptr != '\0')
                casprintf(errorP, "Non-numeric value "
                         "for numeric option value: '%s'", optarg);
            else if (errno == ERANGE || longvalue > INT_MAX)
                casprintf(errorP, "Numeric value out of range: %s", optarg);
            else { 
                if (optionP->type == OPTTYPE_UINT) {
                    if (longvalue < 0)
                        casprintf(errorP, "Unsigned numeric value is "
                                  "negative: %ld", longvalue);
                    else {
                        *errorP = NULL;
                        optionP->value.u = (unsigned int) longvalue;
                    }
                } else {
                    *errorP = NULL;
                    optionP->value.u = (int) longvalue;
                }
            }
        }
    }
    break;
    case OPTTYPE_STRING:
        if (optarg == NULL)
            casprintf(errorP, "Option requires a value");
        else {
            *errorP = NULL;
            optionP->value.s = strdup(optarg);
        }
        break;
    case OPTTYPE_FLAG:
        *errorP = NULL;
        break;
    }
}



static void
processOption(struct optionDesc * const optionP,
              const char *        const optarg,
              const char **       const errorP) {

    const char * error;
    
    parseOptionValue(optarg, optionP, &error);
    if (error)
        casprintf(errorP, "Error in '%s' option: %s", optionP->name, error);
    else
        optionP->present = true;
}



static void
extractArguments(struct cmdlineParserCtl * const cpP,
                 unsigned int              const argc,
                 const char **             const argv) {
    
    cpP->numArguments = argc - getopt_argstart();
    MALLOCARRAY(cpP->argumentArray, cpP->numArguments);

    if (cpP->argumentArray == NULL) {
        fprintf(stderr, "Unable to allocate memory for argument array\n");
        abort();
    } else {
        unsigned int i;

        for (i = 0; i < cpP->numArguments; ++i) {
            cpP->argumentArray[i] = strdup(argv[getopt_argstart() + i]);
            if (cpP->argumentArray[i] == NULL) {
                fprintf(stderr, "Unable to allocate memory for Argument %u\n",
                        i);
                abort();
            }
        }
    }
}



void
cmd_processOptions(cmdlineParser   const cpP,
                   int             const argc,
                   const char **   const argv, 
                   const char **   const errorP) {

    struct optionx * longopts;

    longopts = createLongOptsArray(cpP->optionDescArray, cpP->numOptions);

    if (longopts == NULL) 
        casprintf(errorP, "Unable to get memory for longopts array");
    else {
        int endOfOptions;
        unsigned int i;

        *errorP = NULL;

        /* Set up initial assumption:  No options present */

        for (i = 0; i < cpP->numOptions; ++i)
            cpP->optionDescArray[i].present = false;

        endOfOptions = false;  /* initial value */
            
        while (!endOfOptions && !*errorP) {
            int const opterr0 = 0;
                /* Don't let getopt_long_only() print an error message */
            unsigned int longoptsIndex;
            const char * unrecognizedOption;
            const char * optarg;
            
            getopt_long_onlyx(argc, (char**) argv, "", longopts, 
                              &longoptsIndex, opterr0,
                              &endOfOptions, &optarg, &unrecognizedOption);
                              
            if (unrecognizedOption)
                casprintf(errorP, "Unrecognized option: '%s'", 
                          unrecognizedOption);
            else {
                if (!endOfOptions)
                    processOption(&cpP->optionDescArray[longoptsIndex], optarg,
                                  errorP);
            }
        }
        if (!*errorP)
            extractArguments(cpP, argc, argv);

        free(longopts);
    }
}



cmdlineParser
cmd_createOptionParser(void) {

    struct cmdlineParserCtl * cpP;

    MALLOCVAR(cpP);

    if (cpP != NULL) {
        struct optionDesc * optionDescArray;

        cpP->numOptions = 0;
        MALLOCARRAY(optionDescArray, MAXOPTS);
        if (optionDescArray == NULL) {
            free(cpP);
            cpP = NULL;
        } else 
            cpP->optionDescArray = optionDescArray;
    }
    return cpP;
}



void
cmd_destroyOptionParser(cmdlineParser const cpP) {
    
    unsigned int i;

    for (i = 0; i < cpP->numOptions; ++i) {
        struct optionDesc const option = cpP->optionDescArray[i];
        if (option.type == OPTTYPE_STRING && option.present)
            strfree(option.value.s);
        strfree(option.name);
    }

    for (i = 0; i < cpP->numArguments; ++i)
        strfree(cpP->argumentArray[i]);

    free(cpP->optionDescArray);
    free(cpP);
}



void
cmd_defineOption(cmdlineParser   const cpP,
                 const char *    const name, 
                 enum optiontype const type) {
    
    if (cpP->numOptions < MAXOPTS) {
        cpP->optionDescArray[cpP->numOptions].name = strdup(name);
        cpP->optionDescArray[cpP->numOptions].type = type;
        
        ++cpP->numOptions;
    }
}



static struct optionDesc *
findOptionDesc(struct cmdlineParserCtl * const cpP,
               const char *              const name) {

    struct optionDesc * retval;
    unsigned int i;

    retval = NULL;

    for (i = 0; i < cpP->numOptions && !retval; ++i)
        if (strcmp(cpP->optionDescArray[i].name, name) == 0)
            retval = &cpP->optionDescArray[i];

    return retval;
}



int
cmd_optionIsPresent(cmdlineParser const cpP,
                    const char *  const name) {

    struct optionDesc * const optionDescP = findOptionDesc(cpP, name);

    bool present;

    if (!optionDescP) {
        fprintf(stderr, "cmdlineParser called incorrectly.  "
                "optionIsPresent() called for undefined option '%s'\n",
                name);
        abort();
    } else 
        present = optionDescP->present;

    return present;
}



unsigned int
cmd_getOptionValueUint(cmdlineParser const cpP,
                       const char *  const name) {

    struct optionDesc * const optionDescP = findOptionDesc(cpP, name);

    unsigned int retval;

    if (!optionDescP) {
        fprintf(stderr, "cmdlineParser called incorrectly.  "
                "cmd_getOptionValueUint() called for undefined option '%s'\n",
                name);
        abort();
    } else {
        if (optionDescP->type != OPTTYPE_UINT) {
            fprintf(stderr, "cmdlineParser called incorrectly.  "
                    "cmd_getOptionValueUint() called for non-unsigned integer "
                    "option '%s'\n", optionDescP->name);
            abort();
        } else {
            if (optionDescP->present) 
                retval = optionDescP->value.u;
            else
                retval = 0;
        }
    }
    return retval;
}



int
cmd_getOptionValueInt(cmdlineParser const cpP,
                      const char *  const name) {

    struct optionDesc * const optionDescP = findOptionDesc(cpP, name);

    int retval;

    if (!optionDescP) {
        fprintf(stderr, "cmdlineParser called incorrectly.  "
                "cmd_getOptionValueInt() called for undefined option '%s'\n",
                name);
        abort();
    } else {
        if (optionDescP->type != OPTTYPE_INT) {
            fprintf(stderr, "cmdlineParser called incorrectly.  "
                    "cmd_getOptionValueInt() called for non-integer "
                    "option '%s'\n", optionDescP->name);
            abort();
        } else {
            if (optionDescP->present) 
                retval = optionDescP->value.i;
            else
                retval = 0;
        }
    }

    return retval;
}



const char *
cmd_getOptionValueString(cmdlineParser const cpP,
                         const char *  const name) {

    struct optionDesc * const optionDescP = findOptionDesc(cpP, name);

    const char * retval;

    if (!optionDescP) {
        fprintf(stderr, "cmdlineParser called incorrectly.  "
                "cmd_getOptionValueString() called for " 
                "undefined option '%s'\n",
                name);
        abort();
    } else {
        if (optionDescP->type != OPTTYPE_STRING) {
            fprintf(stderr, "cmdlineParser called incorrectly.  "
                    "getOptionValueString() called for non-string "
                    "option '%s'\n", optionDescP->name);
            abort();
        } else {
            if (optionDescP->present) {
                retval = strdup(optionDescP->value.s);
                if (retval == NULL) {
                    fprintf(stderr, 
                            "out of memory in cmd_getOptionValueString()\n");
                    abort();
                }
            } else
                retval = NULL;
        }
    }
    return retval;
}



unsigned int 
cmd_argumentCount(cmdlineParser const cpP) {

    return cpP->numArguments;

}
                  


const char * 
cmd_getArgument(cmdlineParser const cpP, 
                unsigned int  const argNumber) { 

    const char * retval;
 
    if (argNumber >= cpP->numArguments)
        retval = NULL;
    else {
        retval = strdup(cpP->argumentArray[argNumber]);

        if (retval == NULL) {
            fprintf(stderr, 
                    "out of memory in cmd_getArgument()\n");
            abort();
        }
    }
    return retval;
}
