#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2022, Thiago Oliveira <thiago.eec@gmail.com>'
__docformat__ = 'restructuredtext en'

# Load translation files (.mo) on the folder 'translations'
load_translations()

# Standard libraries
import os
import traceback
import shutil
import time
import threading
import json
import zipfile
import xml.etree.ElementTree as ET
from urllib.request import urlretrieve
from queue import Queue
from functools import partial
from timeit import default_timer as timer
from datetime import datetime, timedelta

# PyQt libraries
try:
    from qt.core import (Qt, QApplication, QVBoxLayout, QTextBrowser, QSize, QDialogButtonBox,
                         QTimer, QEventLoop, QDialog, QListWidget, QAbstractItemView)
except ImportError:
    from PyQt5.Qt import (Qt, QApplication, QVBoxLayout, QTextBrowser, QSize, QDialogButtonBox,
                          QTimer, QEventLoop, QDialog, QListWidget, QAbstractItemView)

# Calibre libraries
from calibre.utils.config import config_dir
from calibre.constants import iswindows, islinux, isosx
from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN
from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.dialogs.message_box import MessageBox
from calibre_plugins.Check_Books.utils import ProgressDialog
from calibre_plugins.Check_Books.utils import (wrapper, get_epc_version, latest_epc_version, string_to_date,
                                               get_icon, is_connected, make_temp_directory, prefs,
                                               get_title_authors_text, show_invalid_rows, stop_event,
                                               error_event, plugin_log, temp_log, check_custom_columns)
from calibre_plugins.Check_Books.config import user_language
from calibre_plugins.Check_Books.__init__ import PLUGIN_NAME


def update_ace(self):
    # Make sure we have an Internet connection
    if is_connected():
        msg = _('Running update check...')
        temp_log.append(('info', msg))
        self.gui.status_bar.show_message(msg)
        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)

        # Check if an update is available
        check_args = ['npm', 'outdated', '-g']
        update_check = wrapper(self, *check_args, origin='ace_update_check')[0]
        stderr = update_check[1]
        if b'\'npm\'' in stderr:
            error_tip = _('Node.js is not installed.')
            if error_tip not in temp_log:
                temp_log.append('error_event_set')
                temp_log.append(error_tip)
                msg = error_tip + _('\nInstall Node.js 10 or higher, then run: '
                                    '\'npm install @daisy/ace -g\' on a cmd/terminal window.')
                temp_log.append(('error', msg))
                error_event.set()
                return

        if '@daisy/ace' in str(update_check):
            msg = _('Updating ACE...')
            temp_log.append(('info', msg))
            self.gui.status_bar.show_message(msg)
            QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)

            # Update ACE to the latest version
            update_args = ['npm', 'install', '@daisy/ace', '-g']
            update_return_code = wrapper(self, *update_args, origin='ace_update')[1]

            if update_return_code == 0:
                version_args = ['ace', '-v']
                result = wrapper(self, *version_args, origin='ace_update_check')[0]
                version_check = result[0].decode('utf-8').replace('\n', '')
                msg = _('ACE was successfully updated to version <b>%s</b>.') % version_check + '\n'
                temp_log.append(('info', msg))
                self.gui.status_bar.show_message(_('ACE was successfully updated to version %s.') % version_check)
                QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
            else:
                msg = _('Update failed.') + '\n'
                temp_log.append(('info', msg))
                self.gui.status_bar.show_message(msg)
                QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
        else:
            msg = _('No updates found.') + '\n'
            temp_log.append(('info', msg))
            self.gui.status_bar.show_message(msg)
            QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)

        # Update time stamp in Check_Books.json
        prefs.set('ace_last_time_checked', str(datetime.now()))
        prefs.commit()
        time.sleep(1)
        self.gui.status_bar.clear_message()
    else:
        msg = _('Update check skipped: no internet.') + '\n'
        temp_log.append(('info', msg))
        self.gui.status_bar.show_message(msg)
        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)


