# coding: utf-8
u"""Классы для работы первично отображаемого интерфейса MIS.

Включают список модулей в меню "Пуск" и список модулей на "Рабочем столе"
"""
import threading
import six
import warnings
from logging import (
    getLogger,
)
from uuid import (
    uuid4,
)

from six.moves import (
    filter,
    map,
)

from m3 import (
    M3JSONEncoder,
)
from m3.actions import (
    Action,
    ControllerCache,
)
from m3.plugins import (
    get_and_execute_callable_from_modules,
)

from .signals import (
    desktop_loader_after_populate,
)


logger = getLogger('django')


try:
    from m3_users.app_meta import (
        GENERIC_USER,
        SUPER_ADMIN,
    )
    from m3_users.helpers import (
        get_assigned_metaroles_query,
    )
    from m3_users.metaroles import (
        UserMetarole,
        get_metarole,
    )
except ImportError:
    # если не установлен пакет m3_users
    # Всё должно работать без метаролей
    GENERIC_USER = SUPER_ADMIN = 'none'

    class UserMetarole(object):
        """
        Заглушка для класса метаролей
        """
        code = GENERIC_USER

        def get_owner_metaroles(self):
            return []

    def get_assigned_metaroles_query(*args):
        return []

    def get_metarole(code):
        return UserMetarole()


# Константы: "Разделитель", "Блок с текущим временем", "Заполняющий блок"
SEPARATOR = '-'
TIMEBLOCK = 'TIMEBLOCK'
FILLBLOCK = 'FILLBLOCK'


class DesktopException(Exception):
    """
    Возникает при ошибках сборки рабочего стола, ярлыков, пунктов меню и т.п.
    """
    pass


class DesktopElementCollection(object):
    """
    Колекция эл-тов Рабочего Стола, определяющая их видимость
    """
    def __init__(self, filt=lambda x: True):
        self._filt = filt
        self._items = []
        self.sorting_key = DEFAULT_SORTING

    def __iter__(self):
        return iter(self._items)

    @property
    def subitems(self):
        """
        Возвращает итератор, обеспечивающий фильтрацию и сортировку элементов
        """
        def prepared(item):
            """
            Возвращает элемент, либо None, если элемент отфильтрован
            или содержит пустой список под-элементов
            """
            if not item.has_subitems:
                result = item if self._filt(item) else None
            else:
                # на всякий случай клонируем
                item = item.clone()
                item.subitems = list(sorted(
                    filter(
                        bool,
                        map(
                            prepared,
                            item.subitems)),
                    key=self.sorting_key
                ))
                result = any(
                    not (isinstance(i, MenuSeparator) or i.name in ('-', u'-'))
                    for i in item.subitems
                ) and item or None
            return result

        # обертка списка элементов в некое подобие LaunchGroup
        stub = prepared(type('stub', (object,), {
            'has_subitems': True,
            'clone': lambda self: self,
            'subitems': self._items[:]
        })())
        return iter(stub.subitems if stub else [])

    def append(self, elem):
        """
        Добавление эл-та в коллекцию
        :param elem: элемент рабочего стола
        :type elem: BaseDesktopElement
        """
        self._items.append(elem)


class DesktopModel(object):
    """
    Класс, агрегирующий в себе список модулей
    (start_menu и toolbox) в меню пуск
    и список модулей на Рабочем Столе (desktop)
    """
    def __init__(self, request, filter_factory):
        self.start_menu = DesktopElementCollection(
            filter_factory(request, DesktopLoader.START_MENU))
        self.toolbox = DesktopElementCollection(
            filter_factory(request, DesktopLoader.TOOLBOX))
        self.desktop = DesktopElementCollection(
            filter_factory(request, DesktopLoader.DESKTOP))
        self.toptoolbar = DesktopElementCollection(
            filter_factory(request, DesktopLoader.TOPTOOLBAR))


class BaseDesktopElement(object):
    """
    Базовый класс для объекта модулей и объекта подменю
    """
    # атрибуты, копируемые в при "клонировании" экземпляра
    # потомки должны добавлять новые атрибуты (конкатенацией)
    clonable_attrs = ('name', 'icon', 'index', 'id')

    has_subitems = False

    def t_is_subitems(self):
        warnings.warn(
            "\"t_has_items\" is deprecated! "
            "Use \".has_subitems\"!",
            FutureWarning
        )
        return self.has_subitems

    def __init__(self, *args, **kwargs):
        """
        :param name: Название модуля или подменю
        :type name: str
        :param icon: Класс CSS, отвечающий за отрисовку иконки
        :type icon: str
        """
        self.name = ''
        self.icon = ''
        self.index = 0
        self.id = str(uuid4())[0:8]

    def _init_component(self, *args, **kwargs):
        """Заполняет атрибуты экземпляра значениями из kwargs"""
        for k, val in kwargs.items():
            assert k in self.__dict__, (
                'Instance attribute "%s" should be defined in class "%s"!' %
                (k, self.__class__.__name__)
            )
            self.__setattr__(k, val)

    def __eq__(self, other):
        return isinstance(other, BaseDesktopElement) and self.id == other.id

    def __hash__(self):
        return hash(self.id)

    def __lt__(self, other):
        assert isinstance(other, BaseDesktopElement), type(other)

        if self.index == other.index:
            result = self.name < other.name
        else:
            result = self.index < other.index

        return result

    def clone(self, *args, **kwargs):
        """
        Возвращает "клон" себя
        """
        clone = self.__class__(*args, **kwargs)
        for k in self.clonable_attrs:
            setattr(clone, k, getattr(self, k))
        return clone


