#!/usr/bin/env python
# License: GPLv3 Copyright: 2012, Kovid Goyal <kovid at kovidgoyal.net>

from qt.core import (
    QAction,
    QCheckBox,
    QDialog,
    QDialogButtonBox,
    QHBoxLayout,
    QIcon,
    QImage,
    QKeySequence,
    QLabel,
    QMenu,
    QPainter,
    QPalette,
    QPixmap,
    QScrollArea,
    QSize,
    QSizePolicy,
    Qt,
    QTransform,
    QUrl,
    QVBoxLayout,
    pyqtSignal,
)

from calibre import fit_image
from calibre.gui2 import NO_URL_FORMATTING, choose_save_file, gprefs, max_available_height, qapplication_or_fail
from calibre.gui2.palette import dark_palette
from calibre.utils.localization import _


def render_svg(widget, path):
    from qt.core import QSvgRenderer

    img = QPixmap()
    rend = QSvgRenderer()
    if rend.load(path):
        dpr = getattr(widget, 'devicePixelRatioF', widget.devicePixelRatio)()
        sz = rend.defaultSize()
        h = max_available_height() - 50
        w = int(h * sz.height() / float(sz.width()))
        pd = QImage(w * dpr, h * dpr, QImage.Format.Format_RGB32)
        pd.fill(Qt.GlobalColor.white)
        p = QPainter(pd)
        rend.render(p)
        p.end()
        img = QPixmap.fromImage(pd)
        img.setDevicePixelRatio(dpr)
    return img


class Label(QLabel):
    toggle_fit = pyqtSignal()
    zoom_requested = pyqtSignal(bool)

    def __init__(self, scrollarea):
        super().__init__(scrollarea)
        scrollarea.zoom_requested.connect(self.zoom_requested)
        self.setBackgroundRole(QPalette.ColorRole.NoRole)
        self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
        self.setScaledContents(True)
        self.default_cursor = self.cursor()
        self.in_drag = False
        self.prev_drag_position = None
        self.scrollarea = scrollarea

    @property
    def is_pannable(self):
        return self.scrollarea.verticalScrollBar().isVisible() or self.scrollarea.horizontalScrollBar().isVisible()

    def mousePressEvent(self, ev):
        if ev.button() == Qt.MouseButton.LeftButton and self.is_pannable:
            self.setCursor(Qt.CursorShape.ClosedHandCursor)
            self.in_drag = True
            self.prev_drag_position = ev.globalPos()
        return super().mousePressEvent(ev)

    def mouseReleaseEvent(self, ev):
        if ev.button() == Qt.MouseButton.LeftButton and self.in_drag:
            self.setCursor(self.default_cursor)
            self.in_drag = False
            self.prev_drag_position = None
        return super().mousePressEvent(ev)

    def mouseMoveEvent(self, ev):
        if self.prev_drag_position is not None:
            p = self.prev_drag_position
            self.prev_drag_position = pos = ev.globalPos()
            self.dragged(pos.x() - p.x(), pos.y() - p.y())
        return super().mouseMoveEvent(ev)

    def dragged(self, dx, dy):
        h = self.scrollarea.horizontalScrollBar()
        if h.isVisible():
            h.setValue(h.value() - dx)
        v = self.scrollarea.verticalScrollBar()
        if v.isVisible():
            v.setValue(v.value() - dy)


class ScrollArea(QScrollArea):
    toggle_fit = pyqtSignal()
    zoom_requested = pyqtSignal(bool)
    current_wheel_angle_delta = 0

    def mouseDoubleClickEvent(self, a0):
        if a0.button() == Qt.MouseButton.LeftButton:
            self.toggle_fit.emit()

    def wheelEvent(self, a0):
        if a0.modifiers() == Qt.KeyboardModifier.ControlModifier:
            ad = a0.angleDelta().y()
            if ad * self.current_wheel_angle_delta < 0:
                self.current_wheel_angle_delta = 0
            self.current_wheel_angle_delta += ad
            if abs(self.current_wheel_angle_delta) >= 120:
                self.zoom_requested.emit(self.current_wheel_angle_delta < 0)
                self.current_wheel_angle_delta = 0
            a0.accept()
        else:
            super().wheelEvent(a0)


