/** @file  psArguments.h
 *
 *  @brief Contains operations for parsing command line input arguments.
 *
 *  @ingroup Arguments
 *
 *  @author David Robbins, MHPCC
 *
 *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
 *  @date $Date: 2006-01-23 23:52:15 $
 *
 *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
 */

#include<stdio.h>
#include<stdarg.h>
#include<string.h>
#include "psArguments.h"
#include "fitsio.h"
#include "psMemory.h"
#include "psError.h"
#include "psAbort.h"
#include "psErrorText.h"
#include "psLogMsg.h"
#include "psTrace.h"

#define NUM_SPACES 4   // Number of spaces between

// Set verbosity level
int psArgumentVerbosity(int *argc, char **argv)
{
    int logLevel = 2;   // Default log level
    int argnum = 0;   // Argument number

    // set in order, so that -vvv overrides -vv overrides -v
    if ( (argnum = psArgumentGet(*argc, argv, "-v")) ) {
        psArgumentRemove(argnum, argc, argv);
        logLevel = 3;
    }
    if ( (argnum = psArgumentGet(*argc, argv, "-vv")) ) {
        psArgumentRemove(argnum, argc, argv);
        logLevel = 4;
    }
    if ( (argnum = psArgumentGet(*argc, argv, "-vvv")) ) {
        psArgumentRemove(argnum, argc, argv);
        logLevel = 5;
    }
    psLogSetLevel(logLevel);  // XXX: This function should return an error if the log level is invalid

    if ( (argnum = psArgumentGet(*argc, argv, "-logfmt")) ) {
        if (*argc < argnum + 2) {
            psError(PS_ERR_IO, true, "-logfmt switch specified without a format.");
        } else {
            psArgumentRemove(argnum, argc, argv);
            psLogSetFormat(argv[argnum]); // XXX EAM : this function should return an error if the log format is invalid
            psArgumentRemove(argnum, argc, argv);
        }
    }

    // Now the trace stuff
    // argument format is: -trace (facil) (level)
    while ( (argnum = psArgumentGet(*argc, argv, "-trace")) ) {
        if ( (*argc < argnum + 3) ) {
            psError(PS_ERR_IO, true, "-trace switch specified without facility and level.");
        }
        psArgumentRemove(argnum, argc, argv);
        psTraceSetLevel(argv[argnum], atoi(argv[argnum+1])); // XXX: This function should return an error if the trace level is invalid
        psArgumentRemove(argnum, argc, argv);
        psArgumentRemove(argnum, argc, argv);
    }
    if ((argnum = psArgumentGet(*argc, argv, "-trace-levels"))) {
        psTracePrintLevels();
        exit(2);
    }

    return logLevel;
}

// Find the location of the specified argument
int psArgumentGet(int argc, char **argv, const char *arg)
{
    for (int i = 1; i < argc; i++) {
        if (!strcmp(argv[i], arg))
            return i;
    }

    return 0;
}

// Remove the specified argument (by location)
bool psArgumentRemove(int argnum, int *argc, char **argv)
{
    if (argnum > 0) {
        (*argc)--;
        for (int i = argnum; i < *argc; i++) {
            argv[i] = argv[i+1];
        }
    } else {
        return false;
    }

    return true;
}


static psMetadataItem *argumentRead(psMetadataItem *item, // Item to read into
                                    int argnum, // Argument number
                                    int *argc, // Number of arguments in total
                                    char **argv) // The arguments

{
    psMetadataItem *newItem = NULL;
    switch(item->type)
    {
        // Only doing a representative set of types
    case PS_DATA_S32:
        newItem = psMetadataItemAlloc(item->name, item->type, item->comment, atoi(argv[argnum]));
        psArgumentRemove(argnum, argc, argv);
        break;
    case PS_DATA_F32:
        newItem = psMetadataItemAlloc(item->name, item->type, item->comment, atof(argv[argnum]));
        psArgumentRemove(argnum, argc, argv);
        break;
    case PS_DATA_BOOL:
        // Turn option on; no optional argument to remove
        newItem = psMetadataItemAlloc(item->name, item->type, item->comment, true);
        break;
        // XXX: Include the other numerical types
    case PS_DATA_STRING: {
            //psString string = psStringCopy(argv[argnum]); // Get the argument into PS memory management
            //psFree(string);
            newItem = psMetadataItemAlloc(item->name, item->type, item->comment, argv[argnum]);
            psArgumentRemove(argnum, argc, argv);
        }
        break;
    default:
        psError(PS_ERR_IO, true, "Argument type (%x) is not supported --- argument %s ignored\n",
                item->type, item->name);
        psFree(newItem);
        return NULL;
    }

    return newItem;
}


