/* 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 <string.h>

#if defined(SOLARIS) && \
    (defined(_PR_PTHREADS) || defined(_PR_GLOBAL_THREADS_ONLY))

#  include <synch.h>
#  define HAVE_UI_RWLOCK
#  define RWLOCK_T rwlock_t
#  define RWLOCK_INIT(lock) rwlock_init(lock, USYNC_THREAD, NULL)
#  define RWLOCK_DESTROY(lock) rwlock_destroy(lock)
#  define RWLOCK_RDLOCK(lock) rw_rdlock(lock)
#  define RWLOCK_WRLOCK(lock) rw_wrlock(lock)
#  define RWLOCK_UNLOCK(lock) rw_unlock(lock)

#endif

/*
 * Reader-writer lock
 */
struct PRRWLock {
  char* rw_name;    /* lock name                    */
  PRUint32 rw_rank; /* rank of the lock             */

#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  RWLOCK_T rw_lock;
#else
  PRLock* rw_lock;
  PRInt32 rw_lock_cnt; /* ==  0, if unlocked           */
  /* == -1, if write-locked       */
  /* > 0  , # of read locks       */
  PRUint32 rw_reader_cnt;     /* number of waiting readers    */
  PRUint32 rw_writer_cnt;     /* number of waiting writers    */
  PRCondVar* rw_reader_waitq; /* cvar for readers             */
  PRCondVar* rw_writer_waitq; /* cvar for writers             */
#  ifdef DEBUG
  PRThread* rw_owner; /* lock owner for write-lock    */
#  endif
#endif
};

#ifdef DEBUG
#  define _PR_RWLOCK_RANK_ORDER_DEBUG /* enable deadlock detection using \
                                         rank-order for locks            \
                                      */
#endif

#ifdef _PR_RWLOCK_RANK_ORDER_DEBUG

static PRUintn pr_thread_rwlock_key; /* TPD key for lock stack */
static PRUintn pr_thread_rwlock_alloc_failed;

#  define _PR_RWLOCK_RANK_ORDER_LIMIT 10

typedef struct thread_rwlock_stack {
  PRInt32 trs_index;                                /* top of stack */
  PRRWLock* trs_stack[_PR_RWLOCK_RANK_ORDER_LIMIT]; /* stack of lock
                                                       pointers */

} thread_rwlock_stack;

static void _PR_SET_THREAD_RWLOCK_RANK(PRRWLock* rwlock);
static PRUint32 _PR_GET_THREAD_RWLOCK_RANK(void);
static void _PR_UNSET_THREAD_RWLOCK_RANK(PRRWLock* rwlock);
static void _PR_RELEASE_LOCK_STACK(void* lock_stack);

#endif

/*
 * Reader/Writer Locks
 */

/*
 * PR_NewRWLock
 *      Create a reader-writer lock, with the given lock rank and lock name
 *
 */

