from __future__ import absolute_import

import time
import os
import errno

from . import (LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout,
               AlreadyLocked)


class LinkLockFile(LockBase):
    """
    Lock access to a file using atomic property of link(2).

    lock = LinkLockFile('somefile'[, threaded=False[, timeout=None]])
    """

    # noinspection PyTypeChecker
    def acquire(self, timeout=None):
        try:
            open(self.unique_name, 'wb').close()
        except IOError:
            raise LockFailed('failed to create %s' % self.unique_name)

        timeout = timeout is not None and timeout or self.timeout
        end_time = time.time()
        if timeout is not None and timeout > 0:
            end_time += timeout

        while True:
            # Try and create a hard link to it.
            try:
                os.link(self.unique_name, self.lock_file)
            except OSError as e:
                if errno.ENOSYS == e.errno:
                    raise LockFailed('%s' % e.strerror)

                # Link creation failed.  Maybe we've double-locked?
                nlinks = os.stat(self.unique_name).st_nlink
                if nlinks == 2:
                    # The original link plus the one I created == 2.  We're
                    # good to go.
                    return
                else:
                    # Otherwise the lock creation failed.
                    if timeout is not None and time.time() > end_time:
                        os.unlink(self.unique_name)
                        if timeout > 0:
                            raise LockTimeout('Timeout waiting to acquire lock for %s' % self.path)
                        else:
                            raise AlreadyLocked('%s is already locked' % self.path)

                    time.sleep(timeout is not None and (timeout / 10) or 0.1)
            else:
                # Link creation succeeded.  We're good to go.
                return

    def release(self):
        if not self.is_locked():
            raise NotLocked('%s is not locked' % self.path)
        elif not os.path.exists(self.unique_name):
            raise NotMyLock('%s is locked, but not by me' % self.path)
        os.unlink(self.unique_name)
        os.unlink(self.lock_file)

    def is_locked(self):
        return os.path.exists(self.lock_file)

    def i_am_locking(self):
        return (self.is_locked() and
                os.path.exists(self.unique_name) and
                os.stat(self.unique_name).st_nlink == 2)

    def break_lock(self):
        if os.path.exists(self.lock_file):
            os.unlink(self.lock_file)