#!/usr/pkg/bin/python3.13


__license__   = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from collections import namedtuple

from qt.core import (
    QWidget, Qt, QLabel, QVBoxLayout, QDialogButtonBox, QApplication, QTimer, QPixmap, QEvent,
    QSize, pyqtSignal, QIcon, QPlainTextEdit, QCheckBox, QPainter, QHBoxLayout, QFontMetrics,
    QPainterPath, QRectF, pyqtProperty, QPropertyAnimation, QEasingCurve, QSizePolicy, QImage, QPalette)

from calibre.constants import __version__
from calibre.gui2.dialogs.message_box import ViewLog

Question = namedtuple('Question', 'payload callback cancel_callback '
        'title msg html_log log_viewer_title log_is_file det_msg '
        'show_copy_button checkbox_msg checkbox_checked action_callback '
        'action_label action_icon focus_action show_det show_ok icon '
        'log_viewer_unique_name')


class Icon(QWidget):

    @pyqtProperty(float)
    def fraction(self):
        return self._fraction

    @fraction.setter
    def fraction(self, val):
        self._fraction = max(0, min(2, float(val)))
        self.update()

    def showEvent(self, ev):
        self.animation.start()
        return QWidget.showEvent(self, ev)

    def hideEvent(self, ev):
        self.animation.stop()
        return QWidget.hideEvent(self, ev)

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
        self.set_icon('dialog_question.png')
        self.default_icon = self.icon
        self._fraction = 0.0
        self.animation = a = QPropertyAnimation(self, b"fraction", self)
        a.setDuration(2000), a.setEasingCurve(QEasingCurve.Type.Linear)
        a.setStartValue(0.0), a.setEndValue(2.0), a.setLoopCount(10)

    def set_icon(self, icon):
        if isinstance(icon, QIcon):
            self.icon = icon.pixmap(self.sizeHint())
        elif icon is None:
            self.icon = self.default_icon
        else:
            self.icon = QIcon(I(icon)).pixmap(self.sizeHint())
        self.update()

    def sizeHint(self):
        return QSize(64, 64)

    def paintEvent(self, ev):
        p = QPainter(self)
        p.setOpacity(min(1, abs(1 - self._fraction)))
        p.drawPixmap(self.rect(), self.icon)
        p.end()


class PlainTextEdit(QPlainTextEdit):

    def sizeHint(self):
        fm = QFontMetrics(self.font())
        ans = QPlainTextEdit.sizeHint(self)
        ans.setWidth(fm.averageCharWidth() * 50)
        return ans


