import enum
from typing import Optional

import axipy
from axipy import MapView, Plugin
from axipy.cpp_gui import CoordinateLineEdit
from axipy.cs import CoordSystem, Unit
from PySide2.QtCore import (
    QAbstractTableModel,
    QEvent,
    QItemSelectionModel,
    QModelIndex,
    QObject,
    Qt,
    Signal,
)
from PySide2.QtGui import QIcon, QMouseEvent
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import (
    QAbstractItemView,
    QGridLayout,
    QHeaderView,
    QStyledItemDelegate,
    QStyleOptionViewItem,
    QTableView,
    QWidget,
)

from .data_types import AngleType, CPModelMode
from .model import CPModel


def add_and_edit(widget: "CPWidget", row: int, column: int):
    widget.add_point.emit()
    index = widget.model.index(row, column)
    flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
    widget.selection_model.select(index, flags)
    widget.selection_model.setCurrentIndex(index, QItemSelectionModel.Select)
    widget._edit(index)


class CPSettings:
    def __init__(self) -> None:
        self.angle_type = None  # type: Optional[AngleType]
        self.distance_type = None  # type: Optional[Unit]
        self.make_polygon = False


class CPPointDelegate(QStyledItemDelegate):
    """
    Делегат предоставляющий элемент для редактирования координат
    """

    def createEditor(self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex) -> QWidget:
        return CoordinateLineEdit(parent)

    def setEditorData(self, editor: CoordinateLineEdit, index: QModelIndex):
        if editor is None or not index.isValid():
            return
        value = index.model().data(index, Qt.EditRole)  # type: float
        if value is None:
            return
        assert isinstance(value, (int, float, bool))
        editor.setValue(value)

    def setModelData(self, editor: CoordinateLineEdit, model: QAbstractTableModel, index: QModelIndex):
        model.setData(index, editor.value(), Qt.EditRole)

    def updateEditorGeometry(self, editor: CoordinateLineEdit, option: QStyleOptionViewItem, index: QModelIndex):
        editor.setGeometry(option.rect)


class TableEventFilter(QObject):

    def __init__(self, cad_polyline_widget: "CPWidget", parent: QObject) -> None:
        super().__init__(parent)
        self.__widget = cad_polyline_widget

    def eventFilter(self, watched: QObject, event: QEvent) -> bool:
        if event.type() == QEvent.KeyPress:
            return self.process_key_events(event)
        return False

    def process_key_events(self, event: QEvent) -> bool:
        sm = self.__widget.selection_model
        if event.key() == Qt.Key_Delete and sm.hasSelection():
            self.__widget.remove_point.emit(sm.currentIndex().row())

        index = sm.currentIndex()
        if event.key() == Qt.Key_Return:
            index = sm.currentIndex()
            if not index.isValid():
                return False
            self.__widget._edit(index)

        def is_navigation_key(key: int):
            return (
                key == Qt.Key_Home
                or key == Qt.Key_End
                or key == Qt.Key_PageUp
                or key == Qt.Key_PageDown
                or key == Qt.Key_Escape
                or key == Qt.Key_Tab
                or key == Qt.Key_Back
                or key == Qt.Key_Shift
                or key == Qt.Key_Control
                or key == Qt.Key_Delete
                or key == Qt.Key_Backspace
                or key == Qt.Key_Select
                or key == Qt.Key_Left
                or key == Qt.Key_Right
                or key == Qt.Key_Up
                or key == Qt.Key_Down
            )

        if self.__widget.model.is_last_row(index) and not is_navigation_key(event.key()):
            add_and_edit(self.__widget, index.row(), index.column())
            return True
        return False


class TableView(QTableView):

    def __init__(self, cad_polyline_widget: "CPWidget") -> None:
        super().__init__(cad_polyline_widget)
        self.__widget = cad_polyline_widget

    def mouseDoubleClickEvent(self, event: QMouseEvent):
        index = self.indexAt(event.pos())
        if not index.isValid():
            pass
        elif self.model().is_last_row(index):
            add_and_edit(self.__widget, index.row(), index.column())
        else:
            self.edit(index)
        return super().mouseDoubleClickEvent(event)


class CoordCsType(enum.Enum):
    LayerCS = 1
    MapCS = 2


