


from __future__ import absolute_import

import errno
import itertools
import logging
import os
import random
import re
import string
import struct
import threading
import uuid
import weakref

from functools import wraps, partial

from six.moves import map

from vdsm import constants
from vdsm.common import commands
from vdsm.common import concurrent
from vdsm.common import logutils
from vdsm.common import proc

from vdsm.storage import exception as se

IOUSER = "vdsm"
DIRECTFLAG = "direct"
STR_UUID_SIZE = 36
UUID_HYPHENS = [8, 13, 18, 23]
MEGA = 1 << 20

log = logging.getLogger('storage.Misc')


def namedtuple2dict(nt):
    return dict(map(lambda f: (f, getattr(nt, f)), nt._fields))


execCmdLogger = logging.getLogger('storage.Misc.excCmd')


execCmd = partial(commands.execCmd, execCmdLogger=execCmdLogger)


def pidExists(pid):
    try:
        os.stat(os.path.join('/proc', str(pid)))
    except OSError as e:
        if e.errno == errno.ENOENT:
            return False
        else:
            log.error("Error on stat pid %s (%s)", pid, str(e))

    return True


def readblock(name, offset, size):

    if (size % 512) or (offset % 512):
        raise se.MiscBlockReadException(name, offset, size)

    left = size
    ret = ""
    baseoffset = offset

    while left > 0:
        (iounit, count, iooffset) = _alignData(left, offset)

        cmd = [constants.EXT_DD, "iflag=%s" % DIRECTFLAG, "skip=%d" % iooffset,
               "bs=%d" % iounit, "if=%s" % name, "count=%s" % count]

        (rc, out, err) = execCmd(cmd, raw=True)
        if rc:
            raise se.MiscBlockReadException(name, offset, size)
        if not validateDDBytes(err.splitlines(), iounit * count):
            raise se.MiscBlockReadIncomplete(name, offset, size)

        ret += out
        left = left % iounit
        offset = baseoffset + size - left
    return ret


def validateDDBytes(ddstderr, size):
    log.debug("err: %s, size: %s" % (ddstderr, size))
    try:
        size = int(size)
    except (ValueError, ):
        raise se.InvalidParameterException("size", str(size))

    if len(ddstderr) != 3:
        raise se.InvalidParameterException("len(ddstderr)", ddstderr)

    try:
        xferred = int(ddstderr[2].split()[0])
    except (ValueError, ):
        raise se.InvalidParameterException("ddstderr", ddstderr[2])

    if xferred != size:
        return False
    return True


def _alignData(length, offset):
    iounit = MEGA
    count = length
    iooffset = offset

    if (length < MEGA) and (offset % length == 0) and (length % 512 == 0):
        count = 1
        iounit = length
        iooffset = offset / iounit
        return (iounit, count, iooffset)

    while iounit > 1:
        if (length >= iounit) and (offset % iounit == 0):
            count = length / iounit
            iooffset = offset / iounit
            break
        iounit = iounit >> 1

    return (iounit, count, iooffset)


def randomStr(strLen):
    return "".join(random.sample(string.letters, strLen))


def parseBool(var):
    if isinstance(var, bool):
        return var
    if var.lower() == 'true':
        return True
    else:
        return False


def packUuid(s):
    value = uuid.UUID(s).int
    high = value // (2 ** 64)
    low = value % (2 ** 64)
    return struct.pack('<QQ', low, high)


def unpackUuid(s):
    low, high = struct.unpack('<QQ', s)
    value = low + high * (2 ** 64)
    return str(uuid.UUID(int=value))


UUID_REGEX = re.compile("^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$")
UUID_BLANK = "00000000-0000-0000-0000-000000000000"


def validateUUID(uuid, name="uuid", blank=True):
    try:
        m = UUID_REGEX.match(uuid)
    except TypeError:
        raise se.InvalidParameterException(name, uuid)

    if m is None:
        raise se.InvalidParameterException(name, uuid)

    if not blank and uuid == UUID_BLANK:
        raise se.InvalidParameterException(name, uuid)


def validateInt(number, name):
    try:
        return int(number)
    except:
        raise se.InvalidParameterException(name, number)


def validateN(number, name):
    n = validateInt(number, name)
    if n < 0:
        raise se.InvalidParameterException(name, number)
    return n


def validateSize(capacity, name):
    if not isinstance(capacity, basestring):
        log.error("Number of blocks as int is not supported, use size in "
                  "bytes as string")
        raise se.InvalidParameterException(name, capacity)
    return validateN(capacity, name)


def parseHumanReadableSize(size):
    if size.isdigit():
        return int(size)

    size = size.upper()

    if size.endswith("T"):
        if size[:-1].isdigit():
            return int(size[:-1]) << 40

    if size.endswith("G"):
        if size[:-1].isdigit():
            return int(size[:-1]) << 30

    if size.endswith("M"):
        if size[:-1].isdigit():
            return int(size[:-1]) << 20

    if size.endswith("K"):
        if size[:-1].isdigit():
            return int(size[:-1]) << 10

    return 0


