/*
  RWLock.cpp

  Read-Write Lock object (default: prefer readers to allow recursively grabbing the read lock)
  prefer writers not yet tested/completely implemented (ex: should throw an exception if a
  thread holding the read lock tries to recursively grab the read lock)

  Qore Programming Language

  Copyright (C) 2003 - 2015 David Nichols

  Permission is hereby granted, free of charge, to any person obtaining a
  copy of this software and associated documentation files (the "Software"),
  to deal in the Software without restriction, including without limitation
  the rights to use, copy, modify, merge, publish, distribute, sublicense,
  and/or sell copies of the Software, and to permit persons to whom the
  Software is furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  DEALINGS IN THE SOFTWARE.

  Note that the Qore library is released under a choice of three open-source
  licenses: MIT (as above), LGPL 2+, or GPL 2+; see README-LICENSE for more
  information.
*/

#include <qore/Qore.h>
#include "qore/intern/RWLock.h"

#include <assert.h>

RWLock::RWLock(bool p) : readRequests(0), prefer_writers(p), num_readers(0) {
}

#ifdef DEBUG
RWLock::~RWLock() {
   assert(tmap.empty());
   assert(cmap.empty());
}
#endif

int RWLock::numReaders() {
   return num_readers;
}

int RWLock::externWaitImpl(int mtid, QoreCondition *cond, ExceptionSink *xsink, int64 timeout_ms) {
    // make sure this TID owns the lock
    if (mtid == tid) { // in write lock
        // insert into cond map
        cond_map_t::iterator i = cmap.find(cond);
        if (i == cmap.end())
            i = cmap.insert(std::make_pair(cond, 1)).first;
        else
            ++(i->second);

        // save vlock
        VLock *nvl = vl;

        // release lock
        release_intern();

        // wait for condition
        int rc = timeout_ms ? cond->wait2(&asl_lock, timeout_ms) : cond->wait(&asl_lock);

        // decrement cond count and delete from map if 0
        if (!--(i->second))
            cmap.erase(i);

        // reacquire the lock
        if (grabImpl(mtid, nvl, xsink))
            return -1;

        grab_intern(mtid, nvl);
        return rc;
    }

    if (tid == Lock_Deleted) {
        xsink->raiseException("LOCK-ERROR", "TID %d trying to wait on %s object, but it has been deleted in another thread", mtid, getName());
        return -1;
    }

    tid_map_t::iterator i = tmap.find(mtid);
    if (i == tmap.end()) {
        xsink->raiseException("LOCK-ERROR", "TID %d trying to wait on %s object while not holding either the read or write lock", mtid, getName());
        return -1;
    }
    // issue #2817: handle the case when the read lock is held recursively
    int read_count = i->second;

    // in read lock
    // insert into cond map
    cond_map_t::iterator ci = cmap.find(cond);
    if (ci == cmap.end())
        ci = cmap.insert(std::make_pair(cond, 1)).first;
    else
        ++(ci->second);

    // save vlock
    VLock *nvl = vmap[mtid];

    // release lock
    // issue #2817: handle the case when the read lock is held recursively
    for (int j = 0; j < read_count; ++j) {
        release_read_lock_intern(i);
    }

    // wait for condition
    int rc = timeout_ms ? cond->wait(&asl_lock, timeout_ms) : cond->wait(&asl_lock);

    // decrement cond count and delete from map if 0
    if (!--(ci->second))
        cmap.erase(ci);

    // reacquire the lock
    // issue #2817: handle the case when the read lock is held recursively
    for (int j = 0; j < read_count; ++j) {
        if (grab_read_lock_intern(mtid, nvl, 0, xsink)) {
            return -1;
        }
    }

    return rc;
}

int RWLock::grabImpl(int mtid, VLock *nvl, ExceptionSink *xsink, int64 timeout_ms) {
   // check for errors
   if (tid == mtid) {
      xsink->raiseException("LOCK-ERROR", "TID %d tried to grab the write lock twice", tid);
      return -1;
   }
   while (tid >= 0 || (tid == Lock_Unlocked && num_readers)) {
      ++waiting;
      int rc;
      // if the write lock is grabbed, send vl (only one thread owns the lock)
      if (tid >= 0)
         rc = nvl->waitOn((AbstractSmartLock *)this, vl, xsink, timeout_ms);
      else  // otherwise the read lock is grabbed, so send vmap (many threads own the lock)
         rc = nvl->waitOn((AbstractSmartLock *)this, vmap, xsink, timeout_ms);
      --waiting;
      if (rc)
         return -1;
   }
   if (tid == Lock_Deleted) {
      xsink->raiseException("LOCK-ERROR", "The %s object has been deleted in another thread", getName());
      return -1;
   }
   return 0;
}

