from typing import List, Optional

from axipy import Notifications, tr
from axipy.concurrent import (
    AxipyProgressHandler,
    ProgressGuiFlags,
    ProgressSpecification,
    task_manager,
)
from axipy.da import (
    CollectionStyle,
    Feature,
    Geometry,
    GeometryCollection,
    LineStyle,
    PointStyle,
    PolygonStyle,
    Style,
    Table,
    TextStyle,
)
from axipy.da import data_manager as dm
from PySide2.QtCore import QObject, Signal

from ..helper import ensure_editable_table

paste_style_observer_key = "PasteStyleObserver"


def flat_collection_style(collection_style: CollectionStyle) -> List[Style]:
    styles = [collection_style.point, collection_style.line, collection_style.polygon, collection_style.text]
    return list(filter(None, styles))


def try_paste_to_collection_style_impl(style: Style, collection_style: CollectionStyle) -> bool:
    if isinstance(style, PolygonStyle):
        if collection_style.polygon is not None and collection_style.polygon != style:
            return False
        collection_style.for_polygon(style)
    elif isinstance(style, LineStyle):
        if collection_style.line is not None and collection_style.line != style:
            return False
        collection_style.for_line(style)
    elif isinstance(style, PointStyle):
        if collection_style.point is not None and collection_style.point != style:
            return False
        collection_style.for_point(style)
    elif isinstance(style, TextStyle):
        if collection_style.text is not None and collection_style.text != style:
            return False
        collection_style.for_text(style)
    return True


def try_paste_to_collection_style(style: Style, collection_style: CollectionStyle) -> bool:
    """
    Если в хранилище уже есть стиль для этого типа геометрии и он отличается от
    нового, значит в выборке пристутствуют несколько стилей для одного типа
    геометрий. Что приведёт к неочивидном результату. Скопируется только один из
    них причем никто не гарантирует какой именно.
    """

    if isinstance(style, CollectionStyle):
        for style in flat_collection_style(style):
            success = try_paste_to_collection_style_impl(style, collection_style)
            if not success:
                return False
        return True
    else:
        return try_paste_to_collection_style_impl(style, collection_style)


def merge_collection(source: CollectionStyle, target: CollectionStyle):
    """
    Копируем только те стили из коллекции, которые там есть
    """
    if source.polygon is not None:
        target.for_polygon(source.polygon)
    if source.line is not None:
        target.for_line(source.line)
    if source.point is not None:
        target.for_point(source.point)
    if source.text is not None:
        target.for_text(source.text)
    return target


class StyleStorage:
    """
    Буфер для хранения стилей при операциях Копирования/Вставки стилей.
    """

    class _StyleStorageSignals(QObject):
        state_changed = Signal(bool)

    signals = _StyleStorageSignals()

    class _CollectionStyleDesc:
        def __init__(self) -> None:
            self.__collection_style: Optional[CollectionStyle] = None

        def __get__(self, instance, owner) -> CollectionStyle:
            if self.__collection_style is None:
                self.__collection_style = CollectionStyle()
            return self.__collection_style

    _collection_style = _CollectionStyleDesc()

    @classmethod
    def for_geometry(cls, geom: Geometry, style: Style) -> Style:
        # Для коллекций разнородных объектов
        if isinstance(geom, GeometryCollection) and isinstance(style, CollectionStyle):
            return merge_collection(cls._collection_style, style)
        # Для коллекций однородных геометрий
        if isinstance(geom, GeometryCollection):
            return cls._get_style_for_single_geometry_collection(style)
        # Для обычных одиночных геометрий
        return cls._collection_style.find_style(geom)

    @classmethod
    def _get_style_for_single_geometry_collection(cls, style) -> Optional[Style]:
        # Если у геометрии нет стиля
        if style is None:
            return None
        if isinstance(style, PolygonStyle) and cls._collection_style.polygon is not None:
            return cls._collection_style.polygon
        if isinstance(style, PointStyle) and cls._collection_style.point is not None:
            return cls._collection_style.point
        if isinstance(style, LineStyle) and cls._collection_style.line is not None:
            return cls._collection_style.line
        if isinstance(style, TextStyle) and cls._collection_style.text is not None:
            return cls._collection_style.text
        return None

    @classmethod
    def set_style(cls, style: Style) -> None:
        if isinstance(style, PolygonStyle):
            cls._collection_style.for_polygon(style)
        elif isinstance(style, LineStyle):
            cls._collection_style.for_line(style)
        elif isinstance(style, PointStyle):
            cls._collection_style.for_point(style)
        elif isinstance(style, TextStyle):
            cls._collection_style.for_text(style)
        elif isinstance(style, CollectionStyle):
            merge_collection(style, cls._collection_style)
        else:
            print(f"Неподдерживаемый тип стиля {style}")
        cls.signals.state_changed.emit(cls.is_empty())

    @classmethod
    def is_empty(cls) -> bool:
        return all(
            (
                cls._collection_style.text is None,
                cls._collection_style.point is None,
                cls._collection_style.polygon is None,
                cls._collection_style.line is None,
            )
        )

    @classmethod
    def clean(cls):
        cls._collection_style = CollectionStyle()
        cls.signals.state_changed.emit(cls.is_empty())


