# -----------------------------------------------------------------------------
# Copyright (c) 2020- Spyder Project Contributors
#
# Released under the terms of the MIT License
# (see LICENSE.txt in the project root directory for details)
# -----------------------------------------------------------------------------

"""Status bar widgets API."""

from __future__ import annotations

# Standard library import
from typing import TYPE_CHECKING

# Third party imports
import qstylizer.parser
from qtpy import PYSIDE2
from qtpy.QtCore import Qt, QSize, QTimer, Signal
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget

# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.utils.palette import SpyderPalette
from spyder.utils.qthelpers import create_waitspinner
from spyder.utils.stylesheet import MAC

if TYPE_CHECKING:
    import qstylizer.style.StyleSheet
    from qtpy.QtGui import QCloseEvent, QEnterEvent, QEvent, QMouseEvent


class StatusBarWidget(QWidget, SpyderWidgetMixin):
    """
    Base class for status bar widgets.

    These widgets consist by default of an icon, a label and a spinner,
    which are organized from left to right in that order.

    You can also add any other :class:`QWidget` to this layout by setting the
    :attr:`CUSTOM_WIDGET_CLASS` class attribute. It'll be put between the label
    and the spinner.
    """

    ID: str | None = None
    """
    Unique string identifier name for this widget.
    """

    CUSTOM_WIDGET_CLASS: type[QWidget] | None = None
    """
    A custom widget class object to add to the default layout, or
    ``None`` to not add one.
    """

    INTERACT_ON_CLICK: bool = False
    """
    If ``True``, the user can interact with widget when clicking it
    (for example, to show a menu). If ``False``, the default, the widget
    is not interactive.
    """

    sig_clicked: Signal = Signal()
    """
    Signal emitted when the widget is clicked.
    """

    def __init__(
        self,
        parent: QWidget | None = None,
        show_icon: bool = True,
        show_label: bool = True,
        show_spinner: bool = False,
    ) -> None:
        """
        Base class for status bar widgets.

        These are composed of the following widgets, which are arranged
        in a :class:`QHBoxLayout` from left to right:

        * Icon
        * Label
        * Custom :class:`QWidget`
        * Spinner

        Parameters
        ----------
        parent: QWidget | None, optional
            The parent widget of this one, or ``None`` (default).
        show_icon: bool, optional
            If ``True`` (default), show an icon in the widget. If ``False``,
            don't show an icon.
        show_label: bool, optional
            If ``True`` (default), show a label in the widget. If ``False``,
            don't show a label.
        show_spinner: bool, optional
            If ``True``, show a progress spinner in the widget. If ``False``
            (default), don't show a spinner.

        Notes
        -----
        * To use an icon, you need to redefine the :meth:`get_icon` method.
        * To use a label, you need to call :meth:`set_value`.
        """
        if not PYSIDE2:
            super().__init__(parent, class_parent=parent)
        else:
            QWidget.__init__(self, parent)
            SpyderWidgetMixin.__init__(self, class_parent=parent)

        self._parent = parent

        self.show_icon = show_icon
        self.show_label = show_label
        self.show_spinner = show_spinner

        self.value = None
        self.label_icon = None
        self.label_value = None
        self.spinner = None
        self.custom_widget = None

        # In case the widget has an associated menu
        self.menu = None

        self._set_layout()
        self._css = self._generate_stylesheet()
        self.setStyleSheet(self._css.toString())

    # ---- Private API
    # -------------------------------------------------------------------------
    def _set_layout(self) -> None:
        """Set layout for default widgets."""
        # Icon
        if self.show_icon:
            self._icon = self.get_icon()
            self._pixmap = None
            self._icon_size = QSize(16, 16)  # Should this be adjustable?
            self.label_icon = QLabel(self)
            self.set_icon()

        # Label
        if self.show_label:
            self.label_value = QLabel(self)
            self.set_value("")
            self.label_value.setAlignment(Qt.AlignRight | Qt.AlignVCenter)

        # Custom widget
        if self.CUSTOM_WIDGET_CLASS:
            if not issubclass(self.CUSTOM_WIDGET_CLASS, QWidget):
                raise SpyderAPIError(
                    "Any custom status widget must subclass QWidget!"
                )
            self.custom_widget = self.CUSTOM_WIDGET_CLASS(self._parent)

        # Spinner
        if self.show_spinner:
            self.spinner = create_waitspinner(size=14, parent=self)
            self.spinner.hide()

        # Layout setup
        layout = QHBoxLayout(self)
        layout.setSpacing(0)  # Reduce space between icon and label
        if self.show_icon:
            layout.addWidget(self.label_icon)
        if self.show_label:
            layout.addWidget(self.label_value)
        if self.custom_widget:
            layout.addWidget(self.custom_widget)
        if self.show_spinner:
            layout.addWidget(self.spinner)

        layout.addSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setAlignment(Qt.AlignVCenter)

        # Setup
        self.update_tooltip()

    def _generate_stylesheet(self) -> qstylizer.style.StyleSheet:
        """Generate the widget's stylesheet."""
        # Remove opacity that comes from QDarkstyle.
        # This work around is necessary because qstylizer doesn't have support
        # for the opacity property.
        initial_css = "QToolTip {opacity: 255;}"
        css = qstylizer.parser.parse(initial_css)

        # Make style match the one set for other tooltips in the app
        css.QToolTip.setValues(
            color=SpyderPalette.COLOR_TEXT_1,
            backgroundColor=SpyderPalette.COLOR_ACCENT_2,
            border="none",
            padding="1px 2px",
        )

        return css

    # ---- Public API
    # -------------------------------------------------------------------------
    def get_icon(self) -> QIcon | None:
        """
        Get the widget's icon.

        Returns
        -------
        QIcon | None
            The widget's icon object, or ``None`` if it doesn't have one.
        """
        return None

    def set_icon(self) -> None:
        """
        Set the icon for the status bar widget.

        Returns
        -------
        None
        """
        if self.label_icon:
            icon = self._icon
            self.label_icon.setVisible(icon is not None)
            if icon is not None and isinstance(icon, QIcon):
                self._pixmap = icon.pixmap(self._icon_size)
                self.label_icon.setPixmap(self._pixmap)

    def set_value(self, value: str) -> None:
        """
        Set formatted text value for the widget.

        Parameters
        ----------
        value : str
            The formatted text value to set.

        Returns
        -------
        None
        """
        if self.label_value:
            self.value = value
            self.label_value.setText(value)

    def get_tooltip(self) -> str:
        """
        Get the widget's tooltip text.

        Returns
        -------
        str
            The widget's tooltip text.
        """
        return ""

    def update_tooltip(self) -> str:
        """
        Update the tooltip for the widget.

        Returns
        -------
        str
            The updated tooltip text.
        """
        tooltip = self.get_tooltip()
        if tooltip:
            if self.label_value:
                self.label_value.setToolTip(tooltip)
            if self.label_icon:
                self.label_icon.setToolTip(tooltip)
            self.setToolTip(tooltip)

    # ---- Qt methods
    # -------------------------------------------------------------------------
    def mousePressEvent(self, event: QMouseEvent) -> None:
        """
        Change the widget's background color when it's clicked.

        Parameters
        ----------
        event : QMouseEvent
            The mouse event that was triggered.

        Returns
        -------
        None
        """
        if self.INTERACT_ON_CLICK:
            self._css.QWidget.setValues(
                backgroundColor=SpyderPalette.COLOR_BACKGROUND_6
            )
            self.setStyleSheet(self._css.toString())

        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        """
        Change the widget's background color & emit a signal when it's clicked.

        Parameters
        ----------
        event : QMouseEvent
            The mouse release event.

        Returns
        -------
        None
        """
        super().mouseReleaseEvent(event)

        if self.INTERACT_ON_CLICK:
            self._css.QWidget.setValues(
                # Mac doesn't correctly restore the background color after
                # clicking on a widget that shows a menu
                backgroundColor=(
                    SpyderPalette.COLOR_BACKGROUND_4
                    if MAC and self.menu
                    else SpyderPalette.COLOR_BACKGROUND_5
                )
            )
            self.setStyleSheet(self._css.toString())

        self.sig_clicked.emit()

    def enterEvent(self, event: QEnterEvent) -> None:
        """
        Change the widget's background color and cursor shape on hover.

        Parameters
        ----------
        event : QEnterEvent
            The mouse hover event.

        Returns
        -------
        None
        """
        if self.INTERACT_ON_CLICK:
            self._css.QWidget.setValues(
                backgroundColor=SpyderPalette.COLOR_BACKGROUND_5
            )
            self.setStyleSheet(self._css.toString())

            self.setCursor(Qt.PointingHandCursor)

        self.update_tooltip()
        super().enterEvent(event)

    def leaveEvent(self, event: QEvent) -> None:
        """
        Restore the widget's background color when not hovering.

        Parameters
        ----------
        event : QEvent
            The mouse leave event.

        Returns
        -------
        None
        """
        if self.INTERACT_ON_CLICK:
            self._css.QWidget.setValues(
                backgroundColor=SpyderPalette.COLOR_BACKGROUND_4
            )
            self.setStyleSheet(self._css.toString())

            self.setCursor(Qt.ArrowCursor)

        super().leaveEvent(event)


