#!/usr/bin/env python


__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import os
import weakref
from collections import OrderedDict, namedtuple
from functools import partial

from qt.core import (
    QAction,
    QApplication,
    QCheckBox,
    QColor,
    QDialog,
    QDialogButtonBox,
    QFont,
    QGridLayout,
    QHBoxLayout,
    QIcon,
    QKeySequence,
    QLabel,
    QMenu,
    QPainter,
    QPen,
    QPixmap,
    QScrollArea,
    QSize,
    QSizePolicy,
    QStackedLayout,
    Qt,
    QToolButton,
    QVBoxLayout,
    QWidget,
    pyqtSignal,
)

from calibre import fit_image
from calibre.ebooks.metadata import authors_to_sort_string, fmt_sidx, title_sort
from calibre.gui2 import gprefs, pixmap_to_data
from calibre.gui2.comments_editor import Editor
from calibre.gui2.complete2 import LineEdit as EditWithComplete
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.languages import LanguagesEdit as LE
from calibre.gui2.metadata.basic_widgets import PubdateEdit, RatingEdit
from calibre.gui2.widgets2 import RightClickButton
from calibre.ptempfile import PersistentTemporaryFile
from calibre.startup import connect_lambda
from calibre.utils.date import UNDEFINED_DATE
from calibre.utils.icu import lower as icu_lower
from calibre.utils.localization import ngettext

Widgets = namedtuple('Widgets', 'new old label button')


# Widgets {{{

class LineEdit(EditWithComplete):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        EditWithComplete.__init__(self, parent)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        if not is_new:
            self.setReadOnly(True)
            self.setClearButtonEnabled(False)
        else:
            sep = metadata['is_multiple']['list_to_ui'] if metadata['is_multiple'] else None
            self.set_separator(sep)
        self.textChanged.connect(self.changed)

    @property
    def value(self):
        val = str(self.text()).strip()
        ism = self.metadata['is_multiple']
        if ism:
            if not val:
                val = []
            else:
                val = val.strip(ism['list_to_ui'].strip())
                val = [x.strip() for x in val.split(ism['list_to_ui']) if x.strip()]
        return val

    @value.setter
    def value(self, val):
        ism = self.metadata['is_multiple']
        if ism:
            if not val:
                val = ''
            else:
                val = ism['list_to_ui'].join(val)
        self.setText(val)
        self.setCursorPosition(0)

    def from_mi(self, mi):
        val = mi.get(self.field, default='') or ''
        self.value = val

    def to_mi(self, mi):
        val = self.value
        mi.set(self.field, val)
        if self.field == 'title':
            mi.set('title_sort', title_sort(val, lang=mi.language))
        elif self.field == 'authors':
            mi.set('author_sort', authors_to_sort_string(val))

    @property
    def current_val(self):
        return str(self.text())

    @current_val.setter
    def current_val(self, val):
        self.setText(val)
        self.setCursorPosition(0)

    def set_undoable(self, val):
        self.selectAll()
        self.insert(val)
        self.setCursorPosition(0)

    @property
    def is_blank(self):
        val = self.current_val.strip()
        if self.field in {'title', 'authors'}:
            return val in {'', _('Unknown')}
        return not val

    def same_as(self, other):
        return self.current_val == other.current_val


class LanguagesEdit(LE):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        LE.__init__(self, parent=parent)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        self.textChanged.connect(self.changed)
        if not is_new:
            self.lineEdit().setReadOnly(True)
            self.lineEdit().setClearButtonEnabled(False)

    @property
    def current_val(self):
        return self.lang_codes

    @current_val.setter
    def current_val(self, val):
        self.lang_codes = val

    def from_mi(self, mi):
        self.lang_codes = mi.languages

    def to_mi(self, mi):
        mi.languages = self.lang_codes

    @property
    def is_blank(self):
        return not self.current_val

    def same_as(self, other):
        return self.current_val == other.current_val

    def set_undoable(self, val):
        self.set_lang_codes(val, True)


