# coding: utf-8
from math import ceil
from collections import defaultdict, Counter
from typing import Tuple, Type

from PyQt5.QtCore import (Qt, QRectF, pyqtSignal, QSize, QModelIndex, QDate, QCalendar, QEasingCurve, QPropertyAnimation,
                          QParallelAnimationGroup, QPoint, QRect, QStringListModel)
from PyQt5.QtGui import QPainter, QColor, QCursor
from PyQt5.QtWidgets import (QApplication, QFrame, QPushButton, QHBoxLayout, QVBoxLayout, QListWidget,
                             QListWidgetItem, QStyledItemDelegate, QStyle, QStyleOptionViewItem,
                             QLabel, QWidget, QStackedWidget, QGraphicsDropShadowEffect, QListView)

from ...common.icon import FluentIcon as FIF
from ...common.style_sheet import isDarkTheme, FluentStyleSheet, themeColor, ThemeColor
from ...common.font import getFont
from ...common.screen import getCurrentScreenGeometry
from ..widgets.button import TransparentToolButton
from ..widgets.scroll_bar import SmoothScrollBar


class ScrollButton(TransparentToolButton):
    """ Scroll button """

    def _drawIcon(self, icon, painter: QPainter, rect: QRectF):
        pass

    def paintEvent(self, e):
        super().paintEvent(e)
        painter = QPainter(self)
        painter.setRenderHints(QPainter.Antialiasing)

        if not self.isPressed:
            w, h = 10, 10
        else:
            w, h = 9, 9

        x = (self.width() - w) / 2
        y = (self.height() - h) / 2

        if not isDarkTheme():
            self._icon.render(painter, QRectF(x, y, w, h), fill="#5e5e5e")
        else:
            self._icon.render(painter, QRectF(x, y, w, h), fill="#9c9c9c")


