import sys
import traceback
from enum import Enum
from typing import List, Optional

import axipy
from axipy import ActiveToolPanel, Notifications, Plugin, Polygon
from axipy.concurrent import (
    AxipyProgressHandler,
    ProgressGuiFlags,
    ProgressSpecification,
    task_manager,
)
from axipy.cs import CoordSystem
from axipy.da import Feature, Geometry, Style, Table
from axipy.gui import AxipyAcceptableActiveToolHandler, MapTool
from axipy.gui import selection_manager as sm
from axipy.gui import view_manager
from axipy.gui.map_tool import SelectToolBehavior
from axipy.sql import geometry_uid, get_database
from PySide2.QtCore import Qt
from PySide2.QtGui import (
    QDoubleValidator,
    QKeyEvent,
    QMouseEvent,
    QPainter,
    QPaintEvent,
)
from PySide2.QtSql import QSqlQuery
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QGridLayout, QWidget

from ..helper import (
    Connection,
    DataManagerGuard,
    ensure_editable_table,
    ensure_sql_init_for_bg_thread,
    selection_as_tmp_table,
    text_to_float,
)
from .outline_action import ConvexHullMode, OutlineAction


class ConcaveHullSettings:
    def __init__(self) -> None:
        self.factor = 0  # type: float
        self.allow_holes = False  # type: bool
        self.tolerance = 0  # type: float


class OutlineMode(Enum):
    ConvexHullForEachObject = 0
    ConcaveHull = 1


class OutlineSettings:
    def __init__(self) -> None:
        self.concave = None  # type: ConcaveHullSettings
        self.mode = OutlineMode.ConcaveHull


class CreateOutlineWidget(QWidget):
    """
    Графический элемент, содержащий в себе параметры для инструмента оконтурить объекты
    """

    def __init__(self, iface: Plugin, ui_file: str, parent: QWidget) -> None:
        super().__init__(parent)
        self.__iface = iface
        self.__ui = QUiLoader().load(ui_file, self)
        grid_layout = QGridLayout()
        grid_layout.addWidget(self.__ui)
        self.setLayout(grid_layout)
        self.init_tolerance_le()
        self.init_sigma_factor_cb()
        self.__ui.concaveHullRB.toggled.connect(lambda: self.__set_active_widget(self.__ui.concaveHullWidget))
        self.__ui.convexHullRB.toggled.connect(lambda: self.__set_active_widget(self.__ui.convexHullForEachGeomWidget))

    def init_sigma_factor_cb(self):
        validator = QDoubleValidator(bottom=0, top=100, decimals=12)
        validator.setNotation(QDoubleValidator.StandardNotation)
        self.__ui.sigma_factor_cb.setValidator(validator)
        default_factors = [1, 2, 2.3, 2.5, 3, 4]
        locale = self.__ui.sigma_factor_cb.locale()
        default_factors = [locale.toString(float(factor), f="f", prec=2) for factor in default_factors]
        self.__ui.sigma_factor_cb.addItems(default_factors)
        self.__ui.sigma_factor_cb.setToolTip(
            axipy.tr(
                "Фактор определяющий уровень фильтрации расстояния между точками. \n"
                "Чем меньше значение этого коэффициента, тем меньше точек используется. "
                "Обычно наиболее подходящими для использования являются значения в диапазоне от 1 до 3. "
                "При построении вогнутого контура исследуется расстояние между соседними точками. "
                "Если расстояние от точки, до двух соседних больше среднеквадратичного отклонения "
                "умноженного на σ-фактор, то эта точка будет отброшена."
            )
        )
        self.__ui.sigma_factor_cb.setCurrentText(locale.toString(float(3), f="f", prec=1))

    def init_tolerance_le(self):
        validator = QDoubleValidator(bottom=sys.float_info.min, top=sys.float_info.max, decimals=15)
        validator.setNotation(QDoubleValidator.StandardNotation)
        self.__ui.tolerance_le.setValidator(validator)
        self.__ui.tolerance_le.setToolTip(
            axipy.tr(
                "Если координаты отличаются друг от друга меньше чем на введенное "
                "значение, то они не будут учитываться при выполнении операции."
            )
        )

    def update_coordinate_unit(self, cs: CoordSystem):
        self.__ui.unit_tolerance_label.setText(cs.unit.localized_name)

    def __set_active_widget(self, widget: QWidget):
        self.__ui.stackedWidget.setCurrentWidget(widget)

    def get_settings(self) -> OutlineSettings:
        settings = OutlineSettings()
        if self.__ui.concaveHullRB.isChecked():
            settings.mode = OutlineMode.ConcaveHull
            concave = ConcaveHullSettings()  # type: ConcaveHullSettings
            concave.allow_holes = self.__ui.allowHoles.isChecked()
            if self.__ui.tolerance_gb.isChecked():
                concave.tolerance = text_to_float(self.__ui.tolerance_le.text(), self.__ui.tolerance_le.locale())
            concave.factor = text_to_float(
                self.__ui.sigma_factor_cb.currentText(), self.__ui.sigma_factor_cb.lineEdit().locale()
            )
            settings.concave = concave
            return settings
        settings.mode = OutlineMode.ConvexHullForEachObject
        return settings


