// $Id$
//
//  Copyright (c) 2010, Novartis Institutes for BioMedical Research Inc.
//  All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met: 
//
//     * Redistributions of source code must retain the above copyright 
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following 
//       disclaimer in the documentation and/or other materials provided 
//       with the distribution.
//     * Neither the name of Novartis Institutes for BioMedical Research Inc. 
//       nor the names of its contributors may be used to endorse or promote 
//       products derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#include <postgres.h>
#include <fmgr.h>
#include <utils/memutils.h>

#include "rdkit.h"
#include "cache.h"

#define MAGICKNUMBER  0xBEEC0DED
/*
 * Deconstructed values cache
 */

typedef enum EntryKind {
  MolKind,
  BfpKind,
  SfpKind,
  ReactionKind
} EntryKind;

typedef struct ValueCacheEntry {
  Datum toastedValue;
  EntryKind kind;
  
  union {
    struct {
      Mol *value;
      bytea *sign;
      CROMol mol;
    } mol;
    struct {
      Bfp *value;
      BfpSignature *sign;
      CBfp fp;
    } bfp;
    struct {
      Sfp *value;
      bytea *sign;
      CSfp fp;
    } sfp;
    struct {
      Reaction *value;
      bytea *sign;
      CChemicalReaction rxn;
    } reaction;
  } detoasted;
  
  struct ValueCacheEntry *prev;
  struct ValueCacheEntry *next;
} ValueCacheEntry;

#define NENTRIES (16)

typedef struct ValueCache {
  uint32 magickNumber;
  MemoryContext ctx;
  int32 nentries;
  ValueCacheEntry *head;
  ValueCacheEntry *tail;
  ValueCacheEntry *entries[NENTRIES];
} ValueCache;

/*********** Managing LRU **********/
/* move the most recently used value to the head */
static void 
moveFirst(ValueCache *ac, ValueCacheEntry *entry)
{
  /*
   * unlink the entry from the list
   */
  Assert( entry != ac->head );
  
  if ( entry == ac->tail ) {
    Assert( entry->next == NULL );
    ac->tail = entry->prev;
    if ( ac->tail ) {
      ac->tail->next = NULL;
    }
    else {
        ac->head = NULL;
    }
  }
  else {
    entry->prev->next = entry->next;
    entry->next->prev = entry->prev;
  }

  /*
   * reinstall into head 
   */

  Assert( ac->head != NULL );
  Assert( ac->tail != NULL );

  entry->next = ac->head;
  entry->prev = NULL;
  ac->head->prev = entry;
  ac->head = entry;
}

#define DATUMSIZE(d)    VARSIZE_ANY(DatumGetPointer(d)) 

/* 
 * define a comparison operator for the cached values.
 * generally applied to the toasted data, to verify the
 * equality of two values and to keep the cached entries
 * sorted (see cmpEntries below).
 */
static int
cmpDatum(Datum a, Datum b)
{
  int32   la = DATUMSIZE(a);
  int32   lb = DATUMSIZE(b);

  if (la == lb) {
    return memcmp(DatumGetPointer(a), DatumGetPointer(b), la);
  }
  return (la > lb) ? 1 : -1;
}

/*
 * free the memory allocated by a cache entry to hold the
 * toasted, detoasted and signature values (called when the
 * cache is destroyed or if the entry is reused for a new
 * value)
 */
