/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "primpl.h"

#include <stdlib.h>
#include <stddef.h>

/* Lock used to lock the monitor cache */
#ifdef _PR_NO_PREEMPT
#  define _PR_NEW_LOCK_MCACHE()
#  define _PR_DESTROY_LOCK_MCACHE()
#  define _PR_LOCK_MCACHE()
#  define _PR_UNLOCK_MCACHE()
#else
#  ifdef _PR_LOCAL_THREADS_ONLY
#    define _PR_NEW_LOCK_MCACHE()
#    define _PR_DESTROY_LOCK_MCACHE()
#    define _PR_LOCK_MCACHE() \
      {                       \
        PRIntn _is;           \
        _PR_INTSOFF(_is)
#    define _PR_UNLOCK_MCACHE() \
      _PR_INTSON(_is);          \
      }
#  else
PRLock* _pr_mcacheLock;
#    define _PR_NEW_LOCK_MCACHE() (_pr_mcacheLock = PR_NewLock())
#    define _PR_DESTROY_LOCK_MCACHE()   \
      PR_BEGIN_MACRO                    \
      if (_pr_mcacheLock) {             \
        PR_DestroyLock(_pr_mcacheLock); \
        _pr_mcacheLock = NULL;          \
      }                                 \
      PR_END_MACRO
#    define _PR_LOCK_MCACHE() PR_Lock(_pr_mcacheLock)
#    define _PR_UNLOCK_MCACHE() PR_Unlock(_pr_mcacheLock)
#  endif
#endif

/************************************************************************/

typedef struct MonitorCacheEntryStr MonitorCacheEntry;

struct MonitorCacheEntryStr {
  MonitorCacheEntry* next;
  void* address;
  PRMonitor* mon;
  long cacheEntryCount;
};

/*
** An array of MonitorCacheEntry's, plus a pointer to link these
** arrays together.
*/

typedef struct MonitorCacheEntryBlockStr MonitorCacheEntryBlock;

struct MonitorCacheEntryBlockStr {
  MonitorCacheEntryBlock* next;
  MonitorCacheEntry entries[1];
};

static PRUint32 hash_mask;
static PRUintn num_hash_buckets;
static PRUintn num_hash_buckets_log2;
static MonitorCacheEntry** hash_buckets;
static MonitorCacheEntry* free_entries;
static PRUintn num_free_entries;
static PRBool expanding;
static MonitorCacheEntryBlock* mcache_blocks;

static void (*OnMonitorRecycle)(void* address);

#define HASH(address)                                                         \
  ((PRUint32)(((PRUptrdiff)(address) >> 2) ^ ((PRUptrdiff)(address) >> 10)) & \
   hash_mask)

/*
** Expand the monitor cache. This grows the hash buckets and allocates a
** new chunk of cache entries and throws them on the free list. We keep
** as many hash buckets as there are entries.
**
** Because we call malloc and malloc may need the monitor cache, we must
** ensure that there are several free monitor cache entries available for
** malloc to get. FREE_THRESHOLD is used to prevent monitor cache
** starvation during monitor cache expansion.
*/

#define FREE_THRESHOLD 5

