# coding:utf-8
from typing import Dict, Union

from PySide6.QtCore import Qt, QRect, QPropertyAnimation, QEasingCurve, Property, QRectF, QPoint
from PySide6.QtGui import QFont, QPainter, QColor, QIcon
from PySide6.QtWidgets import QWidget, QVBoxLayout

from ...common.config import isDarkTheme
from ...common.font import setFont
from ...common.style_sheet import themeColor
from ...common.color import autoFallbackThemeColor
from ...common.icon import drawIcon, FluentIconBase, toQIcon
from ...common.icon import FluentIcon as FIF
from ...common.router import qrouter
from ...common.style_sheet import FluentStyleSheet
from ..widgets.scroll_area import ScrollArea
from .navigation_widget import NavigationPushButton, NavigationWidget, NavigationIndicator
from .navigation_panel import RouteKeyError, NavigationItemPosition


class IconSlideAnimation(QPropertyAnimation):
    """ Icon sliding animation """

    def __init__(self, parent=None):
        super().__init__(parent)
        self._offset = 0
        self.maxOffset = 6
        self.setTargetObject(self)
        self.setPropertyName(b"offset")

    def getOffset(self):
        return self._offset

    def setOffset(self, value: float):
        self._offset = value
        self.parent().update()

    def slideDown(self):
        """ slide down """
        self.setEndValue(self.maxOffset)
        self.setDuration(100)
        self.start()

    def slideUp(self):
        """ slide up """
        self.setEndValue(0)
        self.setDuration(100)
        self.start()

    offset = Property(float, getOffset, setOffset)