class RatingsEdit(RatingEdit):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        RatingEdit.__init__(self, parent)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        self.currentIndexChanged.connect(self.changed)

    def from_mi(self, mi):
        self.current_val = mi.get(self.field, default=0)

    def to_mi(self, mi):
        mi.set(self.field, self.current_val)

    @property
    def is_blank(self):
        return self.current_val == 0

    def same_as(self, other):
        return self.current_val == other.current_val


class DateEdit(PubdateEdit):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        PubdateEdit.__init__(self, parent, create_clear_button=False)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        self.setDisplayFormat(extra)
        self.dateTimeChanged.connect(self.changed)
        if not is_new:
            self.setReadOnly(True)

    def from_mi(self, mi):
        self.current_val = mi.get(self.field, default=None)

    def to_mi(self, mi):
        mi.set(self.field, self.current_val)

    @property
    def is_blank(self):
        return self.current_val.year <= UNDEFINED_DATE.year

    def same_as(self, other):
        return self.text() == other.text()

    def set_undoable(self, val):
        self.set_value(val)


class SeriesEdit(LineEdit):

    def __init__(self, *args, **kwargs):
        LineEdit.__init__(self, *args, **kwargs)
        self.dbref = None
        self.item_selected.connect(self.insert_series_index)

    def from_mi(self, mi):
        series = mi.get(self.field, default='')
        series_index = mi.get(self.field + '_index', default=1.0)
        val = ''
        if series:
            val = f'{series} [{mi.format_series_index(series_index)}]'
        self.setText(val)
        self.setCursorPosition(0)

    def to_mi(self, mi):
        val = str(self.text()).strip()
        try:
            series_index = float(val.rpartition('[')[-1].rstrip(']').strip())
        except Exception:
            series_index = 1.0
        series = val.rpartition('[')[0].strip() or val.rpartition('[')[-1].strip() or None
        mi.set(self.field, series)
        mi.set(self.field + '_index', series_index)

    def set_db(self, db):
        self.dbref = weakref.ref(db)

    def insert_series_index(self, series):
        db = self.dbref()
        if db is None or not series:
            return
        num = db.get_next_series_num_for(series)
        sidx = fmt_sidx(num)
        self.setText(self.text() + f' [{sidx}]')


class IdentifiersEdit(LineEdit):

    def from_mi(self, mi):
        self.as_dict = mi.identifiers

    def to_mi(self, mi):
        mi.set_identifiers(self.as_dict)

    @property
    def as_dict(self):
        parts = (x.strip() for x in self.current_val.split(',') if x.strip())
        return {k:v for k, v in {x.partition(':')[0].strip():x.partition(':')[-1].strip() for x in parts}.items() if k and v}

    @as_dict.setter
    def as_dict(self, val):
        val = (f'{k}:{v}' for k, v in val.items())
        self.setText(', '.join(val))
        self.setCursorPosition(0)


class CommentsEdit(Editor):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        Editor.__init__(self, parent, one_line_toolbar=False)
        self.set_minimum_height_for_editor(150)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        self.hide_tabs()
        if not is_new:
            self.hide_toolbars()
            self.set_readonly(True)

    @property
    def current_val(self):
        return self.html

    @current_val.setter
    def current_val(self, val):
        self.html = val or ''
        self.changed.emit()

    def set_undoable(self, val):
        self.set_html(val, allow_undo=True)
        self.changed.emit()

    def from_mi(self, mi):
        val = mi.get(self.field, default='')
        self.current_val = val

    def to_mi(self, mi):
        mi.set(self.field, self.current_val)

    def sizeHint(self):
        return QSize(450, 200)

    @property
    def is_blank(self):
        return not self.current_val.strip()

    def same_as(self, other):
        return self.current_val == other.current_val


