/** @file  psMetadata.c
*
*
*  @brief Contains metadata structures, enumerations and functions prototypes.
*
*  This file defines metadata item, metadata type, metadata flags, metadata containers, and function
*  prototypes necessary creating psLib metadata APIs
*
*  @ingroup Metadata
*
*  @author Robert DeSonia, MHPCC
*  @author Ross Harman, MHPCC
*
*  @version $Revision: 1.61 $ $Name: not supported by cvs2svn $
*  @date $Date: 2005-04-28 19:52:48 $
*
*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
*/

/******************************************************************************/
/*  INCLUDE FILES                                                             */
/******************************************************************************/
#include<stdio.h>
#include<stdarg.h>
#include<string.h>

#include "fitsio.h"
#include "psType.h"
#include "psMemory.h"
#include "psError.h"
#include "psAbort.h"
#include "psList.h"
#include "psHash.h"
#include "psVector.h"
#include "psMetadata.h"
#include "psLookupTable.h"
#include "psString.h"
#include "psAstronomyErrors.h"
#include "psConstants.h"

static psS32 metadataId = 0;

static psMetadataItem* makeMetaMulti(psHash* table, const char* key, psMetadataItem* existing)
{

    if (existing != NULL && existing->type == PS_META_MULTI) {
        return existing;
    }


    psMetadataItem* item = psMetadataItemAlloc(key,
                           PS_META_MULTI,
                           "List of Metadata Items",
                           NULL);

    psListAdd(item->data.list,PS_LIST_TAIL,existing);

    if (existing != NULL) {
        psHashRemove(table,key); // take out the old entry
    }

    psHashAdd(table, key, item); // put in the new MULTI list entry

    // free local references of newly allocated item.
    psFree(item);

    return item;
}

static void metadataItemFree(psMetadataItem* metadataItem)
{
    psMetadataType type;

    type = metadataItem->type;

    if (metadataItem == NULL) {
        return;
    }

    psFree(metadataItem->name);
    psFree(metadataItem->comment);
    if (! PS_META_IS_PRIMITIVE(type)) {
        psFree(metadataItem->data.V);
    }
}

static void metadataIteratorFree(psMetadataIterator* iter)
{
    if (iter == NULL) {
        return;
    }
    psFree(iter->iter);

    if (iter->preg != NULL) {
        regfree(iter->preg);
    }
}

static void metadataFree(psMetadata* metadata)
{
    if (metadata == NULL) {
        return;
    }
    psFree(metadata->list);
    psFree(metadata->table);
}

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

psMetadataItem* psMetadataItemAlloc(const char *name, psMetadataType type,
                                    const char *comment, ...)
{
    va_list argPtr;
    psMetadataItem* metadataItem = NULL;

    // Get the variable list parameters to pass to allocation function
    va_start(argPtr, comment);

    // Call metadata item allocation
    metadataItem = psMetadataItemAllocV(name, type, comment, argPtr);

    // Clean up stack after variable arguement has been used
    va_end(argPtr);

    return metadataItem;
}

#define METADATAITEM_ALLOC_TYPE(NAME,TYPE,METATYPE) \
psMetadataItem* psMetadataItemAlloc##NAME(const char* name, \
        const char* comment, \
        TYPE value) \
{ \
    return psMetadataItemAlloc(name, METATYPE, comment, value); \
}

METADATAITEM_ALLOC_TYPE(Str,const char*,PS_META_STR)
METADATAITEM_ALLOC_TYPE(F32,psF32,PS_META_F32)
METADATAITEM_ALLOC_TYPE(F64,psF64,PS_META_F64)
METADATAITEM_ALLOC_TYPE(S32,psS32,PS_META_S32)
METADATAITEM_ALLOC_TYPE(Bool,psBool,PS_META_BOOL)