static void
cleanupData(ValueCacheEntry *entry)
{
  pfree(DatumGetPointer(entry->toastedValue));
  
  switch(entry->kind) {
  case MolKind:
    if (entry->detoasted.mol.value) {
      pfree(entry->detoasted.mol.value);
    }
    if (entry->detoasted.mol.mol) {
      freeCROMol(entry->detoasted.mol.mol);
    }
    if (entry->detoasted.mol.sign) {
      pfree(entry->detoasted.mol.sign);
    }  
    break;
  case BfpKind:
    if (entry->detoasted.bfp.value) {
      pfree(entry->detoasted.bfp.value);
    }
    if (entry->detoasted.bfp.fp) {
      freeCBfp(entry->detoasted.bfp.fp);
    }
    if (entry->detoasted.bfp.sign) {
      pfree(entry->detoasted.bfp.sign);
    }  
    break;
  case SfpKind:
    if (entry->detoasted.sfp.value) {
      pfree(entry->detoasted.sfp.value);
    }
    if (entry->detoasted.sfp.fp) {
      freeCSfp(entry->detoasted.sfp.fp);
    }
    if (entry->detoasted.sfp.sign) {
      pfree(entry->detoasted.sfp.sign);
    }  
    break;
  case ReactionKind:
    if (entry->detoasted.reaction.value) {
      pfree(entry->detoasted.reaction.value);
    }
    if (entry->detoasted.reaction.rxn) {
      freeChemReaction(entry->detoasted.reaction.rxn);
    }
    if (entry->detoasted.reaction.sign) {
      pfree(entry->detoasted.reaction.sign);
    }  
    break;
  default:
    elog(ERROR, "Unknown kind: %d", entry->kind);
  }

  /* this function is called both when the memory context
   * is reset or destroyed, and all the cached entries are
   * discarded, but also when the least recently used value
   * in the cache is replaced, to make space for a new one.
   * to support this latter case, the prev/next pointers in
   * the entry are not zeroed by the call to memset here below.
   */
  memset(entry, 0, offsetof(ValueCacheEntry, prev));
}

/* assign a cache entry with the input toasted value and data kind */
static void
makeEntry(ValueCache *ac, ValueCacheEntry *entry, Datum value, EntryKind kind)
{
  entry->toastedValue = (Datum)MemoryContextAlloc(ac->ctx, DATUMSIZE(value));
  entry->kind = kind;
  memcpy(DatumGetPointer(entry->toastedValue), DatumGetPointer(value),
	 DATUMSIZE(value));
}

/* a comparison operator to sort the entries in ValueCache */
static int
cmpEntry(const void *a, const void *b)
{
  return cmpDatum((*(ValueCacheEntry**)a)->toastedValue,
		  (*(ValueCacheEntry**)b)->toastedValue); 
}

/*********** Managing cache structure **********/

typedef struct CacheHolder {
  MemoryContext ctx;
  ValueCache *cache;
  struct CacheHolder *next;
} CacheHolder;

static CacheHolder *holder = NULL;

/*
 * A memory context callback. This function is registered to be
 * automatically called on a CacheHolder when the associated
 * memory context is reset or destroyed.
 */
static void
cleanupRDKitCache(void * ptr)
{
  CacheHolder *h = holder, *p = NULL;
  CacheHolder *holder_arg = (CacheHolder *)ptr;
  bool removed = false;

  /* cleanup the cached data */
  if (holder_arg->cache->magickNumber != MAGICKNUMBER) {
    elog(WARNING, "Something wrong in cleanupRDKitCache");
  }
  else {
    int i;

    for (i = 0; i < holder_arg->cache->nentries; i++) {
      cleanupData(holder_arg->cache->entries[i]);
    }
    holder_arg->cache->nentries = 0;
  }

  /* Find the holder and remove it from the list */
  while (h) {
    if (h != holder_arg) {
      p = h;
      h = h->next;
      continue;
    }

    /* h == holder_arg, remove from the list */
    if (p == NULL) {
      Assert(h == holder);
      holder = h->next;
      h = holder;
    }
    else {
      p->next = h->next;
      h = p->next;
    }

    /* done, exit */
    removed = true;
    break;
  }

  if (!removed) {
    elog(WARNING, "Deallocated cache holder not found in list");
  }
}

