# derived from https://pypi.python.org/pypi/locket/
#
# Copyright (c) 2012, Michael Williamson
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer. 
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution. 
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the FreeBSD Project.


import time
import errno
import threading
import weakref

__all__ = ["lock_file"]


import fcntl

def _lock_file_blocking(file_):
    fcntl.flock(file_.fileno(), fcntl.LOCK_EX)

def _lock_file_non_blocking(file_):
    try:
        fcntl.flock(file_.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        return True
    except IOError as error:
        if error.errno in [errno.EACCES, errno.EAGAIN]:
            return False
        else:
            raise

def _unlock_file(file_):
    fcntl.flock(file_.fileno(), fcntl.LOCK_UN)


_locks_lock = threading.Lock()
_locks = weakref.WeakValueDictionary()


def lock_file(path, **kwargs):
    _locks_lock.acquire()
    try:
        lock = _locks.get(path)
        if lock is None:
            lock = _create_lock_file(path, **kwargs)
            _locks[path] = lock
        return lock
    finally:
        _locks_lock.release()


def _create_lock_file(path, **kwargs):
    thread_lock = _ThreadLock(path, **kwargs)
    file_lock = _LockFile(path, **kwargs)
    return _LockSet([thread_lock, file_lock])


class LockError(Exception):
    pass


def _acquire_non_blocking(acquire, timeout, retry_period, path):
    if retry_period is None:
        retry_period = 0.05

    start_time = time.time()
    while True:
        success = acquire()
        if success:
            return
        elif (timeout is not None and
                time.time() - start_time > timeout):
            raise LockError("Couldn't lock {0}".format(path))
        else:
            time.sleep(retry_period)


class _LockSet(object):
    def __init__(self, locks):
        self._locks = locks

    def acquire(self):
        acquired_locks = []
        try:
            for lock in self._locks:
                lock.acquire()
                acquired_locks.append(lock)
        except:
            for acquired_lock in reversed(acquired_locks):
                # TODO: handle exceptions
                acquired_lock.release()
            raise

    def release(self):
        for lock in reversed(self._locks):
            # TODO: Handle exceptions
            lock.release()

    def __enter__(self):
        self.acquire()
        return self

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


class _ThreadLock(object):
    def __init__(self, path, timeout=None, retry_period=None):
        self._path = path
        self._timeout = timeout
        self._retry_period = retry_period
        self._lock = threading.Lock()

    def acquire(self):
        if self._timeout is None:
            self._lock.acquire()
        else:
            _acquire_non_blocking(
                acquire=lambda: self._lock.acquire(False),
                timeout=self._timeout,
                retry_period=self._retry_period,
                path=self._path,
            )

    def release(self):
        self._lock.release()


class _LockFile(object):
    def __init__(self, path, timeout=None, retry_period=None):
        self._path = path
        self._timeout = timeout
        self._retry_period = retry_period
        self._file = None
        self._thread_lock = threading.Lock()

    def acquire(self):
        if self._file is None:
            self._file = open(self._path, "w")
        if self._timeout is None:
            _lock_file_blocking(self._file)
        else:
            _acquire_non_blocking(
                acquire=lambda: _lock_file_non_blocking(self._file),
                timeout=self._timeout,
                retry_period=self._retry_period,
                path=self._path,
            )

    def release(self):
        _unlock_file(self._file)
        self._file.close()
        self._file = None
