# This file is part of the MapProxy project.
# Copyright (C) 2010-2014 Omniscale <http://omniscale.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Utility methods and classes (file locking, asynchronous execution pools, etc.).
"""

import random
import time
import os
import errno

from mapproxy.util.ext.lockfile import LockFile, LockError
from mapproxy.util.fs import ensure_directory

import logging
log = logging.getLogger(__name__)

__all__ = ['LockTimeout', 'FileLock', 'LockError', 'cleanup_lockdir', 'SemLock']


class LockTimeout(Exception):
    pass


class FileLock(object):
    def __init__(self, lock_file, timeout=60.0, step=0.01, remove_on_unlock=False, directory_permissions=None,
                 file_permissions=None):
        self.lock_file = lock_file
        self.timeout = timeout
        self.step = step
        self.remove_on_unlock = remove_on_unlock
        self._locked = False
        self.directory_permissions = directory_permissions
        self.file_permissions = file_permissions

    def __enter__(self):
        self.lock()

    def __exit__(self, _exc_type, _exc_value, _traceback):
        self.unlock()

    def _try_lock(self):
        return LockFile(self.lock_file, self.file_permissions)

    def lock(self):
        ensure_directory(self.lock_file, self.directory_permissions)

        current_time = time.time()
        stop_time = current_time + self.timeout

        while not self._locked:
            try:
                self._lock = self._try_lock()
            except LockError as last_err:
                current_time = time.time()
                if current_time < stop_time:
                    time.sleep(self.step)
                    continue
                else:
                    raise LockTimeout(last_err.args[0]) from last_err
            else:
                self._locked = True

    def unlock(self):
        if self._locked:
            self._locked = False
            if self.remove_on_unlock:
                try:
                    # try to release lock by removing
                    # this is not a clean way and more than one process might
                    # grab the lock afterwards but it is ok when the task is
                    # solved by the first process that got the lock (i.e. the
                    # tile is created)
                    os.remove(self.lock_file)
                except OSError:
                    self._lock.close()
            else:
                self._lock.close()

    def __del__(self):
        self.unlock()


_cleanup_counter = -1


def cleanup_lockdir(lockdir, suffix='.lck', max_lock_time=300, force=True):
    """
    Remove files ending with `suffix` from `lockdir` if they are older then
    `max_lock_time` seconds.
    It will not cleanup on every call if `force` is ``False``.
    """
    global _cleanup_counter
    _cleanup_counter += 1
    if not force and _cleanup_counter % 50 != 0:
        return
    expire_time = time.time() - max_lock_time
    if not os.path.exists(lockdir):
        return
    if not os.path.isdir(lockdir):
        log.warning('lock dir not a directory: %s', lockdir)
        return
    for entry in os.listdir(lockdir):
        name = os.path.join(lockdir, entry)
        try:
            if os.path.isfile(name) and name.endswith(suffix):
                if os.path.getmtime(name) < expire_time:
                    try:
                        os.unlink(name)
                    except IOError as ex:
                        log.warning('could not remove old lock file %s: %s', name, ex)
        except OSError as e:
            # some one might have removed the file (ENOENT)
            # or we don't have permissions to remove it (EACCES)
            if e.errno in (errno.ENOENT, errno.EACCES):
                # ignore
                pass
            else:
                raise e


class SemLock(FileLock):
    """
    File-lock-based counting semaphore (i.e. this lock can be locked n-times).
    """

    def __init__(self, lock_file, n, timeout=60.0, step=0.01, directory_permissions=None, file_permissions=None):
        super().__init__(lock_file, timeout=timeout, step=step, directory_permissions=directory_permissions,
                         file_permissions=file_permissions)
        self.n = n

    def _try_lock(self):
        tries = 0
        i = random.randint(0, self.n-1)
        while True:
            tries += 1
            try:
                return LockFile(self.lock_file + str(i), self.file_permissions)
            except LockError:
                if tries >= self.n:
                    raise
            i = (i+1) % self.n


class DummyLock(object):
    def __enter__(self):
        pass

    def __exit__(self, _exc_type, _exc_value, _traceback):
        pass

    def lock(self):
        pass

    def unlock(self):
        pass