static ValueCache*
createCache(void *cache, struct MemoryContextData * ctx) 
{
  ValueCache *ac;
  CacheHolder *newholder;
  MemoryContextCallback *ctxcb;

  if (cache != NULL) {
    ac = (ValueCache*)cache;
    if (ac->magickNumber != MAGICKNUMBER || ac->ctx != ctx) {
      elog(ERROR, "We can't use our approach with cache :(");
    }
  }

  /*
   * Try to connect to existing cache!
   */
  newholder = holder;
  while (newholder) {
    if (newholder->ctx == ctx) {
      /* Note: this reassigns the passed cache argument */
      cache = (void*)newholder->cache;
      break;
    }
    newholder = newholder->next;
  }

  if (!cache) {
    /*
     * did not found a cache in current context, make new one
     */
    cache = MemoryContextAllocZero(ctx, sizeof(ValueCache));

    /* init cache */
    ac = (ValueCache*) cache;
    ac->magickNumber = MAGICKNUMBER;
    ac->ctx = ctx;
    
    newholder = MemoryContextAllocZero(ctx, sizeof(CacheHolder));

    /* init holder */
    newholder->ctx = ctx;
    newholder->cache = ac;

    /* store holder */
    newholder->next = holder;
    holder = newholder;

    /*
     * register a context callback to cleanup this cache and
     * unlink the holder
     */
    ctxcb = MemoryContextAlloc(ctx, sizeof(MemoryContextCallback));
    ctxcb->func = cleanupRDKitCache;
    ctxcb->arg = newholder;
    MemoryContextRegisterResetCallback(ctx, ctxcb);
  }

  return (ValueCache*)cache;
}

/*********** SEARCHING **********/

/*
 * entry->kind == MolKind
 *      m, mol, fp, val
 * entry->kind == FpKind
 *      f, fp, val
 */