def update_epubcheck(self):
    check_books_dir = os.path.join(config_dir, 'plugins', 'Check_Books')
    epubcheck_dir = os.path.join(check_books_dir, 'EPUBCheck')
    epc_path = os.path.join(check_books_dir, 'EPUBCheck', 'epubcheck.jar')
    epc_lib_dir = os.path.join(check_books_dir, 'EPUBCheck', 'lib')
    github_url = 'https://api.github.com/repos/w3c/epubcheck/releases'

    # Make sure we have an Internet connection
    if is_connected():
        # Display searching for updates... message
        msg = _('Running update check...')
        temp_log.append(('info', msg))
        self.gui.status_bar.show_message(msg)
        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
        time.sleep(0.5)

        # Update time stamp in Check_Books.json
        prefs.set('epubcheck_last_time_checked', str(datetime.now()))
        prefs.commit()

        # Get current epubcheck version from epubcheck.jar
        epc_version = get_epc_version(epc_path)

        # Get the latest version and browser download url
        latest_version, browser_download_url = latest_epc_version(github_url)

        if 'alpha' in browser_download_url or 'beta' in browser_download_url:  # Exclude alpha/beta versions
            browser_download_url = ''

        # Only run the update if a new version is available
        if latest_version not in ('preview', epc_version, '') and browser_download_url != '' and epc_version != '':

            # Create a temp folder
            with make_temp_directory() as td:
                base_name = os.path.basename(browser_download_url)
                root_path = os.path.splitext(base_name)[0]
                zip_file_name = os.path.join(td, base_name)

                # Display status message
                msg = _('Downloading ') + '{}'.format(browser_download_url)
                temp_log.append(('info', msg))
                self.gui.status_bar.show_message(msg.format(browser_download_url))
                QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)

                # Download the zip file
                urlretrieve(browser_download_url, zip_file_name)

                # Make sure the file was actually downloaded
                if os.path.exists(zip_file_name):
                    # Read zip file
                    # https://stackoverflow.com/questions/19618268/extract-and-rename-zip-file-folder
                    archive = zipfile.ZipFile(zip_file_name)
                    files = archive.namelist()
                    files_to_extract = [m for m in files if
                                        (m.startswith(root_path + '/lib') or m == root_path + '/epubcheck.jar')]
                    archive.extractall(td, files_to_extract)
                    archive.close()

                    # Temp paths to epubcheck.jar and the /lib folder
                    temp_epc_path = os.path.join(td, root_path + '/epubcheck.jar')
                    temp_epc_lib_dir = os.path.join(td, root_path + '/lib')

                    # Make sure the files were actually extracted
                    if os.path.isdir(temp_epc_lib_dir) and os.path.isfile(temp_epc_path):
                        # Delete /lib folder
                        if os.path.isdir(epc_lib_dir):
                            shutil.rmtree(epc_lib_dir)

                        # Delete epubcheck.jar
                        if os.path.exists(epc_path):
                            os.remove(epc_path)

                        # Move new files to the plugin folder
                        shutil.move(temp_epc_lib_dir, epubcheck_dir)
                        shutil.move(temp_epc_path, epubcheck_dir)

                        # Ensure you have execute rights for unix based platforms
                        if isosx or islinux:
                            os.chmod(epc_path, 0o744)

                        # Display update successful message
                        msg = (_('Updated to EPUBCheck {}.') + '\n').format(latest_version)
                        temp_log.append(('info', msg))
                        self.gui.status_bar.show_message(msg)
                        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
                        time.sleep(1)

                    else:
                        # Display unzip error message
                        msg = _('Update failed. The .zip file couldn\'t be unpacked.')
                        temp_log.append(('info', '<span style="color:red">' + msg + '</span>' + '\n'))
                        self.gui.status_bar.show_message(msg)
                        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
                else:
                    # Display download error message
                    msg = _('Update failed. The latest .zip file couldn\'t be downloaded.')
                    temp_log.append(('info', '<span style="color:red">' + msg + '</span>' + '\n'))
                    self.gui.status_bar.show_message(msg)
                    QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)

        else:
            # Display miscellaneous internal error messages
            if latest_version != '':
                msg = _('No updates found.')
                temp_log.append(('info', msg + '\n'))
                self.gui.status_bar.show_message(msg)
                QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
            elif epc_version == '':
                msg = _('Current EPUBCheck version not found.')
                temp_log.append(('info', '<span style="color:red">' + msg + '</span>' + '\n'))
                self.gui.status_bar.show_message(msg)
                QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
            else:
                msg = _('Internal error: update check failed.')
                temp_log.append(('info', '<span style="color:red">' + msg + '</span>' + '\n'))
                self.gui.status_bar.show_message(msg)
                QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
    else:
        # Display no Internet error message
        msg = _('Update check skipped: no internet.')
        temp_log.append(('info', '<span style="color:red">' + msg + '</span>' + '\n'))
        self.gui.status_bar.show_message(msg)
        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)

    time.sleep(1)
    self.gui.status_bar.clear_message()


def do_config(self, current_tab=0):

    from calibre.gui2.widgets2 import Dialog
    from calibre_plugins.Check_Books.config import ConfigWidget

    class ConfigDialog(Dialog):

        def __init__(self, gui, tab):
            self.gui = gui
            self.tab = tab
            Dialog.__init__(self, _('Options'), 'plugin-book-report-config-dialog')
            self.setWindowIcon(get_icon('images/icon.png'))

        def setup_ui(self):
            self.box = QVBoxLayout(self)
            self.widget = ConfigWidget(self, self.gui, self.tab)
            self.box.addWidget(self.widget)
            self.button = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
            self.box.addWidget(self.button)
            self.button.accepted.connect(self.accept)
            self.button.rejected.connect(self.reject)

        def accept(self):
            self.widget.save_settings()
            Dialog.accept(self)

    d = ConfigDialog(self, current_tab)
    d.exec_()


def show_configuration(self, tab=0):
    restart_message = _('Calibre must be restarted before the plugin can be configured.')
    # Check if a restart is needed. If the restart is needed, but the user does not
    # trigger it, the result is true and we do not do the configuration.
    if check_if_restart_needed(self, restart_message=restart_message):
        return

    do_config(self, tab)
    restart_message = _('New custom columns have been created. You will need'
                        '\nto restart calibre for this change to be applied.'
                        )
    check_if_restart_needed(self, restart_message=restart_message)


def check_if_restart_needed(self, restart_message=None, restart_needed=False):
    if self.gui.must_restart_before_config or restart_needed:
        if restart_message is None:
            restart_message = _('Calibre must be restarted before the plugin can be configured.')
        from calibre.gui2 import show_restart_warning
        do_restart = show_restart_warning(restart_message)
        if do_restart:
            self.gui.quit(restart=True)
        else:
            return True
    return False


