import logging
import threading

log = logging.getLogger(__name__)


class LockError(Exception):
    pass


class ReadWriteMutex(object):
    """A mutex which allows multiple readers, single writer.

    :class:`.ReadWriteMutex` uses a Python ``threading.Condition``
    to provide this functionality across threads within a process.

    The Beaker package also contained a file-lock based version
    of this concept, so that readers/writers could be synchronized
    across processes with a common filesystem.  A future Dogpile
    release may include this additional class at some point.

    """

    def __init__(self):
        # counts how many asynchronous methods are executing
        self.async_ = 0

        # pointer to thread that is the current sync operation
        self.current_sync_operation = None

        # condition object to lock on
        self.condition = threading.Condition(threading.Lock())

    def acquire_read_lock(self, wait=True):
        """Acquire the 'read' lock."""
        self.condition.acquire()
        try:
            # see if a synchronous operation is waiting to start
            # or is already running, in which case we wait (or just
            # give up and return)
            if wait:
                while self.current_sync_operation is not None:
                    self.condition.wait()
            else:
                if self.current_sync_operation is not None:
                    return False

            self.async_ += 1
            log.debug("%s acquired read lock", self)
        finally:
            self.condition.release()

        if not wait:
            return True

    def release_read_lock(self):
        """Release the 'read' lock."""
        self.condition.acquire()
        try:
            self.async_ -= 1

            # check if we are the last asynchronous reader thread
            # out the door.
            if self.async_ == 0:
                # yes. so if a sync operation is waiting, notify_all to wake
                # it up
                if self.current_sync_operation is not None:
                    self.condition.notify_all()
            elif self.async_ < 0:
                raise LockError(
                    "Synchronizer error - too many "
                    "release_read_locks called"
                )
            log.debug("%s released read lock", self)
        finally:
            self.condition.release()

    def acquire_write_lock(self, wait=True):
        """Acquire the 'write' lock."""
        self.condition.acquire()
        try:
            # here, we are not a synchronous reader, and after returning,
            # assuming waiting or immediate availability, we will be.

            if wait:
                # if another sync is working, wait
                while self.current_sync_operation is not None:
                    self.condition.wait()
            else:
                # if another sync is working,
                # we dont want to wait, so forget it
                if self.current_sync_operation is not None:
                    return False

            # establish ourselves as the current sync
            # this indicates to other read/write operations
            # that they should wait until this is None again
            self.current_sync_operation = threading.current_thread()

            # now wait again for asyncs to finish
            if self.async_ > 0:
                if wait:
                    # wait
                    self.condition.wait()
                else:
                    # we dont want to wait, so forget it
                    self.current_sync_operation = None
                    return False
            log.debug("%s acquired write lock", self)
        finally:
            self.condition.release()

        if not wait:
            return True

    def release_write_lock(self):
        """Release the 'write' lock."""
        self.condition.acquire()
        try:
            if self.current_sync_operation is not threading.current_thread():
                raise LockError(
                    "Synchronizer error - current thread doesn't "
                    "have the write lock"
                )

            # reset the current sync operation so
            # another can get it
            self.current_sync_operation = None

            # tell everyone to get ready
            self.condition.notify_all()

            log.debug("%s released write lock", self)
        finally:
            # everyone go !!
            self.condition.release()