psMetadataItem* psMetadataItemAllocV(const char *name, psMetadataType type,
                                     const char *comment, va_list argPtr)
{
    psMetadataItem* metadataItem = NULL;
    char tmp;
    int nBytes;

    PS_PTR_CHECK_NULL(name,NULL);

    // Allocate metadata item
    metadataItem = (psMetadataItem*) psAlloc(sizeof(psMetadataItem));
    metadataItem->data.V = NULL;

    // Set deallocator
    psMemSetDeallocator(metadataItem, (psFreeFcn) metadataItemFree);

    // set metadata item comment
    if (comment == NULL) {
        // Per SDRS, null isn't allowed, must use "" instead
        metadataItem->comment = psStringCopy("");
    } else {
        metadataItem->comment = psStringCopy(comment);
    }

    // Set metadata item unique id
    *(psS32 *)(&metadataItem->id) = ++metadataId;

    // Set metadata item type
    metadataItem->type = type & PS_METADATA_TYPE_MASK;

    // Allocate and set metadata item name
    nBytes = vsnprintf(&tmp, 0, name, argPtr) + 1;
    metadataItem->name = (char *)psAlloc(sizeof(char) * nBytes);
    vsprintf(metadataItem->name, name, argPtr);

    // Set metadata item value
    switch(metadataItem->type) {
    case PS_META_BOOL:
        metadataItem->data.B = (psBool)va_arg(argPtr, psS32);
        break;
    case PS_META_S32:
        metadataItem->data.S32 = (psS32)va_arg(argPtr, psS32);
        break;
    case PS_META_F32:
        metadataItem->data.F32 = (psF32)va_arg(argPtr, psF64);
        break;
    case PS_META_F64:
        metadataItem->data.F64 = (psF64)va_arg(argPtr, psF64);
        break;
    case PS_META_STR:
        // Perform copy of input strings
        metadataItem->data.V = psStringCopy(va_arg(argPtr, char *));
        break;
    case PS_META_MULTI:
        // MULTI needs to create a psList entry, value must be NULL
        metadataItem->data.list = psListAlloc(NULL);
        break;
    case PS_META_LIST:
    case PS_META_VEC:
    case PS_META_HASH:
    case PS_META_LOOKUPTABLE:
    case PS_META_JPEG:
    case PS_META_PNG:
    case PS_META_ASTROM:
    case PS_META_UNKNOWN:
        // Copy of input data not performed due to variability of data types
        metadataItem->data.V = psMemIncrRefCounter(va_arg(argPtr, psPtr));
        break;
    default:
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, PS_ERRORTEXT_psMetadata_METATYPE_INVALID, type);
        psFree(metadataItem);
        metadataItem = NULL;
    }

    return metadataItem;
}

psMetadata* psMetadataAlloc(void)
{
    psList* list = NULL;
    psHash* table = NULL;
    psMetadata* metadata = NULL;

    // Allocate metadata
    metadata = (psMetadata*) psAlloc(sizeof(psMetadata));
    // Set deallocator
    psMemSetDeallocator(metadata, (psFreeFcn) metadataFree);

    // Allocate metadata's internal containers
    list = (psList*) psListAlloc(NULL);
    table = (psHash*) psHashAlloc(10);

    metadata->list = list;
    metadata->table = table;

    return metadata;
}

psBool psMetadataAddItem(psMetadata *md, psMetadataItem *metadataItem, psS32 location, psS32 flags)
{
    char * key = NULL;
    psHash *mdTable = NULL;
    psList *mdList = NULL;
    psMetadataItem *existingEntry = NULL;

    PS_PTR_CHECK_NULL(md,NULL);
    PS_PTR_CHECK_NULL(md->table,NULL);
    PS_PTR_CHECK_NULL(md->list,NULL);
    PS_PTR_CHECK_NULL(metadataItem,NULL);
    PS_PTR_CHECK_NULL(metadataItem->name,NULL);

    mdTable = md->table;
    mdList = md->list;
    key = metadataItem->name;

    // See if key is already in table
    existingEntry = psMetadataLookup(md, key);

    // if replace is set, remove any existing items of the same key
    if (existingEntry != NULL && (flags & PS_META_REPLACE) != 0) {
        psMetadataRemove(md,0,key);
        existingEntry = NULL;
    }

    // if the metadataItem is MULTI, just create a MULTI node.
    if (metadataItem->type == PS_META_MULTI) {
        if (metadataItem->data.list == NULL || metadataItem->data.list->size > 0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE,true,
                    "Specified PS_META_MULTI item is not defined properly "
                    "(list allocated with zero size).");
            return false;
        }

        // make sure the existing entry is PS_META_MULTI
        existingEntry = makeMetaMulti(mdTable,key,existingEntry);

        return true; // all done.
    }

    // how the item is added to the hash depends on prior existence, flags, etc.
    if(existingEntry == NULL) { // no prior existence
        // Node doesn't already exist - Add new metadata item to metadata collection's hash
        if(!psHashAdd(mdTable, key, metadataItem)) {
            psError(PS_ERR_UNKNOWN,false,PS_ERRORTEXT_psMetadata_ADD_TABLE_FAILED,key);
            return false;
        }
    } else {
        if (existingEntry->type == PS_META_MULTI || (flags & PS_META_DUPLICATE_OK) != 0) {
            // duplicate entries allowed - add another entry.

            // make sure the existing hash entry is PS_META_MULTI
            existingEntry = makeMetaMulti(mdTable,key,existingEntry);

            // add to the hash key's list of entries
            if (! psListAdd(existingEntry->data.list, PS_LIST_TAIL, metadataItem) ) {
                psError(PS_ERR_UNKNOWN,false,PS_ERRORTEXT_psMetadata_ADD_COLLECTION_FAILED,key);
                return false;
            }
        } else {
            // error on duplicate entry.
            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                    PS_ERRORTEXT_psMetadata_DUPLICATE_NOT_ALLOWED);
            return false;
        }
    }

    // finally, add the metadataItem to the metadata's list.
    if(!psListAdd(mdList, location, metadataItem)) {
        psError(PS_ERR_UNKNOWN,false,PS_ERRORTEXT_psMetadata_ADD_COLLECTION_FAILED,key);
        return false;
    }

    return true;
}