class NavigationBarPushButton(NavigationPushButton):
    """ Navigation bar push button """

    def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, selectedIcon=None, parent=None):
        super().__init__(icon, text, isSelectable, parent)
        self.iconAni = IconSlideAnimation(self)
        self._selectedIcon = selectedIcon
        self._isSelectedTextVisible = True
        self.lightSelectedColor = QColor()
        self.darkSelectedColor = QColor()

        self.setFixedSize(64, 58)
        setFont(self, 11)

    def setSelectedColor(self, light, dark):
        self.lightSelectedColor = QColor(light)
        self.darkSelectedColor = QColor(dark)
        self.update()

    def selectedIcon(self):
        if self._selectedIcon:
            return toQIcon(self._selectedIcon)

        return QIcon()

    def setSelectedIcon(self, icon: Union[str, QIcon, FIF]):
        self._selectedIcon = icon
        self.update()

    def setSelectedTextVisible(self, isVisible):
        self._isSelectedTextVisible = isVisible
        self.update()

    def indicatorRect(self):
        """ get the indicator geometry """
        return QRectF(0, 16, 4, 24)

    def paintEvent(self, e):
        painter = QPainter(self)
        painter.setRenderHints(QPainter.Antialiasing |
                               QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
        painter.setPen(Qt.NoPen)

        self._drawBackground(painter)
        self._drawIcon(painter)
        self._drawText(painter)

    def _drawBackground(self, painter: QPainter):
        if self.isSelected or self.isAboutSelected:
            painter.setBrush(QColor(255, 255, 255, 42) if isDarkTheme() else Qt.white)
            painter.drawRoundedRect(self.rect(), 5, 5)

            # draw indicator
            if not self.isAboutSelected:
                painter.setBrush(autoFallbackThemeColor(self.lightSelectedColor, self.darkSelectedColor))
                if not self.isPressed:
                    painter.drawRoundedRect(0, 16, 4, 24, 2, 2)
                else:
                    painter.drawRoundedRect(0, 19, 4, 18, 2, 2)
        elif self.isPressed or self.isEnter:
            c = 255 if isDarkTheme() else 0
            alpha = 9 if self.isEnter else 6
            painter.setBrush(QColor(c, c, c, alpha))
            painter.drawRoundedRect(self.rect(), 5, 5)

    def _drawIcon(self, painter: QPainter):
        if (self.isPressed or not self.isEnter) and not (self.isSelected or self.isAboutSelected):
            painter.setOpacity(0.6)
        if not self.isEnabled():
            painter.setOpacity(0.4)

        if self._isSelectedTextVisible:
            rect = QRectF(22, 13, 20, 20)
        else:
            rect = QRectF(22, 13 + self.iconAni.offset, 20, 20)

        selectedIcon = self._selectedIcon or self._icon

        if isinstance(selectedIcon, FluentIconBase) and (self.isSelected or self.isAboutSelected):
            color = autoFallbackThemeColor(self.lightSelectedColor, self.darkSelectedColor)
            selectedIcon.render(painter, rect, fill=color.name())
        elif self.isSelected or self.isAboutSelected:
            drawIcon(selectedIcon, painter, rect)
        else:
            drawIcon(self._icon, painter, rect)

    def _drawText(self, painter: QPainter):
        if self.isSelected and not self._isSelectedTextVisible:
            return

        if self.isSelected or self.isAboutSelected:
            painter.setPen(autoFallbackThemeColor(self.lightSelectedColor, self.darkSelectedColor))
        else:
            painter.setPen(Qt.white if isDarkTheme() else Qt.black)

        painter.setFont(self.font())
        rect = QRect(0, 32, self.width(), 26)
        painter.drawText(rect, Qt.AlignCenter, self.text())

    def setSelected(self, isSelected: bool):
        if isSelected == self.isSelected:
            return

        self.isSelected = isSelected
        self.isAboutSelected = False

        if isSelected:
            self.iconAni.slideDown()
        else:
            self.iconAni.slideUp()


class NavigationBar(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.indicator = NavigationIndicator(self)
        self._isIndicatorAnimationEnabled = True
        self._isSelectedTextVisible = True

        self.lightSelectedColor = QColor()
        self.darkSelectedColor = QColor()

        self.scrollArea = ScrollArea(self)
        self.scrollWidget = QWidget()

        self.vBoxLayout = QVBoxLayout(self)
        self.topLayout = QVBoxLayout()
        self.bottomLayout = QVBoxLayout()
        self.scrollLayout = QVBoxLayout(self.scrollWidget)

        self.items = {}   # type: Dict[str, NavigationWidget]
        self.history = qrouter
        self._currentRouteKey = None

        self.__initWidget()

    def __initWidget(self):
        self.resize(48, self.height())
        self.setAttribute(Qt.WA_StyledBackground)
        self.window().installEventFilter(self)

        self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scrollArea.horizontalScrollBar().setEnabled(False)
        self.scrollArea.setWidget(self.scrollWidget)
        self.scrollArea.setWidgetResizable(True)

        self.scrollWidget.setObjectName('scrollWidget')
        FluentStyleSheet.NAVIGATION_INTERFACE.apply(self)
        FluentStyleSheet.NAVIGATION_INTERFACE.apply(self.scrollWidget)
        self.__initLayout()

        self.indicator.aniFinished.connect(self._onIndicatorAniFinished)

    def __initLayout(self):
        self.vBoxLayout.setContentsMargins(0, 5, 0, 5)
        self.topLayout.setContentsMargins(4, 0, 4, 0)
        self.bottomLayout.setContentsMargins(4, 0, 4, 0)
        self.scrollLayout.setContentsMargins(4, 0, 4, 0)
        self.vBoxLayout.setSpacing(4)
        self.topLayout.setSpacing(4)
        self.bottomLayout.setSpacing(4)
        self.scrollLayout.setSpacing(4)

        self.vBoxLayout.addLayout(self.topLayout, 0)
        self.vBoxLayout.addWidget(self.scrollArea)
        self.vBoxLayout.addLayout(self.bottomLayout, 0)

        self.vBoxLayout.setAlignment(Qt.AlignTop)
        self.topLayout.setAlignment(Qt.AlignTop)
        self.scrollLayout.setAlignment(Qt.AlignTop)
        self.bottomLayout.setAlignment(Qt.AlignBottom)

    def widget(self, routeKey: str):
        if routeKey not in self.items:
            raise RouteKeyError(f"`{routeKey}` is illegal.")

        return self.items[routeKey]

    def addItem(self, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None,
                selectable=True, selectedIcon=None, position=NavigationItemPosition.TOP):
        """ add navigation item

        Parameters
        ----------
        routeKey: str
            the unique name of item

        icon: str | QIcon | FluentIconBase
            the icon of navigation item

        text: str
            the text of navigation item

        onClick: callable
            the slot connected to item clicked signal

        selectable: bool
            whether the item is selectable

        selectedIcon: str | QIcon | FluentIconBase
            the icon of navigation item in selected state

        position: NavigationItemPosition
            where the button is added
        """
        return self.insertItem(-1, routeKey, icon, text, onClick, selectable, selectedIcon, position)

    def addWidget(self, routeKey: str, widget: NavigationWidget, onClick=None, position=NavigationItemPosition.TOP):
        """ add custom widget

        Parameters
        ----------
        routeKey: str
            the unique name of item

        widget: NavigationWidget
            the custom widget to be added

        onClick: callable
            the slot connected to item clicked signal

        position: NavigationItemPosition
            where the button is added
        """
        self.insertWidget(-1, routeKey, widget, onClick, position)

    def insertItem(self, index: int, routeKey: str, icon: Union[str, QIcon, FluentIconBase], text: str, onClick=None,
                   selectable=True, selectedIcon=None, position=NavigationItemPosition.TOP):
        """ insert navigation tree item

        Parameters
        ----------
        index: int
            the insert position of parent widget

        routeKey: str
            the unique name of item

        icon: str | QIcon | FluentIconBase
            the icon of navigation item

        text: str
            the text of navigation item

        onClick: callable
            the slot connected to item clicked signal

        selectable: bool
            whether the item is selectable

        selectedIcon: str | QIcon | FluentIconBase
            the icon of navigation item in selected state

        position: NavigationItemPosition
            where the button is added
        """
        if routeKey in self.items:
            return

        w = NavigationBarPushButton(icon, text, selectable, selectedIcon, self)
        w.setSelectedColor(self.lightSelectedColor, self.darkSelectedColor)
        w.setSelectedTextVisible(self.isSelectedTextVisible())
        self.insertWidget(index, routeKey, w, onClick, position)
        return w

    def insertWidget(self, index: int, routeKey: str, widget: NavigationWidget, onClick=None,
                     position=NavigationItemPosition.TOP):
        """ insert custom widget

        Parameters
        ----------
        index: int
            insert position

        routeKey: str
            the unique name of item

        widget: NavigationWidget
            the custom widget to be added

        onClick: callable
            the slot connected to item clicked signal

        position: NavigationItemPosition
            where the button is added
        """
        if routeKey in self.items:
            return

        self._registerWidget(routeKey, widget, onClick)
        self._insertWidgetToLayout(index, widget, position)

    def _registerWidget(self, routeKey: str, widget: NavigationWidget, onClick):
        """ register widget """
        widget.clicked.connect(self._onWidgetClicked)

        if onClick is not None:
            widget.clicked.connect(onClick)

        widget.setProperty('routeKey', routeKey)
        self.items[routeKey] = widget

    def _insertWidgetToLayout(self, index: int, widget: NavigationWidget, position: NavigationItemPosition):
        """ insert widget to layout """
        if position == NavigationItemPosition.TOP:
            widget.setParent(self)
            self.topLayout.insertWidget(
                index, widget, 0, Qt.AlignTop | Qt.AlignHCenter)
        elif position == NavigationItemPosition.SCROLL:
            widget.setParent(self.scrollWidget)
            self.scrollLayout.insertWidget(
                index, widget, 0, Qt.AlignTop | Qt.AlignHCenter)
        else:
            widget.setParent(self)
            self.bottomLayout.insertWidget(
                index, widget, 0, Qt.AlignBottom | Qt.AlignHCenter)

        widget.show()

    def removeWidget(self, routeKey: str):
        """ remove widget

        Parameters
        ----------
        routeKey: str
            the unique name of item
        """
        if routeKey not in self.items:
            return

        widget = self.items.pop(routeKey)
        widget.deleteLater()
        self.history.remove(routeKey)

    def currentItem(self):
        return self.widget(self._currentRouteKey) if self._currentRouteKey else None

    def setCurrentItem(self, routeKey: str):
        """ set current selected item

        Parameters
        ----------
        routeKey: str
            the unique name of item
        """
        if routeKey not in self.items or routeKey == self._currentRouteKey:
            return

        self._stopIndicatorAnimation()

        prevItem = self.currentItem()
        self._currentRouteKey = routeKey

        # early return if indicator is not enabled or previous selected item is None
        if not self.isIndicatorAnimationEnabled() or prevItem is None:
            for k, widget in self.items.items():
                widget.setSelected(k == routeKey)

            return

        # calculate the start and final geometry for animation
        newItem = self.currentItem()
        preIndicatorRect = self._getIndicatorRect(prevItem)
        newIndicatorRect = self._getIndicatorRect(newItem)

        # start animation
        prevItem.setSelected(False)
        newItem.setAboutSelected(True)
        self.indicator.raise_()
        self.indicator.setIndicatorColor(newItem.lightIndicatorColor, newItem.darkIndicatorColor)
        self.indicator.startAnimation(preIndicatorRect, newIndicatorRect)

    def setFont(self, font: QFont):
        """ set the font of navigation item """
        super().setFont(font)

        for widget in self.buttons():
            widget.setFont(font)

    def setSelectedTextVisible(self, isVisible: bool):
        """ set whether the text is visible when button is selected """
        if isVisible == self._isSelectedTextVisible:
            return

        self._isSelectedTextVisible = isVisible
        for widget in self.buttons():
            widget.setSelectedTextVisible(isVisible)

    def isSelectedTextVisible(self):
        return self._isSelectedTextVisible

    def setSelectedColor(self, light, dark):
        """ set the selected color of all items """
        self.lightSelectedColor = QColor(light)
        self.darkSelectedColor = QColor(dark)
        for button in self.buttons():
            button.setSelectedColor(self.lightSelectedColor, self.darkSelectedColor)

    def buttons(self):
        return [i for i in self.items.values() if isinstance(i, NavigationPushButton)]

    def isIndicatorAnimationEnabled(self):
        return self._isIndicatorAnimationEnabled

    def setIndicatorAnimationEnabled(self, isEnabled: bool):
        self._isIndicatorAnimationEnabled = isEnabled

    def _onWidgetClicked(self):
        widget = self.sender()  # type: NavigationWidget
        if widget.isSelectable:
            self.setCurrentItem(widget.property('routeKey'))

    def _getIndicatorRect(self, item: NavigationWidget):
        if not item:
            return QRect()

        pos = item.mapTo(self, QPoint(0, 0))
        rect = item.indicatorRect()
        return rect.translated(pos)

    def _stopIndicatorAnimation(self):
        if not self.isIndicatorAnimationEnabled():
            return

        self.indicator.stopAnimation()
        self._onIndicatorAniFinished()

    def _onIndicatorAniFinished(self):
        item = self.currentItem()
        if not item:
            return

        item.setAboutSelected(False)
        item.setSelected(True)
        self.indicator.hide()