// XXX: There is a memory leak in the MULTI section.  I think it might have something to do with reference
// counting between lists and MD, in the second section of the code (copy newArgs into arguments), but I'm not
// entirely sure.
bool psArgumentParse(psMetadata *arguments,
                     int *argc,
                     char **argv)
{
    // We need to do a bit of mucking around in order to preserve the arguments metadata until the last
    // minute --- if there is a bad argument, we need to return the old "arguments", since they contain
    // the default values, which we probably want to output in a "help" message (we don't want to print
    // the changed values and have the user think that they are default values).

    psMetadata *newArgs = psMetadataAlloc(); // Place to read arguments into
    psList *changed = psListAlloc(NULL);// List of keys that have changed

    for (int i = 1; i < *argc; i++) {
        psTrace(__func__, 7, "Looking at %s\n", argv[i]);
        psMetadataItem *argItem = psMetadataLookup(arguments, argv[i]);
        if (argItem) {
            psArgumentRemove(i, argc, argv); // Remove the switch
            if (argItem->type != PS_DATA_METADATA_MULTI) {
                if (argItem->type != PS_DATA_BOOL && *argc < i + 1) {
                    psError(PS_ERR_IO, true, "Required argument for %s is missing.\n", argItem->name);
                    // XXX: Cleanup before returning
                    psFree(newArgs);
                    return false;
                }
                psMetadataItem *newItem = argumentRead(argItem, i, argc, argv);
                psMetadataAddItem(newArgs, newItem, PS_LIST_TAIL, PS_META_REPLACE);
                psFree(newItem);
            } else {
                // Go through the MULTI
                psList *multi = argItem->data.V; // The list of MULTI psMetadataItems
                if (*argc < i + multi->n) {
                    psError(PS_ERR_IO, true, "Not enough arguments for %s.\n", argItem->name);
                    // Remove the arguments --- they will be ignored
                    for (int j = i; j < *argc; j++) {
                        psArgumentRemove(i, argc, argv);
                    }
                    // XXX: Cleanup before returning
                    psFree(newArgs);
                    return false;
                }

                // Remove any prior existence in the newArgs --- this is important because we specify
                // adding the new items as DUPLICATE_OK, so if some idiot specifies it twice, we'd end
                // up with two copies of everything.
                psMetadataItem *checkItem = psMetadataLookup(newArgs, argItem->name);
                if (checkItem) {
                    (void)psMetadataRemove(newArgs, 0, argItem->name);
                    (void)psListRemoveData(changed, argItem->name);
                }

                psListIterator *multiIter = psListIteratorAlloc(multi, PS_LIST_HEAD, true);
                psMetadataItem *nextItem = NULL; // Item from list
                while ( (nextItem = psListGetAndIncrement(multiIter)) ) {
                    psMetadataItem *newItem = argumentRead(nextItem, i, argc, argv);
                    psMetadataAddItem(newArgs, newItem, PS_LIST_TAIL, PS_META_DUPLICATE_OK);
                    //psFree(newItem);
                }
                psFree(multiIter);
            }

            // Some book-keeping
            //     psString name = psStringCopy(argItem->name);
            psListAdd(changed, PS_LIST_TAIL, argItem->name);
            i--;

        } else if ( (strncmp(argv[i], "-", 1) == 0 ) || (strncmp(argv[i], "+", 1) == 0) ) {
            // Someone's specified a bad option
            psError(PS_ERR_IO, true, "Unknown option: %s\n", argv[i]);
            psFree(newArgs);
            return false;
        }
    }

    // All the arguments are good, so now we can copy the newArgs over
    psListIterator *changedIter = psListIteratorAlloc(changed, PS_LIST_HEAD, false); // Iterator
    psString name = NULL;  // Item from iteration
    while ( (name = psListGetAndIncrement(changedIter)) ) {
        printf("Updating %s\n", name);
        psMetadataItem *oldItem = psMetadataLookup(arguments, name);
        psMetadataItem *newItem = psMetadataLookup(newArgs, name);
        if ( (oldItem->type != newItem->type) ) {
            psAbort(__func__, "Shouldn't reach here!\n");
        }
        switch (oldItem->type) {
            // Only doing a representative set of types
        case PS_DATA_S32:
            oldItem->data.S32 = newItem->data.S32;
            break;
        case PS_DATA_F32:
            oldItem->data.F32 = newItem->data.F32;
            break;
        case PS_DATA_BOOL:
            oldItem->data.B = newItem->data.B;
            break;
            // XXX: Include the other numerical types
        case PS_DATA_STRING:
            psFree(oldItem->data.V);
            oldItem->data.V = psMemIncrRefCounter(newItem->data.V);
            break;
        case PS_DATA_METADATA_MULTI: {
                psList *newMulti = psMemIncrRefCounter(newItem->data.V); // The new list of MULTI
                psList *oldMulti = oldItem->data.V; // The old list of MULTI
                psListIterator *newMultiIter = psListIteratorAlloc(newMulti, PS_LIST_HEAD, false);
                psListIterator *oldMultiIter = psListIteratorAlloc(oldMulti, PS_LIST_HEAD, true);
                psMetadataItem *newMultiItem = NULL; // Item from iterator
                while ( (newMultiItem = psListGetAndIncrement(newMultiIter)) ) {
                    psMetadataItem *oldMultiItem = psListGetAndIncrement(oldMultiIter);
                    if (!oldMultiItem) {
                        psAbort(__func__,
                                "Something went very wrong here!  The lists SHOULD be of the same length!\n");
                    }
                    switch (oldMultiItem->type) {
                        // Only doing a representative set of types
                    case PS_DATA_S32:
                        oldItem->data.S32 = newItem->data.S32;
                        break;
                    case PS_DATA_F32:
                        oldItem->data.F32 = newItem->data.F32;
                        break;
                        // XXX: Include the other numerical types
                    case PS_DATA_STRING:
                        psFree(oldItem->data.V);
                        oldItem->data.V = psMemIncrRefCounter(newItem->data.V);
                        break;
                    default:
                        psAbort(__func__, "Should never ever get here, ever.\n");
                    }
                    psFree(oldMultiItem);
                    psFree(newMultiItem);
                }
                psFree(newMultiIter);
                psFree(oldMultiIter);
            }
            break;
        default:
            psAbort(__func__, "Should never ever ever get here.\n");
        }
    }
    psFree(changedIter);
    psFree(changed);

    // Now, blow away the newArgs and we're done.
    psFree(newArgs);
    return true;
}