class ViewLog(QDialog):

    def __init__(self, title, html, parent=None):
        QDialog.__init__(self, parent)
        self.l = QVBoxLayout(self)

        self.tb = QTextBrowser(self)
        QApplication.setOverrideCursor(Qt.WaitCursor)
        # Rather than formatting the text in <pre> blocks like the calibre
        # ViewLog does, instead just format it inside divs to keep style formatting
        html = html.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;').replace('\n', '<br/>')
        html = html.replace('> ', '>&nbsp;')
        self.tb.setHtml('<div>%s</div>' % html)
        QApplication.restoreOverrideCursor()
        self.l.addWidget(self.tb)

        self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
        self.bb.accepted.connect(self.accept)
        self.bb.rejected.connect(self.reject)
        self.copy_button = self.bb.addButton(_('Copy to clipboard'), self.bb.ActionRole)
        self.copy_button.setIcon(get_icon('edit-copy.png'))
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.l.addWidget(self.bb)
        # self.setModal(False)
        self.resize(QSize(700, 500))
        self.setWindowTitle(title)
        self.setWindowIcon(get_icon('debug.png'))
        self.show()

    def copy_to_clipboard(self):
        txt = self.tb.toPlainText()
        QApplication.clipboard().setText(txt)


class ResultsSummaryDialog(MessageBox):

    def __init__(self, parent, title, msg, det_msg=''):
        '''
        A modal popup that summarises the result of Check Books with
        opportunity to review the log.

        :param title: The title for this popup
        :param msg: The msg to display
        :param log: An HTML or plain text log
        :param det_msg: Detailed message
        '''
        self.first_paint = True
        MessageBox.__init__(self, MessageBox.INFO, title, msg, det_msg=det_msg, q_icon=get_icon('images/icon.png'),
                            show_copy_button=False, parent=parent)
        self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole)
        self.vlb.setIcon(get_icon('debug.png'))
        self.vlb.clicked.connect(self.show_log)
        self.det_msg_toggle.setVisible(bool(det_msg))
        self.process_log()
        if len(temp_log) <= 2:
            self.vlb.setVisible(False)
        else:
            self.vlb.setVisible(True)

    # Format the execution time according to its length
    def timedelta_to_string(self, time_delta):
        t_time = str(timedelta(seconds=time_delta))
        h, m, s = t_time.split(':')[0], t_time.split(':')[1], t_time.split(':')[2].split('.', 2)[0]
        if h == '0':
            if m == '00':
                formatted_time = '%ss' % s
            else:
                formatted_time = '%sm%ss' % (m, s)
        else:
            if len(h) == 1:
                formatted_time = '0%sh%sm%ss' % (h, m, s)
            else:
                formatted_time = '%sh%sm%ss' % (h, m, s)
        return formatted_time

    # Process the temp_log into a GUILog
    def process_log(self):
        plugin_log.clear()
        while True:
            # Opening
            for entry in temp_log:
                if len(entry) == 2 and entry[0] == 'opening':
                    plugin_log.info(entry[1])

            # Errors
            for entry in temp_log:
                # We only use 'error' when the plugin must stop running
                if len(entry) == 2 and entry[0] == 'error':
                    plugin_log.error(entry[1])
                    temp_log.append('error_event_set')
                    break

            if 'error_event_set' in temp_log:
                break

            # General info, like updates
            for entry in temp_log:
                if len(entry) == 2 and entry[0] == 'info':
                    plugin_log.info(entry[1])

            # Create a summary for checked books
            total_time = self.timedelta_to_string(CheckBooksTools.total_time[0])
            plugin_log.info('<b>' + _('Summary') + ':' + '</b>')
            plugin_log.info('\t' + _('Execution: %s') % total_time)
            plugin_log.info('\t' + _('Passed') + ': ' + str(len(CheckBooksTools.passed_ids)))
            plugin_log.info('\t' + _('Failed') + ': ' + str(len(CheckBooksTools.failed_ids)))
            plugin_log.info('\t' + _('Invalid') + ': ' + str(len(CheckBooksTools.invalid_ids)))
            plugin_log.info('\t' + _('Skipped') + ': ' + str(len(CheckBooksTools.skipped_ids)) + '\n')

            # Passed books
            for entry in temp_log:
                if len(entry) == 3 and entry[2] == _('Pass'):
                    plugin_log.info(entry[1])

            # Failed books
            for entry in temp_log:
                if len(entry) == 3 and entry[2] == _('Fail'):
                    plugin_log.info(entry[1])

            # Invalid books
            for entry in temp_log:
                if len(entry) == 3 and entry[2] == _('Invalid EPUB or DRMed'):
                    plugin_log.info(entry[1])

            # Skipped books
            for entry in temp_log:
                if len(entry) == 2 and entry[0] == 'warn':
                    plugin_log.warn(entry[1])
            break

    def show_log(self):
        self.log_viewer = ViewLog(_('Check Books log'), plugin_log.html, parent=self)

    def showEvent(self, event):
        if self.first_paint:
            if 'results_dialog_geometry' in prefs:
                self.restoreGeometry(prefs['results_dialog_geometry'])
            self.first_paint = False
        marked_ids = CheckBooksTools.failed_ids + CheckBooksTools.invalid_ids
        show_invalid_rows(marked_ids)
        return QDialog.showEvent(self, event)

    def save_geometry(self):
        prefs['results_dialog_geometry'] = self.saveGeometry()

    def reject(self):
        self.save_geometry()
        MessageBox.reject(self)

    def accept(self):
        self.save_geometry()
        MessageBox.accept(self)


