
from __future__ import absolute_import
from __future__ import division

import itertools
import logging
import sys
import threading
import traceback

from collections import namedtuple

from six.moves import queue
from six.moves import range

from vdsm.common import pthread
from vdsm.common import time


class Timeout(Exception):
    """ Raised when operation timed out """


class InvalidEvent(Exception):
    """
    Raised when waiting on invalided event.
    """


class Barrier(object):
    FILLING = 0
    DRAINING = 1

    def __init__(self, count):
        if count < 1:
            raise ValueError("Invalid count %d (expecting count >= 1)" % count)
        self._count = count
        self._waiting = 0
        self._state = self.FILLING
        self._cond = threading.Condition(threading.Lock())

    def wait(self, timeout=None):
        if timeout is not None:
            deadline = time.monotonic_time() + timeout
        else:
            deadline = None

        with self._cond:
            self._wait_for_state(self.FILLING, deadline)
            self._waiting += 1
            try:
                if self._waiting == self._count:
                    self._change_state(self.DRAINING)
                else:
                    self._wait_for_state(self.DRAINING, deadline)
            finally:
                self._waiting -= 1
                if self._waiting == 0:
                    self._change_state(self.FILLING)

    def _wait_for_state(self, state, deadline):
        while self._state != state:
            if deadline is not None:
                now = time.monotonic_time()
                if now >= deadline:
                    raise Timeout("Timeout waiting for barrier")
                self._cond.wait(deadline - now)
            else:
                self._cond.wait()

    def _change_state(self, state):
        self._state = state
        self._cond.notify_all()


Result = namedtuple("Result", ["succeeded", "value"])

_tmap_count = itertools.count()


def tmap(func, values, max_workers=10, name=None):

    if max_workers < 1:
        raise ValueError("max_workers {} < 1".format(max_workers))

    if name is None:
        name = "tmap-{}".format(next(_tmap_count))

    pending = queue.Queue()
    results = queue.Queue()

    def worker():
        while True:
            try:
                value = pending.get(block=False)
            except queue.Empty:
                break

            try:
                results.put(Result(True, func(value)))
            except Exception as e:
                results.put(Result(False, e))
                del e

            del value

    for value in values:
        pending.put(value)

    count = pending.qsize()
    workers_needed = min(count, max_workers)
    workers = []

    for i in range(workers_needed):
        w = thread(worker, name="{}/{}".format(name, i))
        w.start()
        workers.append(w)

    for _ in range(count):
        yield results.get()

    for w in workers:
        w.join()


def thread(func, args=(), kwargs=None, name=None, daemon=True, log=None):
    if kwargs is None:
        kwargs = {}
    if log is None:
        log = logging.getLogger()

    def run():
        thread = threading.current_thread()
        try:
            log.debug("START thread %s (func=%s, args=%s, kwargs=%s)",
                      thread, func, args, kwargs)
            pthread.setname(thread.name[:15])
            ret = func(*args, **kwargs)
            log.debug("FINISH thread %s", thread)
            return ret
        except (SystemExit, KeyboardInterrupt) as e:
            log.debug("FINISH thread %s (%s)", thread, e)
        except:
            log.exception("FINISH thread %s failed", thread)

    thread = threading.Thread(target=run, name=name)
    thread.daemon = daemon
    return thread


class ValidatingEvent(object):

    def __init__(self):
        self._cond = threading.Condition(threading.Lock())
        self._flag = False
        self._valid = True

    def is_set(self):
        with self._cond:
            return self._flag

    def set(self):
        with self._cond:
            self._flag = True
            self._cond.notify_all()

    def clear(self):
        with self._cond:
            self._flag = False

    def wait(self, timeout=None):
        with self._cond:
            if not self._valid:
                raise InvalidEvent
            if not self._flag:
                self._cond.wait(timeout)
                if not self._valid:
                    raise InvalidEvent
            return self._flag

    @property
    def valid(self):
        with self._cond:
            return self._valid

    @valid.setter
    def valid(self, value):
        with self._cond:
            wake_up = self._valid and not value
            self._valid = value
            if wake_up:
                self._cond.notify_all()


def format_traceback(ident):
    stack = sys._current_frames()[ident]

    lines = []
    for filename, lineno, name, line in traceback.extract_stack(stack):
        lines.append('File: "%s", line %d, in %s' %
                     (filename, lineno, name))
        if line:
            lines.append("  %s" % (line.strip()))
    return '\n'.join(lines)