static int argLength(psMetadataItem *arg)
{
    switch (arg->type) {
        // Only doing a representative set of types
    case PS_DATA_S32:
        return arg->data.S32 >= 0 ? (int)log10f((float)arg->data.S32) + 1 :
               (int)log10f(-(float)arg->data.S32) + 2;
        // XXX: Other numerical types
    case PS_DATA_F32:
        return arg->data.F32 >= 0 ? 12 : 13; // -d.dddddde?dd
    case PS_DATA_F64:
        return arg->data.F64 >= 0 ? 12 : 13; // -d.dddddde?dd
    case PS_DATA_BOOL:
        return arg->data.B ? 4 : 5;
    case PS_DATA_STRING:
        return strlen(arg->data.V);
    default:
        psAbort(__func__, "Argument type (%x) is not supported.\n", arg->type);
    }

    return 0;
}

void psArgumentHelp(psMetadata *arguments)
{
    printf("Optional arguments, with default values:\n");
    psMetadataIterator *argIter = psMetadataIteratorAlloc(arguments, PS_LIST_HEAD, NULL);
    psMetadataItem *argItem = NULL; // Item from iterator
    int maxName = 4;   // Maximum length of a name
    int maxValue = 4;   // Maximum length of a value

    // First pass to get the sizes
    while ( (argItem = psMetadataGetAndIncrement(argIter)) ) {
        if (strlen(argItem->name) > maxName) {
            maxName = strlen(argItem->name);
        }
        int valLength = argLength(argItem);
        if (valLength > maxValue) {
            maxValue = valLength;
        }
    }

    // Second pass to print
    psMetadataIteratorSet(argIter, PS_LIST_HEAD);
    psString lastName = NULL;  // Last name we printed
    while ( (argItem = psMetadataGetAndIncrement(argIter)) ) {
        // Initial indent
        for (int i = 0; i < NUM_SPACES; i++) {
            printf(" ");
        }

        // Print the name if required
        int position = 0; // Number of spaces in
        if (! lastName || strcmp(lastName, argItem->name) != 0) {
            // A new name
            printf("%s", argItem->name);
            position += strlen(argItem->name);
            lastName = argItem->name;
        }
        for (int i = position; i < maxName + NUM_SPACES; i++) {
            printf(" ");
        }

        // Print the value
        printf("(");
        switch (argItem->type) {
            // Only doing a representative set of types
        case PS_DATA_S32:
            printf("%d", argItem->data.S32);
            break;
            // XXX: Other numerical types
        case PS_DATA_F32:
            printf("%.6e", argItem->data.F32);
            break;
        case PS_DATA_F64:
            printf("%.6e", argItem->data.F64);
            break;
        case PS_DATA_BOOL:
            if (argItem->data.B) {
                printf("TRUE");
            } else {
                printf("FALSE");
            }
            break;
        case PS_DATA_STRING:
            printf("%s", (char*)(argItem->data.V));
            break;
        default:
            psAbort(__func__, "Argument type (%x) is not supported.\n", argItem->type);
        }
        printf(")");
        for (int i = argLength(argItem); i < maxValue + NUM_SPACES; i++) {
            printf(" ");
        }

        // Print the comment
        if (argItem->comment) {
            printf("%s", argItem->comment);
        }
        printf("\n");
    }

    psFree(argIter);
}

