
from __future__ import absolute_import

from collections import namedtuple, deque, OrderedDict
from contextlib import contextmanager
import errno
import functools
import logging
import six
import sys
import os
import socket
import threading
import time

from vdsm.common import zombiereaper
from vdsm.common import time as vdsm_time
from vdsm.common.compat import pickle
from vdsm.common.marks import deprecated
from vdsm.common.proc import pidstat

_THP_STATE_PATH = '/sys/kernel/mm/transparent_hugepage/enabled'
if not os.path.exists(_THP_STATE_PATH):
    _THP_STATE_PATH = '/sys/kernel/mm/redhat_transparent_hugepage/enabled'


class IOCLASS:
    REALTIME = 1
    BEST_EFFORT = 2
    IDLE = 3


class NICENESS:
    NORMAL = 0
    HIGH = 19


def _parseMemInfo(lines):
    meminfo = {}
    for line in lines:
        var, val = line.split()[0:2]
        meminfo[var[:-1]] = int(val)
    return meminfo


def readMemInfo():
    tries = 3
    while True:
        tries -= 1
        try:
            with open('/proc/meminfo') as f:
                lines = f.readlines()
                return _parseMemInfo(lines)
        except:
            logging.warning(lines, exc_info=True)
            if tries <= 0:
                raise
            time.sleep(0.1)


def _parseCmdLine(pid):
    with open("/proc/%d/cmdline" % pid, "rb") as f:
        return tuple(f.read().split("\0")[:-1])


def getCmdArgs(pid):
    res = tuple()
    while len(res) == 0:
        if pidstat(pid).state in ("Z", "z"):
            return tuple()

        res = _parseCmdLine(pid)

    return res


def convertToStr(val):
    varType = type(val)
    if varType is float:
        return '%.2f' % (val)
    elif varType is int:
        return '%d' % (val)
    else:
        return val


def stripNewLines(lines):
    return [l[:-1] if l.endswith('\n') else l for l in lines]


class Canceled(BaseException):
    """
    Raised by methods decorated with @cancelpoint.

    Objects using cancellation points may like to handle this exception for
    cleaning up after cancellation.

    Inherits from BaseException so it can propagate through normal Exception
    handlers.
    """


def cancelpoint(meth):
    @functools.wraps(meth)
    def wrapper(self, *a, **kw):
        if self.__canceled__():
            raise Canceled()
        value = meth(self, *a, **kw)
        if self.__canceled__():
            raise Canceled()
        return value
    return wrapper


symbolerror = {}
for code, symbol in six.iteritems(errno.errorcode):
    symbolerror[os.strerror(code)] = symbol


class closing(object):
    def __init__(self, obj, log="utils.closing"):
        self.obj = obj
        self.log = log

    def __enter__(self):
        return self.obj

    def __exit__(self, t, v, tb):
        try:
            self.obj.close()
        except Exception:
            if t is None:
                raise
            log = logging.getLogger(self.log)
            log.exception("Error closing %s", self.obj)


@deprecated
class AsyncProcessOperation(object):
    def __init__(self, proc, resultParser=None):
        self._lock = threading.Lock()

        self._result = None
        self._resultParser = resultParser

        self._proc = proc

    def wait(self, timeout=None, cond=None):
        return self._proc.wait(timeout, cond)

    def stop(self):
        self._proc.kill()

    def result(self):
        with self._lock:
            if self._result is None:
                out, err = self._proc.communicate()
                rc = self._proc.returncode
                if self._resultParser is not None:
                    try:
                        self._result = (self._resultParser(rc, out, err),
                                        None)
                    except Exception as e:
                        self._result = (None, e)
                else:
                    self._result = ((rc, out, err), None)

            return self._result

    def __del__(self):
        if self._proc.returncode is None:
            zombiereaper.autoReapPID(self._proc.pid)