static void
fetchData(ValueCache *ac, ValueCacheEntry *entry,
          void **detoasted, void **internal, void **sign)
{
  MemoryContext   old;
  void * _tmp;
        
  switch(entry->kind) {
  case MolKind:
    if (detoasted) {
      if (entry->detoasted.mol.value == NULL) {
        Mol *detoastedMol;

        detoastedMol = DatumGetMolP(entry->toastedValue);
        entry->detoasted.mol.value
          = MemoryContextAlloc( ac->ctx, VARSIZE(detoastedMol));
        memcpy(entry->detoasted.mol.value, detoastedMol, VARSIZE(detoastedMol));
      }
      *detoasted = entry->detoasted.mol.value;
    }

    if (internal) {
      if (entry->detoasted.mol.mol == NULL) {
        fetchData(ac, entry, &_tmp, NULL, NULL);
        entry->detoasted.mol.mol = constructROMol(entry->detoasted.mol.value);
      }
      *internal = entry->detoasted.mol.mol;
    }

    if (sign) {
      if (entry->detoasted.mol.sign == NULL) {
        fetchData(ac, entry, NULL, &_tmp, NULL);
        old = MemoryContextSwitchTo( ac->ctx );
        entry->detoasted.mol.sign = makeMolSignature(entry->detoasted.mol.mol);
        MemoryContextSwitchTo(old);
      }
      *sign = entry->detoasted.mol.sign;
    }
    break;
  case BfpKind:
    if (detoasted) {
      if (entry->detoasted.bfp.value == NULL) {
        Bfp *detoastedFP;

        detoastedFP = DatumGetBfpP(entry->toastedValue);
        entry->detoasted.bfp.value
          = MemoryContextAlloc( ac->ctx, VARSIZE(detoastedFP));
        memcpy(entry->detoasted.bfp.value, detoastedFP, VARSIZE(detoastedFP));
      }
      *detoasted = entry->detoasted.bfp.value;
    }

    if (internal) {
      if (entry->detoasted.bfp.fp == NULL) {
        fetchData(ac, entry, &_tmp, NULL, NULL);
        entry->detoasted.bfp.fp
          = constructCBfp(entry->detoasted.bfp.value);
      }
      *internal = entry->detoasted.bfp.fp;
    }

    if (sign) {
      if (entry->detoasted.bfp.sign == NULL) {
        fetchData(ac, entry, NULL, &_tmp, NULL);
        old = MemoryContextSwitchTo( ac->ctx );
        entry->detoasted.bfp.sign
          = makeBfpSignature(entry->detoasted.bfp.fp);
        MemoryContextSwitchTo(old);
      }
      *sign = entry->detoasted.bfp.sign;
    }
    break;
  case SfpKind:
    if (detoasted) { 
      if (entry->detoasted.sfp.value == NULL) {
        Sfp *detoastedFP;

        detoastedFP = DatumGetSfpP(entry->toastedValue);
        entry->detoasted.sfp.value
          = MemoryContextAlloc( ac->ctx, VARSIZE(detoastedFP));
        memcpy(entry->detoasted.sfp.value, detoastedFP, VARSIZE(detoastedFP));
      }
      *detoasted = entry->detoasted.sfp.value;
    }

    if (internal) {
      if (entry->detoasted.sfp.fp == NULL) {
        fetchData(ac, entry, &_tmp, NULL, NULL);
        entry->detoasted.sfp.fp
          = constructCSfp(entry->detoasted.sfp.value);
      }
      *internal = entry->detoasted.sfp.fp;
    }

    if (sign) {
      if (entry->detoasted.sfp.sign == NULL) {
        fetchData(ac, entry, NULL, &_tmp, NULL);
        old = MemoryContextSwitchTo( ac->ctx );
        entry->detoasted.sfp.sign
          = makeSfpSignature(entry->detoasted.sfp.fp, NUMBITS);
        MemoryContextSwitchTo(old);
      }
      *sign = entry->detoasted.sfp.sign;
    }
    break;
  case ReactionKind:
    if (detoasted) {
      if (entry->detoasted.reaction.value == NULL) {
        Reaction *detoastedRxn;

        detoastedRxn = DatumGetReactionP(entry->toastedValue);
        entry->detoasted.reaction.value
          = MemoryContextAlloc( ac->ctx, VARSIZE(detoastedRxn));
        memcpy(entry->detoasted.reaction.value, detoastedRxn,
                VARSIZE(detoastedRxn));
      }
      *detoasted = entry->detoasted.reaction.value;
    }
    
    if (internal) {
      if (entry->detoasted.reaction.rxn == NULL) {
        fetchData(ac, entry, &_tmp, NULL, NULL);
        entry->detoasted.reaction.rxn
          = constructChemReact(entry->detoasted.reaction.value);
      }
      *internal = entry->detoasted.reaction.rxn;
    }
    
    if (sign) {
      if (entry->detoasted.reaction.sign == NULL) {
        fetchData(ac, entry, NULL, &_tmp, NULL);
        old = MemoryContextSwitchTo( ac->ctx );
        entry->detoasted.reaction.sign
          = makeReactionSign(entry->detoasted.reaction.rxn);
        MemoryContextSwitchTo(old);
      }
      *sign = entry->detoasted.reaction.sign;
    }
    break;
  default:
    elog(ERROR, "Unknown kind: %d", entry->kind);
  }
}

