import errno
import os

import time

from parallels.core.utils.common import is_run_on_windows
from parallels.core.utils.common import unused, mkdir_p
from parallels.core.utils.locks.file_lock_exception import FileLockException


class FileLock(object):
    """
    Provide ability to synchronize several processes of Panel Migrator

    Basic idea is:
    1. then first process acquire lock, special file (lock file) will be created
    2. then second process try to acquire lock, it try to remove early created file,
    but lock file descriptor is in use by first process, so FileLockException will be raised
    3. than first process was finished (or killed), lock file descriptor will be closed,
    so other processes would be able to acquire lock
    """

    ACQUIRE_ATTEMPT_TIME = 0.1

    def __init__(self, lock_dir, name):
        self._is_instantiated = False
        self._is_locked = False
        self._name = name
        self._lock_dir = lock_dir
        self._lock_path = os.path.join(lock_dir, name)
        self._lock_file_descriptor = None

    def acquire_block(self):
        while True:
            try:
                self.acquire()
                return
            except FileLockException:
                time.sleep(self.ACQUIRE_ATTEMPT_TIME)

    def acquire(self):
        self._lazy_instantiate()
        self._acquire_windows() if is_run_on_windows() else self._acquire_unix()

    def release(self):
        self._lazy_instantiate()
        self._release_windows() if is_run_on_windows() else self._release_unix()

    def _acquire_unix(self):
        import fcntl
        self._lock_file_descriptor = os.open(self._lock_path, os.O_CREAT | os.O_RDWR)
        try:
            fcntl.lockf(self._lock_file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as e:
            os.close(self._lock_file_descriptor)
            if e.errno != errno.EACCES and e.errno != errno.EAGAIN:
                raise
            raise FileLockException()
        self._is_locked = True

    def _acquire_windows(self):
        try:
            if os.path.exists(self._lock_path):
                os.unlink(self._lock_path)
            self._lock_file_descriptor = os.open(self._lock_path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
        except OSError as e:
            if e.errno != errno.EEXIST and e.errno != errno.EACCES:
                raise
            raise FileLockException()
        self._is_locked = True

    def _release_unix(self):
        import fcntl
        if self._is_locked:
            fcntl.lockf(self._lock_file_descriptor, fcntl.LOCK_UN)
            os.close(self._lock_file_descriptor)
            self._is_locked = False

    def _release_windows(self):
        if self._is_locked:
            os.close(self._lock_file_descriptor)
            self._is_locked = False

    def _lazy_instantiate(self):
        if self._is_instantiated:
            return

        mkdir_p(self._lock_dir)
        self._instantiated = True

    def __enter__(self):
        if not self._is_locked:
            self.acquire()
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        unused(exception_type, exception_value, traceback)
        self.release()

    def __del__(self):
        self.release()
