from contextlib import contextmanager

import threading


class RWLock:
    """Lock enabling multiple readers but only 1 exclusive writer

    Source: https://cutt.ly/Ij70qaq
    """

    def __init__(self):
        self._read_switch = _LightSwitch()
        self._write_switch = _LightSwitch()
        self._no_readers = threading.Lock()
        self._no_writers = threading.Lock()
        self._readers_queue = threading.RLock()

    @contextmanager
    def reader(self):
        self._reader_acquire()
        try:
            yield
        except Exception:  # pylint: disable=W0706
            raise
        finally:
            self._reader_release()

    @contextmanager
    def writer(self):
        self._writer_acquire()
        try:
            yield
        except Exception:  # pylint: disable=W0706
            raise
        finally:
            self._writer_release()

    def _reader_acquire(self):
        """Readers should block whenever a writer has acquired"""
        self._readers_queue.acquire()
        self._no_readers.acquire()
        self._read_switch.acquire(self._no_writers)
        self._no_readers.release()
        self._readers_queue.release()

    def _reader_release(self):
        self._read_switch.release(self._no_writers)

    def _writer_acquire(self):
        """Acquire the writer lock.

        Only the first writer will lock the readtry and then
        all subsequent writers can simply use the resource as
        it gets freed by the previous writer. The very last writer must
        release the readtry semaphore, thus opening the gate for readers
        to try reading.

        No reader can engage in the entry section if the readtry semaphore
        has been set by a writer previously
        """
        self._write_switch.acquire(self._no_readers)
        self._no_writers.acquire()

    def _writer_release(self):
        self._no_writers.release()
        self._write_switch.release(self._no_readers)


class _LightSwitch:

    """An auxiliary "light switch"-like object

    The first thread turns on the "switch", the last one turns it off.

    Source: https://cutt.ly/Ij70qaq
    """

    def __init__(self):
        self._counter = 0
        self._mutex = threading.RLock()

    def acquire(self, lock):
        self._mutex.acquire()
        self._counter += 1
        if self._counter == 1:
            lock.acquire()
        self._mutex.release()

    def release(self, lock):
        self._mutex.acquire()
        self._counter -= 1
        if self._counter == 0:
            lock.release()
        self._mutex.release()
