from __future__ import annotations

import re

from typing import TYPE_CHECKING, Any, Callable

from qtpy.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt
from qtpy.QtGui import (
    QBrush,
    QColor,
    QFont,
    QIcon,
    QImage,
    QMouseEvent,
    QPainter,
    QPalette,
    QPen,
    QPixmap,
    QTextDocument,
    QTextOption,
)
from qtpy.QtWidgets import (
    QApplication,
    QListView,
    QListWidget,
    QStyle,
    QStyledItemDelegate,
    QToolTip,
    QTreeView,
    QTreeWidget,
    QWidget,
)

if TYPE_CHECKING:
    from qtpy.QtCore import QAbstractItemModel, QModelIndex, QObject
    from qtpy.QtGui import QAbstractTextDocumentLayout
    from qtpy.QtWidgets import QStyleOptionViewItem


FONT_SIZE_REPLACE_RE: re.Pattern[str] = re.compile(r"font-size:\d+pt;")
ICONS_DATA_ROLE: int = Qt.ItemDataRole.UserRole + 10


class HTMLDelegate(QStyledItemDelegate):
    """A delegate that paints items with HTML text.

    This delegate is used to paint items in a QListView, QListWidget, QTreeView, or QTreeWidget with HTML text.
    It allows for custom text size, vertical spacing, and word wrapping.
    """

    def __init__(
        self,
        parent: QWidget | None = None,
        *,
        word_wrap: bool = True,
        text_size: int = 12,
        custom_vertical_spacing: int = 0,
    ):
        super().__init__(parent)
        self.text_size: int = text_size
        self.custom_vertical_spacing: int = custom_vertical_spacing
        self.nudged_model_indexes: dict[QModelIndex, tuple[int, int]] = {}
        self.word_wrap: bool = word_wrap

    def parent(self) -> QWidget:
        parent: QObject | None = super().parent()
        assert isinstance(parent, QWidget), f"HTMLDelegate.parent() returned non-QWidget: '{parent.__class__.__name__}'"
        return parent

    def setVerticalSpacing(self, spacing: int):
        self.custom_vertical_spacing = spacing

    def set_text_size(self, size: int):
        self.text_size = size

    def set_word_wrap(self, *, wrap: bool):
        self.word_wrap = wrap

    def nudge_item(self, index: QModelIndex, x: int, y: int):
        self.nudged_model_indexes[index] = (x, y)

    def createTextDocument(self, html: str, font: QFont, width: int) -> QTextDocument:
        doc = QTextDocument()
        doc.setHtml(FONT_SIZE_REPLACE_RE.sub(f"font-size:{self.text_size}pt;", html))
        doc.setDefaultFont(font)
        if not self.word_wrap:
            text_option = QTextOption()
            text_option.setWrapMode(QTextOption.WrapMode.NoWrap)
            doc.setDefaultTextOption(text_option)
        doc.setTextWidth(width)
        return doc

    def draw_badge(self, painter: QPainter, center: QPoint, radius: int, text: str):
        painter.save()
        painter.setBrush(QBrush(QColor(255, 255, 255)))
        painter.setPen(QPen(QColor(200, 200, 200), 2))
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)  # noqa: FBT003
        painter.drawEllipse(center, radius, radius)
        painter.setPen(QPen(QColor(0, 0, 0)))
        painter.setFont(QFont("Arial", max(10, self.text_size - 1), QFont.Weight.Bold))
        painter.drawText(QRect(center.x() - radius, center.y() - radius, radius * 2, radius * 2), Qt.AlignmentFlag.AlignCenter, text)
        painter.restore()

    def process_icons(
        self,
        painter: QPainter | None,
        option: QStyleOptionViewItem,
        index: QModelIndex,
        event: QMouseEvent | None = None,
        *,
        show_tooltip: bool = False,
        execute_action: bool = False,
    ) -> tuple[int, bool]:
        icon_data: dict = index.data(ICONS_DATA_ROLE)
        icon_width_total = 0
        handled_click = False
        if icon_data:
            icon_size = int(self.text_size * 1.5)
            icon_spacing = icon_data["spacing"]
            columns = icon_data["columns"]
            icons: list[tuple[Any, Callable, str]] = icon_data["icons"]
            bottom_badge_info = icon_data.get("bottom_badge")

            if (execute_action or show_tooltip) and bottom_badge_info:
                icons.append((None, bottom_badge_info["action"], bottom_badge_info["tooltip_callable"]()))

            icon_width_total = columns * (icon_size + icon_spacing) - icon_spacing

            for i, (icon_serialized, action, tooltip) in enumerate(icons):
                icon: QIcon | None = self._get_icon(icon_serialized, icon_size)
                col, row = i % columns, i // columns
                x_offset = option.rect.left() + (icon_size + icon_spacing) * col
                y_offset = option.rect.top() + icon_spacing + (icon_size + icon_spacing) * row
                icon_rect = QRect(x_offset, y_offset, icon_size, icon_size)

                if painter and icon:
                    painter.fillRect(icon_rect, QColor(235, 245, 255))
                    icon.paint(painter, icon_rect)

                if event and icon_rect.contains(event.pos()):
                    if show_tooltip:
                        QToolTip.showText(event.globalPosition().toPoint(), tooltip, self.parent())
                        return icon_width_total, True
                    if bool(execute_action) and bool(action):
                        action()
                        handled_click = True

            if bottom_badge_info:
                radius = icon_width_total // 2
                center_y = y_offset + icon_width_total + icon_spacing + radius if icons else option.rect.top() + icon_spacing + radius
                if painter:
                    self.draw_badge(painter, QPoint(option.rect.left() + radius, center_y), radius, bottom_badge_info["text_callable"]())

        if show_tooltip:
            QToolTip.hideText()
        return icon_width_total, handled_click

    def _get_icon(self, iconSerialized: Any, icon_size: int) -> QIcon | None:  # noqa: N803
        if isinstance(iconSerialized, QIcon):
            return iconSerialized
        if isinstance(iconSerialized, QStyle.StandardPixmap):
            q_style: QStyle | None = QApplication.style()
            assert q_style is not None
            return q_style.standardIcon(iconSerialized)
        if isinstance(iconSerialized, str):
            return QIcon(QPixmap(iconSerialized).scaled(icon_size, icon_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
        if isinstance(iconSerialized, QPixmap):
            return QIcon(iconSerialized)
        if isinstance(iconSerialized, QImage):
            return QIcon(QPixmap.fromImage(iconSerialized))
        return None

    def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
        painter.save()
        painter.translate(*self.nudged_model_indexes.get(index, (0, 0)))

        # Handle the Decoration Role (icon)
        decoration = index.data(Qt.ItemDataRole.DecorationRole)
        if decoration:
            icon = QIcon(decoration)
            icon_rect = QRect(option.rect.topLeft(), option.decorationSize)
            icon.paint(painter, icon_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
            option.rect.setLeft(icon_rect.right() + 5)

        display_data: Any = index.data(Qt.ItemDataRole.DisplayRole)
        if not display_data:
            painter.restore()
            return

        icon_width_total, _ = self.process_icons(painter, option, index)
        new_rect: QRect = option.rect.adjusted(icon_width_total, 0, 0, 0)
        painter.setClipRect(new_rect)

        doc: QTextDocument = self.createTextDocument(display_data, option.font, new_rect.width())
        ctx: QAbstractTextDocumentLayout.PaintContext | None = doc.documentLayout().PaintContext()
        assert ctx is not None
        ctx.palette = option.palette

        if bool(option.state & QStyle.StateFlag.State_Selected):
            highlight_color = option.palette.highlight().color() if option.widget.hasFocus() else QColor(100, 100, 100)
            highlight_color.setAlpha(int(highlight_color.alpha() * 0.4))
            painter.fillRect(new_rect, highlight_color)
            ctx.palette.setColor(QPalette.ColorRole.Text, option.palette.highlightedText().color())
        else:
            ctx.palette.setColor(QPalette.ColorRole.Text, option.palette.text().color())

        painter.translate(new_rect.topLeft())
        doc_layout: QAbstractTextDocumentLayout | None = doc.documentLayout()
        assert doc_layout is not None
        doc_layout.draw(painter, ctx)
        painter.restore()

    def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
        html: str | None = index.data(Qt.ItemDataRole.DisplayRole)
        if html is None:
            return super().sizeHint(option, index)

        parent_widget: QWidget = self.parent()
        available_width, available_height = self._get_available_size(parent_widget, index)

        doc: QTextDocument = self.createTextDocument(html, option.font, available_height)
        naturalWidth, naturalHeight = int(doc.idealWidth()), int(doc.size().height())
        ratio = 1.4
        min_width: float = naturalHeight * ratio
        max_height: float = min(naturalWidth / ratio, (naturalHeight * naturalWidth) / min_width)
        finalSize: QSize = QSize(int(max(naturalWidth, min_width)), int(min(naturalHeight, max_height) + self.custom_vertical_spacing))

        icon_data: dict[str, Any] = index.data(ICONS_DATA_ROLE)
        if icon_data:
            finalSize = self._adjust_size_for_icons(finalSize, icon_data)

        return finalSize

    def _get_available_size(self, parent_widget: QWidget, index: QModelIndex) -> tuple[int, int]:  # noqa: N803
        if isinstance(parent_widget, (QListWidget, QListView, QTreeView, QTreeWidget)):
            viewport: QWidget | None = parent_widget.viewport()
            assert viewport is not None
            available_width: int = viewport.width()
            available_height: int = viewport.height()
            if isinstance(parent_widget, (QTreeView, QTreeWidget)):
                depth: int = 1
                parent_index: QModelIndex = index.parent()
                while parent_index.isValid():
                    depth += 1
                    parent_index = parent_index.parent()
                available_width -= depth * parent_widget.indentation()
        else:
            available_width, available_height = parent_widget.width(), parent_widget.height()
        return available_width, available_height

    def _adjust_size_for_icons(self, size: QSize, icon_data: dict[str, Any]) -> QSize:
        icon_size = int(self.text_size * 1.5)
        icon_spacing = icon_data["spacing"]
        columns = icon_data["columns"]
        rows = (len(icon_data["icons"]) + columns - 1) // columns
        total_icon_width = columns * (icon_size + icon_spacing)
        size.setWidth(size.width() + total_icon_width)
        total_icon_height = rows * (icon_size + icon_spacing) + 2 * icon_spacing
        if icon_data.get("bottom_badge"):
            total_icon_height += (icon_size + icon_spacing) + 2 * icon_spacing
        size.setHeight(max(size.height(), total_icon_height))
        return size

    def editorEvent(self, event: QEvent, model: QAbstractItemModel, option: QStyleOptionViewItem, index: QModelIndex) -> bool:
        if event.type() == QEvent.Type.MouseButtonRelease and isinstance(event, QMouseEvent) and event.button() == Qt.MouseButton.LeftButton:
            _, handled_click = self.process_icons(None, option, index, event=event, execute_action=True)
            if handled_click:
                return True
        return super().editorEvent(event, model, option, index)

    def handleIconTooltips(self, event: QMouseEvent, option: QStyleOptionViewItem, index: QModelIndex) -> bool:
        """Must be called from the parent widget directly."""
        return self.process_icons(None, option, index, event=event, show_tooltip=True)[1]