// we have log levels 1 (Error), 2 (Warning), 3 (Info), 4 (Details), 5 (Minutiae)
// 2 = default, -v = 3, -vv = 4, -vvv = 5
psS32 psLogArguments (int *argc, char **argv)
{

    int N, level;

    // default log level is 2
    level = 2;

    // set in order, so that -vvv overrides -vv overrides -v
    if ((N = psArgumentGet (*argc, argv, "-v"))) {
        psArgumentRemove (N, argc, argv);
        level = 3;
    }
    if ((N = psArgumentGet (*argc, argv, "-vv"))) {
        psArgumentRemove (N, argc, argv);
        level = 4;
    }
    if ((N = psArgumentGet (*argc, argv, "-vvv"))) {
        psArgumentRemove (N, argc, argv);
        level = 5;
    }

    if ((N = psArgumentGet (*argc, argv, "-logfmt"))) {
        if (*argc < N + 2) {
            psAbort ("psLogArguments", "USAGE: -logfmt (format)");
        }
        psArgumentRemove (N, argc, argv);
        psLogSetFormat (argv[N]); // XXX EAM : this function should return an error if the log format is invalid
        psArgumentRemove (N, argc, argv);
    }

    // set the level, return the level
    psLogSetLevel (level);
    return (level);
}

// set trace levels by facility
psS32 psTraceArguments (int *argc, char **argv)
{

    int N;

    // argument format is: -trace (facil) (level)
    while ((N = psArgumentGet (*argc, argv, "-trace"))) {
        if (*argc < N + 3) {
            psAbort ("psTraceArguments", "USAGE: -trace (facility) (level)");
        }
        psArgumentRemove (N, argc, argv);
        psTraceSetLevel (argv[N], atoi(argv[N+1]));
        psArgumentRemove (N, argc, argv);
        psArgumentRemove (N, argc, argv);
    }
    if ((N = psArgumentGet (*argc, argv, "-trace-levels"))) {
        psTracePrintLevels ();
        exit (2);
    }
    return (TRUE);
}