static PRStatus ExpandMonitorCache(PRUintn new_size_log2) {
  MonitorCacheEntry **old_hash_buckets, *p;
  PRUintn i, entries, old_num_hash_buckets, added;
  MonitorCacheEntry** new_hash_buckets;
  MonitorCacheEntryBlock* new_block;

  entries = 1L << new_size_log2;

  /*
  ** Expand the monitor-cache-entry free list
  */
  new_block = (MonitorCacheEntryBlock*)PR_CALLOC(
      sizeof(MonitorCacheEntryBlock) +
      (entries - 1) * sizeof(MonitorCacheEntry));
  if (NULL == new_block) {
    return PR_FAILURE;
  }

  /*
  ** Allocate system monitors for the new monitor cache entries. If we
  ** run out of system monitors, break out of the loop.
  */
  for (i = 0, p = new_block->entries; i < entries; i++, p++) {
    p->mon = PR_NewMonitor();
    if (!p->mon) {
      break;
    }
  }
  added = i;
  if (added != entries) {
    MonitorCacheEntryBlock* realloc_block;

    if (added == 0) {
      /* Totally out of system monitors. Lossage abounds */
      PR_DELETE(new_block);
      return PR_FAILURE;
    }

    /*
    ** We were able to allocate some of the system monitors. Use
    ** realloc to shrink down the new_block memory. If that fails,
    ** carry on with the too-large new_block.
    */
    realloc_block = (MonitorCacheEntryBlock*)PR_REALLOC(
        new_block, sizeof(MonitorCacheEntryBlock) +
                       (added - 1) * sizeof(MonitorCacheEntry));
    if (realloc_block) {
      new_block = realloc_block;
    }
  }

  /*
  ** Now that we have allocated all of the system monitors, build up
  ** the new free list. We can just update the free_list because we own
  ** the mcache-lock and we aren't calling anyone who might want to use
  ** it.
  */
  for (i = 0, p = new_block->entries; i < added - 1; i++, p++) {
    p->next = p + 1;
  }
  p->next = free_entries;
  free_entries = new_block->entries;
  num_free_entries += added;
  new_block->next = mcache_blocks;
  mcache_blocks = new_block;

  /* Try to expand the hash table */
  new_hash_buckets =
      (MonitorCacheEntry**)PR_CALLOC(entries * sizeof(MonitorCacheEntry*));
  if (NULL == new_hash_buckets) {
    /*
    ** Partial lossage. In this situation we don't get any more hash
    ** buckets, which just means that the table lookups will take
    ** longer. This is bad, but not fatal
    */
    PR_LOG(_pr_cmon_lm, PR_LOG_WARNING,
           ("unable to grow monitor cache hash buckets"));
    return PR_SUCCESS;
  }

  /*
  ** Compute new hash mask value. This value is used to mask an address
  ** until it's bits are in the right spot for indexing into the hash
  ** table.
  */
  hash_mask = entries - 1;

  /*
  ** Expand the hash table. We have to rehash everything in the old
  ** table into the new table.
  */
  old_hash_buckets = hash_buckets;
  old_num_hash_buckets = num_hash_buckets;
  for (i = 0; i < old_num_hash_buckets; i++) {
    p = old_hash_buckets[i];
    while (p) {
      MonitorCacheEntry* next = p->next;

      /* Hash based on new table size, and then put p in the new table */
      PRUintn hash = HASH(p->address);
      p->next = new_hash_buckets[hash];
      new_hash_buckets[hash] = p;

      p = next;
    }
  }

  /*
  ** Switch over to new hash table and THEN call free of the old
  ** table. Since free might re-enter _pr_mcache_lock, things would
  ** break terribly if it used the old hash table.
  */
  hash_buckets = new_hash_buckets;
  num_hash_buckets = entries;
  num_hash_buckets_log2 = new_size_log2;
  PR_DELETE(old_hash_buckets);

  PR_LOG(
      _pr_cmon_lm, PR_LOG_NOTICE,
      ("expanded monitor cache to %d (buckets %d)", num_free_entries, entries));

  return PR_SUCCESS;
} /* ExpandMonitorCache */

/*
** Lookup a monitor cache entry by address. Return a pointer to the
** pointer to the monitor cache entry on success, null on failure.
*/
static MonitorCacheEntry** LookupMonitorCacheEntry(void* address) {
  PRUintn hash;
  MonitorCacheEntry **pp, *p;

  hash = HASH(address);
  pp = hash_buckets + hash;
  while ((p = *pp) != 0) {
    if (p->address == address) {
      if (p->cacheEntryCount > 0) {
        return pp;
      }
      return NULL;
    }
    pp = &p->next;
  }
  return NULL;
}

/*
** Try to create a new cached monitor. If it's already in the cache,
** great - return it. Otherwise get a new free cache entry and set it
** up. If the cache free space is getting low, expand the cache.
*/
static PRMonitor* CreateMonitor(void* address) {
  PRUintn hash;
  MonitorCacheEntry **pp, *p;

  hash = HASH(address);
  pp = hash_buckets + hash;
  while ((p = *pp) != 0) {
    if (p->address == address) {
      goto gotit;
    }

    pp = &p->next;
  }

  /* Expand the monitor cache if we have run out of free slots in the table */
  if (num_free_entries < FREE_THRESHOLD) {
    /* Expand monitor cache */

    /*
    ** This function is called with the lock held. So what's the 'expanding'
    ** boolean all about? Seems a bit redundant.
    */
    if (!expanding) {
      PRStatus rv;

      expanding = PR_TRUE;
      rv = ExpandMonitorCache(num_hash_buckets_log2 + 1);
      expanding = PR_FALSE;
      if (PR_FAILURE == rv) {
        return NULL;
      }

      /* redo the hash because it'll be different now */
      hash = HASH(address);
    } else {
      /*
      ** We are in process of expanding and we need a cache
      ** monitor.  Make sure we have enough!
      */
      PR_ASSERT(num_free_entries > 0);
    }
  }

  /* Make a new monitor */
  p = free_entries;
  free_entries = p->next;
  num_free_entries--;
  if (OnMonitorRecycle && p->address) {
    OnMonitorRecycle(p->address);
  }
  p->address = address;
  p->next = hash_buckets[hash];
  hash_buckets[hash] = p;
  PR_ASSERT(p->cacheEntryCount == 0);

gotit:
  p->cacheEntryCount++;
  return p->mon;
}

