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


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

import errno
import os
from collections import Counter
from functools import partial
from qt.core import QDialog, QModelIndex, QObject, QTimer

from calibre.constants import ismacos, trash_name
from calibre.gui2 import Aborted, error_dialog, question_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.confirm_delete_location import confirm_location
from calibre.gui2.dialogs.delete_matching_from_device import (
    DeleteMatchingFromDeviceDialog
)
from calibre.utils.recycle_bin import can_recycle

single_shot = partial(QTimer.singleShot, 10)


class MultiDeleter(QObject):  # {{{

    def __init__(self, gui, ids, callback):
        from calibre.gui2.dialogs.progress import ProgressDialog
        QObject.__init__(self, gui)
        self.model = gui.library_view.model()
        self.ids = ids
        self.permanent = False
        if can_recycle and len(ids) > 100:
            if question_dialog(gui, _('Are you sure?'), '<p>'+
                _('You are trying to delete {0} books. '
                    'Sending so many files to the {1}'
                    ' <b>can be slow</b>. Should calibre skip the'
                    ' {1}? If you click Yes the files'
                    ' will be <b>permanently deleted</b>.').format(len(ids), trash_name()),
                add_abort_button=True
            ):
                self.permanent = True
        self.gui = gui
        self.failures = []
        self.deleted_ids = []
        self.callback = callback
        single_shot(self.delete_one)
        self.pd = ProgressDialog(_('Deleting...'), parent=gui,
                cancelable=False, min=0, max=len(self.ids), icon='trash.png')
        self.pd.setModal(True)
        self.pd.show()

    def delete_one(self):
        if not self.ids:
            self.cleanup()
            return
        id_ = self.ids.pop()
        title = 'id:%d'%id_
        try:
            title_ = self.model.db.title(id_, index_is_id=True)
            if title_:
                title = title_
            self.model.db.delete_book(id_, notify=False, commit=False,
                    permanent=self.permanent)
            self.deleted_ids.append(id_)
        except:
            import traceback
            self.failures.append((id_, title, traceback.format_exc()))
        single_shot(self.delete_one)
        self.pd.value += 1
        self.pd.set_msg(_('Deleted') + ' ' + title)

    def cleanup(self):
        self.pd.hide()
        self.pd = None
        self.model.db.commit()
        self.model.db.clean()
        self.model.books_deleted()  # calls recount on the tag browser
        self.callback(self.deleted_ids)
        if self.failures:
            msg = ['==> '+x[1]+'\n'+x[2] for x in self.failures]
            error_dialog(self.gui, _('Failed to delete'),
                    _('Failed to delete some books, click the "Show details" button'
                    ' for details.'), det_msg='\n\n'.join(msg), show=True)
# }}}


