
from __future__ import absolute_import
from __future__ import division
import logging
import sys
import time

import six
from six.moves import range

"""
shared utilities and common code for the virt package
"""

import os.path
import threading

from vdsm.common import supervdsm
from vdsm.common.fileutils import rm_file
from vdsm.common.time import monotonic_time
from . import metadata


log = logging.getLogger('virt.utils')


def isVdsmImage(drive):
    required = ('domainID', 'imageID', 'poolID', 'volumeID')
    return all(k in drive for k in required)


class ItemExpired(KeyError):
    pass


class ExpiringCache(object):
    def __init__(self, ttl, clock=monotonic_time):
        self._ttl = ttl
        self._clock = clock
        self._items = {}
        self._lock = threading.Lock()

    def get(self, key, default=None):
        try:
            return self._get_live(key)
        except KeyError:
            return default

    def clear(self):
        with self._lock:
            self._items.clear()

    def __setitem__(self, key, value):
        item = self._clock() + self._ttl, value
        with self._lock:
            self._items[key] = item

    def __getitem__(self, key):
        return self._get_live(key)

    def __delitem__(self, key):
        with self._lock:
            del self._items[key]

    def __bool__(self):
        now = self._clock()
        with self._lock:
            expired_keys = [
                key for key, (expiration, _)
                in six.iteritems(self._items)
                if expiration <= now]
            for key in expired_keys:
                del self._items[key]

            return bool(self._items)

        return self.__bool__()


    def _get_live(self, key):
        now = self._clock()
        with self._lock:
            expiration, value = self._items[key]

            if expiration <= now:
                del self._items[key]
                raise ItemExpired

            return value


def cleanup_guest_socket(sock):
    if os.path.islink(sock):
        rm_file(os.path.realpath(sock))
    rm_file(sock)


class DynamicBoundedSemaphore(object):

    def __init__(self, value):
        self._cond = threading.Condition(threading.Lock())
        self._value = value
        self._bound = value

    def acquire(self, blocking=True):
        rc = False
        with self._cond:
            while self._value <= 0:
                if not blocking:
                    break
                self._cond.wait()
            else:
                self._value -= 1
                rc = True
        return rc

    __enter__ = acquire

    def release(self):
        with self._cond:
            if self._value >= self._bound:
                raise ValueError("Dynamic Semaphore released too many times")
            self._value += 1
            self._cond.notify()

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

    @property
    def bound(self):
        return self._bound

    @bound.setter
    def bound(self, value):
        with self._cond:
            delta = value - self._bound
            self._bound = value
            if delta < 0:
                self._value += delta

        if delta > 0:
            for i in range(delta):
                self.release()


def has_xml_configuration(params):
    if 'xml' in params:
        return True

    if '_srcDomXML' in params:
        md_desc = metadata.Descriptor.from_xml(params['_srcDomXML'])
        if not bool(md_desc):
            return False

        with md_desc.values() as md:
            cver = extract_cluster_version(md)

        return cver is not None

    return False


def extract_cluster_version(md_values):
    cluster_version = md_values.get('clusterVersion')
    if cluster_version is not None:
        return [int(v) for v in cluster_version.split('.')]
    return None


class TeardownError(Exception):
    pass


class prepared(object):

    def __init__(self, images):
        self._images = images
        self._prepared_images = []

    def __enter__(self):
        for image in self._images:
            try:
                image.prepare()
            except:
                exc = sys.exc_info()
                log.error("Error preparing image %r", image)
                try:
                    self._teardown()
                except TeardownError:
                    log.exception("Error tearing down images")
                try:
                    six.reraise(*exc)
                finally:
                    del exc

            self._prepared_images.append(image)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            self._teardown()
        except TeardownError:
            if exc_type is None:
                raise
            log.exception("Error tearing down images")

    def _teardown(self):
        errors = []
        while self._prepared_images:
            image = self._prepared_images.pop()
            try:
                image.teardown()
            except Exception as e:
                errors.append(e)
        if errors:
            raise TeardownError(errors)


class LockTimeout(RuntimeError):

    def __init__(self, timeout, lockid, flow):
        self.timeout = timeout
        self.lockid = lockid
        self.flow = flow

    def __str__(self):
        msg = 'waiting more than {elapsed}s for lock {lockid} held by {flow}'
        return msg.format(
            elapsed=self.timeout, lockid=self.lockid, flow=self.flow
        )


class TimedAcquireLock(object):

    def __init__(self, lockid):
        self._lockid = lockid
        self._lock = threading.Lock()
        self._flow = None

    @property
    def holder(self):
        return self._flow

    def acquire(self, timeout, flow=None):
        end = time.time() + timeout
        while not self._lock.acquire(False):
            time.sleep(0.1)
            if time.time() > end:
                raise LockTimeout(timeout, self._lockid, self._flow)

        self._flow = flow

    def release(self):
        self._flow = None
        self._lock.release()


def sasl_enabled():
    return supervdsm.getProxy().check_qemu_conf_contains('vnc_sasl', '1')