PR_IMPLEMENT(PRRWLock*)
PR_NewRWLock(PRUint32 lock_rank, const char* lock_name) {
  PRRWLock* rwlock;
#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  int err;
#endif

  if (!_pr_initialized) {
    _PR_ImplicitInitialization();
  }

  rwlock = PR_NEWZAP(PRRWLock);
  if (rwlock == NULL) {
    return NULL;
  }

  rwlock->rw_rank = lock_rank;
  if (lock_name != NULL) {
    rwlock->rw_name = (char*)PR_Malloc(strlen(lock_name) + 1);
    if (rwlock->rw_name == NULL) {
      PR_DELETE(rwlock);
      return (NULL);
    }
    strcpy(rwlock->rw_name, lock_name);
  } else {
    rwlock->rw_name = NULL;
  }

#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  err = RWLOCK_INIT(&rwlock->rw_lock);
  if (err != 0) {
    PR_SetError(PR_UNKNOWN_ERROR, err);
    PR_Free(rwlock->rw_name);
    PR_DELETE(rwlock);
    return NULL;
  }
  return rwlock;
#else
  rwlock->rw_lock = PR_NewLock();
  if (rwlock->rw_lock == NULL) {
    goto failed;
  }
  rwlock->rw_reader_waitq = PR_NewCondVar(rwlock->rw_lock);
  if (rwlock->rw_reader_waitq == NULL) {
    goto failed;
  }
  rwlock->rw_writer_waitq = PR_NewCondVar(rwlock->rw_lock);
  if (rwlock->rw_writer_waitq == NULL) {
    goto failed;
  }
  rwlock->rw_reader_cnt = 0;
  rwlock->rw_writer_cnt = 0;
  rwlock->rw_lock_cnt = 0;
  return rwlock;

failed:
  if (rwlock->rw_reader_waitq != NULL) {
    PR_DestroyCondVar(rwlock->rw_reader_waitq);
  }
  if (rwlock->rw_lock != NULL) {
    PR_DestroyLock(rwlock->rw_lock);
  }
  PR_Free(rwlock->rw_name);
  PR_DELETE(rwlock);
  return NULL;
#endif
}

/*
** Destroy the given RWLock "lock".
*/
PR_IMPLEMENT(void)
PR_DestroyRWLock(PRRWLock* rwlock) {
#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  int err;
  err = RWLOCK_DESTROY(&rwlock->rw_lock);
  PR_ASSERT(err == 0);
#else
  PR_ASSERT(rwlock->rw_reader_cnt == 0);
  PR_DestroyCondVar(rwlock->rw_reader_waitq);
  PR_DestroyCondVar(rwlock->rw_writer_waitq);
  PR_DestroyLock(rwlock->rw_lock);
#endif
  if (rwlock->rw_name != NULL) {
    PR_Free(rwlock->rw_name);
  }
  PR_DELETE(rwlock);
}

/*
** Read-lock the RWLock.
*/
PR_IMPLEMENT(void)
PR_RWLock_Rlock(PRRWLock* rwlock) {
#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  int err;
#endif

#ifdef _PR_RWLOCK_RANK_ORDER_DEBUG
  /*
   * assert that rank ordering is not violated; the rank of 'rwlock' should
   * be equal to or greater than the highest rank of all the locks held by
   * the thread.
   */
  PR_ASSERT((rwlock->rw_rank == PR_RWLOCK_RANK_NONE) ||
            (rwlock->rw_rank >= _PR_GET_THREAD_RWLOCK_RANK()));
#endif

#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  err = RWLOCK_RDLOCK(&rwlock->rw_lock);
  PR_ASSERT(err == 0);
#else
  PR_Lock(rwlock->rw_lock);
  /*
   * wait if write-locked or if a writer is waiting; preference for writers
   */
  while ((rwlock->rw_lock_cnt < 0) || (rwlock->rw_writer_cnt > 0)) {
    rwlock->rw_reader_cnt++;
    PR_WaitCondVar(rwlock->rw_reader_waitq, PR_INTERVAL_NO_TIMEOUT);
    rwlock->rw_reader_cnt--;
  }
  /*
   * Increment read-lock count
   */
  rwlock->rw_lock_cnt++;

  PR_Unlock(rwlock->rw_lock);
#endif

#ifdef _PR_RWLOCK_RANK_ORDER_DEBUG
  /*
   * update thread's lock rank
   */
  if (rwlock->rw_rank != PR_RWLOCK_RANK_NONE) {
    _PR_SET_THREAD_RWLOCK_RANK(rwlock);
  }
#endif
}