class DesktopLaunchGroup(BaseDesktopElement):
    """
    Класс для работы подменю по нажатию на кнопку Пуск
    """
    has_subitems = True

    def __init__(self, *args, **kwargs):
        """
        :param subitem: Хранит список модулей и список подменю
        :type subitem: list
        """
        super(DesktopLaunchGroup, self).__init__(*args, **kwargs)
        self.index = 10
        self.icon = 'default-launch-group'
        self.subitems = []
        self._init_component(*args, **kwargs)

    def clone(self, *args, **kwargs):
        """
        Возвращает "клон" себя
        """
        clone = super(DesktopLaunchGroup, self).clone(*args, **kwargs)
        clone.subitems = self.subitems[:]
        return clone


class DesktopLauncher(BaseDesktopElement):
    """
    Класс для работы модулей.
    Модули могут находится в меню Пуск, на рабочем столе.
    Данные модули могут включать в себя класс
    DesktopLaunchGroup в атрибут subitems
    """
    clonable_attrs = BaseDesktopElement.clonable_attrs + ('url',)

    def __init__(self, *args, **kwargs):
        """
        :param url: url-адрес, запрос по которому возвратит форму
        :type url: str
        """
        super(DesktopLauncher, self).__init__(*args, **kwargs)
        self.url = ''
        self.icon = 'default-launcher'
        self.index = 100
        self.context = {}  # словарь контекста
        self._init_component(*args, **kwargs)

    @property
    def rendered_context(self):
        """
        Контекст в виде JSON
        """
        return M3JSONEncoder().encode(self.context)

    def clone(self, *args, **kwargs):
        """
        Возвращает "клон" себя
        """
        clone = super(DesktopLauncher, self).clone(*args, **kwargs)
        clone.context = self.context.copy()
        return clone


class DesktopShortcut(DesktopLauncher):
    """
    Отличается от DesktopLauncher тем,
    что автоматически подцепляет адрес из пака, в зависимости от его типа.
    """
    def __init__(self, pack, *args, **kwargs):
        """
        Конструктор
        :param pack: Имя или класс пака
        :type pack: str / Action
        :raise: DesktopException
        """
        super(DesktopShortcut, self).__init__(*args, **kwargs)
        self.pack = pack
        # Если это экшен, то получаем его адрес
        if isinstance(pack, Action):
            self.url = pack.get_absolute_url()
            if not getattr(pack, 'title', None):
                pack.title = getattr(pack.parent, 'title', '???')
        else:
            if isinstance(pack, six.string_types):
                # Пробуем найти как пак
                pack = ControllerCache.find_pack(pack)
                if not pack:
                    raise DesktopException(
                        'Pack %s not found in ControllerCache' % pack)

            self.url = pack.get_list_url()
            # Если не задано имя ярлыка, то название берем из справочника
            if not kwargs.get('name', None):
                self.name = pack.title

    def clone(self):
        return super(DesktopShortcut, self).clone(self.pack)


class MenuSeparator(BaseDesktopElement):
    """
    Специальный разделитель для меню
    """
    def __init__(self, *args, **kwargs):
        super(MenuSeparator, self).__init__(*args, **kwargs)
        self._init_component(*args, **kwargs)

    def clone(self):
        return self


# Ключ сортировки элементов меню по умолчанию:
# сначала - по индексу, затем по имени пункта меню
DEFAULT_SORTING = lambda x: (x.index, x.name)


