"""A text editor that automatically adjusts its height to the height of the text
in its document when managed by a layout."""

# standard
from functools import partial

# PyQGIS
from qgis.PyQt.QtCore import QSize, Qt
from qgis.PyQt.QtGui import QContextMenuEvent, QIcon, QKeySequence
from qgis.PyQt.QtWidgets import QAction, QTextEdit

# project
from oslandia.__about__ import DIR_PLUGIN_ROOT


# -- GLobals --
SINGLE_LINE_CODE_SEPARATOR = "`"
BLOCK_CODE_SEPARATOR = "```"


class AutoResizingTextEdit(QTextEdit):
    def __init__(self, parent: QTextEdit | None = None) -> None:
        """Initialize the auto-resizing text edit.

        :param parent: Parent widget, defaults to None
        :type parent: QTextEdit | None, optional
        """
        super().__init__(parent)
        self.textChanged.connect(self.updateGeometry)

        self._edit_actions = []

        format_buttons = [
            {
                "description": self.tr("Bold"),
                "text_before": "**",
                "text_after": "**",
                "icon": str(DIR_PLUGIN_ROOT / "resources/images/bold.svg"),
                "shortcut": QKeySequence(self.tr("Ctrl+B", "Bold")),
            },
            {
                "description": self.tr("Italic"),
                "text_before": "_",
                "text_after": "_",
                "icon": str(DIR_PLUGIN_ROOT / "resources/images/italic.svg"),
                "shortcut": QKeySequence(self.tr("Ctrl+I", "Italic")),
            },
            {
                "description": self.tr("Strike"),
                "text_before": "~~",
                "text_after": "~~",
                "icon": str(DIR_PLUGIN_ROOT / "resources/images/strikethrough.svg"),
            },
            {
                "description": self.tr("Add a link"),
                "text_before": "[",
                "text_after": "](url)",
                "icon": str(DIR_PLUGIN_ROOT / "resources/images/link.svg"),
                "shortcut": QKeySequence(self.tr("Ctrl+K", "Add a link")),
            },
        ]

        for button in format_buttons:
            action = QAction(QIcon(button["icon"]), button["description"], self)
            action.triggered.connect(
                partial(
                    self.insert_after_and_before_selection,
                    button["text_before"],
                    button["text_after"],
                )
            )
            if "shortcut" in button:
                action.setShortcut(button["shortcut"])
                action.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
                self.addAction(action)

            self._edit_actions.append(action)

        self._insert_separator_in_edit_action()
        code_action = QAction(
            QIcon(str(DIR_PLUGIN_ROOT / "resources/images/code.svg")),
            self.tr("Insert code"),
        )
        code_action.triggered.connect(self.insert_code_for_selection)
        self._edit_actions.append(code_action)

    def get_edit_actions(self) -> list[QAction]:
        """Get list of actions available for edition, contains separators

        :return: list of action for edition
        :rtype: List[QAction]
        """
        return self._edit_actions

    def _insert_separator_in_edit_action(self) -> QAction:
        """Insert a QAction as separator in internal edit actions

        :return: _description_
        :rtype: QAction
        """
        sep = QAction(self)
        sep.setSeparator(True)
        self._edit_actions.append(sep)
        return sep

    def contextMenuEvent(self, event: QContextMenuEvent) -> None:  # noqa: N802
        """Display context menu for right click

        :param event: _description_
        :type event: QContextMenuEvent
        """
        # Get standard menu for QTextEdit
        menu = self.createStandardContextMenu()

        # Add custom edit action
        menu.addSeparator()
        for action in self._edit_actions:
            menu.addAction(action)

        menu.exec(event.globalPos())

    def hasHeightForWidth(self) -> bool:  # noqa: N802
        """Returns true if the widget's preferred height depends on its width;
            otherwise returns false.

        :return: hasHeightForWidth
        :rtype: bool
        """
        return True

    def heightForWidth(self, width: int) -> int:  # noqa: N802
        """Returns the preferred height for this widget, given the width w.

        If this widget has a layout, the default implementation returns the layout's
        preferred height. if there is no layout, the default implementation returns -1
        indicating that the preferred height does not depend on the width.

        :param width: input width
        :type width: int

        :return: output height
        :rtype: int
        """
        margins = self.contentsMargins()

        if width >= margins.left() + margins.right():
            document_width = width - margins.left() - margins.right()
        else:
            # If specified width can't even fit the margin, there's no space left for the document
            document_width = 0

        # Cloning the whole document only to check its size at different width seems wasteful
        # but apparently it's the only and preferred way to do this in Qt >= 4. QTextDocument does not
        # provide any means to get height for specified width (as some QWidget subclasses do).
        # Neither does QTextEdit. In Qt3 Q3TextEdit had working implementation of heightForWidth()
        # but it was allegedly just a hack and was removed.
        #
        # The performance probably won't be a problem here because the application is meant to
        # work with a lot of small notes rather than few big ones. And there's usually only one
        # editor that needs to be dynamically resized - the one having focus.
        document = self.document().clone()
        document.setTextWidth(document_width)

        val = int(margins.top() + document.size().height() + margins.bottom())

        return val

    def minimumSizeHint(self) -> QSize:  # noqa: N802
        """Override minimumSizeHint to use minimum height from sizeHint.

        :return: minimumSizeHint update with minimum height from sizeHint
        :rtype: QSize
        """
        original_hint = super().minimumSizeHint()
        size_hint = self.sizeHint()
        if original_hint.height() > size_hint.height():
            original_hint.setHeight(size_hint.height())
        return original_hint

    def sizeHint(self) -> QSize:  # noqa: N802
        """Recommended size for the widget.

        :return: Recommended size
        :rtype: QSize
        """
        original_hint = super().sizeHint()
        return QSize(original_hint.width(), self.heightForWidth(original_hint.width()))

    def insert_after_and_before_selection(
        self, before_txt: str, after_text: str
    ) -> None:
        """Insert text after and before current selection

        :param before_txt: text to insert before
        :type before_txt: str
        :param after_text: text to insert after
        :type after_text: str
        """
        cursor = self.textCursor()
        cursor.insertText(f"{before_txt}{cursor.selectedText()}{after_text}")

    def insert_code_for_selection(self) -> None:
        """Insert code separator for selection
        ` if a single line is selected
        ```
        ```
        if multiple line are selected
        """
        cursor = self.textCursor()
        selection = cursor.selection().toPlainText()
        if "\n" in selection:
            cursor.insertText(BLOCK_CODE_SEPARATOR)
            cursor.insertText("\n")
            cursor.insertText(selection)
            cursor.insertText("\n")
            cursor.insertText(BLOCK_CODE_SEPARATOR)
        else:
            cursor.insertText(
                f"{SINGLE_LINE_CODE_SEPARATOR}{selection}{SINGLE_LINE_CODE_SEPARATOR}"
            )
