#!/usr/bin/env python


__license__   = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os
import sys
import traceback
from contextlib import contextmanager
from threading import Condition, Lock, current_thread
from time import monotonic


@contextmanager
def try_lock(lock):
    got_lock = lock.acquire(blocking=False)
    try:
        yield got_lock
    finally:
        if got_lock:
            lock.release()


class LockingError(RuntimeError):

    is_locking_error = True

    def __init__(self, msg, extra=None):
        RuntimeError.__init__(self, msg)
        self.locking_debug_msg = extra


class DowngradeLockError(LockingError):
    pass


def create_locks():
    '''
    Return a pair of locks: (read_lock, write_lock)

    The read_lock can be acquired by multiple threads simultaneously, it can
    also be acquired multiple times by the same thread.

    Only one thread can hold write_lock at a time, and only if there are no
    current read_locks. While the write_lock is held no
    other threads can acquire read locks. The write_lock can also be acquired
    multiple times by the same thread.

    Both read_lock and write_lock are meant to be used in with statements (they
    operate on a single underlying lock.

    WARNING: Be very careful to not try to acquire a read lock while the same
    thread holds a write lock and vice versa. That is, a given thread should
    always release *all* locks of type A before trying to acquire a lock of type
    B. Bad things will happen if you violate this rule, the most benign of
    which is the raising of a LockingError (I haven't been able to eliminate
    the possibility of deadlocking in this scenario).
    '''
    l = SHLock()
    wrapper = DebugRWLockWrapper if os.environ.get('CALIBRE_DEBUG_DB_LOCKING') == '1' else RWLockWrapper
    return wrapper(l), wrapper(l, is_shared=False)


class SHLock:  # {{{
    '''
    Shareable lock class. Used to implement the Multiple readers-single writer
    paradigm. As best as I can tell, neither writer nor reader starvation
    should be possible.

    Based on code from: https://github.com/rfk/threading2
    '''

    def __init__(self):
        self._lock = Lock()
        # When a shared lock is held, is_shared will give the cumulative
        # number of locks and _shared_owners maps each owning thread to
        # the number of locks is holds.
        self.is_shared = 0
        self._shared_owners = {}
        # When an exclusive lock is held, is_exclusive will give the number
        # of locks held and _exclusive_owner will give the owning thread
        self.is_exclusive = 0
        self._exclusive_owner = None
        # When someone is forced to wait for a lock, they add themselves
        # to one of these queues along with a "waiter" condition that
        # is used to wake them up.
        self._shared_queue = []
        self._exclusive_queue = []
        # This is for recycling waiter objects.
        self._free_waiters = []

    def acquire(self, blocking=True, shared=False):
        '''
        Acquire the lock in shared or exclusive mode.

        If blocking is False this method will return False if acquiring the
        lock failed.
        '''
        with self._lock:
            if shared:
                return self._acquire_shared(blocking)
            else:
                return self._acquire_exclusive(blocking)
            assert not (self.is_shared and self.is_exclusive)

    def owns_lock(self):
        me = current_thread()
        with self._lock:
            return self._exclusive_owner is me or me in self._shared_owners

    def release(self):
        ''' Release the lock. '''
        # This decrements the appropriate lock counters, and if the lock
        # becomes free, it looks for a queued thread to hand it off to.
        # By doing the handoff here we ensure fairness.
        me = current_thread()
        with self._lock:
            if self.is_exclusive:
                if self._exclusive_owner is not me:
                    raise LockingError('release() called on unheld lock')
                self.is_exclusive -= 1
                if not self.is_exclusive:
                    self._exclusive_owner = None
                    # If there are waiting shared locks, issue them
                    # all and them wake everyone up.
                    if self._shared_queue:
                        for thread, waiter in self._shared_queue:
                            self.is_shared += 1
                            self._shared_owners[thread] = 1
                            waiter.notify()
                        del self._shared_queue[:]
                    # Otherwise, if there are waiting exclusive locks,
                    # they get first dibbs on the lock.
                    elif self._exclusive_queue:
                        thread, waiter = self._exclusive_queue.pop(0)
                        self._exclusive_owner = thread
                        self.is_exclusive += 1
                        waiter.notify()
            elif self.is_shared:
                try:
                    self._shared_owners[me] -= 1
                    if self._shared_owners[me] == 0:
                        del self._shared_owners[me]
                except KeyError:
                    raise LockingError('release() called on unheld lock')
                self.is_shared -= 1
                if not self.is_shared:
                    # If there are waiting exclusive locks,
                    # they get first dibbs on the lock.
                    if self._exclusive_queue:
                        thread, waiter = self._exclusive_queue.pop(0)
                        self._exclusive_owner = thread
                        self.is_exclusive += 1
                        waiter.notify()
                    else:
                        assert not self._shared_queue
            else:
                raise LockingError('release() called on unheld lock')

    def _acquire_shared(self, blocking=True):
        me = current_thread()
        # Each case: acquiring a lock we already hold.
        if self.is_shared and me in self._shared_owners:
            self.is_shared += 1
            self._shared_owners[me] += 1
            return True
        # If the lock is already spoken for by an exclusive, add us
        # to the shared queue and it will give us the lock eventually.
        if self.is_exclusive or self._exclusive_queue:
            if self._exclusive_owner is me:
                raise DowngradeLockError("can't downgrade SHLock object")
            if not blocking:
                return False
            waiter = self._take_waiter()
            try:
                self._shared_queue.append((me, waiter))
                waiter.wait()
                assert not self.is_exclusive
            finally:
                self._return_waiter(waiter)
        else:
            self.is_shared += 1
            self._shared_owners[me] = 1
        return True

    def _acquire_exclusive(self, blocking=True):
        me = current_thread()
        # Each case: acquiring a lock we already hold.
        if self._exclusive_owner is me:
            assert self.is_exclusive
            self.is_exclusive += 1
            return True
        # Do not allow upgrade of lock
        if self.is_shared and me in self._shared_owners:
            raise LockingError("can't upgrade SHLock object")
        # If the lock is already spoken for, add us to the exclusive queue.
        # This will eventually give us the lock when it's our turn.
        if self.is_shared or self.is_exclusive:
            if not blocking:
                return False
            waiter = self._take_waiter()
            try:
                self._exclusive_queue.append((me, waiter))
                waiter.wait()
            finally:
                self._return_waiter(waiter)
        else:
            self._exclusive_owner = me
            self.is_exclusive += 1
        return True

    def _take_waiter(self):
        try:
            return self._free_waiters.pop()
        except IndexError:
            return Condition(self._lock)

    def _return_waiter(self, waiter):
        self._free_waiters.append(waiter)