class DesktopLoader(object):
    """
    Загрузчик значков и меню для веб-десктопа
    """
    _lock = threading.RLock()
    _cache = None
    # Флаг того, что кэш загружен.
    # Из-за ошибок при сборке он может недозаполниться, а поскольку
    # это операция разовая, больше ошибки не будет.
    # Флаг будет повторять ошибку для облегчения ее обнаружения.
    _success = False

    # Константы определяющие куда добавить элемент
    DESKTOP = 0  # рабочий стол
    START_MENU = 1  # меню "пуск"
    TOOLBOX = 2  # правая панель меню "пуск"
    TOPTOOLBAR = 3  # верхняя панель рабочего стола

    @classmethod
    def _load_desktop_from_apps(cls):
        """
        Загрузка элементов Раб.Стола, декларированных в приложениях проекта
        :raise: ImportError
        """
        cls._lock.acquire()
        try:
            if not cls._success:
                cls._cache = {}
                # Из инитов всех приложения
                # пытаемся выполнить register_desktop_menu
                get_and_execute_callable_from_modules('app_meta', 'register_desktop_menu')

            cls._success = True
        finally:
            cls._lock.release()

    @classmethod
    def add_el_to_desktop(cls, desktop, metarole_code):
        """
        Добавляет элементы к интерфейсу в зависимости от метароли
        :param desktop: экземпляр DesktopModel
        :type desktop: obj
        :param metarole_code: код метароли
        :type metarole_code: str
        """
        def join_list(existing_list, in_list):
            """
            Складывает два списка с объектами
            DesktopLaunchGroup и DesktopLauncher
            произвольного уровня вложенности.
            Критерием равенства является имя элемента!
            :param existing_list: список элементов
            :type existing_list: list
            :param in_list: список элементов
            :type in_list: list

            """
            names_dict = {}
            for existing_el in existing_list:
                assert isinstance(existing_el, BaseDesktopElement)
                names_dict[existing_el.name] = existing_el

            for in_el in in_list:
                assert isinstance(in_el, BaseDesktopElement)
                # Группа ярлыков, которая уже есть в списке
                if isinstance(in_el, DesktopLaunchGroup) and (
                        in_el.name in names_dict):
                    # Запускаем рекурсивное слияние
                    ex_el = names_dict[in_el.name]
                    join_list(ex_el.subitems, in_el.subitems)

                # Если ярлык уже в списке, то добавлять повторно не нужно
                elif in_el.name in names_dict:
                    continue

                else:
                    existing_list.append(in_el)

        # Загрузка кеша
        if not cls._success:
            cls._load_desktop_from_apps()

        items_for_role = cls._cache.get(metarole_code, {})
        for place, items in items_for_role.items():
            if place == cls.DESKTOP:
                join_list(desktop.desktop, items)
            elif place == cls.START_MENU:
                join_list(desktop.start_menu, items)
            elif place == cls.TOOLBOX:
                join_list(desktop.toolbox, items)
            elif place == cls.TOPTOOLBAR:
                join_list(desktop.toptoolbar, items)

    @classmethod
    def sort_desktop(cls, desktop_list, sorting=DEFAULT_SORTING):
        """
        Сортирует все контейнеры десктопа в зависимости от индекса (index)
        """
        desktop_list.sorting_key = sorting

    @classmethod
    def populate(cls, user, desktop, sorting=DEFAULT_SORTING):
        """
        Метод, который выполняет всю работу по настройке десктопа во вьюшке
        :param user: пользователь или None
        :type user: django.contrib.auth.models.User
        :param desktop: экземпляр DesktopModel
        :type desktop: DesktopModel
        :param sorting: функция сортировки
        :type sorting: callable
        """
        from django.contrib.auth.models import (
            AnonymousUser,
        )

        roles = []
        if not isinstance(user, AnonymousUser):
            roles.extend(get_assigned_metaroles_query(user))
            if getattr(user, 'is_superuser', False):
                roles.append(SUPER_ADMIN)

        cls.populate_desktop(desktop, roles, sorting)
        desktop_loader_after_populate.send(sender=cls)

    @classmethod
    def populate_desktop(cls, desktop, roles=None, sorting=DEFAULT_SORTING):
        """
        Построение Раб.Стола по элементам @desktop(экземпляр DesktopModel),
        относительно набора ролей @roles.
        .. note ::
            Метод НЕ ТРЕБУЕТ django.contrib.auth и экземпляра User в частности
        :param desktop: экземпляр DesktopModel
        :type desktop: DesktopModel
        :param roles: список ролей
        :type roles: list
        """
        assert isinstance(desktop, DesktopModel)

        assign_roles = set([GENERIC_USER] + (roles or []))
        for role in assign_roles:
            cls.add_el_to_desktop(desktop, role)

        cls.sort_desktop(desktop.desktop, sorting=sorting)
        cls.sort_desktop(desktop.start_menu, sorting=sorting)
        cls.sort_desktop(desktop.toolbox, sorting=sorting)
        cls.sort_desktop(desktop.toptoolbar, sorting=sorting)

    @classmethod
    def add(cls, metarole, place, element):
        """
        Добавление элемента дектопа в кэш заданной метароли
        :param metarole: роль
        :type metarole: str
        :param place: место размещения элемента (рабочий стол, меню и тулбоксы)
        :type place: int
        :raise: AssertionError
        """
        def find_by_name(existed_list, name):
            """
            Поиск внутри списка группы с заданным именем name
            :param existed_list: список группы
            :type existing_list: list
            :param name: имя
            :type name: unicode
            """
            for item in existed_list:
                if isinstance(item, DesktopLaunchGroup) and item.name == name:
                    return item

        def insert_item(existed_list, item):
            """
            Если добавляемый элемент - группа,
            то нужно проверить есть ли у нас уже такая группа.
            Если нет - добавляем, иначе нужно зайти в нее
            и продолжить проверку вниз по дереву
            :param existed_list: список группы
            :type existing_list: list
            :param item: элемент
            :type item: DesktopShortcut / DesktopLauncher
            """
            if isinstance(item, DesktopLaunchGroup):
                collision_item = find_by_name(existed_list, item.name)
                if collision_item is None:
                    # Раз нет переcений по имени, то можно добавлять
                    existed_list.append(item)
                else:
                    for i in item.subitems:
                        insert_item(collision_item.subitems, i)
            elif item not in existed_list:
                existed_list.append(item)

        def insert_for_role(metarole, elem, processed_metaroles):
            """
            Добавление элемента для метароли
            :param metarole: роль
            :type metarole: str
            :param elem: элемент
            :type elem: DesktopShortcut / DesktopLauncher
            :param processed_metaroles: обработанные метароли
            :type processed_metaroles: list
            """
            if metarole not in processed_metaroles:
                processed_metaroles.append(metarole)
                element = elem.clone()

                # Кэш состоит из 3х уровней: словарь с метаролями,
                # словарь с местами и список конечных элементов
                items_for_role = cls._cache.get(metarole.code, {})
                items_for_place = items_for_role.get(place, [])
                insert_item(items_for_place, element)
                items_for_role[place] = items_for_place
                cls._cache[metarole.code] = items_for_role

                # Недостаточно добавить элементы только в одну метароль,
                # т.к. она может входить внутрь другой метароли.
                # Так что нужно пробежаться по ролям,
                # которые включают в себя нашу роль.

                for role in metarole.get_owner_metaroles():
                    insert_for_role(role, element, processed_metaroles)

        # ===============================================
        assert place in (
            cls.DESKTOP,
            cls.START_MENU,
            cls.TOOLBOX,
            cls.TOPTOOLBAR
        )
        if isinstance(metarole, six.string_types):
            metarole = get_metarole(metarole)
        assert isinstance(metarole, UserMetarole)

        insert_for_role(metarole, element, processed_metaroles=[])