class OutlineTool(MapTool):
    """
    Составной виджет с настройками для создания контура.  Влючает в себя создание
    выпуклого контура для каждого выбранного объекта (ConvexHull) и вогнутого
    контура ( ConcaveHull, a-shapes)
    """

    def __init__(self, iface: Plugin, title: str, observer_id) -> None:
        super().__init__()
        self.title = title
        self.iface = iface
        ui_file = str(self.iface.plugin_dir / "ui" / "CreateOutlineWidget.ui")
        panel_service = ActiveToolPanel()
        widget = CreateOutlineWidget(self.iface, ui_file, None)
        self.__tool_panel = panel_service.make_acceptable(
            title=self.title, observer_id=observer_id, widget=widget
        )  # type: AxipyAcceptableActiveToolHandler
        self.__tool_panel.accepted.connect(self.__execute_operation)
        self.__connections = []  # type: List[Connection]
        self.__selector = SelectToolBehavior(self)

        def update_coordinate_unit():
            widget.update_coordinate_unit(sm.table.coordsystem)

        self.__tool_panel.activated.connect(update_coordinate_unit)

    def load(self):
        if sm.count > 0:
            self.on_triggered()
        # TODO: Сейчас при смене карты просто выключаем инструмент,
        # нужно попробовать сохранить инструмент доступным.
        self.__connect(view_manager.active_changed, self.reset)
        self.__connect(sm.changed, self.selection_changed)

    def selection_changed(self):
        if self.view.widget != view_manager.active.widget:
            return
        if sm.count == 0:
            self.__tool_panel.disable()
        else:
            self.__tool_panel.try_enable()
            self.on_triggered()

    def unload(self):
        self.__selector = None
        self.__tool_panel.deactivate()
        # отключаем все соедениения
        for conn in self.__connections:
            conn.disconnect()

    def mousePressEvent(self, event: QMouseEvent):
        ok = self.__selector.mousePressEvent(event)
        return self.BlockEvent if ok else self.PassEvent

    def mouseMoveEvent(self, event: QMouseEvent) -> Optional[bool]:
        self.__selector.mouseMoveEvent(event)
        return self.PassEvent

    def paintEvent(self, event: QPaintEvent, painter: QPainter):
        self.__selector.paintEvent(event, painter)

    def mouseReleaseEvent(self, event: QMouseEvent) -> Optional[bool]:
        ok = self.__selector.mouseReleaseEvent(event)
        return self.BlockEvent if ok else self.PassEvent

    def keyPressEvent(self, event: QKeyEvent) -> Optional[bool]:
        if self.__selector.keyPressEvent(event):
            return self.BlockEvent
        if event.key() == Qt.Key_Escape:
            self.reset()
            return self.BlockEvent
        return super().keyPressEvent(event)

    def on_triggered(self):
        ensure_sql_init_for_bg_thread()
        self.__tool_panel.activate()

    def __execute_operation(self):
        self.__tool_panel.disable()
        try:
            settings = self.__tool_panel.widget.get_settings()
            if settings.mode == OutlineMode.ConcaveHull:
                self.concave_hull(settings.concave)
            elif settings.mode == OutlineMode.ConvexHullForEachObject:
                self.convex_hull_for_each_object()
        except (Exception,):
            traceback.print_exc()
        finally:
            # Не забываем включить кнопки на панели чтобы не остаться в промежуточном
            # состоянии после исключения
            self.__tool_panel.try_enable()

    def __notify_about_finish(self, res: bool):
        if res:
            Notifications.push(
                self.title, axipy.tr("Вогнутный контур" " (ConcaveHull) был успешно построен"), Notifications.Success
            )
        else:
            Notifications.push(
                self.title,
                axipy.tr("Не удалось построить контур. Возможно следует изменить параметры создания"),
                Notifications.Warning,
            )

    def concave_hull(self, settings: ConcaveHullSettings):
        editable_table = ensure_editable_table()
        wrapped_table = selection_as_tmp_table()
        spec = ProgressSpecification(description=self.title)
        res = task_manager.run_and_get(
            spec, self.concave_hull_sql, settings, sm.table.coordsystem, wrapped_table.data, editable_table
        )
        self.__notify_about_finish(res)

    @staticmethod
    def concave_hull_sql(
        handler: AxipyProgressHandler,
        settings: ConcaveHullSettings,
        selection_cs: CoordSystem,
        wrapped_table: DataManagerGuard,
        editable_table: Table,
    ) -> bool:
        query = QSqlQuery(get_database())
        # TODO: Добавить поддержку отмены SQL запросов
        query_text = f"SELECT AsBinary(ConcaveHull(Collect(FromAxiGeo({geometry_uid})), \
             {settings.factor}, {settings.allow_holes}, {settings.tolerance})) FROM {wrapped_table.name}"
        try:
            res = query.exec_(query_text)
            if not res:
                print(f"Произошла ошибка при выполнении SQL-запроса {query_text}")
                return False
            if not query.next():
                return False
            wkb = query.value(0)
            if len(wkb) == 0:
                return False
            g = Geometry.from_wkb(wkb, selection_cs)
            if not isinstance(g, Polygon):
                return False
        except (Exception,):
            traceback.print_exc()
            return False

        g = g.normalize()

        if all((not settings.allow_holes, isinstance(g, Polygon), len(g.holes) > 0)):
            g = Polygon(g.points)

        style = Style.for_geometry(g)
        f = Feature(geometry=g, style=style)
        handler.prepare_to_write_changes()
        editable_table.insert(f)
        return True

    def convex_hull_for_each_object(self):
        convex_hull = OutlineAction(self.iface, self.title)
        spec = ProgressSpecification(description=self.title, flags=ProgressGuiFlags.CANCELABLE)
        task_manager.run_and_get(spec, convex_hull.convexhull, ConvexHullMode.OneForEach)

    def __connect(self, signal, slot, auto_connect=True):
        self.__connections.append(Connection(signal, slot, auto_connect))