# }}}


class RWLockWrapper:

    def __init__(self, shlock, is_shared=True):
        self._shlock = shlock
        self._is_shared = is_shared

    def acquire(self):
        self._shlock.acquire(shared=self._is_shared)

    def release(self, *args):
        self._shlock.release()

    __enter__ = acquire
    __exit__ = release

    def owns_lock(self):
        return self._shlock.owns_lock()


class DebugRWLockWrapper(RWLockWrapper):

    def __init__(self, *args, **kwargs):
        RWLockWrapper.__init__(self, *args, **kwargs)
        self.print_lock = Lock()
        self.st = monotonic()

    def acquire(self):
        t = current_thread()
        tid = f'{os.getpid()} - {t.name} - {t.native_id}'
        at = monotonic() - self.st
        with self.print_lock:
            print('#' * 120, file=sys.stderr)
            print(f'acquire called at {at:.4f}: thread id:', tid, 'shared:', self._is_shared, file=sys.stderr)
            traceback.print_stack()
        RWLockWrapper.acquire(self)
        at = monotonic() - self.st
        with self.print_lock:
            print(f'acquire done at {at:.4f}: thread id:', tid, file=sys.stderr)
            print('_' * 120, file=sys.stderr)

    def release(self, *args):
        t = current_thread()
        tid = f'{os.getpid()} - {t.name} - {t.native_id}'
        at = monotonic() - self.st
        with self.print_lock:
            print('*' * 120, file=sys.stderr)
            print(f'release called at {at:.4f}: thread id:', tid, 'shared:', self._is_shared, file=sys.stderr)
            traceback.print_stack()
        at = monotonic() - self.st
        RWLockWrapper.release(self)
        with self.print_lock:
            print(f'release done at {at:.4f}: thread id:', tid, 'is_shared:', self._shlock.is_shared, 'is_exclusive:', self._shlock.is_exclusive,
                  file=sys.stderr)
            print('_' * 120, file=sys.stderr)

    __enter__ = acquire
    __exit__ = release


class SafeReadLock:

    def __init__(self, read_lock):
        self.read_lock = read_lock
        self.acquired = False

    def acquire(self):
        try:
            self.read_lock.acquire()
        except DowngradeLockError:
            pass
        else:
            self.acquired = True
        return self

    def release(self, *args):
        if self.acquired:
            self.read_lock.release()
        self.acquired = False

    __enter__ = acquire
    __exit__  = release