class CopyStyle:
    """
    Копируем стиль выделенных объектов в буфер
    """

    def __init__(self, title: str) -> None:
        self.title = title

    def _notify_about_the_same_style(self):
        message = tr("В выборке содержится несколько стилей для одного и того же типа геометрии.")
        Notifications.push(self.title, message, Notifications.Critical)

    def _notify_about_copy_styles(self, count: int):
        if count == 0:
            message = tr(
                "Не получилось скопировать ни один стиль. "
                "Копирование стиля для текстовых объектов не поддерживается."
            )
            Notifications.push(self.title, message, Notifications.Critical)
        else:
            message = tr(f"Было скопированно стилей: {count}")
            Notifications.push(self.title, message, Notifications.Success)

    def _copy_style(self, handler: AxipyProgressHandler):
        StyleStorage.clean()
        selection_table = dm.selection  # type: Table
        handler.set_max_progress(selection_table.count())
        found_styles = CollectionStyle()
        for feature in selection_table:
            handler.raise_if_canceled()
            if not feature.has_style():
                continue
            success = try_paste_to_collection_style(feature.style, found_styles)
            if not success:
                self._notify_about_the_same_style()
                return None
            handler.add_progress(1)
        StyleStorage.set_style(found_styles)
        self._notify_about_copy_styles(len(flat_collection_style(found_styles)))

    def on_triggered(self):
        spec = ProgressSpecification(description=self.title, flags=ProgressGuiFlags.CANCELABLE)
        task_manager.run_and_get(spec, self._copy_style)


class PasteStyle:
    """
    Вставляем стиль из буфера в выделенные объекты
    """

    def __init__(self, title: str) -> None:
        self.title = title

    def _success_notification(self, amount_of_features: int):
        if amount_of_features == 0:
            Notifications.push(
                self.title,
                tr(
                    "Ни у одного объекта не удалось сменить стиль."
                    " Попробуйте скопировать другой стиль перед тем как применить операцию вставить."
                ),
                Notifications.Warning,
            )
            return
        Notifications.push(
            self.title,
            tr(f"Количество объектов, у которых был изменён стиль: {amount_of_features}"),
            Notifications.Success,
        )

    def _paste_operation(self, handler: AxipyProgressHandler):
        selection_table = dm.selection  # type: Table
        features = []  # type: List[Feature]
        handler.set_max_progress(selection_table.count())
        for feature in selection_table:
            handler.raise_if_canceled()
            if not feature.has_geometry() or not feature.has_style():
                continue
            style = StyleStorage.for_geometry(feature.geometry, feature.style)
            if style is None:
                continue
            feature.style = style
            features.append(feature)
            handler.add_progress(1)
        table = ensure_editable_table()
        handler.prepare_to_write_changes()
        table.update(features)
        self._success_notification(len(features))

    def on_triggered(self):
        spec = ProgressSpecification(description=self.title, flags=ProgressGuiFlags.CANCELABLE)
        task_manager.run_and_get(spec, self._paste_operation)