/*
** Initialize the monitor cache
*/
void _PR_InitCMon(void) {
  _PR_NEW_LOCK_MCACHE();
  ExpandMonitorCache(3);
}

/*
** Destroy the monitor cache
*/
void _PR_CleanupCMon(void) {
  _PR_DESTROY_LOCK_MCACHE();

  while (free_entries) {
    PR_DestroyMonitor(free_entries->mon);
    free_entries = free_entries->next;
  }
  num_free_entries = 0;

  while (mcache_blocks) {
    MonitorCacheEntryBlock* block;

    block = mcache_blocks;
    mcache_blocks = block->next;
    PR_DELETE(block);
  }

  PR_DELETE(hash_buckets);
  hash_mask = 0;
  num_hash_buckets = 0;
  num_hash_buckets_log2 = 0;

  expanding = PR_FALSE;
  OnMonitorRecycle = NULL;
}

/*
** Create monitor for address. Don't enter the monitor while we have the
** mcache locked because we might block!
*/
PR_IMPLEMENT(PRMonitor*) PR_CEnterMonitor(void* address) {
  PRMonitor* mon;

  if (!_pr_initialized) {
    _PR_ImplicitInitialization();
  }

  _PR_LOCK_MCACHE();
  mon = CreateMonitor(address);
  _PR_UNLOCK_MCACHE();

  if (!mon) {
    return NULL;
  }

  PR_EnterMonitor(mon);
  return mon;
}

PR_IMPLEMENT(PRStatus) PR_CExitMonitor(void* address) {
  MonitorCacheEntry **pp, *p;
  PRStatus status = PR_SUCCESS;

  _PR_LOCK_MCACHE();
  pp = LookupMonitorCacheEntry(address);
  if (pp != NULL) {
    p = *pp;
    if (--p->cacheEntryCount == 0) {
      /*
      ** Nobody is using the system monitor. Put it on the cached free
      ** list. We are safe from somebody trying to use it because we
      ** have the mcache locked.
      */
      p->address = 0;         /* defensive move */
      *pp = p->next;          /* unlink from hash_buckets */
      p->next = free_entries; /* link into free list */
      free_entries = p;
      num_free_entries++; /* count it as free */
    }
    status = PR_ExitMonitor(p->mon);
  } else {
    status = PR_FAILURE;
  }
  _PR_UNLOCK_MCACHE();

  return status;
}

PR_IMPLEMENT(PRStatus) PR_CWait(void* address, PRIntervalTime ticks) {
  MonitorCacheEntry** pp;
  PRMonitor* mon;

  _PR_LOCK_MCACHE();
  pp = LookupMonitorCacheEntry(address);
  mon = pp ? ((*pp)->mon) : NULL;
  _PR_UNLOCK_MCACHE();

  if (mon == NULL) {
    return PR_FAILURE;
  }
  return PR_Wait(mon, ticks);
}

PR_IMPLEMENT(PRStatus) PR_CNotify(void* address) {
  MonitorCacheEntry** pp;
  PRMonitor* mon;

  _PR_LOCK_MCACHE();
  pp = LookupMonitorCacheEntry(address);
  mon = pp ? ((*pp)->mon) : NULL;
  _PR_UNLOCK_MCACHE();

  if (mon == NULL) {
    return PR_FAILURE;
  }
  return PR_Notify(mon);
}

PR_IMPLEMENT(PRStatus) PR_CNotifyAll(void* address) {
  MonitorCacheEntry** pp;
  PRMonitor* mon;

  _PR_LOCK_MCACHE();
  pp = LookupMonitorCacheEntry(address);
  mon = pp ? ((*pp)->mon) : NULL;
  _PR_UNLOCK_MCACHE();

  if (mon == NULL) {
    return PR_FAILURE;
  }
  return PR_NotifyAll(mon);
}

PR_IMPLEMENT(void)
PR_CSetOnMonitorRecycle(void (*callback)(void* address)) {
  OnMonitorRecycle = callback;
}
