import traceback
import uuid
from pathlib import Path
from typing import Tuple, Final

import axipy
from PySide2.QtCore import Slot
from axipy import tr, Position, Notifications, Plugin, Separator, ActionButton, ToolButton, Button
from . import helper
from .buttons import change_text_size
from .buttons.angle_between import AngleBetweenTool
from .buttons.arc_by_points import ArcByPoints
from .buttons.cad_polyline.main import CadPolyline
from .buttons.choose_starting_point import ChooseStartingPointToolButton
from .buttons.circle_by_points import CircleByPoints
from .buttons.copy_paste_geometry import PasteGeometry, CopyGeometry, GeometryStorage, paste_geometry_observer_key
from .buttons.copy_paste_style import CopyStyle, PasteStyle, StyleStorage, paste_style_observer_key
from .buttons.mirror import VerticalMirror, HorizontalMirror
from .buttons.outline_action import OutlineAction
from .buttons.outline_tool import OutlineTool
from .buttons.perpendicular import PerpendicularTool
from .buttons.redirect import RedirectLine
from .buttons.redirect_contours import RedirectContoursActionButton
from .buttons.right_angle import RightAngleTool
from .buttons.select_by_style.main import SelectByStyle
from .buttons.voronoi import VoronoiMapTool
from .helper_sql import clear_sqlite_tmp, DbSettings

ICON_PATH: Final[Path] = Path(__file__).parents[0] / "images" / "24px"