class CoverView(QWidget):

    changed = pyqtSignal()
    zoom_requested = pyqtSignal(object)

    def __init__(self, field, is_new, parent, metadata, extra):
        QWidget.__init__(self, parent)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        self.pixmap = None
        ic = QIcon.ic('blank.png')
        self.blank = ic.pixmap(ic.availableSizes()[0])
        self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding)
        self.sizePolicy().setHeightForWidth(True)

    def mouseDoubleClickEvent(self, ev):
        if self.pixmap and not self.pixmap.isNull():
            self.zoom_requested.emit(self.pixmap)

    @property
    def is_blank(self):
        return self.pixmap is None

    @property
    def current_val(self):
        return self.pixmap

    @current_val.setter
    def current_val(self, val):
        self.pixmap = val
        self.changed.emit()
        self.update()

    def from_mi(self, mi):
        p = getattr(mi, 'cover', None)
        if p and os.path.exists(p):
            pmap = QPixmap()
            with open(p, 'rb') as f:
                pmap.loadFromData(f.read())
            if not pmap.isNull():
                self.pixmap = pmap
                self.update()
                self.changed.emit()
                return
        cd = getattr(mi, 'cover_data', (None, None))
        if cd and cd[1]:
            pmap = QPixmap()
            pmap.loadFromData(cd[1])
            if not pmap.isNull():
                self.pixmap = pmap
                self.update()
                self.changed.emit()
                return
        self.pixmap = None
        self.update()
        self.changed.emit()

    def to_mi(self, mi):
        mi.cover, mi.cover_data = None, (None, None)
        if self.pixmap is not None and not self.pixmap.isNull():
            with PersistentTemporaryFile('.jpg') as pt:
                pt.write(pixmap_to_data(self.pixmap))
                mi.cover = pt.name

    def same_as(self, other):
        return self.current_val == other.current_val

    def sizeHint(self):
        return QSize(225, 300)

    def paintEvent(self, event):
        pmap = self.blank if self.pixmap is None or self.pixmap.isNull() else self.pixmap
        target = self.rect()
        scaled, width, height = fit_image(pmap.width(), pmap.height(), target.width(), target.height())
        target.setRect(target.x(), target.y(), width, height)
        p = QPainter(self)
        p.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
        p.drawPixmap(target, pmap)

        if self.pixmap is not None and not self.pixmap.isNull():
            sztgt = target.adjusted(0, 0, 0, -4)
            f = p.font()
            f.setBold(True)
            p.setFont(f)
            sz = f'\xa0{self.pixmap.width()} x {self.pixmap.height()}\xa0'
            flags = int(Qt.AlignmentFlag.AlignBottom|Qt.AlignmentFlag.AlignRight|Qt.TextFlag.TextSingleLine)
            szrect = p.boundingRect(sztgt, flags, sz)
            p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200))
            p.setPen(QPen(QColor(255,255,255)))
            p.drawText(sztgt, flags, sz)
        p.end()
# }}}


