#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>

#include "psType.h"
#include "psError.h"
#include "psAbort.h"
#include "psTrace.h"
#include "psMetadata.h"
#include "psMetadataItemCompare.h"

typedef enum {
    PS_MD_OP_EQ,
    PS_MD_OP_LT,
    PS_MD_OP_LE,
    PS_MD_OP_GT,
    PS_MD_OP_GE,
    PS_MD_OP_NE
} psMetadataItemCompareOp;

/* determine boolean operator specified in comment: @OP: X@ (default is ==) */
psMetadataItemCompareOp p_psMetadataItemCompareGetOp (const psMetadataItem *item) {

    if (!item->comment) return PS_MD_OP_EQ;

    char *p1 = strstr (item->comment, "@OP:");
    if (!p1) return PS_MD_OP_EQ;
    p1 += 4; // point to first char after @OP:
    while (isblank(p1[0])) p1++;

    char *p2 = strchr (p1, '@');
    if (!p2) return PS_MD_OP_EQ;

    // the string starting at p1 and p2 must contain one of the following:
    // == or = PS_MD_OP_EQ,
    // < PS_MD_OP_LT,
    // <= PS_MD_OP_LE,
    // > PS_MD_OP_GT,
    // >= PS_MD_OP_GE,
    // ! PS_MD_OP_NE
    
    // XXX a bit crude: does not catch the case of invalid chars after boolean op
    if (!strncmp (p1, "==", 2)) return PS_MD_OP_EQ;
    if (!strncmp (p1, "=",  1)) return PS_MD_OP_EQ;
    if (!strncmp (p1, "<=", 2)) return PS_MD_OP_LE;
    if (!strncmp (p1, "<",  1)) return PS_MD_OP_LT;
    if (!strncmp (p1, ">=", 2)) return PS_MD_OP_GE;
    if (!strncmp (p1, ">",  1)) return PS_MD_OP_GT;

    return PS_MD_OP_EQ;
}

// XXX better value for tolerance?
# define EQ_TOL 1e-6

# define COMPARE_CASE(TEMPLATENAME, TEMPLATETYPE, COMPARENAME, COMPARETYPENAME) \
  case PS_TYPE_##COMPARETYPENAME: {					 \
      TEMPLATETYPE valueC = (TEMPLATETYPE)compare->data.COMPARENAME;	 \
      TEMPLATETYPE valueT = (TEMPLATETYPE)template->data.TEMPLATENAME;			  \
      /* XXX check the validiy of the type casting? */                 \
      if (valueC != compare->data.COMPARENAME) {			 \
	  return false;						 \
      }								 \
      /* does template specify a boolean operation in comment? */ \
      psMetadataItemCompareOp op = p_psMetadataItemCompareGetOp (template);  \
      switch (op) { \
	case PS_MD_OP_EQ: \
	  if ((template->type == PS_TYPE_F32) || (template->type == PS_TYPE_F64)) { \
	      result = fabs(valueT - valueC) < EQ_TOL; \
	  } else { \
	      result = valueT == valueC; \
	  } \
	  break; \
	case PS_MD_OP_LT: \
	  result = valueC < valueT; \
	  break; \
	case PS_MD_OP_LE: \
	  result = valueC <= valueT; \
	  break; \
	case PS_MD_OP_GT: \
	  result = valueC > valueT; \
	  break; \
	case PS_MD_OP_GE: \
	  result = valueC >= valueT; \
	  break; \
	case PS_MD_OP_NE: \
	  result = valueC != valueT; \
	  break; \
	default: \
	  psAbort ("all cases should have been handled..."); \
      } \
      return (result); \
    }

#define TEMPLATE_CASE(TYPENAME, NAME, TYPE)				\
    case PS_TYPE_##TYPENAME:						\
    switch(compare->type) {						\
        COMPARE_CASE(NAME, TYPE, B  , BOOL);				\
        COMPARE_CASE(NAME, TYPE, U8 , U8 );				\
        COMPARE_CASE(NAME, TYPE, U16, U16);				\
        COMPARE_CASE(NAME, TYPE, U32, U32);				\
        COMPARE_CASE(NAME, TYPE, S8 , S8 );				\
        COMPARE_CASE(NAME, TYPE, S16, S16);				\
        COMPARE_CASE(NAME, TYPE, S32, S32);				\
        COMPARE_CASE(NAME, TYPE, F32, F32);				\
        COMPARE_CASE(NAME, TYPE, F64, F64);				\
      default:								\
        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Don't know how to compare types %x and %x\n", \
                compare->type, template->type);				\
        return false;							\
    }


bool psMetadataItemCompare(const psMetadataItem *compare, // Item to compare to the template
                           const psMetadataItem *template) // The template
{

    // First order checks:

    // both items must exist
    if (! compare || ! template) {
        return false;
    }

    // the names of both items must match
    if (strcmp(compare->name, template->name)) {
        return false;
    }

    bool result = false;

    switch (template->type) {
        TEMPLATE_CASE(BOOL, B,   bool) ;
        TEMPLATE_CASE(U8,   U8,  psU8 );
        TEMPLATE_CASE(U16,  U16, psU16);
        TEMPLATE_CASE(U32,  U32, psU32);
        TEMPLATE_CASE(S8,   S8,  psS8 );
        TEMPLATE_CASE(S16,  S16, psS16);
        TEMPLATE_CASE(S32,  S32, psS32);
        TEMPLATE_CASE(F32,  F32, psF32);
        TEMPLATE_CASE(F64,  F64, psF64);

    case PS_DATA_STRING:

        // for MULTI, try each one & succeed if any match (valid = true is default state)
        if (compare->type == PS_DATA_METADATA_MULTI) {
            for (int j = 0; j < compare->data.list->n; j++) {
                psMetadataItem *entry = psListGet (compare->data.list, j);
		if (!entry) continue;
		if (entry->type != PS_DATA_STRING) {
		  continue;
		}
		if (template->data.V && !entry->data.V) { // expecting valid data, found NULL
		  continue;
		}
		if (!template->data.V && entry->data.V) { // expecting NULL, found valid data
		  continue;
		}
		// XXX should we return true for both NULL?
		if (!template->data.V && !entry->data.V) { // expecting NULL, found NULL
		  return true;
		}
		if (!strcasecmp(entry->data.V, template->data.V)) {
		  return true;
		}
		// XXX this is a hack : also compare with the comment field (for HISTORY, COMMENT)
		if (!strcasecmp(entry->comment, template->data.V)) {
		  return true;
		}
            }
	    return false;
        }

	// any other (non-MULTI) is a mis-match
        if (compare->type != PS_DATA_STRING) {
            return false;
        }
        psTrace("psLib.types", 10, "Comparing '%s' with '%s'\n", compare->data.str, template->data.str);
        if ((!compare->data.V && template->data.V) || (!template->data.V && compare->data.V)) {
            return false;
        }
        return (strcasecmp(compare->data.V, template->data.V) == 0) ? true : false;

    default:
        // Simply don't know how to compare more complex types.
        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Don't know how to compare types %x and %x\n",
                compare->type, template->type);
        return false;
    }

    psAbort("Should never get here.\n");
    return false;
}