static void*
SearchValueCache(void *cache, struct MemoryContextData * ctx, 
		 /*  in: */ Datum a, EntryKind kind, 
		 /* out: */ void **detoasted, void **internal, void **sign)
{
  ValueCache *ac = createCache(cache, ctx);
  ValueCacheEntry *entry;

  /*
   * Fast check of most recently used value 
   */
  if (ac->head && cmpDatum(ac->head->toastedValue, a) == 0) {
    Assert( ac->head->kind == kind );
    fetchData(ac, ac->head, detoasted, internal, sign);
    return cache;
  }

  /*
   * If the cache is empty, insert (and then return) the new value 
   */
  if (ac->head == NULL) {
    ac->entries[0]
      = ac->head
      = ac->tail
      = MemoryContextAllocZero(ac->ctx, sizeof(ValueCacheEntry));
    ac->nentries = 1;
    makeEntry(ac, ac->head, a, kind);
    fetchData(ac, ac->head, detoasted, internal, sign);
    return cache;
  }

  /*
   * Do a binary search across the cached entries 
   */
  do {
    ValueCacheEntry **StopLow = ac->entries;
    ValueCacheEntry **StopHigh = ac->entries + ac->nentries;
    ValueCacheEntry **StopMiddle;
    int cmp;
    
    while (StopLow < StopHigh) {
      StopMiddle = StopLow + ((StopHigh - StopLow) >> 1);
      entry = *StopMiddle;
      cmp = cmpDatum(entry->toastedValue, a); 
      
      if (cmp == 0) {
        /*
         * If the value is found, move it to the head position
         * and return it
         */
        moveFirst(ac, entry);
        Assert( ac->head->kind == kind );
        fetchData(ac, ac->head, detoasted, internal, sign);
        return cache;
      }
      else if (cmp < 0) {
        StopLow = StopMiddle + 1;
      }
      else {
        StopHigh = StopMiddle;
      }
    }
  } while(0);

  /*
   * Not found 
   */

  if (ac->nentries < NENTRIES) {
    /*
     * If the cache is not full, insert the new value in the head position
     */
    entry
      = ac->entries[ac->nentries]
      = MemoryContextAllocZero(ac->ctx, sizeof(ValueCacheEntry));

    /* install first */
    entry->next = ac->head;
    entry->prev = NULL;
    ac->head->prev = entry;
    ac->head = entry;
    
    ac->nentries ++;
    
    makeEntry(ac, ac->head, a, kind);
    fetchData(ac, ac->head, detoasted, internal, sign);
  } 
  else {
    /*
     * If the cache is full, clear the least recently used value (in tail
     * position) and reuse the entry to place the new value in the head
     */
    cleanupData(ac->tail);
    moveFirst(ac, ac->tail);
    makeEntry(ac, ac->head, a, kind);
    fetchData(ac, ac->head, detoasted, internal, sign);
  }

  /*
   * sort the entries in the array after values are added or removed.
   * this allows applying a binary search if the value is not found in
   * the head position.
   */
  qsort(ac->entries, ac->nentries, sizeof(ValueCacheEntry*), cmpEntry);   
  return cache;
}

void* 
searchMolCache(void *cache, struct MemoryContextData * ctx, Datum a, 
               Mol **m, CROMol *mol, bytea ** val)
{
  return  SearchValueCache(cache, ctx, 
                           /*  in: */ a, MolKind, 
                           /* out: */ (void**)m, (void**)mol, (void**)val);
}

void* 
searchBfpCache(void *cache, struct MemoryContextData * ctx, Datum a, 
	       Bfp **f, CBfp *fp, BfpSignature **val)
{
  return  SearchValueCache(cache, ctx, 
                           /*  in: */ a, BfpKind, 
                           /* out: */ (void**)f, (void**)fp, (void**)val);
}

void* 
searchSfpCache(void *cache, struct MemoryContextData * ctx, Datum a, 
	       Sfp **f, CSfp *fp, bytea **val)
{
  return  SearchValueCache(cache, ctx, 
                           /*  in: */ a, SfpKind, 
                           /* out: */ (void**)f, (void**)fp, (void**)val);
}

void* 
searchReactionCache(void *cache, struct MemoryContextData * ctx, Datum a, 
		    Reaction **rxn, CChemicalReaction *crxn, bytea ** val)
{
  return  SearchValueCache(cache, ctx, 
                           /*  in: */ a, ReactionKind, 
                           /* out: */ (void**)rxn, (void**)crxn, (void**)val);
}
