/** @file  psMetadataIO.c
*
*  @brief Contains metadata input/output functions.
*
*  This file defines functions to read and write metadata to/from an external file.
*
*  @ingroup Metadata
*
*  @author Ross Harman, MHPCC
*
*  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
*  @date $Date: 2004-10-08 00:43:12 $
*
*  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
*/

#include <fitsio.h>
#include <string.h>
#include <ctype.h>

#include "psAbort.h"
#include "psType.h"
#include "psMemory.h"
#include "psError.h"
#include "psString.h"
#include "psList.h"
#include "psHash.h"
#include "psVector.h"
#include "psMetadata.h"
#include "psMetadataIO.h"


/******************************************************************************/
/*  DEFINE STATEMENTS                                                         */
/******************************************************************************/

/** Check for FITS errors */
#define FITS_ERROR(STRING,PS_ERROR) \
fits_get_errstatus(status, fitsErr); \
psError(__func__, STRING, PS_ERROR, fitsErr); \
status = 0; \
fits_close_file(fd, &status); \
if(status){ \
    fits_get_errstatus(status, fitsErr); \
    psError(__func__, "Couldn't close FITS file. FITS error: %s", fitsErr); \
} \
status = 0; \
psFree(output); \
return NULL;

/** Free and null temporary variables used by config file parser */
#define CLEAR_TEMPS()                                                                                        \
if(strName) {                                                                                            \
    psFree(strName);                                                                                     \
    strName = NULL;                                                                                      \
}                                                                                                        \
if(strType) {                                                                                            \
    psFree(strType);                                                                                     \
    strType = NULL;                                                                                      \
}                                                                                                        \
if(strValue) {                                                                                           \
    psFree(strValue);                                                                                    \
    strValue = NULL;                                                                                     \
}                                                                                                        \
if(strComment) {                                                                                         \
    psFree(strComment);                                                                                  \
    strComment = NULL;                                                                                   \
}                                                                                                        \
if(tempVec) {                                                                                            \
    psFree(tempVec);                                                                                     \
    tempVec = NULL;                                                                                      \
}

/** Maximum size of a FITS line */
#define FITS_LINE_SIZE 80

/** Maximum size of a string */
#define MAX_STRING_LENGTH 256


/******************************************************************************/
/*  TYPE DEFINITIONS                                                          */
/******************************************************************************/

// None

/*****************************************************************************/
/*  GLOBAL VARIABLES                                                         */
/*****************************************************************************/

// None

/*****************************************************************************/
/*  FILE STATIC VARIABLES                                                    */
/*****************************************************************************/

// None

/*****************************************************************************/
/*  FUNCTION IMPLEMENTATION - LOCAL                                          */
/*****************************************************************************/


/** Determines if a line is blank (whitespace only) or a commentline. It returns true if so. The input string
 *  must be null terminated. */
bool ignoreLine(char *inString)
{
    while(*inString!='\0' && *inString!='#') {
        if(!isspace(*inString)) {
            return false;
        }
        inString++;
    }

    return true;
}


/** Removes leading and trailing whitespace and # characters from a string. The cleaned string is a new null
 *  terminated copy of the original input string. */
char *cleanString(char *inString, int sLen)
{
    char *ptrB = NULL;
    char *ptrE = NULL;
    char *cleaned = NULL;


    ptrB = inString;

    /* Skip over leading # or whitespace */
    while (isspace(*ptrB) || *ptrB=='#') {
        ptrB++;
    }

    /* Skip over trailing whitespace, null terminators, and # characters */
    ptrE = inString + sLen;
    while(isspace(*ptrE) || *ptrE=='\0' || *ptrE=='#') {
        ptrE--;
    }

    // Length, sLen, does not include '\0'
    sLen = ptrE - ptrB + 1;

    // Adds '\0' to end of string and +1 to sLen
    cleaned = psStringNCopy(ptrB, sLen);

    return cleaned;
}