/*
** Write-lock the RWLock.
*/
PR_IMPLEMENT(void)
PR_RWLock_Wlock(PRRWLock* rwlock) {
#if defined(DEBUG)
  PRThread* me = PR_GetCurrentThread();
#endif
#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  int err;
#endif

#ifdef _PR_RWLOCK_RANK_ORDER_DEBUG
  /*
   * assert that rank ordering is not violated; the rank of 'rwlock' should
   * be equal to or greater than the highest rank of all the locks held by
   * the thread.
   */
  PR_ASSERT((rwlock->rw_rank == PR_RWLOCK_RANK_NONE) ||
            (rwlock->rw_rank >= _PR_GET_THREAD_RWLOCK_RANK()));
#endif

#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  err = RWLOCK_WRLOCK(&rwlock->rw_lock);
  PR_ASSERT(err == 0);
#else
  PR_Lock(rwlock->rw_lock);
  /*
   * wait if read locked
   */
  while (rwlock->rw_lock_cnt != 0) {
    rwlock->rw_writer_cnt++;
    PR_WaitCondVar(rwlock->rw_writer_waitq, PR_INTERVAL_NO_TIMEOUT);
    rwlock->rw_writer_cnt--;
  }
  /*
   * apply write lock
   */
  rwlock->rw_lock_cnt--;
  PR_ASSERT(rwlock->rw_lock_cnt == -1);
#  ifdef DEBUG
  PR_ASSERT(me != NULL);
  rwlock->rw_owner = me;
#  endif
  PR_Unlock(rwlock->rw_lock);
#endif

#ifdef _PR_RWLOCK_RANK_ORDER_DEBUG
  /*
   * update thread's lock rank
   */
  if (rwlock->rw_rank != PR_RWLOCK_RANK_NONE) {
    _PR_SET_THREAD_RWLOCK_RANK(rwlock);
  }
#endif
}

/*
** Unlock the RW lock.
*/
PR_IMPLEMENT(void)
PR_RWLock_Unlock(PRRWLock* rwlock) {
#if defined(DEBUG)
  PRThread* me = PR_GetCurrentThread();
#endif
#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  int err;
#endif

#if defined(HAVE_UNIX98_RWLOCK) || defined(HAVE_UI_RWLOCK)
  err = RWLOCK_UNLOCK(&rwlock->rw_lock);
  PR_ASSERT(err == 0);
#else
  PR_Lock(rwlock->rw_lock);
  /*
   * lock must be read or write-locked
   */
  PR_ASSERT(rwlock->rw_lock_cnt != 0);
  if (rwlock->rw_lock_cnt > 0) {
    /*
     * decrement read-lock count
     */
    rwlock->rw_lock_cnt--;
    if (rwlock->rw_lock_cnt == 0) {
      /*
       * lock is not read-locked anymore; wakeup a waiting writer
       */
      if (rwlock->rw_writer_cnt > 0) {
        PR_NotifyCondVar(rwlock->rw_writer_waitq);
      }
    }
  } else {
    PR_ASSERT(rwlock->rw_lock_cnt == -1);

    rwlock->rw_lock_cnt = 0;
#  ifdef DEBUG
    PR_ASSERT(rwlock->rw_owner == me);
    rwlock->rw_owner = NULL;
#  endif
    /*
     * wakeup a writer, if present; preference for writers
     */
    if (rwlock->rw_writer_cnt > 0) {
      PR_NotifyCondVar(rwlock->rw_writer_waitq);
    }
    /*
     * else, wakeup all readers, if any
     */
    else if (rwlock->rw_reader_cnt > 0) {
      PR_NotifyAllCondVar(rwlock->rw_reader_waitq);
    }
  }
  PR_Unlock(rwlock->rw_lock);
#endif

#ifdef _PR_RWLOCK_RANK_ORDER_DEBUG
  /*
   * update thread's lock rank
   */
  if (rwlock->rw_rank != PR_RWLOCK_RANK_NONE) {
    _PR_UNSET_THREAD_RWLOCK_RANK(rwlock);
  }
#endif
  return;
}

#ifndef _PR_RWLOCK_RANK_ORDER_DEBUG

void _PR_InitRWLocks(void) {}

#else

