from os import path, remove
from time import sleep
import logging
from filelock import FileLock, Timeout

__doc__ = """ # AnyLock
A module to handle file-locks and safe-loading/writing of files (mostly for caching analytic results)
"""

def safe_isfile(file, timeout = 900):
    """ Check if a file _and_ its lock exists
    Args:
        file: path to file (w/o lock)
        timeout: total time to wait for the file `file+'.lock'` to disappear
    Returns:
        result of path.isfile(file)

    If timeout has passed, the lock file is removed
    """
    lock_file = file + '.lock'
    total_time = 0
    poll_interval = 0.05
    while path.isfile(lock_file):
        # found a lock file. Wait for it to disappear
        if total_time > 0 and total_time % 10 == 0:
            logging.warning(f'found lock file {lock_file}. Waiting...')
        if total_time >= timeout:
            logging.error(f'lock file {lock_file} seems to be stale. Removing it.')
            try:
                remove(lock_file)
            except FileNotFoundError:
                pass
            return path.isfile(file)
        sleep(poll_interval)
        total_time += poll_interval
        poll_interval = min(poll_interval+0.05, 2)
    return path.isfile(file)

class CreateLock(object):
    """
    Creates a filelock.FileLock object for `lock_file` and tries to acquire a lock.

    Args:
        lock_file: path to file, including .lock file extension
        acquire_timeout: passed to timeout argument of FileLock.acquire
        timeout: total time to wait/retry acquiring the lock. Afterwards lock_file is considered stale and deleted
        open:

    Returns:
        lock object

    Example:
    ```python
    with CreateLock('path/to/file.json'):
        # code that reads/writes file.json
    # or:
    lock = CreateLock('path/file.json')
    try:
        # code in here
    finally:
        lock.release()
    ```
    """

    def __init__(self, lock_file, timeout = 900, acquire_timeout = 10):
        self.lock_file = lock_file
        self.timeout = timeout
        self.acquire_timeout = acquire_timeout

        if not lock_file.endswith('.lock'):
            self.lock_file = lock_file + '.lock'

        self.lock = FileLock(self.lock_file)
        self.lock_count = 0
        self.acquire()

    def release(self, **kwargs):
        self.lock.release(**kwargs)
        if path.isfile(self.lock_file):
            try:
                remove(self.lock_file)
            except:
                logging.warning(f'not able to clean up .lock file {self.lock_file}')

    def acquire(self):
        while not self.lock.is_locked:
            try:
                self.lock.acquire(timeout=self.acquire_timeout)
            except Timeout:
                logging.warning(f'file {self.lock_file} is locked. Waiting...')
                self.lock_count += 1
                sleep(0.1)

            if self.lock_count * (self.acquire_timeout + 0.1) > self.timeout:
                logging.error(f'stale lock file {self.lock_file}! Removing it. Consider clearing the cache to ensure consisten results!')
                try:
                    remove(self.lock_file)
                except FileNotFoundError:
                    pass
                sleep(0.1)

    def __enter__(self):
        return self.lock

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

    def __del__(self):
        self.release(force=True)

    def __str__(self):
        return self.lock_file

class safe_open(object):
    """ drop-in replacement for `open` taking care of filelocks using `CreateLock`

    Example:
    ```python
    with safe_open('/path/to/file.json','w'):
        # code to modify file.json
    ```
    """
    def __init__(self, file, mode, CreateLock_args = {}, **kwargs):
        self.file = file
        self.mode = mode
        self.CreateLock_args = CreateLock_args
        self.kwargs = kwargs
        self.lock = CreateLock(file, **CreateLock_args)

    def __enter__(self):
        self.open = open(self.file, self.mode, **self.kwargs)
        return self.open

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.open.close()
        self.lock.release()

    def __str__(self):
        return self.file