class ImageView(QDialog):
    def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry', prefs=gprefs):
        QDialog.__init__(self)
        self.prefs = prefs
        self.current_image_name = ''
        self.current_image_is_svg = False
        self.maximized_at_last_fullscreen = False
        self.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint)
        self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint)
        screen = self.screen()
        assert screen is not None
        self.avail_geom = screen.availableGeometry()
        self.current_img = current_img
        self.current_url = current_url
        self.transformed = False
        self.factor = 1.0
        self.geom_name = geom_name
        self.zoom_in_action = ac = QAction(QIcon.ic('plus.png'), _('Zoom &in'), self)
        ac.triggered.connect(self.zoom_in)
        ac.setShortcuts([
            QKeySequence(QKeySequence.StandardKey.ZoomIn),
            QKeySequence('+', QKeySequence.SequenceFormat.PortableText),
        ])
        self.addAction(ac)
        self.zoom_out_action = ac = QAction(QIcon.ic('minus.png'), _('Zoom &out'), self)
        ac.triggered.connect(self.zoom_out)
        ac.setShortcuts([
            QKeySequence(QKeySequence.StandardKey.ZoomOut),
            QKeySequence('-', QKeySequence.SequenceFormat.PortableText),
        ])
        self.addAction(ac)
        self.reset_zoom_action = ac = QAction(QIcon.ic('edit-undo.png'), _('Reset &zoom'), self)
        ac.triggered.connect(self.reset_zoom)
        ac.setShortcuts([QKeySequence('=', QKeySequence.SequenceFormat.PortableText)])
        self.addAction(ac)
        self.copy_action = ac = QAction(QIcon.ic('edit-copy.png'), _('&Copy'), self)
        ac.setShortcuts([QKeySequence(QKeySequence.StandardKey.Copy)])
        self.addAction(ac)
        ac.triggered.connect(self.copy_image)
        self.rotate_action = ac = QAction(QIcon.ic('rotate-right.png'), _('&Rotate'), self)
        self.addAction(ac)
        ac.triggered.connect(self.rotate_image)

        self.scrollarea = sa = ScrollArea()
        pal = sa.palette()
        pal.setColor(
            QPalette.ColorRole.Dark,
            dark_palette().color(QPalette.ColorRole.Base) if qapplication_or_fail().is_dark_theme else Qt.GlobalColor.darkGray,
        )
        sa.setPalette(pal)
        sa.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
        sa.setBackgroundRole(QPalette.ColorRole.Dark)
        self.label = l = Label(sa)
        l.zoom_requested.connect(self.zoom_requested)
        sa.toggle_fit.connect(self.toggle_fit)
        sa.setWidget(l)

        self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        self.zi_button = zi = bb.addButton(self.zoom_in_action.text(), QDialogButtonBox.ButtonRole.ActionRole)
        self.zo_button = zo = bb.addButton(self.zoom_out_action.text(), QDialogButtonBox.ButtonRole.ActionRole)
        self.save_button = so = bb.addButton(_('&Save as'), QDialogButtonBox.ButtonRole.ActionRole)
        self.copy_button = co = bb.addButton(self.copy_action.text(), QDialogButtonBox.ButtonRole.ActionRole)
        self.rotate_button = ro = bb.addButton(self.rotate_action.text(), QDialogButtonBox.ButtonRole.ActionRole)
        self.fullscreen_button = fo = bb.addButton(_('F&ull screen'), QDialogButtonBox.ButtonRole.ActionRole)
        assert zi is not None
        assert zo is not None
        assert so is not None
        assert co is not None
        assert ro is not None
        assert fo is not None
        zi.setIcon(self.zoom_in_action.icon())
        zo.setIcon(self.zoom_out_action.icon())
        so.setIcon(QIcon.ic('save.png'))
        co.setIcon(self.copy_action.icon())
        ro.setIcon(self.rotate_action.icon())
        fo.setIcon(QIcon.ic('page.png'))
        zi.clicked.connect(self.zoom_in_action.trigger)
        zo.clicked.connect(self.zoom_out_action.trigger)
        so.clicked.connect(self.save_image)
        co.clicked.connect(self.copy_action.trigger)
        ro.clicked.connect(self.rotate_image)
        fo.setCheckable(True)

        self.l = l = QVBoxLayout(self)
        l.addWidget(sa)
        self.h = h = QHBoxLayout()
        h.setContentsMargins(0, 0, 0, 0)
        l.addLayout(h)
        self.fit_image = i = QCheckBox(_('&Fit image'))
        i.setToolTip(_('Fit image inside the available space'))
        i.setChecked(bool(self.prefs.get('image_popup_fit_image')))
        i.stateChanged.connect(self.fit_changed)
        self.remember_zoom = z = QCheckBox(_('Remember &zoom'))
        z.setChecked(not i.isChecked() and bool(self.prefs.get('image_popup_remember_zoom', False)))
        z.stateChanged.connect(self.remember_zoom_changed)
        h.addWidget(i), h.addWidget(z), h.addStretch(), h.addWidget(bb)
        if self.fit_image.isChecked():
            self.set_to_viewport_size()
        elif z.isChecked():
            factor = self.prefs.get('image_popup_zoom_factor', self.factor)
            if factor != self.factor and not self.fit_image.isChecked():
                self.factor = factor
        self.restore_geometry(self.prefs, self.geom_name)
        fo.setChecked(self.isFullScreen())
        fo.toggled.connect(self.toggle_fullscreen)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.context_menu)

    def context_menu(self, pos):
        m = QMenu(self)
        m.addAction(self.reset_zoom_action)
        m.addAction(self.zoom_in_action)
        m.addAction(self.zoom_out_action)
        m.addAction(self.copy_action)
        m.addAction(self.rotate_action)
        ac = QAction(self.fit_image.text())
        ac.setCheckable(True)
        ac.setChecked(self.fit_image.isChecked())
        ac.toggled.connect(self.toggle_fit)
        m.addAction(ac)
        m.exec(self.mapToGlobal(pos))

    def set_to_viewport_size(self):
        page_size = self.scrollarea.size()
        pw, ph = page_size.width() - 2, page_size.height() - 2
        img_size = self.current_img.size()
        iw, ih = img_size.width(), img_size.height()
        scaled, nw, nh = fit_image(iw, ih, pw, ph)
        if scaled:
            self.factor = min(nw / iw, nh / ih)
        img_size.setWidth(nw), img_size.setHeight(nh)
        self.label.resize(img_size)

    def resizeEvent(self, a0):
        if self.fit_image.isChecked():
            self.set_to_viewport_size()

    def factor_from_fit(self):
        scaled_height = self.label.size().height()
        actual_height = self.current_img.size().height()
        if actual_height:
            return scaled_height / actual_height
        return 1.0

    def zoom_requested(self, zoom_out):
        zo_button = self.zo_button
        assert zo_button is not None
        zi_button = self.zi_button
        assert zi_button is not None
        if (zoom_out and zo_button.isEnabled()) or (not zoom_out and zi_button.isEnabled()):
            (self.zoom_out if zoom_out else self.zoom_in)()

    def reset_zoom(self):
        self.factor = 1.0
        self.prefs.set('image_popup_zoom_factor', self.factor)
        self.adjust_image(1.0)

    def zoom_in(self):
        if self.fit_image.isChecked():
            factor = self.factor_from_fit()
            self.fit_image.setChecked(False)
            self.factor = factor
        self.factor *= 1.25
        self.prefs.set('image_popup_zoom_factor', self.factor)
        self.adjust_image(1.25)

    def zoom_out(self):
        if self.fit_image.isChecked():
            factor = self.factor_from_fit()
            self.fit_image.setChecked(False)
            self.factor = factor
        self.factor *= 0.8
        self.prefs.set('image_popup_zoom_factor', self.factor)
        self.adjust_image(0.8)

    def save_image(self):
        is_svg = self.current_image_is_svg and hasattr(self, 'current_url') and self.current_url.isLocalFile()
        filters = [('Images', ['svg'] if is_svg else ['png', 'jpeg', 'jpg'])]
        f = choose_save_file(
            self,
            'viewer image view save dialog',
            _('Choose a file to save to'),
            filters=filters,
            all_files=False,
            initial_filename=self.current_image_name or None,
        )
        if f:
            if is_svg:
                import shutil

                shutil.copyfile(self.current_url.toLocalFile(), f)
            else:
                from calibre.utils.img import save_image

                save_image(self.current_img.toImage(), f)

    def copy_image(self):
        if self.current_img and not self.current_img.isNull():
            clipboard = qapplication_or_fail().clipboard()
            assert clipboard is not None
            clipboard.setPixmap(self.current_img)

    def fit_changed(self):
        fitted = bool(self.fit_image.isChecked())
        self.prefs.set('image_popup_fit_image', fitted)
        if self.fit_image.isChecked():
            self.set_to_viewport_size()
            self.remember_zoom.setChecked(False)
        else:
            self.factor = 1
            self.prefs.set('image_popup_zoom_factor', self.factor)
            self.adjust_image(1)

    def remember_zoom_changed(self):
        val = bool(self.remember_zoom.isChecked())
        self.prefs.set('image_popup_remember_zoom', val)

    def toggle_fit(self):
        self.fit_image.toggle()

    def adjust_image(self, factor):
        if self.fit_image.isChecked():
            self.set_to_viewport_size()
            return
        self.label.resize(self.factor * self.current_img.size())
        zi_button = self.zi_button
        assert zi_button is not None
        zo_button = self.zo_button
        assert zo_button is not None
        zi_button.setEnabled(self.factor <= 3)
        zo_button.setEnabled(self.factor >= 0.3333)
        self.zoom_in_action.setEnabled(zi_button.isEnabled())
        self.zoom_out_action.setEnabled(zo_button.isEnabled())
        self.reset_zoom_action.setEnabled(self.factor != 1)
        self.adjust_scrollbars(factor)

    def adjust_scrollbars(self, factor):
        for sb in (self.scrollarea.horizontalScrollBar(), self.scrollarea.verticalScrollBar()):
            assert sb is not None
            sb.setValue(int(factor * sb.value()) + int((factor - 1) * sb.pageStep() / 2))

    def rotate_image(self):
        pm = self.label.pixmap()
        t = QTransform()
        t.rotate(90)
        pm = self.current_img = pm.transformed(t)
        self.transformed = True
        self.label.setPixmap(pm)
        self.adjust_image(self.factor)

    def __call__(self, use_exec=False):
        self.transformed = False
        geom = self.avail_geom
        self.label.setPixmap(self.current_img)
        self.label.adjustSize()
        self.resize(QSize(int(geom.width() / 2.5), geom.height() - 50))
        self.restore_geometry(self.prefs, self.geom_name)
        try:
            self.current_image_name = str(self.current_url.toString(NO_URL_FORMATTING)).rpartition('/')[-1]
        except AttributeError:
            self.current_image_name = self.current_url
        reso = ''
        if self.current_img and not self.current_img.isNull():
            self.adjust_image(self.factor)
            reso = f'[{self.current_img.width()}x{self.current_img.height()}]'
        title = _('Image: {name} {resolution}').format(name=self.current_image_name, resolution=reso)
        self.setWindowTitle(title)
        if use_exec:
            self.exec()
        else:
            self.show()

    def done(self, a0):
        self.save_geometry(self.prefs, self.geom_name)
        return QDialog.done(self, a0)

    def toggle_fullscreen(self):
        on = not self.isFullScreen()
        if on:
            self.maximized_at_last_fullscreen = self.isMaximized()
            self.showFullScreen()
        elif self.maximized_at_last_fullscreen:
            self.showMaximized()
        else:
            self.showNormal()


class ImagePopup:
    def __init__(self, parent, prefs=gprefs):
        self.current_img = QPixmap()
        self.current_url = QUrl()
        self.current_image_is_svg = False
        self.parent = parent
        self.dialogs = []
        self.prefs = prefs

    def __call__(self):
        if self.current_img.isNull():
            return
        d = ImageView(self.parent, self.current_img, self.current_url, prefs=self.prefs)
        d.current_image_is_svg = self.current_image_is_svg
        self.dialogs.append(d)
        d.finished.connect(self.cleanup, type=Qt.ConnectionType.QueuedConnection)
        d()

    def cleanup(self):
        for d in tuple(self.dialogs):
            if not d.isVisible():
                self.dialogs.remove(d)


def show_image(path=None):
    if path is None:
        import sys

        path = sys.argv[-1]
    from calibre.gui2 import Application

    app = Application([])
    p = QPixmap()
    p.load(path)
    u = QUrl.fromLocalFile(path)
    d = ImageView(None, p, u)
    d()
    app.exec()


if __name__ == '__main__':
    show_image()