class CPWidget(QWidget):
    add_point = Signal()
    remove_point = Signal(int)
    angle_type_changed = Signal(AngleType)
    unit_type_chagned = Signal(Unit)
    clear_all = Signal()
    cartesian_mode_changed = Signal(CPModelMode)

    def __init__(self, iface: Plugin, model: CPModel, parent: Optional[QWidget]) -> None:
        super().__init__(parent)
        self.iface = iface
        self.__model = model
        self.__view = None  # type: Optional[MapView]
        ui_file = str(self.iface.plugin_dir / "ui" / "CadPolyline.ui")
        self.__ui = self.load_ui(ui_file)
        self.setup_icon()
        self.init_cartesian_mode_cb()
        self.init_angle()
        self.init_units_cb()
        self.init_points_table()
        self.init_points_manage_buttons()
        self.init_ask_coordinates_widget()

        def update_relate_rb_state():
            self.__ui.relate_rb.setEnabled(self.__model.size() > 1)

        update_relate_rb_state()
        self.__model.dataChanged.connect(update_relate_rb_state)

    @property
    def _table_view(self) -> QTableView:
        return self.__ui.table_view

    def _edit(self, index: QModelIndex):
        self.__ui.table_view.edit(index)

    def set_view(self, view: MapView):
        self.__view = view
        self.init_choose_cs_widget()

    def is_cartesian_mode(self) -> bool:
        return self.__ui.cartesian_mode_cb.isChecked()

    def init_cartesian_mode_cb(self):
        if self.__model.mode == CPModelMode.Cartesian:
            self.__ui.cartesian_mode_cb.setCheckState(Qt.Checked)
        else:
            self.__ui.cartesian_mode_cb.setCheckState(Qt.Unchecked)

        def notify(state: int):
            if state == Qt.Checked:
                mode = CPModelMode.Cartesian
            else:
                mode = CPModelMode.Sphere

            self.cartesian_mode_changed.emit(mode)

        self.__ui.cartesian_mode_cb.stateChanged.connect(notify)

    def init_choose_cs_widget(self):
        self.__ui.coord_cs_cb.addItem(axipy.tr("Карты"), CoordCsType.MapCS)
        self.__ui.coord_cs_cb.addItem(axipy.tr("Слоя"), CoordCsType.LayerCS)
        index = self.__ui.coord_cs_cb.findData(CoordCsType.MapCS)
        self.__ui.coord_cs_cb.setCurrentIndex(index)

        def coord_cs_changed():
            self.__model.coordsystem = self.user_cs_for_view()

        self.__ui.coord_cs_cb.currentTextChanged.connect(coord_cs_changed)

        def set_data_cs_from_model(cs: CoordSystem):
            coord_cs_type = CoordCsType.MapCS
            if cs == self.__view.editable_layer.coordsystem:
                coord_cs_type = CoordCsType.LayerCS
            if coord_cs_type == self.__ui.coord_cs_cb.currentData():
                return
            index_arg = self.__ui.coord_cs_cb.findData(coord_cs_type)
            if index_arg != -1:
                self.__ui.coord_cs_cb.blockSignals(True)
                self.__ui.coord_cs_cb.setCurrentIndex(index_arg)
                self.__ui.coord_cs_cb.blockSignals(False)

        self.__model.coordsystem_changed.connect(set_data_cs_from_model)

    def init_points_manage_buttons(self):
        def add_btn_clicked():
            self.add_point.emit()

        self.__ui.add_btn.clicked.connect(add_btn_clicked)
        self.__ui.add_btn.setToolTip(axipy.tr("Добавить строчку ниже выбранной"))
        self.__ui.remove_btn.clicked.connect(self.__remove_point)
        self.__ui.remove_btn.setToolTip(axipy.tr("Удалить выделенную строчку"))
        self.__ui.undo_btn.setDefaultAction(self.__model.undo_action)
        self.__ui.redo_btn.setDefaultAction(self.__model.redo_action)
        self.__ui.clear_all_btn.clicked.connect(lambda: self.clear_all.emit())
        self.__ui.clear_all_btn.setToolTip(axipy.tr("Удалить все точки"))

        def update_button_status():
            def is_enabled_remove_btn() -> bool:
                index = self.selection_model.currentIndex()
                return not self.__model.is_last_row(index) and self.selection_model.hasSelection()

            def is_enabled_clear_all_btn() -> bool:
                return not self.__model.is_empty

            self.__ui.remove_btn.setEnabled(is_enabled_remove_btn())
            self.__ui.clear_all_btn.setEnabled(is_enabled_clear_all_btn())

        self.__model.dataChanged.connect(update_button_status)
        self.selection_model.selectionChanged.connect(update_button_status)

    def __remove_point(self):
        sel_model = self.__ui.table_view.selectionModel()
        index = sel_model.currentIndex()
        if not index.isValid():
            return
        self.remove_point.emit(index.row())

    @property
    def selection_model(self) -> QItemSelectionModel:
        return self.__ui.table_view.selectionModel()

    @property
    def model(self) -> CPModel:
        return self.__model

    def set_coord_cs_widget_visibility(self, visible: bool):
        self.__ui.ask_cs_coordinates_widget.setVisible(visible)

    def init_ask_coordinates_widget(self):
        self.__ui.ask_cs_coordinates_widget.setVisible(False)

    def user_cs_for_view(self) -> Optional[CoordSystem]:
        if not self.__ui.ask_cs_coordinates_widget.isVisible():
            return None
        coord_cs_type = self.__ui.coord_cs_cb.currentData()
        if coord_cs_type == CoordCsType.MapCS:
            return self.__view.coordsystem
        elif coord_cs_type == CoordCsType.LayerCS and self.__view.editable_layer is not None:
            return self.__view.editable_layer.coordsystem

    def load_ui(self, ui_file: str) -> QWidget:
        ui = QUiLoader().load(ui_file, self)
        grid_layout = QGridLayout()
        grid_layout.setHorizontalSpacing(0)
        grid_layout.setVerticalSpacing(0)
        grid_layout.addWidget(ui)
        self.setLayout(grid_layout)
        return ui

    def init_points_table(self):
        view = TableView(self)
        view.setObjectName("table_view")
        self.__ui.main_vertical_layout.addWidget(view)
        self.__ui.table_view = view
        view.setModel(self.__model)
        view.setSelectionBehavior(QAbstractItemView.SelectRows)
        view.setSelectionMode(QAbstractItemView.SingleSelection)
        view.setItemDelegate(CPPointDelegate())
        view.installEventFilter(TableEventFilter(self, self))
        view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        view.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        view.setEditTriggers(QAbstractItemView.EditKeyPressed | QAbstractItemView.AnyKeyPressed)
        self.__model.point_was_added.connect(lambda: view.scrollToBottom())

    def init_units_cb(self):
        for unit in Unit.all_linear:
            name = unit.localized_name
            self.__ui.units_cb.addItem(name, userData=unit)

        def emit_current_unit():
            current_unit = self.__ui.units_cb.currentData()
            self.unit_type_chagned.emit(current_unit)

        self.__ui.units_cb.currentTextChanged.connect(emit_current_unit)
        self.__model.distance_unit_changed.connect(self.set_current_unit)

        class WheelFilter(QObject):
            def eventFilter(self, obj: QObject, event: QEvent) -> bool:
                if event.type() == QEvent.Wheel:
                    event.ignore()
                    return True
                return False

        # Переключение колёсиком единиц измерения не очень удобно, плюс каждый
        # вызывает пересчёт значений в таблице и сохраняет записи для отмены.
        # Поэтому решено отключить.
        self.__ui.units_cb.installEventFilter(WheelFilter(self))

    def set_current_unit(self, unit: Unit):
        if self.__ui.units_cb.currentData() == unit:
            return
        index = self.__ui.units_cb.findText(unit.localized_name)
        if index == -1:
            raise RuntimeError("Неизвестная едница измерения растояний на карте")
        self.__ui.units_cb.setCurrentIndex(index)

    def init_angle(self):
        def make_rb_slot(angle_type: AngleType):
            def react(checked: bool):
                if checked:
                    self.angle_type_changed.emit(angle_type)

            return react

        self.__ui.clockwise_rb.toggled.connect(make_rb_slot(AngleType.CounterClockWiseRight))
        self.__ui.counterclock_wise_rb.toggled.connect(make_rb_slot(AngleType.ClockWiseTop))
        self.__ui.relate_rb.toggled.connect(make_rb_slot(AngleType.Relate))

        def set_angle_type(angle_type: AngleType):
            self.angle_type = angle_type

        self.__model.angle_type_changed.connect(set_angle_type)

    def setup_icon(self):
        self.__ui.clockwise_rb.setIcon(QIcon(str(self.iface.plugin_dir / "images" / "32px" / "clockwise_top.png")))
        self.__ui.clockwise_rb.setToolTip(AngleType.to_str(AngleType.ClockWiseTop))
        self.__ui.counterclock_wise_rb.setIcon(
            QIcon(str(self.iface.plugin_dir / "images" / "32px" / "counterclock-wise_right.png"))
        )
        self.__ui.counterclock_wise_rb.setToolTip(AngleType.to_str(AngleType.CounterClockWiseRight))
        self.__ui.relate_rb.setIcon(QIcon(str(self.iface.plugin_dir / "images" / "32px" / "relate.png")))
        self.__ui.relate_rb.setToolTip(AngleType.to_str(AngleType.Relate))
        self.__ui.remove_btn.setIcon(QIcon.fromTheme("delete"))
        self.__ui.add_btn.setIcon(QIcon.fromTheme("add"))
        self.__ui.clear_all_btn.setIcon(QIcon.fromTheme("clear"))
        self.__model.redo_action.setIcon(QIcon.fromTheme("redo"))
        self.__model.undo_action.setIcon(QIcon.fromTheme("undo"))

    @property
    def angle_type(self) -> AngleType:
        if self.__ui.counterclock_wise_rb.isChecked():
            return AngleType.ClockWiseTop
        elif self.__ui.relate_rb.isChecked():
            return AngleType.Relate
        elif self.__ui.clockwise_rb.isChecked():
            return AngleType.CounterClockWiseRight

    @angle_type.setter
    def angle_type(self, angle_type: AngleType):
        if self.angle_type.value == angle_type.value:
            return
        if angle_type == AngleType.ClockWiseTop:
            self.__ui.counterclock_wise_rb.setChecked(True)
        elif angle_type == AngleType.CounterClockWiseRight:
            self.__ui.clockwise_rb.setChecked(True)
        elif angle_type == AngleType.Relate:
            self.__ui.relate_rb.setChecked(True)

    def get_settings(self) -> CPSettings:
        settings = CPSettings()
        settings.angle_type = self.angle_type
        settings.distance_type = self.__ui.units_cb.currentData()
        settings.make_polygon = self.__ui.make_polygon_cb.isChecked()
        return settings