void _PR_InitRWLocks(void) {
  /*
   * allocated thread-private-data index for rwlock list
   */
  if (PR_NewThreadPrivateIndex(&pr_thread_rwlock_key, _PR_RELEASE_LOCK_STACK) ==
      PR_FAILURE) {
    pr_thread_rwlock_alloc_failed = 1;
    return;
  }
}

/*
 * _PR_SET_THREAD_RWLOCK_RANK
 *      Set a thread's lock rank, which is the highest of the ranks of all
 *      the locks held by the thread. Pointers to the locks are added to a
 *      per-thread list, which is anchored off a thread-private data key.
 */

static void _PR_SET_THREAD_RWLOCK_RANK(PRRWLock* rwlock) {
  thread_rwlock_stack* lock_stack;
  PRStatus rv;

  /*
   * allocate a lock stack
   */
  if ((lock_stack = PR_GetThreadPrivate(pr_thread_rwlock_key)) == NULL) {
    lock_stack =
        (thread_rwlock_stack*)PR_CALLOC(1 * sizeof(thread_rwlock_stack));
    if (lock_stack) {
      rv = PR_SetThreadPrivate(pr_thread_rwlock_key, lock_stack);
      if (rv == PR_FAILURE) {
        PR_DELETE(lock_stack);
        pr_thread_rwlock_alloc_failed = 1;
        return;
      }
    } else {
      pr_thread_rwlock_alloc_failed = 1;
      return;
    }
  }
  /*
   * add rwlock to lock stack, if limit is not exceeded
   */
  if (lock_stack) {
    if (lock_stack->trs_index < _PR_RWLOCK_RANK_ORDER_LIMIT) {
      lock_stack->trs_stack[lock_stack->trs_index++] = rwlock;
    }
  }
}

static void _PR_RELEASE_LOCK_STACK(void* lock_stack) {
  PR_ASSERT(lock_stack);
  PR_DELETE(lock_stack);
}

/*
 * _PR_GET_THREAD_RWLOCK_RANK
 *
 *      return thread's lock rank. If thread-private-data for the lock
 *      stack is not allocated, return PR_RWLOCK_RANK_NONE.
 */

static PRUint32 _PR_GET_THREAD_RWLOCK_RANK(void) {
  thread_rwlock_stack* lock_stack;

  lock_stack = PR_GetThreadPrivate(pr_thread_rwlock_key);
  if (lock_stack == NULL || lock_stack->trs_index == 0) {
    return (PR_RWLOCK_RANK_NONE);
  } else {
    return (lock_stack->trs_stack[lock_stack->trs_index - 1]->rw_rank);
  }
}

/*
 * _PR_UNSET_THREAD_RWLOCK_RANK
 *
 *      remove the rwlock from the lock stack. Since locks may not be
 *      unlocked in a FIFO order, the entire lock stack is searched.
 */

static void _PR_UNSET_THREAD_RWLOCK_RANK(PRRWLock* rwlock) {
  thread_rwlock_stack* lock_stack;
  int new_index = 0, index, done = 0;

  lock_stack = PR_GetThreadPrivate(pr_thread_rwlock_key);

  PR_ASSERT(lock_stack != NULL);

  for (index = lock_stack->trs_index - 1; index >= 0; index--) {
    if (!done && (lock_stack->trs_stack[index] == rwlock)) {
      /*
       * reset the slot for rwlock
       */
      lock_stack->trs_stack[index] = NULL;
      done = 1;
    }
    /*
     * search for the lowest-numbered empty slot, above which there are
     * no non-empty slots
     */
    if (!new_index && (lock_stack->trs_stack[index] != NULL)) {
      new_index = index + 1;
    }
    if (done && new_index) {
      break;
    }
  }
  /*
   * set top of stack to highest numbered empty slot
   */
  lock_stack->trs_index = new_index;
}

#endif /* _PR_RWLOCK_RANK_ORDER_DEBUG */