/** Count repeat occurances of a single character within a line. The input string must be null terminated. */
int repeatedChars(char *inString, char ch)
{
    int count = 0;


    while(*inString!='\0') {
        if(*inString == ch) {
            count++;
        }
        inString++;
    }

    return count;
}

/** Returns cleaned token based on delimiter, but not including delimiter. Also changes the pointer location
 * the beginning of the string. Tokens are newly allocated null terminated strings. */
char* getToken(char **inString, char *delimiter, int *status)
{
    char *cleanToken = NULL;
    int sLen = 0;


    // Skip over leading whitespace
    while(isspace(**inString)) {
        (*inString)++;
    }

    // Length of token, not including delimiter
    sLen = strcspn(*inString, delimiter);
    if(sLen) {

        // Create new, cleaned, and null terminated token
        cleanToken = cleanString(*inString, sLen);

        // Move to end of token
        (*inString) += sLen;
    } else if(**inString!='\0' && sLen==0) {
        *status = 1;
    }

    return cleanToken;
}

/** Returns single parsed value as a double precision number. The input string must be cleaned and null
 * terminated. */
double parseValue(char *inString, int *status)
{
    char *end = NULL;
    double value = 0.0;


    value = strtod(inString, &end);
    if(*end != '\0') {
        *status = 1;
    } else if(inString==end) {
        *status = 1;
    }

    return value;
}

/** Returns true or false. 'T', 't', '1', 'F', 'f', and '0' are acceptable, parsable variations. */
bool parseBool(char *inString, int *status)
{
    bool value = false;


    if(*inString=='T' || *inString=='t' || *inString=='1') {
        value = true;
    } else if(*inString=='F' || *inString=='f' || *inString=='0') {
        value = false;
    } else {
        *status = 1;
    }

    return value;
}

/** Returns parsed vector filled with with data. The input string must be null terminated. */
psVector* parseVector(char *inString, psElemType elemType, int *status)
{
    char *end = NULL;
    char *saveValue = NULL;
    int i = 0;
    int numValues = 0;
    double value = 0.0;
    psVector *vec = NULL;


    // Cycle through string and count entries
    saveValue = inString;
    while(*inString!='\0') {
        strtod(inString, &end);
        if(inString==end) {
            *status = 1;
            return NULL;
        }
        while(*end==' ' || *end==',') { // Commas or spaces may be used as delimiters for vector values
            end++;
        }
        inString=end;
        numValues++;
    }

    // Cycle through string and convert string values to values
    if(numValues) {
        inString = saveValue;
        end = NULL;
        vec = psVectorAlloc(numValues, elemType);

        while(*inString!='\0') {
            value = strtod(inString, &end);
            if(inString==end) {
                *status = 1;
                return vec;
            }
            switch(elemType) {
            case PS_TYPE_U8:
                vec->data.U8[i++] = (psU8)value;
                break;
            case PS_TYPE_S32:
                vec->data.S32[i++] = (psS32)value;
                break;
            case PS_TYPE_F32:
                vec->data.F32[i++] = (psF32)value;
                break;
            case PS_TYPE_F64:
                vec->data.F64[i++] = (psF64)value;
                break;
            default:
                *status = 1;
                psError(__func__, "Invalid type: %d", elemType);
            }

            while(*end==' ' || *end==',') {
                end++;
            }
            inString=end;
        }
    }

    return vec;
}

/*****************************************************************************/
/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
/*****************************************************************************/