void RWLock::signalAllImpl() {
   if (waiting)
      asl_cond.broadcast();
   if (readRequests)
      read.broadcast();
}

void RWLock::signalImpl() {
   if (prefer_writers) {
      if (waiting)
         asl_cond.signal();
      else if (readRequests)
         read.broadcast();
   }
   else {
      if (readRequests)
         read.broadcast();
      else if (waiting)
         asl_cond.signal();
   }
}

void RWLock::destructorImpl(ExceptionSink *xsink) {
   cond_map_t::iterator i = cmap.begin(), e = cmap.end();
   if (i != e) {
      xsink->raiseException("RWLOCK-ERROR", "%s object deleted in TID %d while one or more Condition variables were waiting on it", getName(), gettid());
      // wake up all condition variables waiting on this mutex
      for (; i != e; i++)
         i->first->broadcast();
   }

   if (num_readers)
      asl_cond.broadcast();

   num_readers = 0;

   // release all read locks, but do not remove from the thread resource list
   for (vlock_map_t::iterator vi = vmap.begin(), ve = vmap.end(); vi != ve; ++vi)
      vi->second->pop((AbstractSmartLock*)this);

   vmap.clear();
   tmap.clear();
}

// return value 0 = last read lock for this thread released
int RWLock::cleanup_read_lock_intern(tid_map_t::iterator i) {
   if (!--(i->second)) {
      vlock_map_t::iterator vi = vmap.find(i->first);
      // pop the lock from this thread's lock list
      vi->second->pop((AbstractSmartLock *)this);
      // do not remove from the thread resource list here
      // remove map entries for this thread
      tmap.erase(i);
      vmap.erase(vi);
      assert((!tmap.empty() && num_readers) || (tmap.empty() && !num_readers));
      return 0;
   }
   return -1;
}

// internal use only - releases read and write locks
int RWLock::releaseImpl() {
   if (num_readers) {
      // signal writers if any are waiting
      if (!--num_readers && waiting)
         asl_cond.signal();

      int mtid = gettid();
      // remove reader for this thread
      tid_map_t::iterator ti = tmap.find(mtid);
      assert(ti != tmap.end());

      //if (ti != tmap.end())
         cleanup_read_lock_intern(ti);
      return -1;
   }
   else if (tid >= 0) {
      return 0;
   }
   return -1;
}

// thread exited holding the lock: remove whatever lock was locked
void RWLock::cleanupImpl() {
   // if it was a read lock
   if (num_readers) {
      int mtid = gettid();
      // remove reader for this thread
      vlock_map_t::iterator vi = vmap.find(mtid);

      // vmap entry must be present for this thread
      assert(vi != vmap.end());

      // delete entry from the thread lock list
      vi->second->pop(this);
      // erase vlock map entry
      vmap.erase(vi);
      tid_map_t::iterator ti = tmap.find(mtid);

      // tmap entry must be present if we found a vmap entry
      assert(ti != tmap.end());

      // subtract number of times this thread held the read lock from num_readers
      num_readers -= ti->second;
      // wake up threads waiting on write lock, if any
      if (!num_readers && waiting)
         asl_cond.signal();

      // erase thread map entry
      tmap.erase(ti);
      assert((!tmap.empty() && num_readers) || (tmap.empty() && !num_readers));
   }
   else if (tid >= 0) { // if it was the write lock
      // this thread must own the lock
      assert(tid == gettid());
      // mark lock as unlocked
      tid = -1;

      // delete entry from the thread lock list
      vl->pop(this);

      vl = 0;
      // wake up sleeping thread(s)
      signalImpl();
   }
}

int RWLock::releaseImpl(ExceptionSink *xsink) {
   int mtid = gettid();
   if (tid == Lock_Deleted) {
      xsink->raiseException("LOCK-ERROR", "The %s object has been deleted in another thread", getName());
      return -1;
   }
   if (tid == Lock_Unlocked) {
      xsink->raiseException("LOCK-ERROR", "TID %d called %s::writeUnlock() while not holding the write lock", mtid, getName());
      return -1;
   }
   if (tid != mtid) {
      // use getName() here so it can be safely inherited
      xsink->raiseException("LOCK-ERROR", "%s::writeUnlock() called by TID %d while the write lock is held by TID %d", getName(), mtid, tid);
      return -1;
   }
   return 0;
}