class Callback(namedtuple('Callback_', ('func', 'args', 'kwargs'))):
    log = logging.getLogger("utils.Callback")

    def __call__(self):
        result = None
        try:
            self.log.debug('Calling %s with args=%s and kwargs=%s',
                           self.func.__name__, self.args, self.kwargs)
            result = self.func(*self.args, **self.kwargs)
        except Exception:
            self.log.error("%s failed", self.func.__name__, exc_info=True)
        return result


class CallbackChain(threading.Thread):
    log = logging.getLogger("utils.CallbackChain")

    def __init__(self, callbacks=()):
        super(CallbackChain, self).__init__()
        self.daemon = True
        self.callbacks = deque(callbacks)

    def run(self):
        try:
            self.log.debug("Starting callback chain.")
            while self.callbacks:
                callback = self.callbacks.popleft()
                if callback():
                    self.log.debug("Succeeded after invoking " +
                                   callback.func.__name__)
                    return
            self.log.debug("Ran out of callbacks")
        except Exception:
            self.log.error("Unexpected CallbackChain error", exc_info=True)

    def addCallback(self, func, *args, **kwargs):
        self.callbacks.append(Callback(func, args, kwargs))


class RollbackContext(object):
    def __init__(self, on_exception_only=False):
        self._finally = []
        self._on_exception_only = on_exception_only

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if self._on_exception_only and exc_type is None and exc_value is None:
            return

        undoExcInfo = None
        for undo, args, kwargs in self._finally:
            try:
                undo(*args, **kwargs)
            except Exception:
                if undoExcInfo is None:
                    undoExcInfo = sys.exc_info()

        if exc_type is None and undoExcInfo is not None:
            six.reraise(undoExcInfo[0], undoExcInfo[1], undoExcInfo[2])

    def defer(self, func, *args, **kwargs):
        self._finally.append((func, args, kwargs))

    def prependDefer(self, func, *args, **kwargs):
        self._finally.insert(0, (func, args, kwargs))


@contextmanager
def running(runnable):
    runnable.start()
    try:
        yield runnable
    finally:
        runnable.stop()


def get_selinux_enforce_mode():
    selinux_mnts = ['/sys/fs/selinux', '/selinux']
    for mnt in selinux_mnts:
        enforce_path = os.path.join(mnt, 'enforce')
        if not os.path.exists(enforce_path):
            continue

        with open(enforce_path) as fileStream:
            return int(fileStream.read().strip())

    return -1


def picklecopy(obj):
    return pickle.loads(pickle.dumps(obj, pickle.HIGHEST_PROTOCOL))


def round(n, size):
    count = int(n + size - 1) // size
    return count * size


def create_connected_socket(host, port, sslctx=None, timeout=None):
    addrinfo = socket.getaddrinfo(host, port,
                                  socket.AF_UNSPEC, socket.SOCK_STREAM)
    family, socktype, proto, _, _ = addrinfo[0]
    sock = socket.socket(family, socktype, proto)

    if sslctx:
        sock = sslctx.wrapSocket(sock)

    sock.settimeout(timeout)
    sock.connect((host, port))
    return sock


@contextmanager
def stopwatch(message, level=logging.DEBUG,
              log=logging.getLogger('vds.stopwatch')):
    if log.isEnabledFor(level):
        start = vdsm_time.monotonic_time()
        yield
        elapsed = vdsm_time.monotonic_time() - start
        log.log(level, "%s: %.2f seconds", message, elapsed)
    else:
        yield


def unique(iterable):
    return list(OrderedDict.fromkeys(iterable).keys())


def log_success(success, log, msg_ok, msg_fail):
    if success:
        log.info(msg_ok)
    else:
        log.warn(msg_fail)
    return success


def str2bool(s):
    v = s.lower()
    if v in ['true', 'yes', 'y', 't', '1']:
        return True
    if v in ['false', 'no', 'n', '0']:
        return False
    raise ValueError('Converting ' + s + ' to bool failed.')
