import yt.wrapper as yt
import sys
from collections import deque
import time
from contextlib import contextmanager

from .constants import YtCluster
from .base_table import BaseTable
from .client import create_yt_client


__all__ = [
    'PathCollection',
    'BaseTable',  # deprecated
    'CALC_CLUSTERS',
    'DELIVERY_CLUSTERS',
    'CALC_AND_DELIVERY_CLUSTERS',
]


CALC_CLUSTERS = (YtCluster.HAHN, YtCluster.BANACH)
DELIVERY_CLUSTERS = (YtCluster.SENECA_MAN, YtCluster.SENECA_MYT, YtCluster.SENECA_SAS, YtCluster.SENECA_VLA)
CALC_AND_DELIVERY_CLUSTERS = CALC_CLUSTERS + DELIVERY_CLUSTERS


class PathCollection(object):
    def __init__(self, yt_cluster=None, yt_client=None):
        super(PathCollection, self).__init__()
        if yt_client is None and yt_cluster is not None:
            yt_client = create_yt_client(yt_cluster)
        self.yt_cluster = yt_cluster
        self._yt = yt_client

    @property
    def yt_client(self):
        return self._yt if self._yt is not None else yt

    def get_all_tables(self):
        """ Returns list with all child tables """
        return [t for t in self.__dict__.values() if isinstance(t, BaseTable)]

    def get_all_collections(self):
        """ Returns list with all child path collections """
        return [c for c in self.__dict__.values() if isinstance(c, PathCollection)]

    def verify(self):
        queue = deque([self])
        while queue:
            item = queue.popleft()
            if isinstance(item, PathCollection):
                queue.extend(item.get_all_tables())
                queue.extend(item.get_all_collections())
                continue
            assert isinstance(item, BaseTable)
            for p in item.verify_state():
                yield p

    def make_table(self, path, **kwargs):
        return BaseTable(path, yt_client=self._yt, **kwargs)

    def mount_tables(self):
        plan = build_fix_plan(self.verify())
        for action in plan:
            sys.stderr.write(action.pretty_msg + "\n")
            action()

    def remove_tables(self):
        queue = deque([self])
        while queue:
            item = queue.popleft()
            if isinstance(item, PathCollection):
                queue.extend(item.get_all_tables())
                queue.extend(item.get_all_collections())
                continue
            assert isinstance(item, BaseTable)
            sys.stderr.write("remove {}\n".format(item))
            item.remove()


def build_fix_plan(problems):
    """ Builds actions list and returns in propertly order """
    all_actions = list(p.actions for p in problems)
    return merge_actions(all_actions)


def merge_actions(action_lists):
    """
    Merges multiple actions lists into one.
    Preserves actions order.
    """
    return list(_merge_gen(action_lists))


def _merge_gen(lists):
    deques = [deque(l) for l in lists]
    while True:
        checked_next_items = []
        # find first item in first non-empty deque
        for d in deques:
            if d:
                next_item = d[0]
                break
        else:
            # all lists is empty
            raise StopIteration()
        checked_next_items.append(next_item)
        while True:
            # find selected action in other queues
            for d in deques:
                if next_item in d and d[0] not in checked_next_items:
                    next_item = d[0]
                    break
            else:
                # all queues checked
                break
            checked_next_items.append(next_item)
        # remove selected item from queues
        for d in deques:
            if d and d[0] == next_item:
                d.popleft()
        yield next_item


@contextmanager
def freeze_dynamic_tables(tables, yt_client):
    """
    Context manager for temporary freezing all dynamic tables in given node.
    """
    change_all_tables_state(
        tables,
        yt_client,
        from_state='mounted',
        to_state='frozen'
    )
    yield
    change_all_tables_state(
        tables,
        yt_client,
        from_state='frozen',
        to_state='mounted',
        ignore_from_state=True
    )


def change_all_tables_state(tables, yt_client, from_state, to_state, ignore_from_state=False):
    """
    Change state of all tablets from all dynamic tables in given node path.
    Supports states: 'mounted', 'frozen', 'unmounted'.
    Waits until all tablets has been in given state.
    """
    def get_table_states(table):
        return {x["state"] for x in yt_client.get(str(table) + "/@tablets")}

    process_ops = {
        ('mounted', 'frozen'): yt_client.freeze_table,
        ('frozen', 'mounted'): yt_client.unfreeze_table,
        ('unmounted', 'mounted'): yt_client.mount_table,
        ('mounted', 'unmounted'): yt_client.unmount_table,
    }
    process_fun = process_ops[(from_state, to_state)]
    processing_tables = set()
    for table in tables:
        table_states = get_table_states(table)
        if table_states == {to_state}:
            continue
        if not table_states == {from_state} and not ignore_from_state:
            # Undefined behaviour occures if some table in unexpected state
            raise RuntimeError('Table %s is in state %s (only %s expected)' % (table, table_states, from_state))
        processing_tables.add(str(table))

    for table in processing_tables:
        process_fun(table)
    while processing_tables:
        processing_tables = {
            table
            for table in processing_tables
            if get_table_states(table) - {to_state}
        }
        if processing_tables:
            time.sleep(1)