class CheckBooksProgressDialog(ProgressDialog):

    def __init__(self, gui, book_ids, check_option, db, processed_ids):
        self.user_canceled = False
        self.total_count = len(book_ids)
        self.li = QListWidget()
        self.li.setSelectionMode(QAbstractItemView.NoSelection)
        main_icon = get_icon('images/icon.png')
        if check_option.__name__ == 'check_with_ace':
            check_name = 'ACE'
            action_icon = get_icon('images/ace.png')
        else:
            check_name = 'EPUBCheck'
            action_icon = get_icon('images/epubcheck.png')
        ProgressDialog.__init__(self, PLUGIN_NAME, self.li, max=self.total_count,
                                parent=gui, window_icon=main_icon, icon=action_icon)
        self.book_ids, self.check_option, self.db, self.processed_ids = book_ids, check_option, db, processed_ids
        if self.total_count == 1:
            self.status_msg_type = _('%d book') % self.total_count
        elif self.total_count > 1:
            self.status_msg_type = _('%d books') % self.total_count
        self.set_title(_('%s %s with %s...') % (_('Checking'), self.status_msg_type, check_name))
        self.progress_event = threading.Event()
        self.gui = gui
        self.i = 0

        # List of running checks
        self.running_list = []

        QTimer.singleShot(0, partial(self.run_parallel_in_threads, self.check_option, self.book_ids))

        self.exec_()

    def run_parallel_in_threads(self, check_option, args_list):
        self.running_list.clear()
        self.error_track = None

        # Get preferences
        self.key_threads_idx = prefs['threads_idx']

        self.check_option = check_option
        self.args_list = args_list

        jobs = Queue()

        self.canceled_signal.connect(self._do_close)

        # Get jobs from queue and send to available threads
        def do_check(q):
            while not q.empty():
                if error_event.is_set():
                    self.i = self.total_count
                    self.progress_event.set()
                    break
                book_id = q.get()
                book_path = self.db.format_abspath(book_id, 'EPUB')
                if book_path is not None:
                    # Put book on the list of running checks
                    mi = self.db.get_metadata(book_id)
                    book_title = mi.title
                    self.running_list.append(book_title)
                    # Run the check
                    self.error_track = self.check_option(book_path, book_id, self.db)
                    # Mark the book as processed
                    if 'error_event_set' not in temp_log and 'stop_event_set' not in temp_log:
                        self.processed_ids.append(book_id)
                    # Remove the book from the list, after being processed
                    self.running_list.remove(book_title)
                else:
                    status = _('Skipped - No EPUB format')
                    msg_1 = '<b>%s</b>' % get_title_authors_text(self.db, book_id)
                    msg_2 = '\t<span>%s</span>' % status
                    CheckBooksTools.skipped_ids.append(book_id)
                    temp_log.append(('warn', msg_1))
                    temp_log.append(('warn', msg_2))
                self.i += 1
                self.progress_event.set()
                self.progress_event.clear()
                q.task_done()
            error_event.clear()

        for arg in self.args_list:
            jobs.put(arg)

        # Define the number of threads based on the available CPUs: n - 1
        if self.total_count <= round(self.key_threads_idx+1):
            num_threads = self.total_count
        else:
            num_threads = round(self.key_threads_idx+1)

        for i in range(num_threads):
            worker = threading.Thread(target=do_check, args=(jobs,))
            worker.start()

        # Update dialog info
        def update_progress():
            QApplication.processEvents()
            li = self.li
            # Clear running list
            li.clear()
            # Add the updated list
            li.addItems(self.running_list)
            li.setMinimumHeight(li.sizeHintForRow(0) * li.count() + 4 * li.frameWidth())
            # Update progress bar
            QApplication.processEvents()
            self.set_value(self.i)

        # print('Waiting for queue to complete', jobs.qsize(), 'tasks')

        while self.progress_event.is_set:
            QApplication.processEvents()
            update_progress()
            if self.i >= self.total_count:
                QApplication.processEvents()
                if error_event.is_set():
                    pass
                else:
                    time.sleep(1)
                prefs['main_dialog_geometry'] = self.saveGeometry()
                self.hide()
                break

        # jobs.join()
        # print('All done')

        # We only have an output from the checking, when there are errors
        if self.error_track:
            error_title = self.error_track[0]
            error_msg = self.error_track[1]
            det_msg = self.error_track[2]
            if error_msg is None:
                error_msg = _('Something went wrong. Click on \'Show details\' for more info.')
            error_dialog(self.gui, error_title, error_msg, det_msg=det_msg, show=True)

    def _do_close(self):
        self.user_canceled = True
        stop_event.set()
        temp_log.append('stop_event_set')