psBool psMetadataAdd(psMetadata *md, psS32 location, const char *name,
                     psS32 type, const char *comment, ...)
{
    va_list argPtr;

    va_start(argPtr, comment);
    psBool result = psMetadataAddV(md,location,name,type,comment,argPtr);
    va_end(argPtr);

    return result;
}

psBool psMetadataAddV(psMetadata *md, psS32 location, const char *name,
                      psS32 type, const char *comment, va_list list)
{
    psMetadataItem* metadataItem = NULL;

    metadataItem = psMetadataItemAllocV(name, type & PS_METADATA_TYPE_MASK, comment, list);

    if (!psMetadataAddItem(md, metadataItem, location, type & PS_METADATA_FLAGS_MASK)) {
        psError(PS_ERR_UNKNOWN,false,PS_ERRORTEXT_psMetadata_ADD_FAILED);
        psFree(metadataItem);
        return false;
    }
    // Decrement reference count, since the metadata item is now in metadata collection and no longer needed
    psFree(metadataItem);

    return true;
}

#define METADATA_ADD_TYPE(NAME,TYPE,METATYPE) \
psBool psMetadataAdd##NAME(psMetadata* md, psS32 where, const char* name, \
                           const char* comment, TYPE value) { \
    return psMetadataAdd(md,where,name, METATYPE,comment,value); \
}

METADATA_ADD_TYPE(S32,psS32,PS_META_S32)
METADATA_ADD_TYPE(F32,psF32,PS_META_F32)
METADATA_ADD_TYPE(F64,psF64,PS_META_F64)
METADATA_ADD_TYPE(List,psList*,PS_META_LIST)
METADATA_ADD_TYPE(Str,const char*,PS_META_STR)
METADATA_ADD_TYPE(Vector,psVector*,PS_META_VEC)
METADATA_ADD_TYPE(Image,psImage*,PS_META_IMG)
METADATA_ADD_TYPE(Hash,psHash*,PS_META_HASH)
METADATA_ADD_TYPE(LookupTable,psLookupTable*,PS_META_LOOKUPTABLE)
METADATA_ADD_TYPE(Unknown,void*,PS_META_UNKNOWN)