class ProceedQuestion(QWidget):

    ask_question = pyqtSignal(object, object, object)

    @pyqtProperty(float)
    def show_fraction(self):
        return self._show_fraction

    @show_fraction.setter
    def show_fraction(self, val):
        self._show_fraction = max(0, min(1, float(val)))
        self.update()

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.setVisible(False)
        parent.installEventFilter(self)

        self._show_fraction = 0.0
        self.show_animation = a = QPropertyAnimation(self, b"show_fraction", self)
        a.setDuration(1000), a.setEasingCurve(QEasingCurve.Type.OutQuad)
        a.setStartValue(0.0), a.setEndValue(1.0)
        a.finished.connect(self.stop_show_animation)
        self.rendered_pixmap = None

        self.questions = []

        self.icon = ic = Icon(self)
        self.msg_label = msg = QLabel('some random filler text')
        msg.setWordWrap(True)
        self.bb = QDialogButtonBox()
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.log_button = self.bb.addButton(_('View log'), QDialogButtonBox.ButtonRole.ActionRole)
        self.log_button.setIcon(QIcon(I('debug.png')))
        self.log_button.clicked.connect(self.show_log)
        self.copy_button = self.bb.addButton(_('&Copy to clipboard'),
                QDialogButtonBox.ButtonRole.ActionRole)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.action_button = self.bb.addButton('', QDialogButtonBox.ButtonRole.ActionRole)
        self.action_button.clicked.connect(self.action_clicked)
        self.show_det_msg = _('Show &details')
        self.hide_det_msg = _('Hide &details')
        self.det_msg_toggle = self.bb.addButton(self.show_det_msg, QDialogButtonBox.ButtonRole.ActionRole)
        self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
        self.det_msg_toggle.setToolTip(
                _('Show detailed information about this error'))
        self.det_msg = PlainTextEdit(self)
        self.det_msg.setReadOnly(True)
        self.bb.setStandardButtons(
            QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No | QDialogButtonBox.StandardButton.Ok)
        self.bb.button(QDialogButtonBox.StandardButton.Yes).setDefault(True)
        self.title_label = title = QLabel('A dummy title')
        f = title.font()
        f.setBold(True)
        title.setFont(f)

        self.checkbox = QCheckBox('', self)

        self._l = l = QVBoxLayout(self)
        self._h = h = QHBoxLayout()
        self._v = v = QVBoxLayout()
        v.addWidget(title), v.addWidget(msg)
        h.addWidget(ic), h.addSpacing(10), h.addLayout(v), l.addLayout(h)
        l.addSpacing(5)
        l.addWidget(self.checkbox)
        l.addWidget(self.det_msg)
        l.addWidget(self.bb)

        self.ask_question.connect(self.do_ask_question,
                type=Qt.ConnectionType.QueuedConnection)
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        for child in self.findChildren(QWidget):
            child.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.setFocusProxy(self.parent())
        self.resize_timer = t = QTimer(self)
        t.setSingleShot(True), t.setInterval(100), t.timeout.connect(self.parent_resized)

    def eventFilter(self, obj, ev):
        if ev.type() == QEvent.Type.Resize and self.isVisible():
            self.resize_timer.start()
        return False

    def parent_resized(self):
        if self.isVisible():
            self.do_resize()

    def copy_to_clipboard(self, *args):
        QApplication.clipboard().setText(
                'calibre, version %s\n%s: %s\n\n%s' %
                (__version__, str(self.windowTitle()),
                    str(self.msg_label.text()),
                    str(self.det_msg.toPlainText())))
        self.copy_button.setText(_('Copied'))

    def action_clicked(self):
        if self.questions:
            q = self.questions[0]
            self.questions[0] = q._replace(callback=q.action_callback)
        self.accept()

    def accept(self):
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(callback, payload, cb)
        self.hide()

    def reject(self):
        if self.questions:
            payload, callback, cancel_callback = self.questions[0][:3]
            self.questions = self.questions[1:]
            cb = None
            if self.checkbox.isVisible():
                cb = bool(self.checkbox.isChecked())
            self.ask_question.emit(cancel_callback, payload, cb)
        self.hide()

    def do_ask_question(self, callback, payload, checkbox_checked):
        if callable(callback):
            args = [payload]
            if checkbox_checked is not None:
                args.append(checkbox_checked)
            callback(*args)
        self.show_question()

    def toggle_det_msg(self, *args):
        vis = str(self.det_msg_toggle.text()) == self.hide_det_msg
        self.det_msg_toggle.setText(self.show_det_msg if vis else
                self.hide_det_msg)
        self.det_msg.setVisible(not vis)
        self.do_resize()

    def do_resize(self):
        sz = self.sizeHint()
        sz.setWidth(min(self.parent().width(), sz.width()))
        sb = self.parent().statusBar().height() + 10
        sz.setHeight(min(self.parent().height() - sb, sz.height()))
        self.resize(sz)
        self.position_widget()

    def show_question(self):
        if not self.questions:
            return
        if not self.isVisible():
            question = self.questions[0]
            self.msg_label.setText(question.msg)
            self.icon.set_icon(question.icon)
            self.title_label.setText(question.title)
            self.log_button.setVisible(bool(question.html_log))
            self.copy_button.setText(_('&Copy to clipboard'))
            if question.action_callback is not None:
                self.action_button.setText(question.action_label or '')
                self.action_button.setIcon(
                    QIcon() if question.action_icon is None else question.action_icon)
            # Force the button box to relayout its buttons, as button text
            # might have changed
            self.bb.setOrientation(Qt.Orientation.Vertical), self.bb.setOrientation(Qt.Orientation.Horizontal)
            self.det_msg.setPlainText(question.det_msg or '')
            self.det_msg.setVisible(False)
            self.det_msg_toggle.setVisible(bool(question.det_msg))
            self.det_msg_toggle.setText(self.show_det_msg)
            self.checkbox.setVisible(question.checkbox_msg is not None)
            if question.checkbox_msg is not None:
                self.checkbox.setText(question.checkbox_msg)
                self.checkbox.setChecked(question.checkbox_checked)
            self.bb.button(QDialogButtonBox.StandardButton.Ok).setVisible(question.show_ok)
            self.bb.button(QDialogButtonBox.StandardButton.Yes).setVisible(not question.show_ok)
            self.bb.button(QDialogButtonBox.StandardButton.No).setVisible(not question.show_ok)
            self.copy_button.setVisible(bool(question.show_copy_button))
            self.action_button.setVisible(question.action_callback is not None)
            self.toggle_det_msg() if question.show_det else self.do_resize()
            self.show_widget()
            button = self.action_button if question.focus_action and question.action_callback is not None else \
                (self.bb.button(QDialogButtonBox.StandardButton.Ok) if question.show_ok else self.bb.button(QDialogButtonBox.StandardButton.Yes))
            button.setDefault(True)
            self.raise_()
            self.start_show_animation()

    def start_show_animation(self):
        if self.rendered_pixmap is not None:
            return

        dpr = getattr(self, 'devicePixelRatioF', self.devicePixelRatio)()
        p = QImage(dpr * self.size(), QImage.Format.Format_ARGB32_Premultiplied)
        p.setDevicePixelRatio(dpr)
        # For some reason, Qt scrolls the book view when rendering this widget,
        # for the very first time, so manually preserve its position
        pr = getattr(self.parent(), 'library_view', None)
        if not hasattr(pr, 'preserve_state'):
            self.render(p)
        else:
            with pr.preserve_state():
                self.render(p)
        self.rendered_pixmap = QPixmap.fromImage(p)
        self.original_visibility = v = []
        for child in self.findChildren(QWidget):
            if child.isVisible():
                child.setVisible(False)
                v.append(child)
        self.show_animation.start()

    def stop_show_animation(self):
        self.rendered_pixmap = None
        [c.setVisible(True) for c in getattr(self, 'original_visibility', ())]
        self.update()
        for child in self.findChildren(QWidget):
            child.update()
            if hasattr(child, 'viewport'):
                child.viewport().update()

    def position_widget(self):
        geom = self.parent().geometry()
        x = geom.width() - self.width() - 5
        sb = self.parent().statusBar()
        if sb is None:
            y = geom.height() - self.height()
        else:
            y = sb.geometry().top() - self.height()
        self.move(x, y)

    def show_widget(self):
        self.show()
        self.position_widget()

    def dummy_question(self, action_label=None):
        self(lambda *args:args, (), 'dummy log', 'Log Viewer', 'A Dummy Popup',
             'This is a dummy popup to easily test things, with a long line of text that should wrap. '
             'This is a dummy popup to easily test things, with a long line of text that should wrap',
             checkbox_msg='A dummy checkbox',
             action_callback=lambda *args: args, action_label=action_label or 'An action')

    def __call__(self, callback, payload, html_log, log_viewer_title, title,
            msg, det_msg='', show_copy_button=False, cancel_callback=None,
            log_is_file=False, checkbox_msg=None, checkbox_checked=False,
            action_callback=None, action_label=None, action_icon=None, focus_action=False,
            show_det=False, show_ok=False, icon=None, log_viewer_unique_name=None, **kw):
        '''
        A non modal popup that notifies the user that a background task has
        been completed. This class guarantees that only a single popup is
        visible at any one time. Other requests are queued and displayed after
        the user dismisses the current popup.

        :param callback: A callable that is called with payload if the user
        asks to proceed. Note that this is always called in the GUI thread.
        :param cancel_callback: A callable that is called with the payload if
        the users asks not to proceed.
        :param payload: Arbitrary object, passed to callback
        :param html_log: An HTML or plain text log
        :param log_viewer_title: The title for the log viewer window
        :param title: The title for this popup
        :param msg: The msg to display
        :param det_msg: Detailed message
        :param log_is_file: If True the html_log parameter is interpreted as
                            the path to a file on disk containing the log
                            encoded with utf-8
        :param checkbox_msg: If not None, a checkbox is displayed in the
                             dialog, showing this message. The callback is
                             called with both the payload and the state of the
                             checkbox as arguments.
        :param checkbox_checked: If True the checkbox is checked by default.
        :param action_callback: If not None, an extra button is added, which
                                when clicked will cause action_callback to be called
                                instead of callback. action_callback is called in
                                exactly the same way as callback.
        :param action_label: The text on the action button
        :param action_icon: The icon for the action button, must be a QIcon object or None
        :param focus_action: If True, the action button will be focused instead of the Yes button
        :param show_det: If True, the Detailed message will be shown initially
        :param show_ok: If True, OK will be shown instead of YES/NO
        :param icon: The icon to be used for this popop (defaults to question mark). Can be either a QIcon or a string to be used with I()
        :log_viewer_unique_name: If set, ViewLog will remember/reuse its size for this name in calibre.gui2.gprefs
        '''
        question = Question(
            payload, callback, cancel_callback, title, msg, html_log,
            log_viewer_title, log_is_file, det_msg, show_copy_button,
            checkbox_msg, checkbox_checked, action_callback, action_label,
            action_icon, focus_action, show_det, show_ok, icon, log_viewer_unique_name)
        self.questions.append(question)
        self.show_question()

    def show_log(self):
        if self.questions:
            q = self.questions[0]
            log = q.html_log
            if q.log_is_file:
                with open(log, 'rb') as f:
                    log = f.read().decode('utf-8')
            self.log_viewer = ViewLog(q.log_viewer_title, log,
                        parent=self, unique_name=q.log_viewer_unique_name)

    def paintEvent(self, ev):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
        try:
            if self.rendered_pixmap is None:
                self.paint_background(painter)
            else:
                self.animated_paint(painter)
        finally:
            painter.end()

    def animated_paint(self, painter):
        top = (1 - self._show_fraction) * self.height()
        painter.drawPixmap(0, int(top), self.rendered_pixmap)

    def paint_background(self, painter):
        br = 12  # border_radius
        bw = 1  # border_width
        pal = self.palette()
        c = pal.color(QPalette.ColorRole.Window)
        c.setAlphaF(0.9)
        p = QPainterPath()
        p.addRoundedRect(QRectF(self.rect()), br, br)
        painter.fillPath(p, c)
        p.addRoundedRect(QRectF(self.rect()).adjusted(bw, bw, -bw, -bw), br, br)
        painter.fillPath(p, pal.color(QPalette.ColorRole.WindowText))


def main():
    from calibre.gui2 import Application
    from qt.core import QMainWindow, QStatusBar, QTimer
    app = Application([])
    w = QMainWindow()
    s = QStatusBar(w)
    w.setStatusBar(s)
    s.showMessage('Testing ProceedQuestion')
    w.show()
    p = ProceedQuestion(w)

    def doit():
        p.dummy_question()
        p.dummy_question(action_label='A very long button for testing relayout (indeed)')
        p(
            lambda p:None, None, 'ass2', 'ass2', 'testing2', 'testing2',
            det_msg='details shown first, with a long line to test wrapping of text and width layout',
            show_det=True, show_ok=True)
    QTimer.singleShot(10, doit)
    app.exec()


if __name__ == '__main__':
    main()