class DeleteAction(InterfaceAction):

    name = 'Remove Books'
    action_spec = (_('Remove books'), 'remove_books.png', _('Delete books'), 'Backspace' if ismacos else 'Del')
    action_type = 'current'
    action_add_menu = True
    action_menu_clone_qaction = _('Remove selected books')

    accepts_drops = True

    def accept_enter_event(self, event, mime_data):
        if mime_data.hasFormat("application/calibre+from_library"):
            return True
        return False

    def accept_drag_move_event(self, event, mime_data):
        if mime_data.hasFormat("application/calibre+from_library"):
            return True
        return False

    def drop_event(self, event, mime_data):
        mime = 'application/calibre+from_library'
        if mime_data.hasFormat(mime):
            self.dropped_ids = tuple(map(int, mime_data.data(mime).data().split()))
            QTimer.singleShot(1, self.do_drop)
            return True
        return False

    def do_drop(self):
        book_ids = self.dropped_ids
        del self.dropped_ids
        if book_ids:
            self.do_library_delete(book_ids)

    def genesis(self):
        self.qaction.triggered.connect(self.delete_books)
        self.delete_menu = self.qaction.menu()
        m = partial(self.create_menu_action, self.delete_menu)
        m('delete-specific',
                _('Remove files of a specific format from selected books'),
                triggered=self.delete_selected_formats)
        m('delete-except',
                _('Remove all formats from selected books, except...'),
                triggered=self.delete_all_but_selected_formats)
        self.delete_menu.addSeparator()
        m('delete-all',
                _('Remove all formats from selected books'),
                triggered=self.delete_all_formats)
        m('delete-covers',
                _('Remove covers from selected books'),
                triggered=self.delete_covers)
        self.delete_menu.addSeparator()
        m('delete-matching',
                _('Remove matching books from device'),
                triggered=self.remove_matching_books_from_device)
        self.qaction.setMenu(self.delete_menu)
        self.delete_memory = {}

    def location_selected(self, loc):
        enabled = loc == 'library'
        for action in list(self.delete_menu.actions())[1:]:
            action.setEnabled(enabled)

    def _get_selected_formats(self, msg, ids, exclude=False, single=False):
        from calibre.gui2.dialogs.select_formats import SelectFormats
        c = Counter()
        db = self.gui.library_view.model().db
        for x in ids:
            fmts_ = db.formats(x, index_is_id=True, verify_formats=False)
            if fmts_:
                for x in frozenset(x.lower() for x in fmts_.split(',')):
                    c[x] += 1
        d = SelectFormats(c, msg, parent=self.gui, exclude=exclude,
                single=single)
        if d.exec() != QDialog.DialogCode.Accepted:
            return None
        return d.selected_formats

    def _get_selected_ids(self, err_title=_('Cannot delete')):
        rows = self.gui.library_view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            d = error_dialog(self.gui, err_title, _('No book selected'))
            d.exec()
            return set()
        return set(map(self.gui.library_view.model().id, rows))

    def remove_format_by_id(self, book_id, fmt):
        title = self.gui.current_db.title(book_id, index_is_id=True)
        if not confirm('<p>'+(_(
            'The %(fmt)s format will be <b>permanently deleted</b> from '
            '%(title)s. Are you sure?')%dict(fmt=fmt, title=title)) +
                       '</p>', 'library_delete_specific_format', self.gui):
            return

        self.gui.library_view.model().db.remove_format(book_id, fmt,
                index_is_id=True, notify=False)
        self.gui.library_view.model().refresh_ids([book_id])
        self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
                self.gui.library_view.currentIndex())
        self.gui.tags_view.recount_with_position_based_index()

    def restore_format(self, book_id, original_fmt):
        self.gui.current_db.restore_original_format(book_id, original_fmt)
        self.gui.library_view.model().refresh_ids([book_id])
        self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
                self.gui.library_view.currentIndex())
        self.gui.tags_view.recount_with_position_based_index()

    def delete_selected_formats(self, *args):
        ids = self._get_selected_ids()
        if not ids:
            return
        fmts = self._get_selected_formats(
            _('Choose formats to be deleted'), ids)
        if not fmts:
            return
        m = self.gui.library_view.model()
        m.db.new_api.remove_formats({book_id:fmts for book_id in ids})
        m.refresh_ids(ids)
        m.current_changed(self.gui.library_view.currentIndex(),
                self.gui.library_view.currentIndex())
        if ids:
            self.gui.tags_view.recount_with_position_based_index()

    def delete_all_but_selected_formats(self, *args):
        ids = self._get_selected_ids()
        if not ids:
            return
        fmts = self._get_selected_formats(
            '<p>'+_('Choose formats <b>not</b> to be deleted.<p>Note that '
                'this will never remove all formats from a book.'), ids,
            exclude=True)
        if fmts is None:
            return
        m = self.gui.library_view.model()
        removals = {}
        for id in ids:
            bfmts = m.db.formats(id, index_is_id=True)
            if bfmts is None:
                continue
            bfmts = {x.lower() for x in bfmts.split(',')}
            rfmts = bfmts - set(fmts)
            if bfmts - rfmts:
                # Do not delete if it will leave the book with no
                # formats
                removals[id] = rfmts
        if removals:
            m.db.new_api.remove_formats(removals)
            m.refresh_ids(ids)
            m.current_changed(self.gui.library_view.currentIndex(),
                    self.gui.library_view.currentIndex())
            if ids:
                self.gui.tags_view.recount_with_position_based_index()

    def delete_all_formats(self, *args):
        ids = self._get_selected_ids()
        if not ids:
            return
        if not confirm('<p>'+_('<b>All formats</b> for the selected books will '
                               'be <b>deleted</b> from your library.<br>'
                               'The book metadata will be kept. Are you sure?') +
                       '</p>', 'delete_all_formats', self.gui):
            return
        db = self.gui.library_view.model().db
        removals = {}
        for id in ids:
            fmts = db.formats(id, index_is_id=True, verify_formats=False)
            if fmts:
                removals[id] = fmts.split(',')
        if removals:
            db.new_api.remove_formats(removals)
            self.gui.library_view.model().refresh_ids(ids)
            self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
                    self.gui.library_view.currentIndex())
            if ids:
                self.gui.tags_view.recount_with_position_based_index()

    def remove_matching_books_from_device(self, *args):
        if not self.gui.device_manager.is_device_present:
            d = error_dialog(self.gui, _('Cannot delete books'),
                             _('No device is connected'))
            d.exec()
            return
        ids = self._get_selected_ids()
        if not ids:
            # _get_selected_ids shows a dialog box if nothing is selected, so we
            # do not need to show one here
            return
        to_delete = {}
        some_to_delete = False
        for model,name in ((self.gui.memory_view.model(), _('Main memory')),
                           (self.gui.card_a_view.model(), _('Storage card A')),
                           (self.gui.card_b_view.model(), _('Storage card B'))):
            to_delete[name] = (model, model.paths_for_db_ids(ids))
            if len(to_delete[name][1]) > 0:
                some_to_delete = True
        if not some_to_delete:
            d = error_dialog(self.gui, _('No books to delete'),
                             _('None of the selected books are on the device'))
            d.exec()
            return
        d = DeleteMatchingFromDeviceDialog(self.gui, to_delete)
        if d.exec():
            paths = {}
            ids = {}
            for (model, id, path) in d.result:
                if model not in paths:
                    paths[model] = []
                    ids[model] = []
                paths[model].append(path)
                ids[model].append(id)
            cv, row = self.gui.current_view(), -1
            if cv is not self.gui.library_view:
                row = cv.currentIndex().row()
            for model in paths:
                job = self.gui.remove_paths(paths[model])
                self.delete_memory[job] = (paths[model], model)

                model.mark_for_deletion(job, ids[model], rows_are_ids=True)
            self.gui.status_bar.show_message(_('Deleting books from device.'), 1000)
            if row > -1:
                nrow = row - 1 if row > 0 else row + 1
                cv.set_current_row(min(cv.model().rowCount(None), max(0, nrow)))

    def delete_covers(self, *args):
        ids = self._get_selected_ids()
        if not ids:
            return
        if not confirm('<p>'+ngettext(
                'The cover from the selected book will be <b>permanently deleted</b>. Are you sure?',
                'The covers from the {} selected books will be <b>permanently deleted</b>. '
                'Are you sure?', len(ids)).format(len(ids)),
                'library_delete_covers', self.gui):
            return

        for id in ids:
            self.gui.library_view.model().db.remove_cover(id)
        self.gui.library_view.model().refresh_ids(ids)
        self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
                self.gui.library_view.currentIndex())

    def library_ids_deleted(self, ids_deleted, current_row=None):
        view = self.gui.library_view
        for v in (self.gui.memory_view, self.gui.card_a_view, self.gui.card_b_view):
            if v is None:
                continue
            v.model().clear_ondevice(ids_deleted)
        if current_row is not None:
            ci = view.model().index(current_row, 0)
            if not ci.isValid():
                # Current row is after the last row, set it to the last row
                current_row = view.row_count() - 1
            view.set_current_row(current_row)
        if view.model().rowCount(QModelIndex()) < 1:
            self.gui.book_details.reset_info()

    def library_ids_deleted2(self, ids_deleted, next_id=None):
        view = self.gui.library_view
        current_row = None
        if next_id is not None:
            rmap = view.ids_to_rows([next_id])
            current_row = rmap.get(next_id, None)
        self.library_ids_deleted(ids_deleted, current_row=current_row)

    def do_library_delete(self, to_delete_ids):
        view = self.gui.current_view()
        next_id = view.next_id
        # Ask the user if they want to delete the book from the library or device if it is in both.
        if self.gui.device_manager.is_device_present:
            on_device = False
            on_device_ids = self._get_selected_ids()
            for id in on_device_ids:
                res = self.gui.book_on_device(id)
                if res[0] or res[1] or res[2]:
                    on_device = True
                if on_device:
                    break
            if on_device:
                loc = confirm_location('<p>' + _('Some of the selected books are on the attached device. '
                                            '<b>Where</b> do you want the selected files deleted from?'),
                            self.gui)
                if not loc:
                    return
                elif loc == 'dev':
                    self.remove_matching_books_from_device()
                    return
                elif loc == 'both':
                    self.remove_matching_books_from_device()
        # The following will run if the selected books are not on a connected device.
        # The user has selected to delete from the library or the device and library.
        if not confirm('<p>'+ngettext(
                'The selected book will be <b>permanently deleted</b> and the files '
                'removed from your calibre library. Are you sure?',
                'The {} selected books will be <b>permanently deleted</b> and the files '
                'removed from your calibre library. Are you sure?', len(to_delete_ids)).format(len(to_delete_ids)),
                'library_delete_books', self.gui):
            return
        if len(to_delete_ids) < 5:
            try:
                view.model().delete_books_by_id(to_delete_ids)
            except OSError as err:
                if err.errno == errno.EACCES:
                    import traceback
                    fname = os.path.basename(getattr(err, 'filename', 'file') or 'file')
                    return error_dialog(self.gui, _('Permission denied'),
                            _('Could not access %s. Is it being used by another'
                            ' program? Click "Show details" for more information.')%fname, det_msg=traceback.format_exc(),
                            show=True)
                raise
            self.library_ids_deleted2(to_delete_ids, next_id=next_id)
        else:
            try:
                self.__md = MultiDeleter(self.gui, to_delete_ids,
                        partial(self.library_ids_deleted2, next_id=next_id))
            except Aborted:
                pass

    def delete_books(self, *args):
        '''
        Delete selected books from device or library.
        '''
        view = self.gui.current_view()
        rows = view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return
        # Library view is visible.
        if self.gui.stack.currentIndex() == 0:
            to_delete_ids = [view.model().id(r) for r in rows]
            self.do_library_delete(to_delete_ids)
        # Device view is visible.
        else:
            cv, row = self.gui.current_view(), -1
            if cv is not self.gui.library_view:
                row = cv.currentIndex().row()
            if self.gui.stack.currentIndex() == 1:
                view = self.gui.memory_view
            elif self.gui.stack.currentIndex() == 2:
                view = self.gui.card_a_view
            else:
                view = self.gui.card_b_view
            paths = view.model().paths(rows)
            ids = view.model().indices(rows)
            if not confirm('<p>'+ngettext(
                    'The selected book will be <b>permanently deleted</b> from your device. Are you sure?',
                    'The {} selected books will be <b>permanently deleted</b> from your device. Are you sure?', len(paths)).format(len(paths)),
                    'device_delete_books', self.gui):
                return
            job = self.gui.remove_paths(paths)
            self.delete_memory[job] = (paths, view.model())
            view.model().mark_for_deletion(job, ids, rows_are_ids=True)
            self.gui.status_bar.show_message(_('Deleting books from device.'), 1000)
            if row > -1:
                nrow = row - 1 if row > 0 else row + 1
                cv.set_current_row(min(cv.model().rowCount(None), max(0, nrow)))
