import functools
import html
import logging
import yaml
from qgis.PyQt.QtGui import QFontMetrics, QIcon, QCloseEvent
from qgis.PyQt.QtCore import QAbstractItemModel, Qt, QModelIndex, QSettings, QSortFilterProxyModel, pyqtSlot, QObject, QEvent, QPoint, QSize
from qgis.PyQt.QtWidgets import QTreeView, QHeaderView, QAbstractItemView, QToolTip, QMenu, QAction, QLineEdit
from sqlalchemy import inspect
from SAGisXPlanung.config import export_version
from SAGisXPlanung.core.mixins.mixins import ElementOrderMixin
from SAGisXPlanung.config import xplan_tooltip
from SAGisXPlanung.gui.style import TagStyledDelegate, HighlightRowProxyStyle
from SAGisXPlanung.utils import CLASSES
from .basepage import SettingsPage
logger = logging.getLogger(__name__)

class AttributeConfigPage(SettingsPage):

    def __init__(self, parent=None):
        super(AttributeConfigPage, self).__init__(parent)
        self.ui = None

    def setup_ui(self, ui):
        self.ui = ui
        self.ui.attribute_view = QAttributeConfigView()
        self.ui.tab_attributes.layout().addWidget(self.ui.attribute_view)
        self.ui.filter_edit.setPlaceholderText('Suchen...')
        self.ui.filter_edit.addAction(QIcon(':/images/themes/default/search.svg'), QLineEdit.ActionPosition.LeadingPosition)
        self.ui.filter_edit.textChanged.connect(self.ui.attribute_view.onFilterTextChanged)

    def setup_data(self):
        self.ui.attribute_view.setupModelData()

    def closeEvent(self, event: QCloseEvent):
        conf = self.ui.attribute_view.config_dict()
        s = QSettings()
        s.setValue(f'plugins/xplanung/attribute_config', yaml.dump(conf, default_flow_style=False))

class QAttributeConfigView(QTreeView):

    def __init__(self, parent=None):
        super(QAttributeConfigView, self).__init__(parent)
        self._model = AttributeConfigModel()
        self.proxy = SortProxyModel(self)
        self.proxy.setSourceModel(self._model)
        self.setModel(self.proxy)
        self.setItemDelegate(TagStyledDelegate())
        self.proxy_style = HighlightRowProxyStyle('Fusion')
        self.proxy_style.setParent(self)
        self.setStyle(self.proxy_style)
        self.setMouseTracking(True)
        self.viewport().installEventFilter(QToolTipper(self))
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.onContextMenuRequested)
        self.header().setStretchLastSection(False)
        self.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        self.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        self.setupModelData()

    def setupModelData(self):
        self.clear()
        s = QSettings()
        config = yaml.safe_load(s.value(f'plugins/xplanung/attribute_config', '')) or {}
        for xplan_class_name, xplan_class in CLASSES.items():
            if hasattr(xplan_class, 'xp_versions') and export_version() not in xplan_class.xp_versions:
                continue
            node = AttributeTreeNode(xplan_class_name, '', False)
            if not issubclass(xplan_class, ElementOrderMixin):
                continue
            cols = inspect(xplan_class).columns
            for attribute in xplan_class.element_order(include_base=False, only_columns=True, version=export_version()):
                try:
                    index = cols.keys().index(attribute)
                    nullable = cols[index].nullable
                except ValueError:
                    nullable = True
                desc = xplan_tooltip(xplan_class, attribute, plain=True)
                unchecked = attribute in config.get(xplan_class_name, [])
                attribute_node = AttributeTreeNode(attribute, desc, not unchecked, required=not nullable)
                node.addChild(attribute_node)
            self._model.addChild(node)
        self.expandAll()

    def sizeHint(self):
        size = super(QAttributeConfigView, self).sizeHint()
        return QSize(size.width(), 80)

    def clear(self):
        self._model.clear()

    @pyqtSlot(str)
    def onFilterTextChanged(self, text: str):
        self.proxy.setFilterRegularExpression(text)

    def config_dict(self):
        config = {}
        for top_node in self._model._root._children:
            config[top_node.name] = [node.name for node in top_node._children if not node.is_checked]
        return config

    @pyqtSlot(QPoint)
    def onContextMenuRequested(self, pos: QPoint):
        """ context menu to select/deselect all attributes within a category """
        index = self.proxy.mapToSource(self.indexAt(pos))
        if index.parent().isValid():
            return
        menu = QMenu()
        menu.setToolTipsVisible(True)
        select_all_action = QAction(QIcon(':/images/themes/default/mActionSelectAllTree.svg'), 'Alles wählen')
        select_all_action.setToolTip('Alle Attribute dieser Objektklasse wählen.')
        select_all_action.triggered.connect(functools.partial(self.onSelectCategoryTriggered, index, True))
        menu.addAction(select_all_action)
        deselect_all_action = QAction(QIcon(':/images/themes/default/mActionDeselectAllTree.svg'), 'Alles abwählen')
        deselect_all_action.setToolTip('Alle Attribute dieser Objektklasse abwählen. Verpflichtende Attribute werden nicht abgewählt.')
        deselect_all_action.triggered.connect(functools.partial(self.onSelectCategoryTriggered, index, False))
        menu.addAction(deselect_all_action)
        menu.exec(self.viewport().mapToGlobal(pos))

    @pyqtSlot(QModelIndex, bool, bool)
    def onSelectCategoryTriggered(self, index: QModelIndex, select: bool, checked: bool):
        node = index.internalPointer()
        for i in range(node.childCount()):
            match = self._model.index(i, 0, index)
            self._model.setData(match.siblingAtColumn(2), select, Qt.ItemDataRole.CheckStateRole)

