/*
 * A strawman implementation of the Pan-STARRS metadata system
 */
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include "psLib.h"
#include "psMetadata.h"

/*****************************************************************************/
/*
 * Handle sets of metadata
 */
psMetadata *psMetadataAlloc(void)	// make a new set of metadata
{
    psMetadata *ms = psAlloc(sizeof(psMetadata));

    ms->list = psDlistAlloc(NULL);	// list of metadata
    ms->table = psHashAlloc();		// hash table

    return ms;
}

void psMetadataFree(psMetadata *ms) // destroy a set of metadata
{
    if (ms == NULL) {
	return;
    } else if (psMemGetRefCounter(ms) > 1) {
	psMemDecrRefCounter(ms);
	return;
    }
    
    psDlistFree(ms->list, (void (*)(void *))NULL);
    psHashFree(ms->table, (void (*)(void *))psMetadataItemFree);

    psFree(ms);
}

/*****************************************************************************/
/*
 * Return a properly filled-out psMetadataItem
 *
 * N.b. For types PS_META_FLOAT, PS_META_INT, and PS_META_STR, a COPY
 * of the data will be kept. In all other cases, only a POINTER
 * to the data will be kept.
 */
psMetadataItem *psMetadataItemAlloc(
    const char *name,			// name of new item of metadata (may be an sprintf format)
    int format,				// type of this piece of metadata + flags
    const char *comment,		// comment associated with item; may be NULL
    ...)				// data + possible arguments for name format
{
    va_list ap;
    va_start(ap, comment);

    psMetadataItem *mi = psMetadataItemAllocV(name, format, comment, ap);

    va_end(ap);

    return mi;
}

psMetadataItem *psMetadataItemAllocV(
    const char *name,			// name of new item of metadata (may be an sprintf format)
    int format,				// type of this piece of metadata + flags
    const char *comment,		// comment associated with item; may be NULL
    va_list ap)				// possible arguments for name format
{
    const psMetadataType type = format & PS_META_TYPE_MASK; // the type part of format
    psMetadataFlags flags = format & ~PS_META_TYPE_MASK;    // the flags part of format
    static int id = 0;			// unique ID for an item of metadata
    psMetadataItem *ms = psAlloc(sizeof(psMetadataItem));

    *(int *)&ms->id = id++;		// need to cast away const

    assert ((PS_META_NTYPE & PS_META_TYPE_MASK) == PS_META_NTYPE); // we have enough type names available
    if (flags & PS_META_NON_UNIQUE) {
	if (flags & PS_META_UNIQUE) {
	    psTrace("metadata.name", 1,
		    "Keyword \"%s\" was specified as both UNIQUE and NON_UNIQUE; adopting UNIQUE\n", name);
	    flags &= ~PS_META_NON_UNIQUE;
	}
    } else {
	flags |= PS_META_UNIQUE;
    }
    
    ms->type = type;
    ms->flags = flags;
    ms->items = NULL;

    assert (type >= 0 && type < PS_META_NTYPE);
    switch (type) {
      case PS_META_ITEM_SET:
	ms->data.V = NULL;
	ms->items = psDlistAlloc(NULL);
	break;

      case PS_META_F32:			// psF32 promoted to psF64 through ...
      case PS_META_F64:
	ms->data.F64 = va_arg (ap, psF64);
	break;
	
      case PS_META_S32:
	ms->data.S32 = va_arg (ap, psS32);
	break;
	
      case PS_META_STR:
	ms->data.V = psStringCopy(va_arg(ap, char *));
	break;
	
      case PS_META_IMG:
      case PS_META_JPEG:
      case PS_META_PNG:
      case PS_META_ASTROM:
      case PS_META_UNKNOWN:
	ms->data.V = va_arg(ap, void *);
	break;

      case PS_META_NTYPE:
	psAbort(__FILE__, "You cannot get here (%d)\n", __LINE__);
	break;				// NOTREACHED
    }

    assert (name != NULL);
    if (strchr(name, '%') != NULL) {
	static char tmp[201];		// should be enough; we won't overflow even if it isn't

	if(vsnprintf(tmp, sizeof(tmp), name, ap) > sizeof(tmp) - 1) {
	    psTrace("metadata.name", 1, "Keyword \"%s\" was truncated to \"%s\" upon expansion\n", name, tmp);
	}

	name = tmp;	    
    }
    ms->name = psStringCopy(name);

    ms->comment = psStringCopy(comment == NULL ? "" : comment);

    return ms;
}

