# -*- coding: utf-8 -*-
""" rwlock.py

    A class to implement read-write locks on top of the standard threading
    library.

    This is implemented with two mutexes (threading.Lock instances) as per this
    wikipedia pseudocode:

    https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes

    Code written by Tyler Neylon at Unbox Research.

    This file is public domain.

    Modified to add a w_demote function to convert a writer lock to a reader lock
"""


# _______________________________________________________________________
# Imports

from contextlib import contextmanager
from threading import Lock


# _______________________________________________________________________
# Class


class RWLock(object):
    """ RWLock class; this is meant to allow an object to be read from by
        multiple threads, but only written to by a single thread at a time. See:
        https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

        Usage:

            from rwlock import RWLock

            my_obj_rwlock = RWLock()

            # When reading from my_obj:
            with my_obj_rwlock.r_locked():
                do_read_only_things_with(my_obj)

            # When writing to my_obj:
            with my_obj_rwlock.w_locked():
                mutate(my_obj)
    """

    def __init__(self):

        self.w_lock = Lock()
        self.num_r_lock = Lock()
        self.num_r = 0

        # The d_lock is needed to handle the demotion case,
        # so that the writer can become a reader without releasing the w_lock.
        # the d_lock is held by the writer, and prevents any other thread from taking the
        # num_r_lock during that time, which means the writer thread is able to take the
        # num_r_lock to update the num_r.
        self.d_lock = Lock()

    # ___________________________________________________________________
    # Reading methods.

    def r_acquire(self):
        self.d_lock.acquire()
        self.num_r_lock.acquire()
        self.num_r += 1

        if self.num_r == 1:
            self.w_lock.acquire()

        self.num_r_lock.release()
        self.d_lock.release()

    def r_release(self):
        assert self.num_r > 0
        self.num_r_lock.acquire()
        self.num_r -= 1
        if self.num_r == 0:
            self.w_lock.release()

        self.num_r_lock.release()

    @contextmanager
    def r_locked(self):
        """ This method is designed to be used via the `with` statement. """
        try:
            self.r_acquire()
            yield
        finally:
            self.r_release()

    # ___________________________________________________________________
    # Writing methods.

    def w_acquire(self):
        self.d_lock.acquire()
        self.w_lock.acquire()

    def w_acquire_non_blocking(self):
        # if d_lock and w_lock can be acquired without blocking, acquire and return True,
        # else immediately return False.
        if self.d_lock.acquire(blocking=False):
            if self.w_lock.acquire(blocking=False):
                return True
            else:
                self.d_lock.release()
        return False

    def w_release(self):
        self.w_lock.release()
        self.d_lock.release()

    def w_demote(self):
        """demote a writer lock to a reader lock"""

        # the d_lock is already held from w_acquire.
        # releasing the d_lock at the end of this function allows multiple readers.
        # incrementing num_r makes this thread one of those readers.
        self.num_r_lock.acquire()
        self.num_r += 1
        self.num_r_lock.release()
        self.d_lock.release()

    @contextmanager
    def w_locked(self):
        """ This method is designed to be used via the `with` statement. """
        try:
            self.w_acquire()
            yield
        finally:
            self.w_release()