class AttributeConfigModel(QAbstractItemModel):

    def __init__(self, nodes=None):
        super(AttributeConfigModel, self).__init__()
        if nodes is None:
            nodes = []
        self._horizontal_header = ['XPlanung-Attribut', 'Beschreibung', 'Anzeigen']
        self._root = AttributeTreeNode()
        for node in nodes:
            self._root.addChild(node)

    def clear(self):
        self.beginResetModel()
        for i in range(self._root.childCount()):
            item = self._root.child(i)
            del item
        self._root._children.clear()
        self.endResetModel()

    def addChild(self, node, _parent=QModelIndex()):
        row = self.rowCount(_parent)
        if not _parent or not _parent.isValid():
            parent = self._root
        else:
            parent = _parent.internalPointer()
        self.beginInsertRows(_parent, row, row)
        parent.addChild(node)
        self.endInsertRows()

    def removeRows(self, row: int, count: int, parent: QModelIndex=None) -> bool:
        self.beginRemoveRows(parent, row, row + count - 1)
        parent_item = self.itemAtIndex(parent)
        for i in range(count):
            parent_item.removeChild(row + i)
        self.endRemoveRows()
        return True

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None
        node = index.internalPointer()
        if role == Qt.ItemDataRole.DisplayRole:
            return node.data(index.column())
        if role == Qt.ItemDataRole.ToolTipRole:
            return '<qt>{}</qt>'.format(html.escape(node.data(index.column())))
        if role == Qt.ItemDataRole.CheckStateRole and index.column() == 2 and index.parent().isValid():
            return node.checked()
        return None

    def flags(self, index: QModelIndex):
        current_flags = super(AttributeConfigModel, self).flags(index)
        flags = current_flags & ~Qt.ItemFlag.ItemIsSelectable
        if not index.parent().isValid():
            return flags & ~Qt.ItemFlag.ItemIsEnabled
        node = index.internalPointer()
        if node.required:
            return flags & ~Qt.ItemFlag.ItemIsEnabled
        if index.column() == 2:
            return flags | Qt.ItemFlag.ItemIsUserCheckable
        return flags

    def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
        if index.column() == 2:
            if role == Qt.ItemDataRole.EditRole:
                return False
            if role == Qt.ItemDataRole.CheckStateRole:
                item = self.itemAtIndex(index)
                if item.required:
                    return True
                item.is_checked = value
                self.dataChanged.emit(index, index)
                return True
        return super(AttributeConfigModel, self).setData(index, value, role)

    def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self._horizontal_header[col]

    def itemAtIndex(self, index: QModelIndex):
        if not index.isValid():
            return self._root
        return index.internalPointer()

    def index(self, row, column, _parent=QModelIndex()):
        parentItem = self.itemAtIndex(_parent)
        child = parentItem.child(row)
        if child:
            return self.createIndex(row, column, child)
        else:
            return QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()
        child = self.itemAtIndex(index)
        parentItem = child.parent()
        if parentItem == self._root:
            return QModelIndex()
        return self.createIndex(parentItem.row(), 0, parentItem)

    def rowCount(self, index):
        if index.isValid():
            return index.internalPointer().childCount()
        return self._root.childCount()

    def columnCount(self, index):
        return 3

class SortProxyModel(QSortFilterProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setRecursiveFilteringEnabled(True)
        self.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)

    def filterAcceptsRow(self, source_row: int, parent: QModelIndex):
        index = self.sourceModel().index(source_row, 0, parent)
        while parent.isValid():
            if self.filterAcceptsRow(parent.row(), parent.parent()):
                return True
            parent = parent.parent()
        return self.filterRegularExpression().pattern().lower() in str(self.sourceModel().data(index)).lower()

class AttributeTreeNode:

    def __init__(self, name: str=None, description: str=None, is_checked: bool=None, required=False):
        self._children = []
        self._parent = None
        self._row = 0
        self.name = name
        self.description = description
        self.is_checked = is_checked
        self.column_count = 3
        self.required = required

    def data(self, column):
        if column == 0:
            return self.name
        if column == 1:
            return self.description

    def checked(self):
        if self.required:
            return Qt.CheckState.Checked
        return Qt.CheckState.Checked if self.is_checked else Qt.CheckState.Unchecked

    def childCount(self):
        return len(self._children)

    def child(self, row):
        if 0 <= row < self.childCount():
            return self._children[row]

    def parent(self):
        return self._parent

    def row(self):
        return self._row

    def addChild(self, child):
        child._parent = self
        child._row = len(self._children)
        self._children.append(child)

    def removeChild(self, row):
        if row < 0 or row > len(self._children):
            return
        child = self._children.pop(row)
        child._parent = None
        for i in range(len(self._children)):
            self.child(i)._row = i

class QToolTipper(QObject):

    def eventFilter(self, obj: QObject, event: QEvent):
        if event.type() == QEvent.Type.ToolTip:
            view: QAbstractItemView = obj.parent()
            if not view:
                return False
            index = view.indexAt(event.pos())
            if not index.isValid():
                return False
            item_text = view.model().data(index)
            fm = QFontMetrics(view.font())
            item_text_width = fm.width(item_text)
            rect = view.visualRect(index)
            if len(item_text) != 0 and item_text_width > rect.width():
                tooltip = view.model().data(index, Qt.ItemDataRole.ToolTipRole)
                QToolTip.showText(event.globalPos(), tooltip, view, rect)
            else:
                QToolTip.hideText()
            return True
        return False