void psMetadataItemFree(psMetadataItem *ms) // piece of metadata to destroy
{
    if(ms == NULL) {
	return;
    }

    psFree(ms->name);
    psFree(ms->comment);

    assert (ms->type >= 0 && ms->type < PS_META_NTYPE);
    switch (ms->type) {
      case PS_META_ITEM_SET:
	psDlistFree(ms->items, (void (*)(void *))NULL); // they're still in the hash table
	ms->items = NULL;
	break;
	
      case PS_META_F32:
      case PS_META_F64:
      case PS_META_S32:
	break;				// nothing to do
	
      case PS_META_STR:
	psFree(ms->data.V);
	break;
	
      case PS_META_IMG:
      case PS_META_JPEG:
      case PS_META_PNG:
      case PS_META_ASTROM:
      case PS_META_UNKNOWN:
	break;				// we don't own the memory
	
      case PS_META_NTYPE:
	psAbort(__FILE__, "You cannot get here (%d)\n", __LINE__);
	break;				// NOTREACHED
    }

    psFree(ms);
    
}

/*****************************************************************************/
/*
 * Add an new psMetadataItem to a psMetadata
 */
psMetadataItem *psMetadataAppendItem(psMetadata *restrict ms, psMetadataItem *restrict item)
{
    psMetadataItem *meta = NULL;	// an item of metadata
    assert (ms != NULL && ms->list != NULL && ms->table != NULL);

    // Have we already seen this item?
    meta = psHashLookup(ms->table, item->name);
    
    if (item->flags & PS_META_NON_UNIQUE) {
	psMetadataItem *head = meta;
	if (head == NULL) {		// this is the first item of this name
	    head = psMetadataAppend(ms, item->name, PS_META_ITEM_SET, NULL);
	}	    

	psDlistSetIterator(head->items, PS_DLIST_TAIL, 0);
	meta = psDlistGetPrev(head->items, 0);

	int i = 0;			// index of this instance of name
	if (meta != NULL) {		// no items of this name
	    char *ptr = strrchr(meta->name, '.');
	    assert (ptr != NULL);
	    i = atoi(ptr + 1) + 1;	// next value
	}

	char newname[strlen(item->name) + 2 + 1];
	sprintf(newname, "%s.%d", item->name, i);

	assert (psHashLookup(ms->table, newname) == NULL); // name isn't in use

	psFree(item->name);
	item->name = psStringCopy(newname);

	psDlistAppend(head->items, item);
	psHashInsert(ms->table, item->name, item, (void (*)(void *))psMetadataItemFree);

	return item;
    } else if (meta != NULL) {
	psError(__func__, PS_ERR_UNKNOWN, 1,
		"Key \"%s\" is already present in the metaDataSet 0x%x", item->name, ms);
	psMetadataItemFree(item);

	return NULL;
    }

    psDlistAppend(ms->list, item);
    psHashInsert(ms->table, item->name, item, (void (*)(void *))psMetadataItemFree);

    return item;
}

/************************************************************************************************************/
/*
 * Convenience wrapper for psMetadataAppendItem()
 */
psMetadataItem *psMetadataAppend(psMetadata *restrict md, ///< Metadata to add to
    const char *name,			///< name of new item of metadata
    int format,				///< type of this piece of metadata + flags
    const char *comment,		///< comment associated with item
    ...)				///< possible arguments for name format
{
    va_list ap;
    va_start(ap, comment);

    psMetadataItem *mi = psMetadataItemAllocV(name, format, comment, ap);

    va_end(ap);

    return psMetadataAppendItem(md, mi);
}

/*****************************************************************************/
/*
 * Remove and return an item of metadata from a set
 */