void psMetadataItemPrint(FILE * fd, const char *format, const psMetadataItem* restrict metadataItem)
{
    psMetadataType type;

    if (fd == NULL) {
        psError(__func__, "Null file descriptor not allowed");
        return;
    }

    if (format == NULL) {
        psError(__func__, "Null format not allowed");
        return;
    }

    if (metadataItem == NULL) {
        psError(__func__, "Null metadata not allowed");
        return;
    }

    type = metadataItem->type;

    switch(type) {
    case PS_META_BOOL:
        fprintf(fd, format, metadataItem->data.B);
        break;
    case PS_META_S32:
        fprintf(fd,format, metadataItem->data.S32);
        break;
    case PS_META_F32:
        fprintf(fd, format, metadataItem->data.F32);
        break;
    case PS_META_F64:
        fprintf(fd, format, metadataItem->data.F64);
        break;
    case PS_META_STR:
        fprintf(fd, format, metadataItem->data.V);
        break;
    default:
        psError(__func__, " Invalid psMetadataType to print: %d", type);
    }
}


psMetadata* psMetadataReadHeader(psMetadata* output, char *extName, int extNum, char *fileName)
{
    bool tempBool;
    bool success;
    char keyType;
    char keyName[FITS_LINE_SIZE];
    char keyValue[FITS_LINE_SIZE];
    char keyComment[FITS_LINE_SIZE];
    char fitsErr[MAX_STRING_LENGTH];
    int i;
    int hduType = 0;
    int status = 0;
    int numKeys = 0;
    int keyNum = 0;
    psMetadataType metadataItemType;
    fitsfile *fd = NULL;

    if(fileName == NULL) {
        psError(__func__, "Null fileName not allowed");
        return NULL;
    }

    fits_open_file(&fd, fileName, READONLY, &status);
    if(fd == NULL || status != 0) {
        FITS_ERROR("FITS error while opening file: %s %s", fileName);
        return NULL;
    }

    if (extName == NULL && extNum == 0) {
        psError(__func__, "Null extName and extNum = 0 not allowed");
        return NULL;
    } else if (extName && (extNum != -1) ) {
        psError(__func__, "If extName specified, extNum arguments should be -1.");
        return NULL;
    }

    // Allocate metadata if user didn't
    if (output == NULL) {
        output = psMetadataAlloc();
    }

    // Move to user designated HDU number or HDU name in FITS file. HDU numbers starts at one.
    if (extName != NULL) {
        if (fits_movnam_hdu(fd, ANY_HDU, extName, 0, &status) != 0) {
            FITS_ERROR("FITS error while locating header %s: %s", extName);
        }
    } else {
        if (fits_movabs_hdu(fd, extNum, &hduType, &status) != 0) {
            FITS_ERROR("FITS error while locating header %d: %s", extNum);
        }
    }

    // Get number of key names
    if (fits_get_hdrpos(fd, &numKeys, &keyNum, &status) != 0) {
        FITS_ERROR("FITS error while reading key %d: %s", keyNum);
    }

    // Get each key name. Keywords start at one.
    for (i = 1; i <= numKeys; i++) {
        if (fits_read_keyn(fd, i, keyName, keyValue, keyComment, &status) != 0) {
            FITS_ERROR("FITS error while reading key %d: %s", keyNum);
        }
        if (fits_get_keytype(keyValue, &keyType, &status) != 0) {
            fits_get_errstatus(status, fitsErr);
            if (status != VALUE_UNDEFINED) {
                FITS_ERROR("FITS error while determining key %d type: %s", keyNum);
            } else {
                // Some keywords are still valid if they don't have a type (like COMMENTS and HISTORY)
                keyType = 'C';
                status = 0;
            }
        }

        switch (keyType) {
        case 'I':
            metadataItemType = PS_META_S32;
            success = psMetadataAdd(output, PS_LIST_TAIL, keyName, metadataItemType, keyComment, atoi(keyValue));
            break;
        case 'F':
            metadataItemType = PS_META_F64;
            success = psMetadataAdd(output, PS_LIST_TAIL, keyName, metadataItemType, keyComment, atof(keyValue));
            break;
        case 'C':
            metadataItemType = PS_META_STR;
            success = psMetadataAdd(output, PS_LIST_TAIL, keyName, metadataItemType, keyComment, keyValue);
            break;
        case 'L':
            metadataItemType = PS_META_BOOL;
            tempBool = (keyValue[0] == 'T') ? 1 : 0;
            success = psMetadataAdd(output, PS_LIST_TAIL, keyName, metadataItemType, keyComment, tempBool);
            break;
        case 'U':
        case 'X':
        default:
            psError(__func__, "Invalid psMetadataType: %c", keyType);
            return output;
        }

        if (!success) {
            psError(__func__, "Failed to add metadata item. Name: %s", keyName);
            return output;
        }
    }

    return output;
}