class DynamicBarrier(object):
    def __init__(self):
        self._cond = threading.Condition()
        self._busy = False

    def enter(self):
        with self._cond:
            if not self._busy:
                self._busy = True
                return True

            self._cond.wait()


            if not self._busy:
                self._busy = True
                return True

            self._cond.wait()


            return False

    def exit(self):
        with self._cond:
            if not self._busy:
                raise AssertionError("Attempt to exit a barrier without "
                                     "entering")
            self._busy = False
            self._cond.notifyAll()


class SamplingMethod(object):
    _log = logging.getLogger("storage.SamplingMethod")

    def __init__(self, func):
        self.__func = func
        self.__lastResult = None
        self.__barrier = DynamicBarrier()

        if hasattr(self.__func, "func_name"):
            self.__funcName = self.__func.__name__
        else:
            self.__funcName = str(self.__func)

        self.__funcParent = None

    def __call__(self, *args, **kwargs):
        if self.__funcParent is None:
            if (hasattr(self.__func, "func_code") and
                    self.__func.__code__.co_varnames == 'self'):
                self.__funcParent = args[0].__class__.__name__
            else:
                self.__funcParent = self.__func.__module__

        self._log.debug("Trying to enter sampling method (%s.%s)",
                        self.__funcParent, self.__funcName)
        if self.__barrier.enter():
            try:
                self._log.debug("Got in to sampling method")
                self.__lastResult = self.__func(*args, **kwargs)
            finally:
                self.__barrier.exit()
        else:
            self._log.debug("Some one got in for me")

        self._log.debug("Returning last result")
        return self.__lastResult


def samplingmethod(func):
    sm = SamplingMethod(func)

    @wraps(func)
    def helper(*args, **kwargs):
        return sm(*args, **kwargs)
    return helper


def getfds():
    return [int(fd) for fd in os.listdir("/proc/self/fd")]


class Event(object):

    _count = itertools.count()

    def __init__(self, name, sync=False):
        self._log = logging.getLogger("storage.Event.%s" % name)
        self.name = name
        self._syncRoot = threading.Lock()
        self._registrar = {}
        self._sync = sync

    def register(self, func, oneshot=False):
        with self._syncRoot:
            self._registrar[id(func)] = (weakref.ref(func), oneshot)

    def unregister(self, func):
        with self._syncRoot:
            del self._registrar[id(func)]

    def _emit(self, *args, **kwargs):
        self._log.debug("Emitting event")
        with self._syncRoot:
            for funcId, (funcRef, oneshot) in self._registrar.items():
                func = funcRef()
                if func is None or oneshot:
                    del self._registrar[funcId]
                    if func is None:
                        continue
                try:
                    self._log.debug("Calling registered method `%s`",
                                    logutils.funcName(func))
                    if self._sync:
                        func(*args, **kwargs)
                    else:
                        self._start_thread(func, args, kwargs)
                except:
                    self._log.warn("Could not run registered method because "
                                   "of an exception", exc_info=True)

        self._log.debug("Event emitted")

    def emit(self, *args, **kwargs):
        if len(self._registrar) > 0:
            self._start_thread(self._emit, args, kwargs)

    def _start_thread(self, func, args, kwargs):
        name = "event/%d" % next(self._count)
        t = concurrent.thread(func, args=args, kwargs=kwargs, name=name)
        t.start()


_NO_ERROR = Exception("No error")


def killall(name, signum, group=False):
    exception = _NO_ERROR
    knownPgs = set()
    pidList = proc.pgrep(name)
    if len(pidList) == 0:
        raise OSError(errno.ESRCH,
                      "Could not find processes named `%s`" % name)

    for pid in pidList:
        try:
            if group:
                pgid = os.getpgid(pid)
                if pgid in knownPgs:
                    continue
                knownPgs.add(pgid)

                os.killpg(pgid, signum)
            else:
                os.kill(pid, signum)
        except OSError as e:
            if e.errno == errno.ESRCH:
                continue
            exception = e

    if exception is not _NO_ERROR:
        raise exception


def isAscii(s):
    try:
        s.decode('ascii')
        return True
    except (UnicodeDecodeError, UnicodeEncodeError):
        return False


def walk(top, topdown=True, onerror=None, followlinks=False, blacklist=()):
    try:
        names = os.listdir(top)
    except OSError as err:
        if onerror is not None:
            onerror(err)
        return

    normblacklist = [os.path.abspath(x) for x in blacklist]

    dirs, nondirs = [], []
    for name in names:
        path = os.path.join(top, name)

        if os.path.abspath(path) in normblacklist:
            continue

        if not followlinks:
            try:
                os.readlink(path)
            except OSError as err:
                if err.errno != errno.EINVAL:
                    raise
            else:
                nondirs.append(name)
                continue

        if os.path.isdir(path):
            dirs.append(name)
        else:
            nondirs.append(name)

    if topdown:
        yield top, dirs, nondirs
    for name in dirs:
        path = os.path.join(top, name)
        if followlinks or not os.path.islink(path):
            for x in walk(path, topdown, onerror, followlinks, blacklist):
                yield x
    if not topdown:
        yield top, dirs, nondirs