class ScrollItemDelegate(QStyledItemDelegate):

    def __init__(self, min, max):
        super().__init__()
        self.setRange(min, max)
        self.font = getFont()
        self.pressedIndex = QModelIndex()
        self.currentIndex = QModelIndex()
        self.selectedIndex = QModelIndex()

    def setRange(self, min, max):
        self.min = min
        self.max = max

    def setPressedIndex(self, index: QModelIndex):
        self.pressedIndex = index

    def setCurrentIndex(self, index: QModelIndex):
        self.currentIndex = index

    def setSelectedIndex(self, index: QModelIndex):
        self.selectedIndex = index

    def paint(self, painter, option, index):
        painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
        self._drawBackground(painter, option, index)
        self._drawText(painter, option, index)

    def _drawBackground(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
        painter.save()

        # outer ring
        if index != self.selectedIndex:
            painter.setPen(Qt.NoPen)
        else:
            painter.setPen(themeColor())

        if index == self.currentIndex:
            if index == self.pressedIndex:
                painter.setBrush(ThemeColor.LIGHT_2.color())
            elif option.state & QStyle.State_MouseOver:
                painter.setBrush(ThemeColor.LIGHT_1.color())
            else:
                painter.setBrush(themeColor())
        else:
            c = 255 if isDarkTheme() else 0
            if index == self.pressedIndex:
                painter.setBrush(QColor(c, c, c, 7))
            elif option.state & QStyle.State_MouseOver:
                painter.setBrush(QColor(c, c, c, 9))
            else:
                painter.setBrush(Qt.transparent)

        m = self._itemMargin()
        painter.drawEllipse(option.rect.adjusted(m, m, -m, -m))
        painter.restore()

    def _drawText(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
        painter.save()
        painter.setFont(self.font)

        if index == self.currentIndex:
            c = 0 if isDarkTheme() else 255
            painter.setPen(QColor(c, c, c))
        else:
            painter.setPen(Qt.white if isDarkTheme() else Qt.black)
            if not (self.min <= index.data(Qt.UserRole) <= self.max or option.state & QStyle.State_MouseOver) or \
                    index == self.pressedIndex:
                painter.setOpacity(0.6)

        text = index.data(Qt.DisplayRole)
        painter.drawText(option.rect, Qt.AlignCenter, text)
        painter.restore()

    def _itemMargin(self):
        return 0


class YearScrollItemDelegate(ScrollItemDelegate):
    """ Year scroll item delegate """

    def _itemMargin(self):
        return 8


class DayScrollItemDelegate(ScrollItemDelegate):
    """ Day scroll item delegate """

    def _itemMargin(self):
        return 3


class ScrollViewBase(QListWidget):
    """ Scroll view base class """

    pageChanged = pyqtSignal(int)

    def __init__(self, Delegate: Type[ScrollItemDelegate], parent=None):
        super().__init__(parent)
        self.cols = 4
        self.pageRows = 3
        self.currentPage = 0
        self.vScrollBar = SmoothScrollBar(Qt.Vertical, self)

        self.delegate = Delegate(0, 0)
        self.currentDate = QDate.currentDate()
        self.date = QDate.currentDate()

        self.minYear = self.currentDate.year() - 100
        self.maxYear = self.currentDate.year() + 100

        self.setUniformItemSizes(True)
        self._initItems()
        self.__initWidget()

    def __initWidget(self):
        self.setSpacing(0)
        self.setMovement(QListWidget.Static)
        self.setGridSize(self.gridSize())
        self.setViewportMargins(0, 0, 0, 0)
        self.setItemDelegate(self.delegate)
        self.setViewMode(QListWidget.IconMode)
        self.setResizeMode(QListWidget.Adjust)

        self.vScrollBar.ani.finished.connect(self._onFirstScrollFinished)
        self.vScrollBar.setScrollAnimation(1)
        self.setDate(self.date)

        self.vScrollBar.setForceHidden(True)
        self.setVerticalScrollMode(self.ScrollPerPixel)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def _onFirstScrollFinished(self):
        self.vScrollBar.setScrollAnimation(300, QEasingCurve.OutQuad)
        self.vScrollBar.ani.disconnect()

    def scrollUp(self):
        self.scrollToPage(self.currentPage - 1)

    def scrollDown(self):
        self.scrollToPage(self.currentPage + 1)

    def scrollToPage(self, page: int):
        if not 0 <= page <= ceil(self.model().rowCount() / (self.pageRows * self.cols)):
            return

        self.currentPage = page
        y = self.gridSize().height() * self.pageRows * page
        self.vScrollBar.setValue(y)
        self.delegate.setRange(*self.currentPageRange())
        self.pageChanged.emit(page)

    def currentPageRange(self):
        return 0, 0

    def setDate(self, date: QDate):
        self.scrollToDate(date)

    def scrollToDate(self, date: QDate):
        pass

    def _setPressedIndex(self, index):
        self.delegate.setPressedIndex(index)
        self.viewport().update()

    def _setSelectedIndex(self, index):
        self.delegate.setSelectedIndex(index)
        self.viewport().update()

    def wheelEvent(self, e):
        if self.vScrollBar.ani.state() == QPropertyAnimation.Running:
            return

        if e.angleDelta().y() < 0:
            self.scrollDown()
        else:
            self.scrollUp()

    def mousePressEvent(self, e):
        super().mousePressEvent(e)
        if e.button() == Qt.LeftButton and self.indexAt(e.pos()).row() >= 0:
            self._setPressedIndex(self.currentIndex())

    def mouseReleaseEvent(self, e):
        super().mouseReleaseEvent(e)
        self._setPressedIndex(QModelIndex())

    def gridSize(self) -> QSize:
        return QSize(76, 76)


class CalendarViewBase(QFrame):
    """ Calendar view base class """

    titleClicked = pyqtSignal()
    itemClicked = pyqtSignal(QDate)

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

        self.titleButton = QPushButton(self)
        self.upButton = ScrollButton(FIF.CARE_UP_SOLID, self)
        self.downButton = ScrollButton(FIF.CARE_DOWN_SOLID, self)

        self.scrollView = None  # type: ScrollViewBase

        self.hBoxLayout = QHBoxLayout()
        self.vBoxLayout = QVBoxLayout(self)

        self.__initWidget()

    def __initWidget(self):
        self.setFixedSize(314, 355)
        self.upButton.setFixedSize(32, 34)
        self.downButton.setFixedSize(32, 34)
        self.titleButton.setFixedHeight(34)

        self.hBoxLayout.setContentsMargins(9, 8, 9, 8)
        self.hBoxLayout.setSpacing(7)
        self.hBoxLayout.addWidget(self.titleButton, 1, Qt.AlignVCenter)
        self.hBoxLayout.addWidget(self.upButton, 0, Qt.AlignVCenter)
        self.hBoxLayout.addWidget(self.downButton, 0, Qt.AlignVCenter)

        self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
        self.vBoxLayout.setSpacing(0)
        self.vBoxLayout.addLayout(self.hBoxLayout)
        self.vBoxLayout.setAlignment(Qt.AlignTop)

        self.titleButton.setObjectName('titleButton')
        FluentStyleSheet.CALENDAR_PICKER.apply(self)

        self.titleButton.clicked.connect(self.titleClicked)
        self.upButton.clicked.connect(self._onScrollUp)
        self.downButton.clicked.connect(self._onScrollDown)

    def setScrollView(self, view: ScrollViewBase):
        self.scrollView = view
        self.scrollView.itemClicked.connect(lambda i: self.itemClicked.emit(i.data(Qt.UserRole)))
        self.vBoxLayout.addWidget(view)
        view.pageChanged.connect(self._updateTitle)
        self._updateTitle()

    def setDate(self, date: QDate):
        self.scrollView.setDate(date)
        self._updateTitle()

    def setTitle(self, title: str):
        self.titleButton.setText(title)

    def currentPageDate(self) -> QDate:
        raise NotImplementedError

    def _onScrollUp(self):
        self.scrollView.scrollUp()
        self._updateTitle()

    def _onScrollDown(self):
        self.scrollView.scrollDown()
        self._updateTitle()

    def _updateTitle(self):
        pass


class YearScrollView(ScrollViewBase):
    """ Year scroll view """

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

    def _initItems(self):
        years = range(self.minYear, self.maxYear+1)
        self.addItems([str(i) for i in years])

        for i, year in enumerate(years):
            item = self.item(i)
            item.setData(Qt.UserRole, QDate(year, 1, 1))
            item.setSizeHint(self.sizeHint())
            if year == self.currentDate.year():
                self.delegate.setCurrentIndex(self.indexFromItem(item))

    def scrollToDate(self, date: QDate):
        page = (date.year() - self.minYear) // 12
        self.scrollToPage(page)

    def currentPageRange(self):
        pageSize = self.pageRows * self.cols
        left = self.currentPage * pageSize + self.minYear

        years = defaultdict(int)
        for i in range(left, left + 16):
            y = i // 10 * 10
            years[y] += 1

        year = Counter(years).most_common()[0][0]
        return QDate(year, 1, 1), QDate(year + 10, 1, 1)


class YearCalendarView(CalendarViewBase):
    """ Year calendar view """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setScrollView(YearScrollView(self))
        self.titleButton.setEnabled(False)

    def _updateTitle(self):
        left, right = self.scrollView.currentPageRange()
        self.setTitle(f'{left.year()} - {right.year()}')


class MonthScrollView(ScrollViewBase):
    """ Month scroll view """

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

    def _initItems(self):
        self.months = [
            self.tr('Jan'), self.tr('Feb'), self.tr('Mar'), self.tr('Apr'),
            self.tr('May'), self.tr('Jun'), self.tr('Jul'), self.tr('Aug'),
            self.tr('Sep'), self.tr('Oct'), self.tr('Nov'), self.tr('Dec'),
        ]
        self.addItems(self.months * 201)

        # add month items
        for i in range(12 * 201):
            year = i // 12 + self.minYear
            m = i % 12 + 1
            item = self.item(i)
            item.setData(Qt.UserRole, QDate(year, m, 1))
            item.setSizeHint(self.gridSize())

            if year == self.currentDate.year() and m == self.currentDate.month():
                self.delegate.setCurrentIndex(self.indexFromItem(item))

    def scrollToDate(self, date: QDate):
        page = date.year() - self.minYear
        self.scrollToPage(page)

    def currentPageRange(self):
        year = self.minYear + self.currentPage
        return QDate(year, 1, 1), QDate(year, 12, 31)


class MonthCalendarView(CalendarViewBase):
    """ Month calendar view """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setScrollView(MonthScrollView(self))

    def _updateTitle(self):
        date, _ = self.scrollView.currentPageRange()
        self.setTitle(str(date.year()))

    def currentPageDate(self) -> QDate:
        date, _ = self.scrollView.currentPageRange()
        item = self.scrollView.currentItem()
        month = item.data(Qt.UserRole).month() if item else 1
        return QDate(date.year(), month, 1)


class DayScrollView(ScrollViewBase):
    """ Day scroll view """

    def __init__(self, parent=None):
        super().__init__(DayScrollItemDelegate, parent)
        self.cols = 7
        self.pageRows = 4
        self.vBoxLayout = QHBoxLayout(self)

        # add week day labels
        self.weekDays = [
            self.tr('Mo'), self.tr('Tu'), self.tr('We'),
            self.tr('Th'), self.tr('Fr'), self.tr('Sa'), self.tr('Su')
        ]
        self.weekDayGroup = QWidget(self)
        self.weekDayLayout = QHBoxLayout(self.weekDayGroup)
        self.weekDayGroup.setObjectName('weekDayGroup')
        for day in self.weekDays:
            label = QLabel(day)
            label.setObjectName('weekDayLabel')
            self.weekDayLayout.addWidget(label, 1, Qt.AlignHCenter)

        self.setViewportMargins(0, 38, 0, 0)
        self.vBoxLayout.setAlignment(Qt.AlignTop)
        self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
        self.weekDayLayout.setSpacing(0)
        self.weekDayLayout.setContentsMargins(3, 12, 3, 12)
        self.vBoxLayout.addWidget(self.weekDayGroup)

    def gridSize(self) -> QSize:
        return QSize(44, 44)

    def _initItems(self):
        startDate = QDate(self.minYear, 1, 1)
        endDate = QDate(self.maxYear, 12, 31)
        currentDate = startDate

        # add placeholder
        bias = currentDate.dayOfWeek() - 1
        for i in range(bias):
            item = QListWidgetItem(self)
            item.setFlags(Qt.NoItemFlags)
            self.addItem(item)

        # add day items
        items, dates = [], []
        while currentDate <= endDate:
            items.append(str(currentDate.day()))
            dates.append(QDate(currentDate))
            currentDate = currentDate.addDays(1)

        self.addItems(items)
        for i in range(bias, self.count()):
            item = self.item(i)
            item.setData(Qt.UserRole, dates[i-bias])
            item.setSizeHint(self.gridSize())

        self.delegate.setCurrentIndex(self.model().index(self._dateToRow(self.currentDate)))

    def setDate(self, date: QDate):
        self.scrollToDate(date)
        self.setCurrentIndex(self.model().index(self._dateToRow(date)))
        self.delegate.setSelectedIndex(self.currentIndex())

    def scrollToDate(self, date: QDate):
        page = (date.year() - self.minYear) * 12 + date.month() - 1
        self.scrollToPage(page)

    def scrollToPage(self, page: int):
        if not 0 <= page <= 201 * 12 - 1:
            return

        self.currentPage = page

        index = self._dateToRow(self._pageToDate())
        y = index // self.cols * self.gridSize().height()
        self.vScrollBar.scrollTo(y)

        self.delegate.setRange(*self.currentPageRange())
        self.pageChanged.emit(page)

    def currentPageRange(self):
        date = self._pageToDate()
        return date, date.addMonths(1).addDays(-1)

    def _pageToDate(self):
        year = self.currentPage // 12 + self.minYear
        month = self.currentPage % 12 + 1
        return QDate(year, month, 1)

    def _dateToRow(self, date: QDate):
        startDate = QDate(self.minYear, 1, 1)
        days = startDate.daysTo(date)
        return days + startDate.dayOfWeek() - 1

    def mouseReleaseEvent(self, e):
        super().mouseReleaseEvent(e)
        self._setSelectedIndex(self.currentIndex())


class DayCalendarView(CalendarViewBase):
    """ Day calendar view """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setScrollView(DayScrollView(self))

    def _updateTitle(self):
        date = self.currentPageDate()
        name = QCalendar().monthName(self.locale(), date.month(), date.year())
        self.setTitle(f'{name} {date.year()}')

    def currentPageDate(self) -> QDate:
        date, _ = self.scrollView.currentPageRange()
        return date

    def scrollToDate(self, date: QDate):
        self.scrollView.scrollToDate(date)
        self._updateTitle()


class CalendarView(QWidget):
    """ Calendar view """

    dateChanged = pyqtSignal(QDate)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.hBoxLayout = QHBoxLayout(self)
        self.date = QDate()

        self.stackedWidget = QStackedWidget(self)
        self.yearView = YearCalendarView(self)
        self.monthView = MonthCalendarView(self)
        self.dayView = DayCalendarView(self)

        self.opacityAni = QPropertyAnimation(self, b'windowOpacity', self)
        self.slideAni = QPropertyAnimation(self, b'geometry', self)
        self.aniGroup = QParallelAnimationGroup(self)

        self.__initWidget()

    def __initWidget(self):
        self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint |
                            Qt.NoDropShadowWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setAttribute(Qt.WA_DeleteOnClose, True)

        self.stackedWidget.addWidget(self.dayView)
        self.stackedWidget.addWidget(self.monthView)
        self.stackedWidget.addWidget(self.yearView)

        self.hBoxLayout.setContentsMargins(12, 8, 12, 20)
        self.hBoxLayout.addWidget(self.stackedWidget)
        self.setShadowEffect()

        self.dayView.setDate(QDate.currentDate())

        self.aniGroup.addAnimation(self.opacityAni)
        self.aniGroup.addAnimation(self.slideAni)

        self.dayView.titleClicked.connect(self._onDayViewTitleClicked)
        self.monthView.titleClicked.connect(self._onMonthTitleClicked)

        self.monthView.itemClicked.connect(self._onMonthItemClicked)
        self.yearView.itemClicked.connect(self._onYearItemClicked)
        self.dayView.itemClicked.connect(self._onDayItemClicked)

    def setShadowEffect(self, blurRadius=30, offset=(0, 8), color=QColor(0, 0, 0, 30)):
        """ add shadow to dialog """
        self.shadowEffect = QGraphicsDropShadowEffect(self.stackedWidget)
        self.shadowEffect.setBlurRadius(blurRadius)
        self.shadowEffect.setOffset(*offset)
        self.shadowEffect.setColor(color)
        self.stackedWidget.setGraphicsEffect(None)
        self.stackedWidget.setGraphicsEffect(self.shadowEffect)

    def _onDayViewTitleClicked(self):
        self.stackedWidget.setCurrentWidget(self.monthView)
        self.monthView.setDate(self.dayView.currentPageDate())

    def _onMonthTitleClicked(self):
        self.stackedWidget.setCurrentWidget(self.yearView)
        self.yearView.setDate(self.monthView.currentPageDate())

    def _onMonthItemClicked(self, date: QDate):
        self.stackedWidget.setCurrentWidget(self.dayView)
        self.dayView.scrollToDate(date)

    def _onYearItemClicked(self, date: QDate):
        self.stackedWidget.setCurrentWidget(self.monthView)
        self.monthView.setDate(date)

    def _onDayItemClicked(self, date: QDate):
        self.close()
        if date != self.date:
            self.date = date
            self.dateChanged.emit(date)

    def setDate(self, date: QDate):
        """ set the selected date """
        self.dayView.setDate(date)
        self.date = date

    def exec(self, pos: QPoint, ani=True):
        """ show calendar view """
        if self.isVisible():
            return

        rect = getCurrentScreenGeometry()
        w, h = self.sizeHint().width() + 5, self.sizeHint().height()
        pos.setX(max(rect.left(), min(pos.x(), rect.right() - w)))
        pos.setY(max(rect.top(), min(pos.y() - 4, rect.bottom() - h + 5)))
        self.move(pos)

        if not ani:
            return self.show()

        self.opacityAni.setStartValue(0)
        self.opacityAni.setEndValue(1)
        self.opacityAni.setDuration(150)
        self.opacityAni.setEasingCurve(QEasingCurve.OutQuad)

        self.slideAni.setStartValue(QRect(pos-QPoint(0, 8), self.sizeHint()))
        self.slideAni.setEndValue(QRect(pos, self.sizeHint()))
        self.slideAni.setDuration(150)
        self.slideAni.setEasingCurve(QEasingCurve.OutQuad)
        self.aniGroup.start()

        self.show()