int psMetadataParseConfig(psMetadata** md, char *fileName, bool overwrite)
{
    bool tempBool;
    char *line = NULL;
    char *strName = NULL;
    char *strType = NULL;
    char *strValue = NULL;
    char *strComment = NULL;
    char *linePtr = NULL;
    int status = 0;
    int lineCount = 0;
    int failedLines = 0;
    psF64 tempDbl = 0.0;
    psS32 tempInt = 0.0;
    psMetadataItem *metadataItem = NULL;
    psVector *tempVec = NULL;
    FILE *fp = NULL;
    psElemType elemType;
    psMetadataType mdType;


    // Check for nulls
    if(fileName == NULL) {
        psError(__func__, "Null fileName not allowed");
        return -1;
    } else if((fp=fopen(fileName, "r")) == NULL) {
        psError(__func__, "Couldn't open file. Name: %s", fileName);
        return -1;
    }

    // Allocate metadata if necessary
    if (*md == NULL) {
        *md = psMetadataAlloc();
    }

    // Create reusable line for continuous read
    line = (char*)psAlloc(MAX_STRING_LENGTH*sizeof(char));
    if (line == NULL) {
        psAbort(__func__, "Failed to allocate reusable line");
    }

    // While loop to parse the file
    while(fgets(line, MAX_STRING_LENGTH, fp) != NULL) {

        // Initialize variables for new line
        linePtr = line;
        lineCount++;
        CLEAR_TEMPS();

        // If line is not a comment or blank, then extract data
        if(!ignoreLine(linePtr)) {

            // Check for more than one '*' or '@' in a line
            if(repeatedChars(linePtr, '@') > 1) {
                failedLines++;
                psError(__func__, "More than one @ charecter not allowed. Line %d: %s", lineCount, line);
                continue;
            } else if(repeatedChars(linePtr, '*') > 1) {
                failedLines++;
                psError(__func__, "More than one * charecter not allowed. Line %d: %s", lineCount, line);
                continue;
            } else if(repeatedChars(linePtr, '~') > 0) {
                failedLines++;
                psError(__func__, "Illegal character. Character:~ Line %d: %s", lineCount, line);
                continue;
            }

            // Get metadata item name
            strName = getToken(&linePtr, " ", &status);
            if(strName==NULL || status) {
                failedLines++;
                status = 0;
                psError(__func__, "Null name not allowed. Line %d: %s", lineCount, line);
                continue;
            }

            // Get the metadata item type
            strType = getToken(&linePtr, " ", &status);
            if(strType==NULL) {
                failedLines++;
                status = 0;
                psError(__func__, "Null type not allowed. Line %d: %s", lineCount, line);
                continue;
            } else if(!strncmp(strType, "*", 1)) {
                mdType = PS_META_ITEM_SET;
            } else if(!strncmp(strType, "STR", 3)) {
                mdType = PS_META_STR;
                elemType = PS_TYPE_PTR;
            } else if(!strncmp(strType, "BOOL", 3)) {
                mdType = PS_META_BOOL;
                elemType = PS_TYPE_U8;
            } else if(!strncmp(strType, "S32", 3)) {
                mdType = PS_META_S32;
                elemType = PS_TYPE_S32;
            } else if(!strncmp(strType, "F32", 3)) {
                mdType = PS_META_F32;
                elemType = PS_TYPE_F32;
            } else if(!strncmp(strType, "F64", 3)) {
                mdType = PS_META_F64;
                elemType = PS_TYPE_F64;
            } else {
                failedLines++;
                psError(__func__, "Invalid psMetadataType. Type: %s Line %d: %s", strType, lineCount, line);
                continue;
            }

            if(*strName == '@') {
                mdType = PS_META_VEC;
            }

            // Get the metadata item value if there is one. Lines with * don't have values.
            if(mdType != PS_META_ITEM_SET) {
                strValue = getToken(&linePtr, "#", &status);
                if(strValue==NULL) {
                    failedLines++;
                    status = 0;
                    psError(__func__, "Null value not allowed. Line %d: %s", lineCount, line);
                    continue;
                }
            }

            // Get the metadata item value if there is one. Not all lines will have comments, so NULL is ok.
            strComment = getToken(&linePtr,"~", &status);
            if(status) {
                failedLines++;
                status = 0;
                psError(__func__, "Failed to parse comment. Line %d: %s", lineCount, line);
                continue;
            }

            /* If metadata item is found, is not a folder node, and overwrite is allowed, then remove existing
            and allow switch/case below to add new item. If overwrite is false, then report error. If found
            item is folder node, then psMetadataAdd will automatically add a new child. */
            metadataItem = psMetadataLookup(*md, strName);
            if(metadataItem != NULL) {
                if(metadataItem->type!=PS_META_ITEM_SET) {
                    if(overwrite) {
                        psMetadataRemove(*md, PS_LIST_UNKNOWN, strName);
                    } else {
                        failedLines++;
                        psError(__func__, "Overwrite for name not allowed. Name: %s. Line %d: %s", strName,
                                lineCount, line);
                        continue;
                    }
                }
            }

            // Create and add metadata item to metadata and parse values
            switch (mdType) {
            case PS_META_ITEM_SET:
                psMetadataAdd(*md, PS_LIST_TAIL, strName, mdType, NULL, NULL);
                break;
            case PS_META_BOOL:
                tempBool = parseBool(strValue, &status);
                if(!status) {
                    psMetadataAdd(*md, PS_LIST_TAIL, strName, mdType, strComment, tempBool);
                } else {
                    status = 0;
                    failedLines++;
                    psError(__func__, "Failed to parse value. Value: %s Line %d: %s", strValue, lineCount, line);
                    continue;
                }
                break;
            case PS_META_S32:
                tempInt = (psS32)parseValue(strValue, &status);
                if(!status) {
                    psMetadataAdd(*md, PS_LIST_TAIL, strName, mdType, strComment, tempInt);
                } else {
                    status = 0;
                    failedLines++;
                    psError(__func__, "Failed to parse value. Value: %s Line %d: %s", strValue, lineCount, line);
                    continue;
                }
                break;
            case PS_META_F32:
            case PS_META_F64:
                tempDbl = parseValue(strValue, &status);
                if(!status) {
                    psMetadataAdd(*md, PS_LIST_TAIL, strName, mdType, strComment, tempDbl);
                } else {
                    status = 0;
                    failedLines++;
                    psError(__func__, "Failed to parse value. Value: %s Line %d: %s", strValue, lineCount, line);
                    continue;
                }
                break;
            case PS_META_STR:
                psMetadataAdd(*md, PS_LIST_TAIL, strName, mdType, strComment, strValue);
                break;
            case PS_META_VEC:
                tempVec = parseVector(strValue, elemType, &status);
                if(!status) {
                    psMetadataAdd(*md, PS_LIST_TAIL, strName+1, mdType, strComment, tempVec);
                } else {
                    status = 0;
                    failedLines++;
                    psError(__func__, "Failed to parse value. Value: %s Line %d: %s", strValue, lineCount, line);
                    continue;
                }
                break;
            default:
                failedLines++;
                psError(__func__, "Invalid psMetadataType. Type: %d Line %d: %s", mdType, lineCount, line);
                continue;
            } // switch
        } // if ignoreLine
    } // while loop

    psFree(line);

    return failedLines;
}