class CompareSingle(QWidget):

    zoom_requested = pyqtSignal(object)

    def __init__(
            self, field_metadata, parent=None, revert_tooltip=None,
            datetime_fmt='MMMM yyyy', blank_as_equal=True,
            fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'languages', 'comments', 'cover'), db=None):
        QWidget.__init__(self, parent)
        self.l = l = QGridLayout()
        # l.setContentsMargins(0, 0, 0, 0)
        self.setLayout(l)
        revert_tooltip = revert_tooltip or _('Revert %s')
        self.current_mi = None
        self.changed_font = QFont(QApplication.font())
        self.changed_font.setBold(True)
        self.changed_font.setItalic(True)
        self.blank_as_equal = blank_as_equal

        self.widgets = OrderedDict()
        row = 0

        for field in fields:
            m = field_metadata[field]
            dt = m['datatype']
            extra = None
            if 'series' in {field, dt}:
                cls = SeriesEdit
            elif field == 'identifiers':
                cls = IdentifiersEdit
            elif field == 'languages':
                cls = LanguagesEdit
            elif 'comments' in {field, dt}:
                cls = CommentsEdit
            elif 'rating' in {field, dt}:
                cls = RatingsEdit
            elif dt == 'datetime':
                extra = datetime_fmt
                cls = DateEdit
            elif field == 'cover':
                cls = CoverView
            elif dt in {'text', 'enum'}:
                cls = LineEdit
            else:
                continue
            neww = cls(field, True, self, m, extra)
            neww.setObjectName(field)
            connect_lambda(neww.changed, self, lambda self: self.changed(self.sender().objectName()))
            if isinstance(neww, EditWithComplete):
                try:
                    neww.update_items_cache(db.new_api.all_field_names(field))
                except ValueError:
                    pass  # A one-one field like title
            if isinstance(neww, SeriesEdit):
                neww.set_db(db.new_api)
            oldw = cls(field, False, self, m, extra)
            newl = QLabel('&{}:'.format(m['name']))
            newl.setBuddy(neww)
            button = RightClickButton(self)
            button.setIcon(QIcon.ic('back.png'))
            button.setObjectName(field)
            connect_lambda(button.clicked, self, lambda self: self.revert(self.sender().objectName()))
            button.setToolTip(revert_tooltip % m['name'])
            if field == 'identifiers':
                button.m = m = QMenu(button)
                button.setMenu(m)
                button.setPopupMode(QToolButton.ToolButtonPopupMode.DelayedPopup)
                m.addAction(button.toolTip()).triggered.connect(button.click)
                m.actions()[0].setIcon(button.icon())
                m.addAction(_('Merge identifiers')).triggered.connect(self.merge_identifiers)
                m.actions()[1].setIcon(QIcon.ic('merge.png'))
            elif field == 'tags':
                button.m = m = QMenu(button)
                button.setMenu(m)
                button.setPopupMode(QToolButton.ToolButtonPopupMode.DelayedPopup)
                m.addAction(button.toolTip()).triggered.connect(button.click)
                m.actions()[0].setIcon(button.icon())
                m.addAction(_('Merge tags')).triggered.connect(self.merge_tags)
                m.actions()[1].setIcon(QIcon.ic('merge.png'))
            elif field == 'comments':
                button.m = m = QMenu(button)
                button.setMenu(m)
                button.setPopupMode(QToolButton.ToolButtonPopupMode.DelayedPopup)
                m.addAction(button.toolTip()).triggered.connect(button.click)
                m.actions()[0].setIcon(button.icon())
                m.addAction(_('Merge Comments')).triggered.connect(self.merge_comments)
                m.actions()[1].setIcon(QIcon.ic('merge.png'))

            if cls is CoverView:
                neww.zoom_requested.connect(self.zoom_requested)
                oldw.zoom_requested.connect(self.zoom_requested)
            self.widgets[field] = Widgets(neww, oldw, newl, button)
            for i, w in enumerate((newl, neww, button, oldw)):
                c = i if i < 2 else i + 1
                if w is oldw:
                    c += 1
                l.addWidget(w, row, c)
            row += 1

        if 'comments' in self.widgets and not gprefs.get('diff_widget_show_comments_controls', True):
            self.widgets['comments'].new.hide_toolbars()

    def save_comments_controls_state(self):
        if 'comments' in self.widgets:
            vis = self.widgets['comments'].new.toolbars_visible
            if vis != gprefs.get('diff_widget_show_comments_controls', True):
                gprefs.set('diff_widget_show_comments_controls', vis)

    def changed(self, field):
        w = self.widgets[field]
        if not w.new.same_as(w.old) and (not self.blank_as_equal or not w.new.is_blank):
            w.label.setFont(self.changed_font)
        else:
            w.label.setFont(QApplication.font())

    def revert(self, field):
        widgets = self.widgets[field]
        neww, oldw = widgets[:2]
        if hasattr(neww, 'set_undoable'):
            neww.set_undoable(oldw.current_val)
        else:
            neww.current_val = oldw.current_val

    def merge_identifiers(self):
        widgets = self.widgets['identifiers']
        neww, oldw = widgets[:2]
        val = neww.as_dict
        val.update(oldw.as_dict)
        neww.as_dict = val

    def merge_tags(self):
        widgets = self.widgets['tags']
        neww, oldw = widgets[:2]
        lval = {icu_lower(x) for x in neww.value}
        extra = [x for x in oldw.value if icu_lower(x) not in lval]
        if extra:
            neww.value += extra

    def merge_comments(self):
        widgets = self.widgets['comments']
        neww, oldw = widgets[:2]
        val = oldw.current_val
        neww.current_val = (neww.current_val or '') + '\n\n' + (val or '')

    def __call__(self, oldmi, newmi):
        self.current_mi = newmi
        self.initial_vals = {}
        for field, widgets in self.widgets.items():
            widgets.old.from_mi(oldmi)
            widgets.new.from_mi(newmi)
            self.initial_vals[field] = widgets.new.current_val

    def apply_changes(self):
        changed = False
        for field, widgets in self.widgets.items():
            val = widgets.new.current_val
            if val != self.initial_vals[field]:
                widgets.new.to_mi(self.current_mi)
                changed = True
        if changed and not self.current_mi.languages:
            # this is needed because blank language setting
            # causes current UI language to be set
            widgets = self.widgets['languages']
            neww, oldw = widgets[:2]
            if oldw.current_val:
                self.current_mi.languages = oldw.current_val
        return changed