psBool psMetadataRemove(psMetadata *md, psS32 where, const char *key)
{
    PS_PTR_CHECK_NULL(md,NULL);

    PS_PTR_CHECK_NULL(md->list,NULL);
    psList* mdList = md->list;

    PS_PTR_CHECK_NULL(md->table,NULL);
    psHash* mdTable = md->table;

    // Select removal by key or index
    if (key != NULL) {
        // Remove by key name
        psMetadataItem* entry = psHashLookup(mdTable,key);
        if (entry == NULL) {
            psError(PS_ERR_UNKNOWN, false, PS_ERRORTEXT_psMetadata_REMOVE_TABLE_FAILED, key);
            return false;
        }
        if (entry->type == PS_META_MULTI) {
            psMetadataItem* listItem;
            psListIterator* iter = psListIteratorAlloc(
                                       entry->data.list,
                                       PS_LIST_HEAD,true);
            while ((listItem=psListGetAndIncrement(iter)) != NULL) {
                psListRemoveData(mdList, listItem);
            }
            psFree(iter);
            psHashRemove(mdTable,key);

        } else {
            psListRemoveData(mdList, entry);
            psHashRemove(mdTable, key);
        }
    } else {
        // Remove by index
        psMetadataItem* entry = psListGet(mdList, where);
        if (entry == NULL) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, PS_ERRORTEXT_psMetadata_FIND_INDEX_FAILED, where);
            return false;
        }
        key = entry->name;

        if (key == NULL) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, PS_ERRORTEXT_psMetadata_REMOVE_LIST_INDEX_FAILED, where);
            return false;
        }

        psMetadataItem* tableItem = psHashLookup(mdTable, key);
        if (tableItem == NULL) {
            psError(PS_ERR_UNKNOWN, false, PS_ERRORTEXT_psMetadata_REMOVE_TABLE_FAILED, key);
            return false;
        }

        if (tableItem->type == PS_META_MULTI) {
            // multiple entries with same key, remove just the specified one
            psListRemoveData(tableItem->data.list, entry);
        } else {
            if (!psHashRemove(mdTable, key)) {
                psError(PS_ERR_UNKNOWN, false, PS_ERRORTEXT_psMetadata_REMOVE_TABLE_FAILED, key);
                return false;
            }
        }
        psListRemove(mdList, where);
    }

    return true;
}

psMetadataItem* psMetadataLookup(psMetadata *md, const char *key)
{
    psHash* mdTable = NULL;
    psMetadataItem* entry = NULL;


    PS_PTR_CHECK_NULL(md,NULL);
    PS_PTR_CHECK_NULL(md->table,NULL);
    PS_PTR_CHECK_NULL(key,NULL);

    mdTable = md->table;
    entry = (psMetadataItem*)psHashLookup(mdTable, key);

    return entry;
}

void* psMetadataLookupPtr(psBool *status, psMetadata *md, const char *key)
{
    psMetadataItem *metadataItem = NULL;

    if (status) {
        *status = true;
    }

    metadataItem = psMetadataLookup(md, key);
    if(metadataItem == NULL) {
        if (status) {
            *status = false;
        }
        return NULL;
    }
    if (metadataItem->type == PS_META_MULTI) {
        // if multiple keys found, use the first.
        metadataItem = (psMetadataItem*)((metadataItem->data.list)->head);
    }

    if(PS_META_IS_PRIMITIVE(metadataItem->type)) {
        if (status) {
            *status = false;
        }
        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                PS_ERRORTEXT_psMetadata_METATYPE_INVALID,
                metadataItem->type);
        return NULL;
    } else {
        return metadataItem->data.V;
    }
}

#define psMetadataLookupNumTYPE(TYPE) \
ps##TYPE psMetadataLookup##TYPE(psBool *status, psMetadata *md, const char *key) \
{ \
    psMetadataItem *metadataItem = NULL; \
    ps##TYPE value = 0; \
    \
    if (status) { \
        *status = true; \
    } \
    \
    metadataItem = psMetadataLookup(md, key); \
    if(metadataItem == NULL) { \
        if (status) { \
            *status = false; \
        } \
        return 0; \
    } \
    if (metadataItem->type == PS_META_MULTI) { \
        /* if multiple keys found, use the first. */ \
        metadataItem = (psMetadataItem*)((metadataItem->data.list)->head); \
    } \
    \
    switch (metadataItem->type) { \
    case PS_META_S32: \
        value = (ps##TYPE)metadataItem->data.S32; \
        break; \
    case PS_META_F32: \
        value = (ps##TYPE)metadataItem->data.F32; \
        break; \
    case PS_META_F64: \
        value = (ps##TYPE)metadataItem->data.F64; \
        break; \
    case PS_META_BOOL: \
        if (metadataItem->data.B) { \
            value = 1; \
        } \
        break; \
    default: \
        /* if you get to this point, the value is not a number. */ \
        if (status) { \
            *status = false; \
        } \
        break; \
    } \
    \
    /* psFree(metadataItem); currently, the lookup doesn't increment the ref count */ \
    return value; \
}

psMetadataLookupNumTYPE(F32)
psMetadataLookupNumTYPE(F64)
psMetadataLookupNumTYPE(S32)
psMetadataLookupNumTYPE(Bool)

psMetadataItem* psMetadataGet(psMetadata *md, psS32 where)
{
    psMetadataItem* entry = NULL;

    PS_PTR_CHECK_NULL(md,NULL);
    PS_PTR_CHECK_NULL(md->list,NULL);

    entry = (psMetadataItem*) psListGet(md->list, where);
    if (entry == NULL) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, PS_ERRORTEXT_psMetadata_FIND_INDEX_FAILED, where);
        return NULL;
    }

    return entry;
}

psMetadataIterator* psMetadataIteratorAlloc(psMetadata* md,
        int location,
        const char* regex)
{
    PS_PTR_CHECK_NULL(md,NULL);
    PS_PTR_CHECK_NULL(md->list,NULL);

    psMetadataIterator* newIter = psAlloc(sizeof(psMetadataIterator));
    newIter->preg = NULL;
    newIter->iter = NULL;

    // Set deallocator
    psMemSetDeallocator(newIter, (psFreeFcn) metadataIteratorFree);

    if (regex == NULL) {
        newIter->iter = psListIteratorAlloc(md->list, location, false);
        return newIter;
    } else {
        int regRtn = regcomp(newIter->preg,regex,0);
        if (regRtn != 0) {
            char errMsg[256];
            regerror(regRtn, newIter->preg, errMsg, 256);
            regfree(newIter->preg);
            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                    PS_ERRORTEXT_psMetadata_REGEX_INVALID,
                    errMsg);
            psFree(newIter);
            return NULL;
        }
    }

    psMetadataIteratorSet(newIter, location); // XXX: do we error if no match is found?

    return newIter;
}