psMetadataItem *psMetadataRemove(psMetadata *restrict ms, const char *restrict key)
{
    assert (ms != NULL && ms->list != NULL && ms->table != NULL);

    psMetadataItem *meta = psHashRemove(ms->table, key);

    if (meta == NULL) {
	return NULL;
    } else {
	if (meta->flags & PS_META_NON_UNIQUE) {	// it's on someone's ->items list; find that someone
	    char baseKey[strlen(key) + 1];	// Name without the .nnn tacked on
	    strcpy(baseKey, key);

	    char *ptr = strrchr(baseKey, '.');
	    assert (ptr != NULL);
	    *ptr = '\0';

	    psMetadataItem *base = psHashLookup(ms->table, baseKey); // here's the non-qualified metadata
	    assert (base != NULL);

	    return psDlistRemove(base->items, meta, PS_DLIST_UNKNOWN);
	} else {
	    return psDlistRemove(ms->list, meta, PS_DLIST_UNKNOWN);
	}
    }
}

/*****************************************************************************/
/*
 * Iterators for the psDlist representation of the metadata
 */
void psMetadataSetIterator(psMetadata *ms)
{
    assert (ms != NULL);
    psDlistSetIterator(ms->list, PS_DLIST_HEAD, 0);
}

psMetadataItem *psMetadataGetNext(psMetadata *restrict ms,
				  const char *restrict match,
				  int which)
{
    assert (ms != NULL);
    psMetadataItem *meta = psDlistGetNext(ms->list, 0);

    if (match == NULL || match[0] == '\0' || meta == NULL) {
	return meta;
    } else {
	if (strncmp(meta->name, match, strlen(match)) == 0) { // a match
	    return meta;
	} else {
	    return psMetadataGetNext(ms, match, which);
	}
    }
}

/*****************************************************************************/
/*
 * Access using the hash table
 */
psMetadataItem *psMetadataLookup(const psMetadata *restrict ms,
				 const char *restrict key)
{
    assert (ms != NULL);

    return psHashLookup(ms->table, key);
}

/*****************************************************************************/
void psMetadataItemPrint(FILE *fd,		// file descriptor to write to
			 const psMetadataItem *restrict ms, // item of metadata to print
			 const char *prefix)	   // print this at the beginning of each line
{
    if(ms == NULL) {
	return;
    }

    fprintf(fd, "%sName:     %-40s (ID: %d)\n", prefix, ms->name, ms->id);
    if (*ms->comment != '\0') {
	fprintf(fd, "%sComment:  %s\n", prefix, ms->comment);
    }
    fprintf(fd, "%sValue:    ", prefix);

    assert (ms->type >= 0 && ms->type < PS_META_NTYPE);
    fprintf(fd, "%s", prefix);
    switch (ms->type) {
      case PS_META_ITEM_SET:
	{				// gcc 3.3 doesn't allow a declaration here
	    psMetadataItem *meta = NULL;

	    fprintf(fd, "\n");
	    psDlistSetIterator(ms->items, PS_DLIST_HEAD, 0);
	    while ((meta = psDlistGetNext(ms->items, 0)) != NULL) {
		psMetadataItemPrint(fd, meta, "\t");
	    }
	}
	
	break;
	
      case PS_META_F64:
      case PS_META_F32:
	fprintf(fd, "%g\n", ms->data.F32);
	break;
	
      case PS_META_S32:
	fprintf(fd, "%d\n", ms->data.S32);
	break;
	
      case PS_META_STR:
	fprintf(fd, "%s\n", (char *)ms->data.V);
	break;
	
      case PS_META_IMG:
	fprintf(fd, "(IMG)\n");
	break;
	
      case PS_META_JPEG:
	fprintf(fd, "(JPG)\n");
	break;
	
      case PS_META_PNG:
	fprintf(fd, "(PNG)\n");
	break;
	
      case PS_META_ASTROM:
	fprintf(fd, "(ASTROM)\n");
	break;
	
      case PS_META_UNKNOWN:
	fprintf(fd, "(UNKNOWN)\n");
	break;
	
      case PS_META_NTYPE:
	psAbort(__FILE__, "You cannot get here (%d)\n", __LINE__);
	break;				// NOTREACHED
    }
}