class BaseTimerStatus(StatusBarWidget):
    """
    Base class for status bar widgets that update based on timers.
    """

    def __init__(self, parent: QWidget | None = None) -> None:
        """
        Base class for status bar widgets that update based on timers.

        Parameters
        ----------
        parent : QWidget | None, optional
            The parent widget of this one, or ``None`` (default).

        Returns
        -------
        None
        """
        self.timer = None  # Needs to come before parent call
        super().__init__(parent)
        self._interval = 2000

        # Widget setup
        fm = self.label_value.fontMetrics()
        self.label_value.setMinimumWidth(fm.width("000%"))

        # Setup
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_status)
        self.timer.start(self._interval)

    # ---- Qt methods
    # -------------------------------------------------------------------------
    def closeEvent(self, event: QCloseEvent) -> None:
        """
        Handle the widget close event by stopping the timer.

        Parameters
        ----------
        event : QCloseEvent
            The widget close event.

        Returns
        -------
        None
        """
        self.timer.stop()
        super().closeEvent(event)

    def setVisible(self, value: bool) -> None:
        """
        Stop the timer if the widget is not visible.

        Parameters
        ----------
        value : bool
            ``True`` if the timer should be started, ``False`` to stop it.

        Returns
        -------
        None
        """
        if self.timer is not None:
            if value:
                self.timer.start(self._interval)
            else:
                self.timer.stop()
        super().setVisible(value)

    # ---- Public API
    # -------------------------------------------------------------------------
    def update_status(self) -> None:
        """
        Update the status label widget, if the widget is visible.

        Returns
        -------
        None
        """
        if self.isVisible():
            self.label_value.setText(self.get_value())

    def set_interval(self, interval: int) -> None:
        """
        Set the timer interval.

        Parameters
        ----------
        interval : int
            The timer interval, in integer milliseconds.

        Returns
        -------
        None
        """
        self._interval = interval
        if self.timer is not None:
            self.timer.setInterval(interval)

    def get_value(self) -> str:
        """
        Return the formatted text value shown in the widget.

        Returns
        -------
        str
            The formatted text value.

        Raises
        ------
        NotImplementedError
            Must be implemented by subclasses to work.
        """
        raise NotImplementedError
