import time
import errno
import threading
import weakref

__all__ = ["lock_file"]


try:
    import fcntl
except ImportError:
    try:
        import msvcrt
    except ImportError:
        raise ImportError("Platform not supported (failed to import fcntl, msvcrt)")
    else:
        _lock_file_blocking_available = False

        def _lock_file_non_blocking(file_):
            try:
                msvcrt.locking(file_.fileno(), msvcrt.LK_NBLCK, 1)
                return True
            # TODO: check errno
            except IOError:
                return False

        def _unlock_file(file_):
            msvcrt.locking(file_.fileno(), msvcrt.LK_UNLCK, 1)

else:
    _lock_file_blocking_available = True
    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 and _lock_file_blocking_available:
            _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