class CadTools(Plugin):

    def __init__(self) -> None:
        self.global_connections = []
        self.registered_observers = []
        self.buttons = []
        self.db_settings = DbSettings(uuid.uuid4().hex, self.get_plugin_data_dir())

        try:
            self.load_buttons()
        except (Exception,):
            traceback.print_exc()
            Notifications.push(
                tr("Плагин дополнительные инструменты"), tr("Не удалось загрузить плагин"), Notifications.Critical
            )
            self.unload_buttons()

        clear_sqlite_tmp(self.db_settings.data_dir)

    def unload(self) -> None:
        self._disconnect_global_connections()
        self._clear_observers()
        self.unload_buttons()

        clear_sqlite_tmp(self.db_settings.data_dir)

    def load_buttons(self) -> None:

        tab_name = tr("Геометрия")

        def add_and_save(group_arg: str, arg) -> None:
            """
            Добавление кнопок на позиции и сбор кнопок в список для последующей выгрузки.
            """

            def add_and_save_single(btn: Button) -> None:
                if not isinstance(btn, Button):
                    return None
                self.buttons.append(btn)
                pos = Position(tab_name, group_arg)
                pos.add(btn, size=1)

            if isinstance(arg, tuple):
                for elem in arg:
                    add_and_save_single(elem)
            else:
                add_and_save_single(arg)

        group = tr("Буфер обмена")
        add_and_save(group, self.init_copy_paste_geometry())
        add_and_save(group, Separator())
        add_and_save(group, self.init_copy_paste_style())
        add_and_save(group, self.init_find_by_style())

        group = tr("Операции")
        add_and_save(group, self.init_outline_action())
        add_and_save(group, self.init_outline_tool())
        add_and_save(group, self.init_mirror())
        add_and_save(group, self.init_redirect())
        add_and_save(group, self.init_change_text_size())
        add_and_save(group, self.init_voronoi())
        add_and_save(group, self.init_redirect_contours_action())
        add_and_save(group, self.init_choose_starting_point())
        add_and_save(group, self.init_angle_between())

        group = tr("Рисование")
        add_and_save(group, self.init_arc_by_points())
        add_and_save(group, self.init_circle_by_points())
        add_and_save(group, self.init_perpendicular())
        add_and_save(group, self.init_right_angle())
        add_and_save(group, self.init_cad_polyline())

    def unload_buttons(self) -> None:
        if self.buttons is None:
            return None

        for button in self.buttons:
            if isinstance(button, Button):
                button.remove()
        self.buttons = None

    def _disconnect_global_connections(self) -> None:
        n = 0
        for connection in self.global_connections:
            connection.disconnect()
            n += 1

        GeometryStorage.signals.state_changed.disconnect()
        StyleStorage.signals.state_changed.disconnect()

        helper.print_(f"Plugin: cleared {n} global connections")

    def _clear_observers(self) -> None:
        n = 0
        for observer_name in self.registered_observers:
            helper.print_(observer_name)
            if observer_name in axipy.observer_manager:
                axipy.observer_manager.remove(observer_name)
                n += 1
        helper.print_(f"Plugin: cleared {n} registered observers")

    """ Buttons """

    """ Copy/Paste """

    def init_copy_paste_geometry(self) -> Tuple[ActionButton, ToolButton]:
        title = tr("Копировать геометрию")
        CopyGeometry.title = title

        copy_button = self.create_action(
            title=title,
            enable_on=axipy.observer_manager.Selection,
            on_click=lambda: CopyGeometry().on_triggered(),
            icon=str(ICON_PATH / "copy_geometry.png"),
            tooltip=tr("Копирует геометрию в буфер обмена."),
            doc_file="html",
        )

        title = tr("Вставить геометрию")
        PasteGeometry.title = title
        observer_id = paste_geometry_observer_key
        PasteGeometry.observer_id = observer_id

        # Регистрируем наблюдатель
        def register_paste_geometry_observer() -> None:
            """
            Создаём и регистрируем наблюдателя для инструментов Копировать/Вставить геометрию
            """

            observer_editable = axipy.observer_manager.Editable

            def is_enabled() -> bool:
                return GeometryStorage.has_features() and observer_editable.value

            paste_geometry_observer = axipy.observer_manager.get(paste_geometry_observer_key, None)
            if paste_geometry_observer is None:
                init_value = is_enabled()
                paste_geometry_observer = axipy.Observer(paste_geometry_observer_key, init_value)
                self.registered_observers.append(paste_geometry_observer_key)

            @Slot()
            def refresh_value() -> None:
                paste_geometry_observer.value = is_enabled()

            self.global_connections.extend(
                (
                    helper.Connection(observer_editable.changed, refresh_value),
                    # helper.Connection(GeometryStorage.signals.state_changed, refresh_value),
                )
            )

            GeometryStorage.signals.state_changed.connect(refresh_value)

            refresh_value()

        register_paste_geometry_observer()

        paste_button = self.create_tool(
            title=title,
            enable_on=observer_id,
            on_click=lambda: PasteGeometry(),
            icon=str(ICON_PATH / "paste_geometry.png"),
            tooltip=tr("Вставляет геометрию из буфера обмена в указанное место."),
            doc_file="html",
        )

        return copy_button, paste_button

    def init_copy_paste_style(self) -> Tuple[ActionButton, ActionButton]:

        copy_title = tr("Копировать стиль")

        copy_button = self.create_action(
            title=copy_title,
            on_click=lambda: CopyStyle(copy_title).on_triggered(),
            enable_on=axipy.observer_manager.Selection,
            icon=str(ICON_PATH / "copy_style.png"),
            tooltip=tr("Копирует стиль оформления объекта в буфер обмена."),
            doc_file="copy_paste_style.html",
        )

        def register_paste_style_observer() -> axipy.Observer:
            """
            Создаём наблюдателя, чтобы управлять доступностью кнопки вставить стиль.
            Её доступность отличается от 'Копировать' тем, что для вставки необходим
            заполненный буфер стилей.
            """
            selection_editable_is_same = axipy.observer_manager.SelectionEditableIsSame

            def is_enabled() -> bool:
                return not StyleStorage.is_empty() and selection_editable_is_same.value

            paste_style_observer = axipy.observer_manager.get(paste_style_observer_key, None)
            if paste_style_observer is None:
                paste_style_observer = axipy.Observer(paste_style_observer_key, is_enabled())
                self.registered_observers.append(paste_style_observer_key)

            @Slot()
            def set_value() -> None:
                paste_style_observer.value = is_enabled()

            self.global_connections.extend(
                (
                    helper.Connection(selection_editable_is_same.changed, set_value),
                    # helper.Connection(StyleStorage.signals.state_changed, set_value),
                )
            )

            StyleStorage.signals.state_changed.connect(set_value)

            # выставляем начальное значение
            set_value()
            return paste_style_observer

        # Регистрируем наблюдателя
        register_paste_style_observer()

        paste_title = tr("Вставить стиль")

        paste_button = self.create_action(
            title=paste_title,
            on_click=lambda: PasteStyle(paste_title).on_triggered(),
            enable_on=paste_style_observer_key,
            icon=str(ICON_PATH / "paste_style.png"),
            tooltip=tr("Применяет скопированный ранее стиль на выделенные объекты."),
            doc_file="copy_paste_style.html",
        )

        return copy_button, paste_button

    def init_find_by_style(self) -> ToolButton:
        title = tr("Выбор по стилю")
        observer_id = axipy.observer_manager.ActiveMapView

        button = self.create_tool(
            title=title,
            on_click=lambda: SelectByStyle(self, title, observer_id),
            icon=str(ICON_PATH / "find_by_style.png"),
            enable_on=observer_id,
        )

        return button

    """ Actions """

    def init_outline_action(self) -> ActionButton:
        title = tr("Оконтурить - действие")

        button = self.create_action(
            title=title,
            on_click=lambda: OutlineAction(self, title).on_triggered(),
            enable_on=axipy.observer_manager.SelectionEditable,
            icon=str(ICON_PATH / "outline_action.png"),
            tooltip=tr("Позволяет создавать контур (полигон) вокруг выбранных объектов."),
            doc_file="outline_action.html",
        )

        return button

    def init_mirror(self) -> Tuple[ActionButton, ActionButton]:
        vertical_title = tr("Отразить по вертикали")

        vertical_button = self.create_action(
            title=vertical_title,
            on_click=lambda: VerticalMirror(self, vertical_title).on_triggered(),
            enable_on=axipy.observer_manager.SelectionEditableIsSame,
            icon=str(ICON_PATH / "vertical_mirror.png"),
            tooltip=tr("Объекты зеркально отражаются относительно центроида описанного вокруг них прямоугольника."),
            doc_file="vertical_mirror.html",
        )

        horizontal_title = tr("Отразить по горизонтали")

        horizontal_button = self.create_action(
            title=horizontal_title,
            on_click=lambda: HorizontalMirror(self, horizontal_title).on_triggered(),
            enable_on=axipy.observer_manager.SelectionEditableIsSame,
            icon=str(ICON_PATH / "horizontal_mirror.png"),
            tooltip=tr("Объекты зеркально отражаются относительно центроида описанного вокруг них прямоугольника."),
            doc_file="horizontal_mirror.html",
        )

        return vertical_button, horizontal_button

    def init_redirect(self) -> ActionButton:
        title = tr("Изменить направление линии")

        button = self.create_action(
            title=title,
            on_click=lambda: RedirectLine(title).on_triggered(),
            enable_on=axipy.observer_manager.SelectionEditableIsSame,
            icon=str(ICON_PATH / "redirect.png"),
            tooltip=tr("Изменяет направление элементов выбранных объектов."),
            doc_file="redirect.html",
        )

        return button

    def init_change_text_size(self) -> ActionButton:
        title = tr("Изменить размер текста")

        button = self.create_action(
            title=title,
            on_click=lambda: change_text_size.Dialog(title).open(),
            enable_on=axipy.observer_manager.SelectionEditableIsSame,
            icon=str(ICON_PATH / "change_text_size.png"),
            tooltip=tr("Изменяет размер текста в выборке"),
        )

        return button

    def init_redirect_contours_action(self) -> axipy.ActionButton:
        return RedirectContoursActionButton(self)

    def init_choose_starting_point(self) -> axipy.ToolButton:
        return ChooseStartingPointToolButton(self)

    """ Tools """

    def init_outline_tool(self) -> ToolButton:
        title = tr("Оконтурить - инструмент")
        observer_id = axipy.observer_manager.Editable

        button = self.create_tool(
            title=title,
            on_click=lambda: OutlineTool(self, title, observer_id),
            enable_on=observer_id,
            icon=str(ICON_PATH / "outline_tool.png"),
            tooltip=tr("Позволяет создавать вогнутый контур вокруг выбранных объектов."),
            doc_file="outline_tool.html",
        )

        return button

    def init_voronoi(self) -> ToolButton:

        title = tr("Полигоны Вороного")
        observer_id = axipy.observer_manager.ActiveMapView

        VoronoiMapTool.title = title
        VoronoiMapTool.observer_id = observer_id
        VoronoiMapTool.db_settings = self.db_settings

        button = self.create_tool(
            title=title,
            enable_on=observer_id,
            on_click=lambda: VoronoiMapTool(),
            icon=str(ICON_PATH / "voronoi.png"),
            tooltip=tr("Построение полигонов Вороного по выбранным объектам."),
            doc_file="voronoi.html",
        )

        return button

    def init_cad_polyline(self) -> ToolButton:
        title = tr("По углу и расстоянию")
        observer_id = axipy.observer_manager.Editable

        button = self.create_tool(
            title=title,
            on_click=lambda: CadPolyline(self, title, observer_id),
            icon=str(ICON_PATH / "cad_polyline.png"),
            enable_on=observer_id,
        )

        return button

    """ Drawing """

    def init_arc_by_points(self) -> ToolButton:
        title = tr("Дуга по 3 точкам")

        button = self.create_tool(
            title=title,
            on_click=lambda: ArcByPoints(self, title),
            icon=str(ICON_PATH / "arc_by_points.png"),
            enable_on=axipy.observer_manager.Editable,
        )

        return button

    def init_circle_by_points(self) -> ToolButton:
        title = tr("Окружность по 3 точкам")

        button = self.create_tool(
            title=title,
            on_click=lambda: CircleByPoints(self, title),
            icon=str(ICON_PATH / "circle_by_points.png"),
            enable_on=axipy.observer_manager.Editable,
        )

        return button

    def init_perpendicular(self) -> ToolButton:

        button = self.create_tool(
            title=tr("Перпендикуляр"),
            on_click=lambda: PerpendicularTool(),
            icon=str(ICON_PATH / "perpendicular.png"),
            enable_on=axipy.observer_manager.Editable,
            tooltip=tr("Создаёт новый линейный объект перпендикулярный сегменту объекта."),
            doc_file="perpendicular.html",
        )

        return button

    def init_right_angle(self) -> ToolButton:

        button = self.create_tool(
            title=tr("Прямой угол"),
            on_click=lambda: RightAngleTool(),
            icon=str(ICON_PATH / "right_angle.png"),
            enable_on=axipy.observer_manager.Editable,
            tooltip=tr("Создаёт новый линейный объект перпендикулярный сегменту объекта."),
            doc_file="right_angle.html",
        )

        return button

    def init_angle_between(self) -> ToolButton:
        title = tr("Вычислить угол")
        observer_id = axipy.observer_manager.ActiveMapView

        button = self.create_tool(
            title=title,
            enable_on=observer_id,
            on_click=AngleBetweenTool,
            icon=str(ICON_PATH / "angle_tool.png"),
            tooltip=tr("Вычисляет угол между двумя отрезками"),
        )

        return button