class CheckBooksTools:
    # Lis of ids based on status
    processed_ids = []
    passed_ids = []
    failed_ids = []
    invalid_ids = []
    skipped_ids = []

    total_time = []

    def __init__(self, gui):
        self.gui = gui

    def check_book(self, check_option):
        # Check if restart is needed
        restart_message = _('Calibre must be restarted before using the plugin.')
        if check_if_restart_needed(self, restart_message=restart_message):
            return

        # Check if the saved custom columns still exist
        prefs_custom_columns = {
            'ace_report_column': prefs['ace_report_column'],
            'ace_result_column': prefs['ace_result_column'],
            'epubcheck_report_column': prefs['epubcheck_report_column'],
            'epubcheck_result_column': prefs['epubcheck_result_column']
        }
        for key, column in prefs_custom_columns.items():
            if not check_custom_columns(self, column):
                prefs[key] = ''

        # Clear canceled state
        stop_event.clear()

        # Clear plugin log before new run
        temp_log.clear()

        # Clear id lists
        self.processed_ids.clear()
        self.failed_ids.clear()
        self.passed_ids.clear()
        self.invalid_ids.clear()
        self.skipped_ids.clear()

        # Clear timer
        self.total_time.clear()

        # Get preferences
        self.key_ace_report_column = prefs['ace_report_column']
        self.key_ace_result_column = prefs['ace_result_column']
        self.key_epubcheck_report_column = prefs['epubcheck_report_column']
        self.key_epubcheck_result_column = prefs['epubcheck_result_column']
        from calibre.utils.config import prefs as calibre_prefs
        self.lp = calibre_prefs['library_path']

        # Get currently selected books
        rows = self.gui.library_view.selectionModel().selectedRows()

        # Check if selection is empty
        if not rows or len(rows) == 0:
            return error_dialog(self.gui, _('Empty selection'), _('No books selected'),
                                show_copy_button=False, show=True)

        if check_option.__name__ == 'check_with_ace':
            # Open new log
            msg = '*******************************************'\
                  + _('***** ACE log *****') \
                  + '******************************************* \n'
            temp_log.append(('opening', msg))

            # Check if ACE report column is set
            # if len(self.key_ace_report_column) == 0:
            #     if question_dialog(self.gui, _('Configuration needed'),
            #                        _('You must choose the ACE report custom column') + '. '
            #                        + _('\nDo you want to configure the plugin now?'), show_copy_button=False):
            #         return show_configuration(self, tab=1)
            #     else:
            #         return

            # Check if ACE result column is set
            if len(self.key_ace_result_column) == 0:
                if question_dialog(self.gui, _('Configuration needed'),
                                   _('You must choose the ACE result custom column') + '. '
                                   + _('\nDo you want to configure the plugin now?'), show_copy_button=False):
                    return show_configuration(self, tab=1)
                else:
                    return

            # Get update preferences
            key_ace_update = prefs['ace_update']
            key_ace_check_interval = prefs['ace_check_interval']
            key_ace_last_time_checked = prefs['ace_last_time_checked']

            # Check for ACE updates
            if key_ace_update:
                # Compare current date against last update check date
                time_delta = (datetime.now() - string_to_date(key_ace_last_time_checked)).days
                if time_delta >= key_ace_check_interval:
                    update_ace(self)

        elif check_option.__name__ == 'check_with_epubcheck':
            # Open new log
            msg = '****************************************'\
                  + _('**** EPUBCheck log ****') \
                  + '**************************************** \n'
            temp_log.append(('opening', msg))

            # Check if EPUBCheck report column is set
            # if len(self.key_epubcheck_report_column) == 0:
            #     if question_dialog(self.gui, _('Configuration needed'),
            #                        _('You must choose the EPUBCheck report custom column') + '. '
            #                        + _('\nDo you want to configure the plugin now?'), show_copy_button=False):
            #         return show_configuration(self, tab=2)
            #     else:
            #         return

            # Check if EPUBCheck result column is set
            if len(self.key_epubcheck_result_column) == 0:
                if question_dialog(self.gui, _('Configuration needed'),
                                   _('You must choose the EPUBCheck result custom column') + '. '
                                   + _('\nDo you want to configure the plugin now?'), show_copy_button=False):
                    return show_configuration(self, tab=2)
                else:
                    return

            # Get update preferences
            key_epubcheck_update = prefs['epubcheck_update']
            key_epubcheck_check_interval = prefs['epubcheck_check_interval']
            key_epubcheck_last_time_checked = prefs['epubcheck_last_time_checked']

            # Check for EPUBCheck updates
            if key_epubcheck_update:
                # Compare current date against last update check date
                time_delta = (datetime.now() - string_to_date(key_epubcheck_last_time_checked)).days
                if time_delta >= key_epubcheck_check_interval:
                    update_epubcheck(self)

        # Map the rows to book ids
        book_ids = list(map(self.gui.library_view.model().id, rows))
        db = self.gui.current_db.new_api

        start = timer()

        # Show the main dialog, with a progress bar and a detailed list of active checks
        d = CheckBooksProgressDialog(self.gui, book_ids, check_option, db, self.processed_ids)

        end = timer()
        self.total_time.append(end - start)
        print(timedelta(seconds=self.total_time[0]))

        canceled_msg = ''
        if d.user_canceled:
            canceled_msg = _(' (canceled)')
        if len(d.processed_ids) == 0:
            msg = _('No books checked%s') % canceled_msg
        elif len(d.processed_ids) == 1:
            msg = _('%d book checked%s, see log for details') % (len(d.processed_ids), canceled_msg)
        else:
            msg = _('%d books checked%s, see log for details') % (len(d.processed_ids), canceled_msg)
        sd = ResultsSummaryDialog(self.gui, 'Check Books', msg)
        sd.exec_()

        # Refresh the GUI, so the Book Info panel shows the new info
        self.gui.library_view.model().refresh_ids(book_ids, current_row=self.gui.library_view.currentIndex().row())

    def check_with_ace(self, book_path, book_id, db):
        # Check if the user canceled the checking
        if stop_event.is_set():
            return

        # Get preferences
        self.key_user_lang = prefs['user_lang']
        self.key_report_path = prefs['report_path']
        self.key_calibre_library_folders = prefs['calibre_library_folders']
        self.key_ace_report_format = prefs['ace_report_format']
        self.key_ace_report_column = prefs['ace_report_column']
        self.key_ace_result_column = prefs['ace_result_column']

        if self.key_calibre_library_folders:
            check_books_path = os.path.join(os.path.dirname(book_path), 'data/ACE')
        else:
            check_books_path = os.path.join(self.key_report_path,
                                            os.path.dirname(os.path.relpath(book_path, self.lp)), 'ACE')
        json_file_name = os.path.join(check_books_path, 'report.json')
        html_file_name = os.path.join(check_books_path, 'report.html')
        html_data_dir = os.path.join(check_books_path, 'data')
        # New folder introduced with ACE 1.2.6
        report_html_files_dir = os.path.join(check_books_path, 'report-html-files')

        error_title = _('ACE error')

        # Define ACE command line parameters
        args = ['ace', '-s', '-f', '-o', check_books_path, '-l', self.key_user_lang, book_path]

        # Run ACE
        try:
            try:
                # Run
                result, return_code = wrapper(self, *args, origin='run_ace')
                # print('Book: ', get_title_authors_text(db, book_id),
                #       '\n    Result: ', result,
                #       '\n    Return code: ', return_code )
                stdout = result[0]
                stderr = result[1]
                # try:
                #     stdout = result[0].decode('utf-8')
                #     stderr = result[1].decode('utf-8')
                # except:
                #     pass
            except:
                error_event.set()
                temp_log.append('error_event_set')
                return error_title, None, traceback.format_exc()

            # Check again if the user canceled the checking
            if stop_event.is_set():
                return

            # Avoid duplication of error messages
            if 'error_event_set' not in temp_log:
                if return_code == 1:
                    # Get ACE errors
                    # ACE only gives 1 as return code when the file can't be processed.
                    # Otherwise, it returns 0, even if the book has errors.
                    if b'\'ace\'' in stderr:
                        error_tip = _('ACE is not installed.')
                        if error_tip not in temp_log:
                            temp_log.append('error_event_set')
                            temp_log.append(error_tip)
                            msg = error_tip + _('\nInstall Node.js 10 or higher, then run: '
                                                '\'npm install @daisy/ace -g\' on a cmd/terminal window.')
                            temp_log.append(('error', msg))
                            error_event.set()
                    else:
                        # Rerun ACE on books that failed to complete the check
                        # This bug was introduced after ACE 1.3.1 (updated from 1.2.7)
                        if b'-fail-load' in stdout:
                            self.check_with_ace(book_path, book_id, db)
                        else:
                            status = _('Invalid EPUB or DRMed')
                            self.invalid_ids.append(book_id)
                            msg_1 = '<span style="color:red"><b>%s</b></span>' % get_title_authors_text(db, book_id)
                            msg_2 = '\t<span style="color:red">%s</span>' % status
                            temp_log.append(('info', msg_1, status))
                            temp_log.append(('info', msg_2, status))
                else:
                    # If ACE succeeded, there should be a report file
                    if os.path.isfile(json_file_name):
                        with open(json_file_name, 'r') as file:
                            json_string = file.read()
                        parsed_json = json.loads(json_string)
                        earl_outcome = parsed_json['earl:result']['earl:outcome']

                        # Create a link for the report
                        if len(self.key_ace_report_column) != 0:
                            if self.key_ace_report_format == 'JSON':
                                link = ('<a href="file:///' + json_file_name.replace('\\', '/')
                                        + '">ACE JSON</a>')
                            elif self.key_ace_report_format == 'HTML':
                                link = ('<a href="file:///' + html_file_name.replace('\\', '/')
                                        + '">ACE HTML</a>')
                            else:
                                link = ('<p><a href="file:///' + json_file_name.replace('\\', '/')
                                        + '">ACE JSON</a></p>' +
                                        '<p><a href="file:///' + html_file_name.replace('\\', '/')
                                        + '">ACE HTML</a></p>')
                            db.new_api.set_field(self.key_ace_report_column, {book_id: link})

                        # Get ACE result
                        if earl_outcome == 'fail':
                            status, color = _('Fail'), 'red'
                            self.failed_ids.append(book_id)
                            db.new_api.set_field(self.key_ace_result_column, {book_id: False})
                        else:
                            status, color = _('Pass'), 'green'
                            self.passed_ids.append(book_id)
                            db.new_api.set_field(self.key_ace_result_column, {book_id: True})

                        msg_1 = '<b>%s</b>' % get_title_authors_text(db, book_id)
                        msg_2 = '\t<span style="color:%s">%s</span>' % (color, status)
                        temp_log.append(('book', msg_1, status))
                        temp_log.append(('info', msg_2, status))

                    elif not os.path.isfile(json_file_name):
                        error_event.set()
                        temp_log.append('error_event_set')
                        error_msg = _('Something went wrong. ACE did not produce a report.')
                        return _('No report found'), error_msg, traceback.format_exc()

                    else:
                        return

                    # Ensure you have execute rights for unix based platforms
                    if isosx or islinux:
                        os.chmod(check_books_path, 0o744)

                    # Keep only preferred output
                    if len(self.key_ace_report_column) != 0:
                        self.key_ace_report_format = prefs['ace_report_format']
                        if self.key_ace_report_format == 'JSON':
                            if os.path.isfile(html_file_name):
                                os.remove(html_file_name)
                                shutil.rmtree(html_data_dir)
                                try:
                                    shutil.rmtree(report_html_files_dir)
                                except:
                                    pass
                        elif self.key_ace_report_format == 'HTML':
                            if os.path.isfile(json_file_name):
                                os.remove(json_file_name)
                    else:
                        shutil.rmtree(check_books_path)
                        if self.key_calibre_library_folders:
                            extra_files = db.list_extra_files(int(book_id), pattern=DATA_FILE_PATTERN)
                            if not extra_files:
                                data_folder = os.path.join(os.path.dirname(book_path), 'data')
                                shutil.rmtree(data_folder)

        except:
            error_event.set()
            temp_log.append('error_event_set')
            return error_title, None, traceback.format_exc()

    def check_with_epubcheck(self, book_path, book_id, db):
        # Check if the user canceled the checking
        if stop_event.is_set():
            return

        # Get preferences
        self.key_report_path = prefs['report_path']
        self.key_calibre_library_folders = prefs['calibre_library_folders']
        self.key_user_lang = prefs['user_lang']
        self.key_epubcheck_report_format = prefs['epubcheck_report_format']
        self.key_epubcheck_report_column = prefs['epubcheck_report_column']
        self.key_epubcheck_result_column = prefs['epubcheck_result_column']
        self.key_epubcheck_usage = prefs['epubcheck_usage']
        self.key_epubcheck_java_path = prefs['epubcheck_java_path']
        self.key_epubcheck_32bits = prefs['epubcheck_32bits']

        error_title = _('EPUBCheck error')

        # Report path
        if self.key_calibre_library_folders:
            check_books_path = os.path.join(os.path.dirname(book_path), 'data/EPUBCheck')
        else:
            check_books_path = os.path.join(self.key_report_path,
                                            os.path.dirname(os.path.relpath(book_path, self.lp)), 'EPUBCheck')
        report_file_name = os.path.join(check_books_path, 'EPUBCheck ' + self.key_epubcheck_report_format + ' report.'
                                        + self.key_epubcheck_report_format.lower())

        # Create Check Books folder, if it doesn't exist
        if not os.path.isdir(check_books_path):
            os.makedirs(check_books_path)

        # Define EPUBCheck paths and URL
        check_books_dir = os.path.join(config_dir, 'plugins', 'Check_Books')
        epc_path = os.path.join(check_books_dir, 'EPUBCheck', 'epubcheck.jar')
        epc_lib_dir = os.path.join(check_books_dir, 'EPUBCheck', 'lib')

        # Unpack EPUBCheck java files, if either epubcheck.jar or the lib folder is missing
        if not os.path.isdir(check_books_dir) or not os.path.isfile(epc_path) or not os.path.isdir(epc_lib_dir):
            plugin_zip = os.path.join(config_dir, 'plugins', PLUGIN_NAME + '.zip')
            with zipfile.ZipFile(plugin_zip, 'r') as zf:
                # Get a list of all archived file names from the zip
                file_list = zf.namelist()
                # Iterate over the file names
                for file_name in file_list:
                    if file_name.endswith('.jar'):
                        # Extract the file from zip
                        try:
                            zf.extract(file_name, check_books_dir)
                        except FileExistsError:
                            pass

            # Ensure you have execute rights for unix based platforms
            if isosx or islinux:
                os.chmod(epc_path, 0o744)

        # Make sure that EPUBCheck java files were actually unpacked
        if not os.path.isfile(epc_path) or not os.path.isdir(epc_lib_dir):
            error_event.set()
            temp_log.append('error_event_set')
            msg = _('The EPUBCheck Java files couldn\'t be installed. Please re-install the plugin.')
            return temp_log.append(('error', msg))

        # Run EPUBCheck
        try:
            # Define EPUBCheck command line parameters
            if self.key_epubcheck_32bits:
                args = [self.key_epubcheck_java_path, '-Dfile.encoding=UTF8', '-Xss1024k', '-jar', epc_path, '--quiet']
            else:
                args = [self.key_epubcheck_java_path, '-Dfile.encoding=UTF8', '-jar', epc_path, '--quiet']

            # Override user default locale
            if self.key_user_lang != user_language[0]:
                # EPUBCheck uses the user locale by default.
                # BUG: It falls back to EN when you use the system locale as the argument
                args.extend(['--locale', self.key_user_lang])

            # Display usage messages
            if self.key_epubcheck_usage:
                args.append('--usage')

            # Define report format
            if self.key_epubcheck_report_format == 'JSON':
                args.extend(['--json', report_file_name])
            elif self.key_epubcheck_report_format == 'XML':
                args.extend(['--out', report_file_name])
            elif self.key_epubcheck_report_format == 'XMP':
                args.extend(['--xmp', report_file_name])

            # The book path must be the last argument
            args.append(book_path)

            # Run
            try:
                result, return_code = wrapper(self, *args, origin='run_epubcheck')
                stdout = result[0]
                stderr_file = os.path.join(config_dir, 'plugins', 'Check_Books', 'stderr_file.txt')
                with open(stderr_file, 'r') as file:
                    stderr = file.read()
                try:
                    stdout = result[0].decode('utf-8')
                    stderr = result[1].decode('utf-8')
                except:
                    pass

                # Check for Java errors
                try:
                    if return_code == 1 and 'java.lang.' in stderr:
                        error_tip = _('Fatal Java error')
                        if error_tip not in temp_log:
                            error_event.set()
                            temp_log.append('error_event_set')
                            temp_log.append(error_tip)
                            msg = error_tip + '. ' + _('Something went wrong with Java. Maybe you have a 32 bits '
                                                      'Java installed. Try checking the \'32 bits Java\' option.')
                            temp_log.append(('error', msg))
                except:
                    pass

            except FileNotFoundError:
                error_tip = _('Java binary could not be found.')
                if error_tip not in temp_log:
                    error_event.set()
                    temp_log.append('error_event_set')
                    temp_log.append(error_tip)
                    msg = error_tip + ' ' + _('Check the Java path option, or install/update Java.')
                    temp_log.append(('error', msg))
                    return
            except:
                error_event.set()
                temp_log.append('error_event_set')
                return error_title, None, traceback.format_exc()

            # Check again if the user canceled the checking
            if stop_event.is_set():
                return

            # Avoid duplication of error messages
            if 'error_event_set' not in temp_log:
                report_file_name_new = os.path.join(check_books_path, 'EPUBCheck ' + self.key_epubcheck_report_format +
                                                    ' report (new).' + self.key_epubcheck_report_format.lower())

                # EPUBCheck JSON reports don't accept non-ascii characters. So, we need to correct this.
                if self.key_epubcheck_report_format == 'JSON':
                    with open(report_file_name, encoding='utf-8') as fh:
                        data = json.load(fh)
                        with open(report_file_name_new, 'w', encoding='utf-8') as jsonfile:
                            json.dump(data, jsonfile, indent=2, ensure_ascii=False)

                    # Remove the old report
                    filelist = [f for f in os.listdir(check_books_path) if 'new' not in f]
                    for f in filelist:
                        os.remove(os.path.join(check_books_path, f))

                    # Rename the new report file to match standard
                    os.rename(report_file_name_new, report_file_name)

                # Keep only preferred output
                filelist = [f for f in os.listdir(check_books_path)]
                for f in filelist:
                    if f not in report_file_name:
                        os.remove(os.path.join(check_books_path, f))

                # Ensure you have execute rights for unix based platforms
                if isosx or islinux:
                    os.chmod(check_books_path, 0o744)

                # If EPUBCheck succeeded, there should be a report file
                status, color = None, 'red'
                if os.path.isfile(report_file_name) and 'error_event_set' not in temp_log:

                    # Get FATAL errors. Those indicate invalid or DRMed EPUBs.
                    if self.key_epubcheck_report_format == 'JSON':
                        with open(report_file_name, 'r') as file:
                            json_string = file.read()
                        parsed_json = json.loads(json_string)
                        for message in parsed_json['messages']:
                            if message['severity'] == 'FATAL':
                                status = _('Invalid EPUB or DRMed')
                    else:
                        tree = ET.parse(report_file_name)
                        root = tree.getroot()
                        if self.key_epubcheck_report_format == 'XML':
                            search_string = './/{http://schema.openpreservation.org/ois/xml/ns/jhove}message'
                        elif self.key_epubcheck_report_format == 'XMP':
                            search_string = './/{http://www.loc.gov/premis/rdf/v1#}hasEventOutcome'
                        for message in root.findall(search_string):
                            if ', FATAL,' in message.text:
                                status = _('Invalid EPUB or DRMed')

                    # Create a link for the report
                    if len(self.key_epubcheck_report_column) != 0:
                        link = '<a href="file:///' + report_file_name.replace('\\', '/') + '"> EPUBCheck '\
                               + self.key_epubcheck_report_format + ' </a>'
                        db.new_api.set_field(self.key_epubcheck_report_column, {book_id: link})

                    # Set EPUBCheck result
                    try:
                        if status is None:
                            if return_code == 1:
                                status, color = _('Fail'), 'red'
                                self.failed_ids.append(book_id)
                                db.new_api.set_field(self.key_epubcheck_result_column, {book_id: False})
                            else:
                                status, color = _('Pass'), 'green'
                                self.passed_ids.append(book_id)
                                db.new_api.set_field(self.key_epubcheck_result_column, {book_id: True})
                            msg_1 = '<b>%s</b>' % get_title_authors_text(db, book_id)
                            msg_2 = '\t<span style="color:%s">%s</span>' % (color, status)
                            temp_log.append(('book', msg_1, status))
                            temp_log.append(('info', msg_2, status))
                        else:
                            self.invalid_ids.append(book_id)
                            msg_1 = '<span style="color:red"><b>%s</b></span>' % get_title_authors_text(db, book_id)
                            msg_2 = '\t<span style="color:red">%s</span>' % status
                            temp_log.append(('info', msg_1, status))
                            temp_log.append(('info', msg_2, status))
                    except:
                        pass

                elif not os.path.isfile(report_file_name):
                    error_event.set()
                    temp_log.append('error_event_set')
                    error_msg = _('Something went wrong. EPUBCheck did not produce a report.')
                    return _('No report found'), error_msg, traceback.format_exc()
                else:
                    return

                if len(self.key_epubcheck_report_column) == 0:
                    shutil.rmtree(check_books_path)
                    if self.key_calibre_library_folders:
                        extra_files = db.list_extra_files(int(book_id), pattern=DATA_FILE_PATTERN)
                        if not extra_files:
                            data_folder = os.path.join(os.path.dirname(book_path), 'data')
                            shutil.rmtree(data_folder)

        except:
            error_event.set()
            temp_log.append('error_event_set')
            return error_title, None, traceback.format_exc()