# =============================================================================
# Разные полезные шорткаты
# =============================================================================

def add_desktop_launcher(
    name='', url='', icon='', path=None, metaroles=None, places=None
):
    """
    Шорткат для добавления ланчеров в элементы рабочего стола.

    :param name: подпись ланчера на рабочем столе/в меню
    :type name: str
    :param url: url, по которому происходит обращение к ланчеру
    :type url: str
    :param icon: класс, на основе которого рисуется иконка ланчера
    :type icon: m3_ext.icons.Icon
    :param path: список кортежей, которые задают путь к ланчеру в случае, если
                этот ланчер спрятан в подменю.
    .. note::
                Каждый элемент пути задается либо в одном из форматов:
                    "(название,)",
                    "(название, иконка,)",
                    "(название, иконка, индекс)"
    :type path: list
    :param metaroles: список метаролей (либо одиночная метароль)
    """
    if not metaroles or not places:
        return

    launcher = DesktopLauncher(url=url, name=name, icon=icon)
    # "чистый" список метаролей, для которых выполняется регистрация метаролей

    cleaned_metaroles = []
    cleaned_places = []

    cleaned_metaroles.extend(
        metaroles if isinstance(metaroles, list) else [metaroles])
    cleaned_places.extend(
        places if isinstance(places, list) else [places])

    root = None
    parent_group = None
    for slug in (path or []):
        group = DesktopLaunchGroup(name=slug[0])
        if len(slug) > 1 and slug[1]:
            group.icon = slug[1]
        elif len(slug) > 2 and isinstance(slug[2], int):
            group.index = slug[2]
        if not root:
            root = group
        if parent_group:
            parent_group.subitems.append(group)

        parent_group = group

    if root:
        root.subitems.append(launcher)
        launcher = root  # мы в ланчеры, значится, будем добавлять самого рута.

    for metarole in cleaned_metaroles:
        if isinstance(metarole, six.string_types):
            metarole = get_metarole(metarole)
        if metarole:
            for place in cleaned_places:
                DesktopLoader.add(metarole, place, launcher)