int RWLock::tryGrabImpl(int mtid, class VLock *nvl) {
   if (tid != Lock_Unlocked || num_readers)
      return -1;

   return 0;
}

void RWLock::set_initial_read_lock_intern(int mtid, VLock *nvl) {
   // only set these values the first time the lock is acquired
   tmap[mtid] = 1;
   vmap[mtid] = nvl;
   // now register that we have grabbed this lock with the thread list
   nvl->push((AbstractSmartLock *)this);
   // register the thread resource
   set_thread_resource((AbstractThreadResource *)this);
}

void RWLock::mark_read_lock_intern(int mtid, VLock *nvl) {
   ++num_readers;

   // add read lock to thread and vlock maps
   // (do not set vl, set in vmap instead)
   tid_map_t::iterator i = tmap.find(mtid);
   if (i == tmap.end())
      set_initial_read_lock_intern(mtid, nvl);
   else // increment lock count otherwise
      ++(i->second);
}

int RWLock::readLock(ExceptionSink *xsink, int64 timeout_ms) {
   int mtid = gettid();
   VLock *nvl = getVLock();
   SafeLocker sl(&asl_lock);

   if (tid == mtid) {
      xsink->raiseException("LOCK-ERROR", "TID %d called %s::readLock() while holding the write lock", mtid, getName());
      return -1;
   }

   return grab_read_lock_intern(mtid, nvl, timeout_ms, xsink);
}

// assumes the write lock is not grabbed by this thread
int RWLock::grab_read_lock_intern(int mtid, VLock *nvl, int64 timeout_ms, ExceptionSink *xsink) {
   if (tid >= 0) {
      do {
         ++readRequests;
         int rc;
         rc = nvl->waitOn((AbstractSmartLock *)this, &read, vl, xsink, timeout_ms);
         --readRequests;
         if (rc)
            return -1;
      } while (tid >= 0);

      if (tid == Lock_Deleted) {
         xsink->raiseException("LOCK-ERROR", "The %s object has been deleted in another thread", getName());
         return -1;
      }
      ++num_readers;

      // we know that this thread was not in the read lock because the write lock was grabbed before
      set_initial_read_lock_intern(mtid, nvl);
      return 0;
   }

   if (tid == Lock_Deleted) {
      xsink->raiseException("LOCK-ERROR", "The %s object has been deleted in another thread", getName());
      return -1;
   }

   // read lock must be open
   mark_read_lock_intern(mtid, nvl);

   return 0;
}

void RWLock::release_read_lock_intern(tid_map_t::iterator i) {
   // signal writers if any are waiting
   if (!--num_readers && waiting)
      asl_cond.signal();

   // if this thread has released it's last read lock, then remove the thread resource
   if (!cleanup_read_lock_intern(i))
      remove_thread_resource((AbstractThreadResource *)this);
}

int RWLock::readUnlock(ExceptionSink* xsink) {
   int mtid = gettid();
   AutoLocker al(&asl_lock);
   if (tid == mtid) {
      xsink->raiseException("LOCK-ERROR", "TID %d called %s::readUnlock() while holding the write lock", mtid, getName());
      return -1;
   }

   if (tid == Lock_Deleted) {
      xsink->raiseException("LOCK-ERROR", "The %s object has been deleted in another thread", getName());
      return -1;
   }

   tid_map_t::iterator i = tmap.find(mtid);
   if (i == tmap.end()) {
      xsink->raiseException("LOCK-ERROR", "TID %d called %s::readUnlock() while not holding the read lock", mtid, getName());
      return -1;
   }

   release_read_lock_intern(i);
   return 0;
}

int RWLock::tryReadLock() {
   AutoLocker al(&asl_lock);
   if (tid != Lock_Unlocked)
      return -1;

   mark_read_lock_intern(gettid(), getVLock());

   return 0;
}

// while holding the write lock changes it to a read lock
// and wakes up any blocked readers
// not sure if this function should be allowed if the object
// prefers writers
/*
void RWLock::writeToRead() {
   AutoLocker al(&asl_lock);
   XXX
   if (readRequests)
      read.broadcast();
}
*/