class ZoomedCover(QWidget):
    pixmap = None

    def paintEvent(self, event):
        pmap = self.pixmap
        if pmap is None:
            return
        target = self.rect()
        scaled, width, height = fit_image(pmap.width(), pmap.height(), target.width(), target.height())
        dx = 0
        if target.width() > width + 1:
            dx += (target.width() - width) // 2
        target.setRect(target.x() + dx, target.y(), width, height)
        p = QPainter(self)
        p.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
        p.drawPixmap(target, pmap)


class CoverZoom(QWidget):

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        self.l = l = QVBoxLayout(self)
        self.cover = ZoomedCover(self)
        l.addWidget(self.cover)
        self.h = QHBoxLayout()
        l.addLayout(self.h)
        self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, self)
        self.size_label = QLabel(self)
        self.h.addWidget(self.size_label)
        self.h.addStretch(10)
        self.h.addWidget(self.bb)

    def set_pixmap(self, pixmap):
        self.cover.pixmap = pixmap
        self.size_label.setText(_('Cover size: {0}x{1}').format(pixmap.width(), pixmap.height()))
        self.cover.update()


class CompareMany(QDialog):

    def __init__(self, ids, get_metadata, field_metadata, parent=None,
                 window_title=None,
                 reject_button_tooltip=None,
                 accept_all_tooltip=None,
                 reject_all_tooltip=None,
                 revert_tooltip=None,
                 intro_msg=None,
                 action_button=None,
                 **kwargs):
        QDialog.__init__(self, parent)
        self.stack = s = QStackedLayout(self)
        self.w = w = QWidget(self)
        self.l = l = QVBoxLayout(w)
        s.addWidget(w)
        self.next_called = False

        # initialize the previous items list, we will use it to store the watched items that were rejected or accepted
        # when the user clicks on the next or reject button we will add the current item to the previous items list
        # when the user presses the back button we will pop the last item from the previous items list and set it as current item
        # also the popped item will be removed from the rejected or accepted items list (and will be unmarked if it was marked)
        self.previous_items = []

        self.setWindowIcon(QIcon.ic('auto_author_sort.png'))
        self.get_metadata = get_metadata
        self.ids = list(ids)
        self.total = len(self.ids)
        self.accepted = OrderedDict()
        self.rejected_ids = set()
        self.window_title = window_title or _('Compare metadata')

        if intro_msg:
            self.la = la = QLabel(intro_msg)
            la.setWordWrap(True)
            l.addWidget(la)

        self.compare_widget = CompareSingle(field_metadata, parent=parent, revert_tooltip=revert_tooltip, **kwargs)
        self.sa = sa = QScrollArea()
        l.addWidget(sa)
        sa.setWidget(self.compare_widget)
        sa.setWidgetResizable(True)
        self.cover_zoom = cz = CoverZoom(self)
        cz.bb.rejected.connect(self.reject)
        s.addWidget(cz)
        self.compare_widget.zoom_requested.connect(self.show_zoomed_cover)

        self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel)
        bb.button(QDialogButtonBox.StandardButton.Cancel).setAutoDefault(False)
        bb.rejected.connect(self.reject)

        if self.total > 1:

            self.aarb = b = bb.addButton(_('&Accept all remaining'), QDialogButtonBox.ButtonRole.YesRole)
            b.setIcon(QIcon.ic('ok.png')), b.setAutoDefault(False)
            if accept_all_tooltip:
                b.setToolTip(accept_all_tooltip)
            b.clicked.connect(self.accept_all_remaining)
            self.rarb = b = bb.addButton(_('Re&ject all remaining'), QDialogButtonBox.ButtonRole.ActionRole)
            b.setIcon(QIcon.ic('minus.png')), b.setAutoDefault(False)
            if reject_all_tooltip:
                b.setToolTip(reject_all_tooltip)
            b.clicked.connect(self.reject_all_remaining)
            self.sb = b = bb.addButton(_('R&eject'), QDialogButtonBox.ButtonRole.ActionRole)
            ac = QAction(self)
            ac.setShortcut(QKeySequence(Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier | Qt.Key.Key_Right))
            ac.triggered.connect(b.click)
            self.addAction(ac)
            b.setToolTip(_('Reject changes and move to next [{}]').format(ac.shortcut().toString(QKeySequence.SequenceFormat.NativeText)))
            connect_lambda(b.clicked, self, lambda self: self.next_item(False))
            b.setIcon(QIcon.ic('minus.png')), b.setAutoDefault(False)
            if reject_button_tooltip:
                b.setToolTip(reject_button_tooltip)
            self.next_action = ac = QAction(self)
            ac.setShortcut(QKeySequence(Qt.KeyboardModifier.AltModifier | Qt.Key.Key_Right))
            self.addAction(ac)
        if action_button is not None:
            self.acb = b = bb.addButton(action_button[0], QDialogButtonBox.ButtonRole.ActionRole)
            b.setIcon(QIcon.ic(action_button[1]))
            self.action_button_action = action_button[2]
            b.clicked.connect(self.action_button_clicked)

        # Add a Back button, which allows the user to go back to the previous book cancel any reject/edit/accept that was done to it, and review it again
        # create a Back action that will be triggered when the user presses the back button or the back shortcut
        self.back_action = QAction(self)
        self.back_action.setShortcut(QKeySequence(Qt.KeyboardModifier.AltModifier | Qt.Key.Key_Left))
        self.back_action.triggered.connect(self.previous_item)
        self.addAction(self.back_action)
        # create the back button, set it's name, tooltip, icon and action to call the previous_item method
        self.back_button = bb.addButton(_('P&revious'), QDialogButtonBox.ButtonRole.ActionRole)
        self.back_button.setToolTip(_('Move to previous [{}]').format(self.back_action.shortcut().toString(QKeySequence.SequenceFormat.NativeText)))
        self.back_button.setIcon(QIcon.ic('back.png'))
        self.back_button.clicked.connect(self.previous_item)
        self.back_button.setDefault(True)
        self.back_button.setAutoDefault(False)

        self.nb = b = bb.addButton(_('&Next') if self.total > 1 else _('&OK'), QDialogButtonBox.ButtonRole.ActionRole)
        if self.total > 1:
            b.setToolTip(_('Move to next [{}]').format(self.next_action.shortcut().toString(QKeySequence.SequenceFormat.NativeText)))
            self.next_action.triggered.connect(b.click)
        b.setIcon(QIcon.ic('forward.png' if self.total > 1 else 'ok.png'))
        connect_lambda(b.clicked, self, lambda self: self.next_item(True))
        b.setDefault(True), b.setAutoDefault(True)
        self.bbh = h = QHBoxLayout()
        h.setContentsMargins(0, 0, 0, 0)
        l.addLayout(h)
        self.markq = m = QCheckBox(_('&Mark rejected books'))
        m.setChecked(gprefs['metadata_diff_mark_rejected'])
        connect_lambda(m.stateChanged[int], self, lambda self: gprefs.set('metadata_diff_mark_rejected', self.markq.isChecked()))
        m.setToolTip(_('Mark rejected books in the book list after this dialog is closed'))
        h.addWidget(m), h.addWidget(bb)

        self.next_item(True)

        geom = (parent or self).screen().availableSize()
        width = max(700, min(950, geom.width()-50))
        height = max(650, min(1000, geom.height()-100))
        self.resize(QSize(width, height))
        self.restore_geometry(gprefs, 'diff_dialog_geom')
        b.setFocus(Qt.FocusReason.OtherFocusReason)
        self.next_called = False

    def show_zoomed_cover(self, pixmap):
        self.cover_zoom.set_pixmap(pixmap)
        self.stack.setCurrentIndex(1)

    @property
    def mark_rejected(self):
        return self.markq.isChecked()

    def action_button_clicked(self):
        self.action_button_action(self.ids[0])

    def accept(self):
        self.save_geometry(gprefs, 'diff_dialog_geom')
        self.compare_widget.save_comments_controls_state()
        super().accept()

    def reject(self):
        if self.stack.currentIndex() == 1:
            self.stack.setCurrentIndex(0)
            return
        if self.next_called and not confirm(_(
            'All reviewed changes will be lost! Are you sure you want to Cancel?'),
            'confirm-metadata-diff-dialog-cancel'):
            return
        self.save_geometry(gprefs, 'diff_dialog_geom')
        self.compare_widget.save_comments_controls_state()
        super().reject()

    @property
    def current_mi(self):
        return self.compare_widget.current_mi

    def show_current_item(self):
        self.setWindowTitle(self.window_title + _(' [%(num)d of %(tot)d]') % dict(
            num=(self.total - len(self.ids) + 1), tot=self.total))
        oldmi, newmi = self.get_metadata(self.ids[0])
        self.compare_widget(oldmi, newmi)
        self.update_back_button_state()

    def update_back_button_state(self):
        enabled = bool(self.previous_items)
        self.back_action.setEnabled(enabled)
        self.back_button.setEnabled(enabled)

    def next_item(self, accept):
        self.next_called = True
        if not self.ids:
            return self.accept()

        if self.current_mi is not None:
            changed = self.compare_widget.apply_changes()
        if self.current_mi is not None:
            old_id = self.ids.pop(0)

            # Save the current book that was just reviewed and accepted or rejected to the previous_items list
            # this book can be displayed again if the user presses the back button
            self.previous_items.append(old_id)

            if not accept:
                self.rejected_ids.add(old_id)
            self.accepted[old_id] = (changed, self.current_mi) if accept else (False, None)
        if not self.ids:
            return self.accept()
        self.show_current_item()

    def previous_item(self):
        if self.previous_items:
            # get the last book id from the previous items list and remove it from the previous items list
            # this book id is the last book id that was reviewed and accepted or rejected
            last_previous_item = self.previous_items.pop()

            # if this book id was rejected, remove it from the rejected ids set
            if last_previous_item in self.rejected_ids:
                self.rejected_ids.remove(last_previous_item)
                self.markq.setChecked(False)
            # if this book id was accepted, remove it from the accepted dictionary
            elif last_previous_item in self.accepted:
                self.accepted.pop(last_previous_item)

            # move the last previous item to the beginning of the pending list
            self.ids.insert(0, last_previous_item)
            self.show_current_item()

    def accept_all_remaining(self):
        self.next_item(True)
        for id_ in self.ids:
            oldmi, newmi = self.get_metadata(id_)
            self.accepted[id_] = (False, newmi)
        self.ids = []
        self.accept()

    def reject_all_remaining(self):
        from calibre.gui2.dialogs.confirm_delete import confirm
        if not confirm(ngettext(
                'Are you sure you want to reject the remaining result?',
                'Are you sure you want to reject all {} remaining results?', len(self.ids)).format(len(self.ids)),
                       'confirm_metadata_review_reject', parent=self):
            return
        self.next_item(False)
        for id_ in self.ids:
            self.rejected_ids.add(id_)
            oldmi, newmi = self.get_metadata(id_)
            self.accepted[id_] = (False, None)
        self.ids = []
        self.accept()

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
            ev.accept()
            return
        return QDialog.keyPressEvent(self, ev)


if __name__ == '__main__':
    from calibre.gui2 import Application
    from calibre.library import db
    app = Application([])
    db = db()
    ids = sorted(db.all_ids(), reverse=True)
    ids = tuple(zip(ids[0::2], ids[1::2]))
    gm = partial(db.get_metadata, index_is_id=True, get_cover=True, cover_as_data=True)

    def get_metadata(x):
        return list(map(gm, ids[x]))
    d = CompareMany(list(range(len(ids))), get_metadata, db.field_metadata, db=db)
    d.exec()
    for changed, mi in d.accepted.values():
        if changed and mi is not None:
            print(mi)