psBool psMetadataIteratorSet(psMetadataIterator* iterator,
                             int location)
{
    int match;
    psMetadataItem* cursor;

    PS_PTR_CHECK_NULL(iterator,NULL);

    psListIterator* iter = iterator->iter;
    PS_PTR_CHECK_NULL(iterator->iter,NULL);

    regex_t* preg = iterator->preg;

    // handle trivial case where no regex subsetting is required.
    if (preg == NULL) {
        return psListIteratorSet(iter,location);
    }

    if (location < 0) {
        // match from the tail
        match = 0;
        psListIteratorSet(iter,PS_LIST_TAIL);
        while ( (cursor=(psMetadataItem*)iter->cursor) != NULL) {
            if (regexec(preg, cursor->name, 0, NULL, 0) == 0) {
                // this key is a match
                match--;
                if (match == location) {
                    break;
                }
            }
            (void)psListGetAndDecrement(iter);
        }
        return (match == location);
    }

    // find the n-th match from the head
    match = -1;
    psListIteratorSet(iter,PS_LIST_HEAD);
    while ( (cursor=(psMetadataItem*)iter->cursor) != NULL) {
        if (regexec(preg, cursor->name, 0, NULL, 0) == 0) {
            // this key is a match
            match++;
            if (match == location) {
                break;
            }
        }
        (void)psListGetAndIncrement(iter);
    }
    return (match == location);
}

psMetadataItem* psMetadataGetAndIncrement(psMetadataIterator* iterator)
{
    psMetadataItem* oldValue;

    PS_PTR_CHECK_NULL(iterator,NULL);

    psListIterator* iter = iterator->iter;
    PS_PTR_CHECK_NULL(iterator->iter,NULL);

    regex_t* preg = iterator->preg;

    // handle trivial case where no regex subsetting is required.
    if (preg == NULL) {
        return (psMetadataItem*)psListGetAndIncrement(iter);
    }

    oldValue = (psMetadataItem*)iter->cursor;

    while (psListGetAndIncrement(iter) != NULL) {
        if (iter->cursor != NULL &&
                regexec(preg, ((psMetadataItem*)iter->cursor)->name, 0, NULL, 0) == 0) {
            // this key is a match
            break;
        }
    }
    return oldValue;
}

psMetadataItem* psMetadataGetAndDecrement(psMetadataIterator* iterator)
{
    psMetadataItem* oldValue;

    PS_PTR_CHECK_NULL(iterator,NULL);

    psListIterator* iter = iterator->iter;
    PS_PTR_CHECK_NULL(iterator->iter,NULL);

    regex_t* preg = iterator->preg;

    // handle trivial case where no regex subsetting is required.
    if (preg == NULL) {
        return (psMetadataItem*)psListGetAndDecrement(iter);
    }

    oldValue = (psMetadataItem*)iter->cursor;

    while (psListGetAndDecrement(iter) != NULL) {
        if (iter->cursor != NULL &&
                regexec(preg, ((psMetadataItem*)iter->cursor)->name, 0, NULL, 0) == 0) {
            // this key is a match
            break;
        }
    }
    return oldValue;
}
