"""Module that contains the class for the main window of PhysBiblio.

This file is part of the physbiblio package.
"""
import ast
import glob
import os
import signal
import sys
import traceback
from queue import Queue

import bibtexparser
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QAction, QGuiApplication, QIcon, QPixmap
from PySide6.QtWidgets import (
    QApplication,
    QFrame,
    QMainWindow,
    QMessageBox,
    QSplitter,
    QStatusBar,
    QTabBar,
    QTabWidget,
    QWidget,
)

try:
    from physbiblio.bibtexWriter import pbWriter
    from physbiblio.config import configuration_params, pbConfig
    from physbiblio.database import dbStats, pBDB
    from physbiblio.errors import pBErrorManager, pBLogger
    from physbiblio.export import pBExport
    from physbiblio.gui.basicDialogs import (
        LongInfoMessage,
        askFileName,
        askFileNames,
        askGenericText,
        askSaveFileName,
        askYesNo,
        infoMessage,
    )
    from physbiblio.gui.bibWindows import (
        AbstractFormulas,
        BibtexInfo,
        BibtexListWindow,
        FieldsFromArxiv,
        SearchBibsWindow,
        editBibtex,
    )
    from physbiblio.gui.catWindows import CatsTreeWindow, editCategory
    from physbiblio.gui.commonClasses import ObjectWithSignal, PBComboBox, WriteStream
    from physbiblio.gui.dialogWindows import (
        AdvancedImportDialog,
        AdvancedImportSelect,
        ConfigWindow,
        DailyArxivDialog,
        DailyArxivSelect,
        ExportForTexDialog,
        LogFileContentDialog,
        PrintText,
    )
    from physbiblio.gui.errorManager import pBGUILogger
    from physbiblio.gui.expWindows import (
        EditExperimentDialog,
        ExpsListWindow,
        editExperiment,
    )
    from physbiblio.gui.inspireStatsGUI import AuthorStatsPlots, PaperStatsPlots
    from physbiblio.gui.profilesManager import SelectProfiles, editProfile
    from physbiblio.gui.threadElements import (
        Thread_authorStats,
        Thread_checkUpdated,
        Thread_citationCount,
        Thread_cleanAllBibtexs,
        Thread_cleanSpare,
        Thread_cleanSparePDF,
        Thread_duplicates,
        Thread_exportTexBib,
        Thread_fieldsArxiv,
        Thread_findBadBibtexs,
        Thread_importDailyArxiv,
        Thread_importFromBib,
        Thread_loadAndInsert,
        Thread_paperStats,
        Thread_replace,
        Thread_updateAllBibtexs,
        Thread_updateInspireInfo,
    )
    from physbiblio.inspireStats import pBStats
    from physbiblio.pdf import pBPDF
    from physbiblio.strings.gui import MainWindowStrings as mwstr
    from physbiblio.strings.main import DatabaseStrings as dbstr
    from physbiblio.view import pBView
    from physbiblio.webimport.webInterf import physBiblioWeb
except ImportError as e:
    print("Could not find physbiblio and its modules!", e)
    print(traceback.format_exc())
try:
    import physbiblio.gui.resourcesPyside6
except ImportError as e:
    print("Missing Resources_pyside6: run script update_resources.sh", e)

class MainWindow(QMainWindow):
    """This is the class that manages the main window of the PhysBiblio
    graphical interface
    """

    errormessage = Signal(type, type, Exception, traceback)

    def __init__(self, testing=False):
        """Main window constructor.
        Call many functions to set all the widgets and the properties

        Parameter:
            testing (default False): if True, return without
                executing all the creation of objects (used for tests)
        """
        QMainWindow.__init__(self)
        self.errormessage.connect(self.excepthook)
        availableWidth = QGuiApplication.primaryScreen().availableGeometry().width()
        availableHeight = QGuiApplication.primaryScreen().availableGeometry().height()
        self.setWindowTitle(mwstr.winTitle)
        # x,y of topleft corner, width, height
        self.setGeometry(0, 0, availableWidth, availableHeight)
        self.setMinimumHeight(400)
        self.setMinimumWidth(600)
        self.mainStatusBar = QStatusBar()
        self.lastAuthorStats = None
        self.lastPaperStats = None
        self.fileMenu = None
        self.bibMenu = None
        self.catMenu = None
        self.expMenu = None
        self.toolMenu = None
        self.searchMenu = None
        self.replaceMenu = None
        self.helpMenu = None
        self.mainToolBar = None
        self.bibtexListWindows = []
        self.bibtexListQueries = {}
        self.currentTab = 0
        self.bottomLeft = None
        self.bottomCenter = None
        self.bottomRight = None
        self.replaceResults = []
        self.loadedAndInserted = []
        self.selectedCats = []
        self.selectedExps = []
        self.badBibtexs = []
        self.importArXivResults = []
        self.tabWidget = QTabWidget(self)
        self.tabWidget.setTabBarAutoHide(True)
        self.tabWidget.setTabsClosable(True)
        self.tabWidget.currentChanged.connect(self.newTabAtEnd)
        self.tabWidget.tabBarDoubleClicked.connect(self.renameTab)
        self.tabWidget.tabCloseRequested.connect(self.closeTab)
        if testing:
            return
        self.createActions()
        self.createMenusAndToolBar()
        self.createMainLayout()
        self.setIcon()
        self.createStatusBar()

        # When the DB is locked, ask if the user wants to close the PB instance
        self.onIsLockedClass = ObjectWithSignal()
        pBDB.onIsLocked = self.onIsLockedClass.customSignal
        pBDB.onIsLocked.connect(self.lockedDatabase)

        # Catch Ctrl+C in shell
        signal.signal(signal.SIGINT, signal.SIG_DFL)

        self.checkUpdated = Thread_checkUpdated(self)
        self.checkUpdated.finished.connect(self.checkUpdated.deleteLater)
        self.checkUpdated.result.connect(self.printNewVersion)
        self.checkUpdated.start()

    def closeEvent(self, event):
        """Intercept close events. If uncommitted changes exist,
        ask before closing the application

        Parameter:
            event: a QEvent
        """
        if pBDB.checkUncommitted():
            if not askYesNo("%s\n%s" % (mwstr.unsavedChanges, mwstr.wantToExit)):
                event.ignore()
            else:
                event.accept()
        elif pbConfig.params["askBeforeExit"] and not askYesNo(mwstr.wantToExit):
            event.ignore()
        else:
            event.accept()

    def excepthook(self, cls, exception, trcbk):
        """Function that will replace `sys.excepthook` to log
        any error that occurs

        Parameters:
            cls, exception, trcbk as in `sys.excepthook`
        """
        pBGUILogger.error(mwstr.unhandled, exc_info=(cls, exception, trcbk))

    def mainWindowTitle(self, title):
        """Set the window title

        Parameter:
            title: the window title
        """
        self.setWindowTitle(title)

    def printNewVersion(self, outdated, newVersion):
        """Open a warning notifying the existence of a new version,
        if present, or just save an info in the log

        Parameters:
            outdated: boolean, tells if there is a new version available
                in the PyPI repositories for update
            newVersion: the number of the new version or an empty string
        """
        if outdated:
            if pbConfig.params["notifyUpdate"]:
                pBGUILogger.warning(mwstr.newVersion % newVersion)
            else:
                self.statusBarMessage(
                    (mwstr.newVersion.replace("\n", " ")) % newVersion
                )
        else:
            pBLogger.info(mwstr.noNewVersion)

    def lockedDatabase(self):
        """Action to be performed when the database is locked
        by another instance of the program.
        Ask what to do and close the main window.
        """
        if askYesNo(
            mwstr.dbLockedAskClose,
            title=mwstr.attentionE,
        ):
            raise SystemExit

    def setIcon(self):
        """Set the icon of the main window"""
        appIcon = QIcon(":/images/icon.png")
        self.setWindowIcon(appIcon)

    def keyPressEvent(self, e):
        """Manage the key press events.

        Parameters:
            e: the `PySide6.QtGui.QKeyEvent`
        """
        modifiers = QApplication.keyboardModifiers()
        if e.key() == Qt.Key_W and modifiers == Qt.ControlModifier:
            self.closeTab(self.tabWidget.currentIndex())
        elif e.key() == Qt.Key_N and modifiers == (
            Qt.ControlModifier | Qt.ShiftModifier
        ):
            self.newTabAtEnd(self.tabWidget.count() - 1)
        elif e.key() == Qt.Key_Tab and modifiers == (Qt.ControlModifier):
            index = self.tabWidget.currentIndex()
            self.tabWidget.setCurrentIndex(
                index + 1 if index < self.tabWidget.count() - 2 else 0
            )
        elif (e.key() == Qt.Key_Backtab and modifiers == Qt.ControlModifier) or (
            e.key() == Qt.Key_Backtab
            and modifiers == (Qt.ControlModifier | Qt.ShiftModifier)
        ):
            index = self.tabWidget.currentIndex()
            self.tabWidget.setCurrentIndex(
                index - 1 if index > 0 else self.tabWidget.count() - 2
            )
        elif e.key() == Qt.Key_F and modifiers == (
            Qt.ControlModifier | Qt.ShiftModifier
        ):
            index = self.currentTabWidget().filterInput.setFocus()

    def createActions(self):
        """Create the QActions used in menu and in the toolbar."""
        self.exitAct = QAction(
            QIcon(":/images/application-exit.png"),
            mwstr.Act.exitT,
            self,
            shortcut="Ctrl+Q",
            statusTip=mwstr.Act.exitD,
            triggered=lambda x=False: self.close(),
        )

        self.profilesAct = QAction(
            QIcon(":/images/profiles.png"),
            mwstr.Act.profT,
            self,
            shortcut="Ctrl+P",
            statusTip=mwstr.Act.profD,
            triggered=lambda x=False: self.manageProfiles(),
        )

        self.editProfileWindowsAct = QAction(
            mwstr.Act.editProfT,
            self,
            shortcut="Ctrl+Alt+P",
            statusTip=mwstr.Act.editProfD,
            triggered=lambda x=False: self.editProfile(),
        )

        self.undoAct = QAction(
            QIcon(":/images/edit-undo.png"),
            mwstr.Act.undoT,
            self,
            shortcut="Ctrl+Z",
            statusTip=mwstr.Act.undoD,
            triggered=lambda x=False: self.undoDB(),
        )

        self.saveAct = QAction(
            QIcon(":/images/file-save.png"),
            mwstr.Act.saveT,
            self,
            shortcut="Ctrl+S",
            statusTip=mwstr.Act.saveD,
            triggered=lambda x=False: self.save(),
        )

        self.importBibAct = QAction(
            mwstr.Act.impT,
            self,
            shortcut="Ctrl+B",
            statusTip=mwstr.Act.impD,
            triggered=lambda x=False: self.importFromBib(),
        )

        self.exportAct = QAction(
            QIcon(":/images/export.png"),
            mwstr.Act.expLT,
            self,
            statusTip=mwstr.Act.expLD,
            triggered=lambda x=False: self.export(),
        )

        self.exportAllAct = QAction(
            QIcon(":/images/export-table.png"),
            mwstr.Act.expAT,
            self,
            shortcut="Ctrl+A",
            statusTip=mwstr.Act.expAD,
            triggered=lambda x=False: self.exportAll(),
        )

        self.exportFileAct = QAction(
            mwstr.Act.expTT,
            self,
            shortcut="Ctrl+X",
            statusTip=mwstr.Act.expTD,
            triggered=lambda x=False: self.exportFile(),
        )

        self.exportUpdateAct = QAction(
            mwstr.Act.updT,
            self,
            shortcut="Ctrl+Shift+X",
            statusTip=mwstr.Act.updD,
            triggered=lambda x=False: self.exportUpdate(),
        )

        self.catAct = QAction(
            mwstr.Act.catT,
            self,
            shortcut="Ctrl+T",
            statusTip=mwstr.Act.catD,
            triggered=lambda x=False: self.categories(),
        )

        self.newCatAct = QAction(
            mwstr.Act.catNT,
            self,
            shortcut="Ctrl+Shift+T",
            statusTip=mwstr.Act.catND,
            triggered=lambda x=False: self.newCategory(),
        )

        self.expAct = QAction(
            mwstr.Act.expT,
            self,
            shortcut="Ctrl+E",
            statusTip=mwstr.Act.expD,
            triggered=lambda x=False: self.experiments(),
        )

        self.newExpAct = QAction(
            mwstr.Act.expNT,
            self,
            shortcut="Ctrl+Shift+E",
            statusTip=mwstr.Act.expND,
            triggered=lambda x=False: self.newExperiment(),
        )

        self.searchBibAct = QAction(
            QIcon(":/images/find.png"),
            mwstr.Act.findT,
            self,
            shortcut="Ctrl+F",
            statusTip=mwstr.Act.findD,
            triggered=lambda x=False: self.searchBiblio(),
        )

        self.searchReplaceAct = QAction(
            QIcon(":/images/edit-find-replace.png"),
            mwstr.Act.srT,
            self,
            shortcut="Ctrl+H",
            statusTip=mwstr.Act.srD,
            triggered=lambda x=False: self.searchAndReplace(),
        )

        self.newBibAct = QAction(
            QIcon(":/images/file-add.png"),
            mwstr.Act.bibNT,
            self,
            shortcut="Ctrl+N",
            statusTip=mwstr.Act.bibND,
            triggered=lambda x=False: self.newBibtex(),
        )

        self.inspireLoadAndInsertAct = QAction(
            mwstr.Act.loadInsT,
            self,
            shortcut="Ctrl+Shift+I",
            statusTip=mwstr.Act.loadInsD,
            triggered=lambda x=False: self.inspireLoadAndInsert(),
        )

        self.inspireLoadAndInsertWithCatsAct = QAction(
            mwstr.Act.loadInsCatT,
            self,
            shortcut="Ctrl+I",
            statusTip=mwstr.Act.loadInsCatD,
            triggered=lambda x=False: self.inspireLoadAndInsertWithCats(),
        )

        self.advImportAct = QAction(
            mwstr.Act.advImpT,
            self,
            shortcut="Ctrl+Alt+I",
            statusTip=mwstr.Act.advImpD,
            triggered=lambda x=False: self.advancedImport(),
        )

        self.updateAllBibtexsAct = QAction(
            mwstr.Act.updBT,
            self,
            shortcut="Ctrl+U",
            statusTip=mwstr.Act.updBD,
            triggered=lambda x=False: self.updateAllBibtexs(),
        )

        self.updateAllBibtexsAskAct = QAction(
            mwstr.Act.updPT,
            self,
            shortcut="Ctrl+Shift+U",
            statusTip=mwstr.Act.updPD,
            triggered=lambda x=False: self.updateAllBibtexsAsk(),
        )

        self.updateCitationCountAct = QAction(
            mwstr.Act.ccT,
            self,
            statusTip=mwstr.Act.ccD,
            triggered=lambda x=False: self.getInspireCitationCount(),
        )

        self.cleanAllBibtexsAct = QAction(
            mwstr.Act.cleBT,
            self,
            shortcut="Ctrl+L",
            statusTip=mwstr.Act.cleBD,
            triggered=lambda x=False: self.cleanAllBibtexs(),
        )

        self.findBadBibtexsAct = QAction(
            mwstr.Act.corrT,
            self,
            shortcut="Ctrl+Shift+B",
            statusTip=mwstr.Act.corrD,
            triggered=lambda x=False: self.findBadBibtexs(),
        )

        self.checkDuplicatesAct = QAction(
            mwstr.Act.duplT,
            self,
            statusTip=mwstr.Act.duplD,
            triggered=lambda x=False: self.checkDuplicates(),
        )

        self.infoFromArxivAct = QAction(
            mwstr.Act.arxIT,
            self,
            shortcut="Ctrl+V",
            statusTip=mwstr.Act.arxID,
            triggered=lambda x=False: self.infoFromArxiv(),
        )

        self.dailyArxivAct = QAction(
            mwstr.Act.arxBT,
            self,
            shortcut="Ctrl+D",
            statusTip=mwstr.Act.arxBD,
            triggered=lambda x=False: self.browseDailyArxiv(),
        )

        self.cleanAllBibtexsAskAct = QAction(
            mwstr.Act.cleFT,
            self,
            shortcut="Ctrl+Shift+L",
            statusTip=mwstr.Act.cleFD,
            triggered=lambda x=False: self.cleanAllBibtexsAsk(),
        )

        self.authorStatsAct = QAction(
            mwstr.Act.autT,
            self,
            shortcut="Ctrl+Shift+A",
            statusTip=mwstr.Act.autD,
            triggered=lambda x=False: self.authorStats(),
        )

        self.configAct = QAction(
            QIcon(":/images/settings.png"),
            mwstr.Act.settT,
            self,
            shortcut="Ctrl+Shift+S",
            statusTip=mwstr.Act.settD,
            triggered=lambda x=False: self.config(),
        )

        self.refreshAct = QAction(
            QIcon(":/images/refresh2.png"),
            mwstr.Act.refT,
            self,
            shortcut="F5",
            statusTip=mwstr.Act.refD,
            triggered=lambda x=False: self.refreshMainContent(),
        )

        self.reloadAct = QAction(
            QIcon(":/images/refresh.png"),
            mwstr.Act.resT,
            self,
            shortcut="Shift+F5",
            statusTip=mwstr.Act.resD,
            triggered=lambda x=False: self.reloadMainContent(),
        )

        self.changesAct = QAction(
            mwstr.Act.chaT,
            self,
            statusTip=mwstr.Act.chaD,
            triggered=lambda x=False: self.recentChanges(),
        )

        self.aboutAct = QAction(
            QIcon(":/images/help-about.png"),
            mwstr.Act.abT,
            self,
            statusTip=mwstr.Act.abD,
            triggered=lambda x=False: self.showAbout(),
        )

        self.logfileAct = QAction(
            mwstr.Act.logT,
            self,
            shortcut="Ctrl+G",
            statusTip=mwstr.Act.logD,
            triggered=lambda x=False: self.logfile(),
        )

        self.dbstatsAct = QAction(
            QIcon(":/images/stats.png"),
            mwstr.Act.dbT,
            self,
            statusTip=mwstr.Act.dbD,
            triggered=lambda x=False: self.showDBStats(),
        )

        self.cleanSpareAct = QAction(
            mwstr.Act.cleET,
            self,
            statusTip=mwstr.Act.cleED,
            triggered=lambda x=False: self.cleanSpare(),
        )

        self.cleanSparePDFAct = QAction(
            mwstr.Act.clePT,
            self,
            statusTip=mwstr.Act.clePD,
            triggered=lambda x=False: self.cleanSparePDF(),
        )

    def createMenusAndToolBar(self):
        """Set the content of the menus and of the toolbar."""
        self.menuBar().clear()
        self.fileMenu = self.menuBar().addMenu(mwstr.Act.fileM)
        self.fileMenu.addAction(self.undoAct)
        self.fileMenu.addAction(self.saveAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exportAct)
        self.fileMenu.addAction(self.exportFileAct)
        self.fileMenu.addAction(self.exportAllAct)
        self.fileMenu.addAction(self.exportUpdateAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.profilesAct)
        self.fileMenu.addAction(self.editProfileWindowsAct)
        self.fileMenu.addAction(self.configAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)

        self.bibMenu = self.menuBar().addMenu(mwstr.Act.bibM)
        self.bibMenu.addAction(self.newBibAct)
        self.bibMenu.addAction(self.importBibAct)
        self.bibMenu.addAction(self.inspireLoadAndInsertWithCatsAct)
        self.bibMenu.addAction(self.inspireLoadAndInsertAct)
        self.bibMenu.addAction(self.advImportAct)
        self.bibMenu.addSeparator()
        self.bibMenu.addAction(self.cleanAllBibtexsAct)
        self.bibMenu.addAction(self.cleanAllBibtexsAskAct)
        self.bibMenu.addAction(self.findBadBibtexsAct)
        self.bibMenu.addSeparator()
        self.bibMenu.addAction(self.infoFromArxivAct)
        self.bibMenu.addAction(self.updateAllBibtexsAct)
        self.bibMenu.addAction(self.updateAllBibtexsAskAct)
        self.bibMenu.addAction(self.updateCitationCountAct)
        self.bibMenu.addSeparator()
        self.bibMenu.addAction(self.searchBibAct)
        self.bibMenu.addAction(self.searchReplaceAct)
        self.bibMenu.addSeparator()
        self.bibMenu.addAction(self.refreshAct)
        self.bibMenu.addAction(self.reloadAct)

        self.catMenu = self.menuBar().addMenu(mwstr.Act.catM)
        self.catMenu.addAction(self.catAct)
        self.catMenu.addAction(self.newCatAct)

        self.expMenu = self.menuBar().addMenu(mwstr.Act.expM)
        self.expMenu.addAction(self.expAct)
        self.expMenu.addAction(self.newExpAct)

        self.toolMenu = self.menuBar().addMenu(mwstr.Act.toolsM)
        self.toolMenu.addAction(self.dailyArxivAct)
        self.toolMenu.addSeparator()
        self.toolMenu.addAction(self.cleanSpareAct)
        self.toolMenu.addAction(self.cleanSparePDFAct)
        self.toolMenu.addSeparator()
        self.toolMenu.addAction(self.authorStatsAct)

        pBDB.convertSearchFormat()
        freqSearches = pbConfig.globalDb.getSearchList(manual=True, replacement=False)
        if len(freqSearches) > 0:
            self.searchMenu = self.menuBar().addMenu(mwstr.Act.freqSM)
            for fs in freqSearches:
                self.searchMenu.addAction(
                    QAction(
                        fs["name"],
                        self,
                        triggered=lambda c=False, sD=ast.literal_eval(
                            fs["searchDict"]
                        ), l=fs["limitNum"], o=fs["offsetNum"], n=fs[
                            "name"
                        ]: self.runSearchBiblio(
                            sD, l, o, newTab=n
                        ),
                    )
                )
            self.searchMenu.addSeparator()
            for fs in freqSearches:
                tmp = self.searchMenu.addMenu(mwstr.Act.manage % fs["name"])
                tmp.addAction(
                    QAction(
                        mwstr.Act.edit % fs["name"],
                        self,
                        triggered=lambda c=False, idS=fs["idS"], n=fs[
                            "name"
                        ]: self.editSearchBiblio(idS, n),
                    )
                )
                tmp.addAction(
                    QAction(
                        mwstr.Act.rename % fs["name"],
                        self,
                        triggered=lambda c=False, idS=fs["idS"], n=fs[
                            "name"
                        ]: self.renameSearchBiblio(idS, n),
                    )
                )
                tmp.addAction(
                    QAction(
                        mwstr.Act.delete % fs["name"],
                        self,
                        triggered=lambda c=False, idS=fs["idS"], n=fs[
                            "name"
                        ]: self.delSearchBiblio(idS, n),
                    )
                )
        else:
            self.searchMenu = None

        freqReplaces = pbConfig.globalDb.getSearchList(manual=True, replacement=True)
        if len(freqReplaces) > 0:
            self.replaceMenu = self.menuBar().addMenu(mwstr.Act.freqRM)
            for fs in freqReplaces:
                self.replaceMenu.addAction(
                    QAction(
                        fs["name"],
                        self,
                        triggered=lambda c=False, sD=ast.literal_eval(
                            fs["searchDict"]
                        ), r=ast.literal_eval(fs["replaceFields"]), o=fs[
                            "offsetNum"
                        ], n=fs[
                            "name"
                        ]: self.runSearchReplaceBiblio(
                            sD, r, o, newTab=n
                        ),
                    )
                )
            self.replaceMenu.addSeparator()
            for fs in freqReplaces:
                tmp = self.replaceMenu.addMenu(mwstr.Act.manage % fs["name"])
                tmp.addAction(
                    QAction(
                        mwstr.Act.edit % fs["name"],
                        self,
                        triggered=lambda c=False, idS=fs["idS"], n=fs[
                            "name"
                        ]: self.editSearchBiblio(idS, n),
                    )
                )
                tmp.addAction(
                    QAction(
                        mwstr.Act.rename % fs["name"],
                        self,
                        triggered=lambda c=False, idS=fs["idS"], n=fs[
                            "name"
                        ]: self.renameSearchBiblio(idS, n),
                    )
                )
                tmp.addAction(
                    QAction(
                        mwstr.Act.delete % fs["name"],
                        self,
                        triggered=lambda c=False, idS=fs["idS"], n=fs[
                            "name"
                        ]: self.delSearchBiblio(idS, n),
                    )
                )
        else:
            self.replaceMenu = None

        self.helpMenu = self.menuBar().addMenu(mwstr.Act.helpM)
        self.helpMenu.addAction(self.dbstatsAct)
        self.helpMenu.addAction(self.logfileAct)
        self.helpMenu.addSeparator()
        self.helpMenu.addAction(self.changesAct)
        self.helpMenu.addAction(self.aboutAct)

        try:
            self.removeToolBar(self.mainToolBar)
        except AttributeError:
            pass
        self.mainToolBar = self.addToolBar(mwstr.Act.toolbar)
        self.mainToolBar.addAction(self.undoAct)
        self.mainToolBar.addAction(self.saveAct)
        self.mainToolBar.addSeparator()
        self.mainToolBar.addAction(self.newBibAct)
        self.mainToolBar.addAction(self.searchBibAct)
        self.mainToolBar.addAction(self.searchReplaceAct)
        self.mainToolBar.addAction(self.exportAct)
        self.mainToolBar.addAction(self.exportAllAct)
        self.mainToolBar.addSeparator()
        self.mainToolBar.addAction(self.refreshAct)
        self.mainToolBar.addAction(self.reloadAct)
        self.mainToolBar.addSeparator()
        self.mainToolBar.addAction(self.configAct)
        self.mainToolBar.addAction(self.dbstatsAct)
        self.mainToolBar.addAction(self.aboutAct)
        self.mainToolBar.addSeparator()
        self.mainToolBar.addAction(self.exitAct)

    def createMainLayout(self):
        """Set the layout of the main window, i.e. create the splitters
        and locate the bibtex list widget and the info panels
        """
        # tabs with the bibtex tables
        self.fillTabs()
        self.tabWidget.setCurrentIndex(0)

        # will contain the bibtex code:
        self.bottomLeft = BibtexInfo(self)
        self.bottomLeft.setFrameShape(QFrame.StyledPanel)

        # will contain the abstract of the bibtex entry:
        self.bottomCenter = BibtexInfo(self)
        self.bottomCenter.setFrameShape(QFrame.StyledPanel)

        # will contain other info on the bibtex entry:
        self.bottomRight = BibtexInfo(self)
        self.bottomRight.setFrameShape(QFrame.StyledPanel)

        splitter = QSplitter(Qt.Vertical)
        splitter.addWidget(self.tabWidget)
        splitterBottom = QSplitter(Qt.Horizontal)
        splitterBottom.addWidget(self.bottomLeft)
        splitterBottom.addWidget(self.bottomCenter)
        splitterBottom.addWidget(self.bottomRight)
        splitter.addWidget(splitterBottom)
        splitter.setStretchFactor(0, 3)
        splitter.setStretchFactor(1, 1)

        availableWidth = QGuiApplication.primaryScreen().availableGeometry().width()
        availableHeight = QGuiApplication.primaryScreen().availableGeometry().height()
        splitter.setGeometry(0, 0, availableWidth, availableHeight)

        self.setCentralWidget(splitter)

    def addBibtexListWindow(self, label, bibs=None, askBibs=False, previous=[]):
        """Function that creates a new BibtexListWindow and add it
        (with a label) to the list bibtexListWindows.

        Parameters:
            label: a string that identifies the tab
            bibs, askBibs, previous: directly passed to BibtexListWindow
        """
        self.bibtexListWindows.append(
            [
                BibtexListWindow(
                    parent=self, bibs=bibs, askBibs=askBibs, previous=previous
                ),
                label,
            ]
        )

    def currentTabWidget(self):
        """Return the current BibtexListWindow in the self.tabWidget

        Output:
            a BibtexListWindow item, currently displayed in the QTabWidget
        """
        return self.bibtexListWindows[self.tabWidget.currentIndex()][0]

    def fillTabs(self):
        """Create as many tabs as the number of bibtexListWindows items"""
        # will contain the list of bibtex entries
        if len(self.bibtexListWindows) < 1:
            self.addBibtexListWindow(mwstr.mainTab)

        self.tabWidget.blockSignals(True)
        self.tabWidget.clear()
        for tab, lab in self.bibtexListWindows:
            self.tabWidget.addTab(tab, lab)
        # add fake tab that will allow to add new ones
        self.tabWidget.addTab(QWidget(self), QIcon(":/images/file-add.png"), "")
        # hide close buttons for first and last tabs
        for i in (0, self.tabWidget.count() - 1):
            try:
                self.tabWidget.tabBar().tabButton(i, QTabBar.RightSide).deleteLater()
            except AttributeError:
                pBLogger.debug("", exc_info=True)
            self.tabWidget.tabBar().setTabButton(i, QTabBar.RightSide, None)
        self.tabWidget.blockSignals(False)

    def closeAllTabs(self):
        """Close all the available tabs,
        except the main and the "new tab" one:
        delete the corresponding BibtexListWindow item
        and recreate the tabs
        """
        self.currentTab = 0
        for index in range(self.tabWidget.count() - 2, 0, -1):
            try:
                del self.bibtexListWindows[index]
                del self.bibtexListQueries[index]
            except (IndexError, KeyError):
                pass
        try:
            pBDB.bibs.lastQuery, pBDB.bibs.lastVals = self.bibtexListQueries[
                self.currentTab
            ]
        except KeyError:
            pBDB.bibs.getAll()
            self.bibtexListQueries[self.currentTab] = (
                pBDB.bibs.lastQuery,
                pBDB.bibs.lastVals,
            )
        self.fillTabs()

    def closeTab(self, index):
        """Close a tab, if it is not the main nor the "new tab" one:
        delete the corresponding BibtexListWindow item
        and recreate the tabs

        Parameter:
            index: the index of the tab to be deleted
        """
        closed = False
        if index != 0 and index != self.tabWidget.count() - 1:
            try:
                del self.bibtexListWindows[index]
            except IndexError:
                pass
            else:
                closed = True
        self.fillTabs()
        if closed:
            self.bibtexListQueries[self.currentTab] = (
                pBDB.bibs.lastQuery,
                pBDB.bibs.lastVals,
            )
            for i in range(index, self.tabWidget.count() - 1):
                self.bibtexListQueries[i] = self.bibtexListQueries[i + 1]
            del self.bibtexListQueries[self.tabWidget.count() - 1]
            pBDB.bibs.lastQuery, pBDB.bibs.lastVals = self.bibtexListQueries[index - 1]
            self.currentTab = index - 1
            self.tabWidget.blockSignals(True)
            self.tabWidget.setCurrentIndex(self.currentTab)
            self.tabWidget.blockSignals(False)

    def newTabAtEnd(self, index, label=None, bibs=None, askBibs=False, previous=[]):
        """Function that checks if the "open new tab" tab is triggered.
        If yes, create a new bibtexListWindow and recreate the tabWidget

        Parameters:
            index: the index of the clicked tab
            label (default None): if not None, the label of the new tab
            bibs, askBibs, previous: directly passed to BibtexListWindow
        """
        self.previousTab = self.currentTab
        self.bibtexListQueries[self.previousTab] = (
            pBDB.bibs.lastQuery,
            pBDB.bibs.lastVals,
        )
        self.currentTab = index
        if self.tabWidget.count() > 1 and index == self.tabWidget.count() - 1:
            self.addBibtexListWindow(
                mwstr.newTab if label is None else label,
                bibs=bibs,
                askBibs=askBibs,
                previous=previous,
            )
            self.fillTabs()
            self.bibtexListQueries[index] = pBDB.bibs.lastQuery, pBDB.bibs.lastVals
            self.tabWidget.blockSignals(True)
            self.tabWidget.setCurrentIndex(index)
            self.tabWidget.blockSignals(False)
        try:
            pBDB.bibs.lastQuery, pBDB.bibs.lastVals = self.bibtexListQueries[index]
        except KeyError:
            pBLogger.warning(mwstr.missingIndexQueries % index)

    def renameTab(self, index):
        """Rename a tab, if it is not the main nor the "new tab" one

        Parameter:
            index: the index of the tab to be renamed
        """
        if index != 0 and index != self.tabWidget.count() - 1:
            try:
                oldname = self.bibtexListWindows[index][1]
            except IndexError:
                return
            newname, res = askGenericText(
                mwstr.newTabAsk, mwstr.newTabName, previous=oldname
            )
            if res:
                self.tabWidget.setTabText(index, newname)
                self.bibtexListWindows[index][1] = newname

    def undoDB(self):
        """Reset database changes, window title and table content"""
        pBDB.undo()
        self.mainWindowTitle(mwstr.winTitle)
        self.reloadMainContent()

    def refreshMainContent(self):
        """Delete the previous table widget and create a new one,
        using last used query
        """
        self.statusBarMessage(mwstr.reloadMain)
        self.currentTabWidget().recreateTable(pBDB.bibs.fetchFromLast().lastFetched)
        self.done()

    def reloadMainContent(self, bibs=None, newTab=None):
        """Delete the previous table widget and create a new one,
        using the default query

        Parameters:
            bibs (default None): the list of bibtexs to show in the table
            newTab (default None): if not None, the label of the new tab
        """
        if newTab:
            self.newTabAtEnd(self.tabWidget.count() - 1, label=newTab)
        self.statusBarMessage(mwstr.reloadMain)
        self.currentTabWidget().recreateTable(bibs)
        self.done()

    def manageProfiles(self):
        """Ask and change profile"""
        profilesWin = SelectProfiles(self)
        profilesWin.exec()

    def editProfile(self):
        """Wrapper for profilesManager.editProfile"""
        editProfile(self)

    def config(self):
        """Open a window to manage the configuration of PhysBiblio,
        then read its output and save the results
        """
        cfgWin = ConfigWindow(self)
        cfgWin.exec()
        if cfgWin.result:
            changed = False
            for q in cfgWin.textValues:
                if isinstance(q[1], PBComboBox):
                    s = "%s" % q[1].currentText()
                else:
                    s = "%s" % q[1].text()
                if q[0] == "loggingLevel":
                    s = s.split(" - ")[0]
                if str(pbConfig.params[q[0]]) != s:
                    pBLogger.info(mwstr.configNewVal % (q[0], s, pbConfig.params[q[0]]))
                    pbConfig.params[q[0]] = s
                    if configuration_params[q[0]].isGlobal:
                        pbConfig.globalDb.config.update(q[0], s)
                        pbConfig.globalDb.commit()
                    else:
                        pBDB.config.update(q[0], s)
                    changed = True
                pBLogger.debug(mwstr.configUseVal % (q[0], s))
            if changed:
                pBDB.commit()
                pbConfig.readConfig()
                self.reloadConfig()
                self.refreshMainContent()
                self.statusBarMessage(mwstr.configSave)
            else:
                self.statusBarMessage(mwstr.configNoChanges)
        else:
            self.statusBarMessage(mwstr.configDiscard)

    def logfile(self):
        """Open a dialog to see the content of the log file"""
        logfileWin = LogFileContentDialog(self)
        logfileWin.exec()

    def reloadConfig(self):
        """Reload the configuration from the database.
        Typically used when a new profile has been opened
        """
        self.statusBarMessage(mwstr.configReload)
        pBView.webApp = pbConfig.params["webApplication"]
        pBPDF.pdfApp = pbConfig.params["pdfApplication"]
        if pbConfig.params["pdfFolder"][0] == "/":
            pBPDF.pdfDir = pbConfig.params["pdfFolder"]
        else:
            pBPDF.pdfDir = os.path.join(
                os.path.split(os.path.abspath(sys.argv[0]))[0],
                pbConfig.params["pdfFolder"],
            )
        pBPDF.checkFolderExists()
        self.currentTabWidget().reloadColumnContents()

    def recentChanges(self):
        """Function to show the recent changes in the current version"""
        mbox = QMessageBox(
            QMessageBox.Information,
            mwstr.recentCh,
            mwstr.recentNew % (physbiblio.__version__, physbiblio.__version_date__)
            + "%s<br>" % physbiblio.__recent_changes__,
            parent=self,
        )
        mbox.setTextFormat(Qt.RichText)
        mbox.setIconPixmap(QPixmap(":/images/icon.png"))
        mbox.exec()

    def showAbout(self):
        """Function to show the About dialog"""
        mbox = QMessageBox(
            QMessageBox.Information,
            mwstr.aboutTitle,
            mwstr.aboutText.format(
                cp=pbConfig.configPath,
                dp=pbConfig.dataPath,
                ver=physbiblio.__version__,
                vd=physbiblio.__version_date__,
                pyv=sys.version,
            ),
            parent=self,
        )
        mbox.setTextFormat(Qt.RichText)
        mbox.setIconPixmap(QPixmap(":/images/icon.png"))
        mbox.exec()

    def showDBStats(self):
        """Function to show a dialog with the statistics
        of the database content and on the number of stored PDF files
        """
        dbStats(pBDB)
        onlyfiles = pBPDF.numberOfFiles(pBPDF.pdfDir)
        mbox = QMessageBox(
            QMessageBox.Information,
            mwstr.dbStatsTitle,
            mwstr.dbStatsText.format(
                bib=pBDB.stats["bibs"],
                cat=pBDB.stats["cats"],
                exp=pBDB.stats["exps"],
                catbib=pBDB.stats["catBib"],
                catexp=pBDB.stats["catExp"],
                bibexp=pBDB.stats["bibExp"],
                nf=onlyfiles,
                pdfs=pBPDF.getSizeWUnits(pBPDF.dirSize(pBPDF.pdfDir)),
            ),
            parent=self,
        )
        mbox.setIconPixmap(QPixmap(":/images/icon.png"))
        mbox.show()

    def _runInThread(self, Thread_func, title, *args, **kwargs):
        """Function which simplifies the creation of the objects which
        are needed to run a function in a thread

        Parameters:
            Thread_func: the thread class name
            title: the title of the window where the output is shown
            *args, **kwargs: all the other parameters which will
                be passed to the thread constructor
        """

        def getDelKwargs(key):
            """Extract a parameter from kwargs and delete it
            from the original dictionary
            """
            if key in kwargs:
                tmp = kwargs.get(key)
                kwargs.pop(key)
                return tmp
            return None

        def closePrintText(a, t):
            try:
                t.wait()
            except RuntimeError:
                pass
            a.reject()

        addMessage = getDelKwargs("addMessage")
        stopFlag = getDelKwargs("stopFlag")
        app = PrintText(title=title, noStopButton=stopFlag == False)

        outMessage = getDelKwargs("outMessage")
        minProgress = getDelKwargs("minProgress")
        if minProgress:
            app.progressBarMin(minProgress)
        queue = Queue()
        ws = WriteStream(queue)
        ws.newText.connect(app.appendText)
        kwargs["pbMax"] = app.pbMax.emit
        kwargs["pbVal"] = app.pbVal.emit
        thr = Thread_func(ws, *args, parent=self, **kwargs)

        ws.finished.connect(ws.deleteLater)
        thr.finished.connect(app.enableClose)
        thr.finished.connect(thr.deleteLater)
        app.closeButton.clicked.connect(lambda a=app, t=thr: closePrintText(a, t))
        if stopFlag:
            app.stopped.connect(thr.setStopFlag)

        pBErrorManager.tempHandler(ws, format="%(message)s")
        if addMessage:
            pBLogger.info(addMessage)
        thr.start()
        app.exec()
        pBLogger.info(mwstr.closing)
        pBErrorManager.rmTempHandler()
        if outMessage:
            self.statusBarMessage(outMessage)
        else:
            self.done()

    def cleanSpare(self):
        """Run a thread to clean the spare connections and records
        in the database (if something has not been properly deleted)
        """
        self._runInThread(Thread_cleanSpare, mwstr.cleanSpareT)

    def cleanSparePDF(self):
        """Ask and run a thread to remove the spare PDF files/folder
        in the database (if something has not been properly deleted)
        """
        if askYesNo(mwstr.cleanPDFAsk):
            self._runInThread(Thread_cleanSparePDF, mwstr.cleanPDFT)

    def createStatusBar(self):
        """Function to create Status Bar"""
        self.mainStatusBar.showMessage(mwstr.ready, 0)
        self.setStatusBar(self.mainStatusBar)

    def statusBarMessage(self, message, time=4000):
        """Show a message in the status bar

        Parameters:
            message: the message text
            time (default 4000): how long (in ms) to display the message
        """
        pBLogger.info(message)
        self.mainStatusBar.showMessage(message, time)

    def save(self):
        """Ask and save the database changes"""
        if askYesNo(mwstr.saveAsk):
            pBDB.commit()
            self.mainWindowTitle(mwstr.winTitle)
            self.statusBarMessage(mwstr.saveCh)
        else:
            self.statusBarMessage(mwstr.saveNoth)

    def importFromBib(self):
        """Ask and import (from a thread) the entries in a .bib file"""
        filename = askFileName(
            self, title=mwstr.importFromWhere, filter="Bibtex (*.bib)"
        )
        if filename != "":
            self._runInThread(
                Thread_importFromBib,
                mwstr.importing,
                filename,
                askYesNo(mwstr.importInspire),
                minProgress=0,
                stopFlag=True,
                outMessage=mwstr.importAllDone % filename,
            )
            self.statusBarMessage(mwstr.importFile % filename)
            self.reloadMainContent()
        else:
            self.statusBarMessage(mwstr.emptyFN)

    def export(self):
        """Ask and export the last database query result in a .bib file"""
        filename = askSaveFileName(
            self,
            title=mwstr.exportWhereG,
            filter="Bibtex (*.bib)",
        )
        if filename != "":
            pBExport.exportLast(filename)
            self.statusBarMessage(mwstr.exportLastDone % filename)
        else:
            self.statusBarMessage(mwstr.emptyFN)

    def exportSelection(self, entries):
        """Ask and export the last selected entries in a .bib file

        Parameter:
            entries: the list of entries to be exported
        """
        filename = askSaveFileName(
            self,
            title=mwstr.exportWhereS,
            filter="Bibtex (*.bib)",
        )
        if filename != "":
            pBExport.exportSelected(filename, entries)
            self.statusBarMessage(mwstr.exportSelDone % filename)
        else:
            self.statusBarMessage(mwstr.emptyFN)

    def exportFile(self):
        """Ask and export in a .bib file the bibtex entries
        which are needed to compile one or more tex files
        """
        eft = ExportForTexDialog(self)
        eft.exec()
        if eft.result:
            outFName = eft.bibName
            if outFName != "":
                texFNames = eft.texNames
                if (not isinstance(texFNames, list) and texFNames != "") or (
                    isinstance(texFNames, list) and len(texFNames) > 0
                ):
                    self._runInThread(
                        Thread_exportTexBib,
                        mwstr.exporting,
                        texFNames,
                        outFName,
                        minProgress=0,
                        stopFlag=True,
                        updateExisting=eft.update,
                        removeUnused=eft.remove,
                        reorder=eft.reorder,
                        outMessage=mwstr.exportAllSaved % outFName,
                    )
                else:
                    self.statusBarMessage(mwstr.emptyInFNs)
            else:
                self.statusBarMessage(mwstr.emptyOutFN)
        else:
            self.statusBarMessage(mwstr.nothingToDo)

    def exportUpdate(self):
        """Ask and update a .bib file
        which already contains bibtex entries
        with the newer information from the database
        """
        filename = askSaveFileName(
            self, title=mwstr.updateFileQ, filter="Bibtex (*.bib)"
        )
        if filename != "":
            overwrite = askYesNo(mwstr.askOverwriteBib, mwstr.overwrite)
            pBExport.updateExportedBib(filename, overwrite=overwrite)
            self.statusBarMessage(mwstr.updateFileD % filename)
        else:
            self.statusBarMessage(mwstr.emptyOutFN)

    def exportAll(self):
        """Ask and export all the entries in a .bib file"""
        filename = askSaveFileName(
            self,
            title=mwstr.exportWhereG,
            filter="Bibtex (*.bib)",
        )
        if filename != "":
            pBExport.exportAll(filename)
            self.statusBarMessage(mwstr.exportAllSaved % filename)
        else:
            self.statusBarMessage(mwstr.emptyOutFN)

    def categories(self):
        """Open a window to show the list categories"""
        self.statusBarMessage(mwstr.trigCat)
        try:
            self.catListWin.close()
        except AttributeError:
            pass
        self.catListWin = CatsTreeWindow(self)
        self.catListWin.show()

    def newCategory(self):
        """Wrapper for catWindows.editCategory"""
        editCategory(self, self)

    def experiments(self):
        """Open a window to show the list experiments"""
        self.statusBarMessage(mwstr.trigExp)
        try:
            self.expListWin.close()
        except AttributeError:
            pass
        self.expListWin = ExpsListWindow(self)
        self.expListWin.show()

    def newExperiment(self):
        """Wrapper for expWindows.editExperiment"""
        editExperiment(self, self)

    def newBibtex(self):
        """Wrapper for bibWindows.editBibtex"""
        editBibtex(self)

    def searchBiblio(self, replace=False):
        """Open a dialog for performing searches in the database

        Parameter:
            replace (default False): to be passed to SearchBibsWindow,
                in order to show (or not) the replace field inputs
        """
        newSearchWin = SearchBibsWindow(self, replace=replace)
        newSearchWin.exec()
        if newSearchWin.result:
            searchFields = newSearchWin.values
            lim = newSearchWin.limit
            offs = newSearchWin.offset
            if replace:
                replaceFields = newSearchWin.replaceFields
                if newSearchWin.save:
                    name = ""
                    cancel = False
                    while name.strip() == "":
                        res = askGenericText(
                            mwstr.replaceAskName,
                            mwstr.replaceName,
                            parent=self,
                        )
                        if res[1]:
                            name = res[0]
                        else:
                            cancel = True
                            break
                    if not cancel:
                        pbConfig.globalDb.insertSearch(
                            name=name,
                            count=0,
                            searchFields=searchFields,
                            manual=True,
                            replacement=True,
                            limit=lim,
                            offset=offs,
                            replaceFields=replaceFields,
                        )
                        self.createMenusAndToolBar()
                else:
                    pbConfig.globalDb.updateSearchOrder(replacement=True)
                    pbConfig.globalDb.insertSearch(
                        count=0,
                        searchFields=searchFields,
                        manual=False,
                        replacement=True,
                        limit=lim,
                        offset=offs,
                        replaceFields=replaceFields,
                    )
                if newSearchWin.newTabCheck.isChecked():
                    self.newTabAtEnd(self.tabWidget.count() - 1, label=mwstr.newTab)
                noLim = pBDB.bibs.fetchFromDict(
                    searchFields, limitOffset=offs, doFetch=False
                )
                return replaceFields
            if newSearchWin.save:
                name = ""
                cancel = False
                while name.strip() == "":
                    res = askGenericText(
                        mwstr.searchAskName,
                        mwstr.searchName,
                        parent=self,
                    )
                    if res[1]:
                        name = res[0]
                    else:
                        cancel = True
                        break
                if not cancel:
                    pbConfig.globalDb.insertSearch(
                        name=name,
                        count=0,
                        searchFields=searchFields,
                        manual=True,
                        replacement=False,
                        limit=lim,
                        offset=offs,
                    )
                    self.createMenusAndToolBar()
            else:
                pbConfig.globalDb.updateSearchOrder()
                pbConfig.globalDb.insertSearch(
                    count=0,
                    searchFields=searchFields,
                    manual=False,
                    replacement=False,
                    limit=lim,
                    offset=offs,
                )
            self.runSearchBiblio(
                searchFields,
                lim,
                offs,
                newTab=mwstr.newTab if newSearchWin.newTabCheck.isChecked() else None,
            )
        elif replace:
            return False

    def runSearchBiblio(self, searchFields, lim, offs, newTab=None):
        """Run a search with some parameters

        Parameters:
            searchFields: the lsit of dictionaries
                with the search parameters
                to be used in `database.Entries.fetchFromDict`
            lim: the maximum number of entries to fetch
            offs: the offset for the search
            newTab (default None): if not None, the label of the new tab
        """
        if newTab is not None:
            self.newTabAtEnd(self.tabWidget.count() - 1, label=newTab)
        QApplication.setOverrideCursor(Qt.WaitCursor)
        noLim = pBDB.bibs.fetchFromDict(searchFields, limitOffset=offs).lastFetched
        lastFetched = pBDB.bibs.fetchFromDict(
            searchFields, limitTo=lim, limitOffset=offs
        ).lastFetched
        if len(noLim) > len(lastFetched):
            infoMessage(mwstr.srMoreEntries % (len(lastFetched), len(noLim)))
        self.reloadMainContent(lastFetched)
        QApplication.restoreOverrideCursor()

    def runSearchReplaceBiblio(self, searchFields, replaceFields, offs, newTab=None):
        """Run a search&replace with some parameters

        Parameters:
            searchFields: the list of dictionaries
                with the search parameters
                to be used in `database.Entries.fetchFromDict`
            replaceFields: a tuple/list of 5 elements,
                as in the output of `self.searchBiblio`
            offs: the offset for the search
            newTab (default None): if not None, the label of the new tab
        """
        pBDB.bibs.fetchFromDict(searchFields, limitOffset=offs)
        self.runReplace(replaceFields, newTab=newTab)

    def renameSearchBiblio(self, idS, oldname):
        """Rename a saved search

        Parameters:
            idS: the search id in the database
            oldname: the old search name
        """
        name = ""
        cancel = False
        while name.strip() == "":
            res = askGenericText(
                mwstr.srAskNewName % oldname,
                mwstr.srNewName,
                parent=self,
                previous=oldname,
            )
            if res[1]:
                name = res[0]
            else:
                cancel = True
                break
        if not cancel:
            pbConfig.globalDb.updateSearchField(idS, "name", name)
            pbConfig.globalDb.commit()
            self.createMenusAndToolBar()

    def editSearchBiblio(self, idS, name):
        """Edit a saved search

        Parameters:
            idS: the search id in the database
            name: the search name
        """
        record = pbConfig.globalDb.getSearchByID(idS)
        if len(record) < 1:
            pBLogger.error(mwstr.searchCantFind % idS)
            return
        newSearchWin = SearchBibsWindow(replace=record[0]["isReplace"], edit=idS)
        newSearchWin.exec()
        if newSearchWin.result:
            searchFields = newSearchWin.values
            lim = newSearchWin.limit
            offs = newSearchWin.offset
            if newSearchWin.replace:
                replaceFields = newSearchWin.replaceFields
                if newSearchWin.save:
                    pbConfig.globalDb.updateSearchField(idS, "searchDict", searchFields)
                    pbConfig.globalDb.updateSearchField(
                        idS, "replaceFields", replaceFields
                    )
                    pbConfig.globalDb.commit()
                pBDB.bibs.fetchFromDict(searchFields, limitOffset=offs)
                self.runReplace(
                    replaceFields,
                    newTab=mwstr.newTab
                    if newSearchWin.newTabCheck.isChecked()
                    else None,
                )
            else:
                if newSearchWin.save:
                    pbConfig.globalDb.updateSearchField(idS, "searchDict", searchFields)
                    pbConfig.globalDb.updateSearchField(idS, "limitNum", lim)
                    pbConfig.globalDb.updateSearchField(idS, "offsetNum", offs)
                    pbConfig.globalDb.commit()
                self.runSearchBiblio(
                    searchFields,
                    lim,
                    offs,
                    newTab=mwstr.newTab
                    if newSearchWin.newTabCheck.isChecked()
                    else None,
                )

    def delSearchBiblio(self, idS, name):
        """Delete a saved search from the database

        Parameters:
            idS: the search id in the database
            name: the search name
        """
        if askYesNo(mwstr.searchAskDelete % name):
            pbConfig.globalDb.deleteSearch(idS)
            self.createMenusAndToolBar()

    def searchAndReplace(self):
        """Open a search+replace form,
        then use the result to run the replace function
        """
        result = self.searchBiblio(replace=True)
        if not result:
            return
        self.runReplace(result)

    def runReplace(self, replace, newTab=None):
        """Run the `database.Entries.replace` function,
        then print some output in a dialog

        Parameter:
            replace: a tuple/list of 5 elements,
                as in the output of `self.searchBiblio`
            newTab (default None): if not None, the label of the new tab
        """
        regex = replace["regex"]
        fiOld = replace["fieOld"]
        old = replace["old"]
        if replace["double"]:
            fiNew = [replace["fieNew"], replace["fieNew1"]]
            new = [replace["new"], replace["new1"]]
        else:
            fiNew = [replace["fieNew"]]
            new = [replace["new"]]
        if old == "":
            infoMessage(mwstr.replaceEmptyStr)
            return
        if any(n == "" for n in new):
            if not askYesNo(mwstr.replaceEmptyStrAsk):
                return
        self._runInThread(
            Thread_replace,
            mwstr.replace,
            fiOld,
            fiNew,
            old,
            new,
            regex=regex,
            minProgress=0.0,
            stopFlag=True,
        )
        success, changed, failed = self.replaceResults
        LongInfoMessage(
            mwstr.replaceCompl.format(
                suc=len(success),
                ncha=len(changed),
                nfai=len(failed),
                cha=changed,
                fai=failed,
            )
        )
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.reloadMainContent(pBDB.bibs.fetchFromLast().lastFetched, newTab=newTab)
        QApplication.restoreOverrideCursor()

    def updateAllBibtexsAsk(self):
        """Same as updateAllBibtexs, but ask the values of
        `startFrom` and `force` before the execution
        """
        force = askYesNo(
            mwstr.forceUpdText,
            mwstr.forceUpdTitle,
        )
        text, out = askGenericText(
            mwstr.updINumText,
            mwstr.updINumTitle,
            self,
        )
        if not out:
            return
        try:
            startFrom = int(text)
        except ValueError:
            if askYesNo(mwstr.invalidIntAsk, mwstr.invalidEntry):
                startFrom = 0
            else:
                return
        self.updateAllBibtexs(startFrom, force=force)

    def updateAllBibtexs(
        self,
        startFrom=pbConfig.params["defaultUpdateFrom"],
        useEntries=None,
        force=False,
        reloadAll=False,
    ):
        """Use INSPIRE to obtain updated information of a list of papers
        and update their bibtex with the new data.
        Typically used to add journal information to arXiv papers.

        Parameters (see `physbiblio.database.Entries.searchOAIUpdates`):
            startFrom (default from the app settings): the index
                of the entry where to start from
            useEntries (default None): None (then use all)
                or a list of entries
            force (default False): if True, force the update
                also of entries which already have journal information
            reloadAll (default False): if True, completely reload
                the bibtex instead of updating it
                (may solve some format problems)
        """
        self.statusBarMessage(mwstr.updateStartFrom % startFrom)
        self._runInThread(
            Thread_updateAllBibtexs,
            mwstr.updateTitle,
            startFrom,
            useEntries=useEntries,
            force=force,
            reloadAll=reloadAll,
            minProgress=0.0,
            stopFlag=True,
        )
        self.refreshMainContent()

    def updateInspireInfo(self, bibkey, inspireID=None):
        """Use a thread to look for the INSPIRE ID of one or more papers
        and then use it to obtain more information
        (identifiers like DOI/arXiv, first appearance date, publication)

        Parameters:
            bibkey: the (list of) bibtex key of the entry
            inspireID (default None): the (list of) INSPIRE ID,
                if already known
        """
        self.statusBarMessage(mwstr.updateInfoStart)
        self._runInThread(
            Thread_updateInspireInfo,
            mwstr.updateInfo,
            bibkey,
            inspireID,
            minProgress=0.0,
            stopFlag=False,
        )
        self.refreshMainContent()

    def authorStats(self):
        """Ask the author name(s) and then
        use a thread to obtain the citation statistics
        for one or more authors using the INSPIRE-HEP database
        """
        authorName, out = askGenericText(
            mwstr.authorStNameAsk,
            mwstr.authorStNameTit,
            self,
        )
        if not out:
            return False
        else:
            authorName = str(authorName)
        if authorName == "":
            pBGUILogger.warning(mwstr.authorStEmptyName)
            return False
        if "[" in authorName:
            try:
                authorName = ast.literal_eval(authorName.strip())
            except (ValueError, SyntaxError):
                pBGUILogger.exception(mwstr.listSyntax)
                return False
        self.statusBarMessage(mwstr.authorStStart)

        self._runInThread(
            Thread_authorStats,
            mwstr.authorStatsTitle,
            authorName,
            minProgress=0.0,
            stopFlag=True,
        )

        if self.lastAuthorStats is None or len(self.lastAuthorStats["paLi"][0]) == 0:
            infoMessage(mwstr.importInspNoRes)
            return False
        self.lastAuthorStats["figs"] = pBStats.plotStats(author=True)
        aSP = AuthorStatsPlots(
            self.lastAuthorStats["figs"],
            title=mwstr.authorStStats % authorName,
            parent=self,
        )
        aSP.show()
        self.done()
        return True

    def getInspireCitationCount(self, inspireID=[]):
        """Use a thread to obtain the citation statistics
        of a paper using the INSPIRE-HEP database

        Parameter:
            inspireID: the ID of the paper in the INSPIRE database
        """
        if not isinstance(inspireID, list):
            inspireID = [inspireID]
        if inspireID == []:
            inspireID = [e["inspire"] for e in pBDB.bibs.getAll()]
        inspireID = [i for i in inspireID if i != "" and i is not None]
        self._runInThread(
            Thread_citationCount,
            mwstr.citCount,
            inspireID,
            minProgress=0.0,
            stopFlag=True,
        )
        self.done()

    def getInspireStats(self, inspireId):
        """Use a thread to obtain the citation statistics
        of a paper using the INSPIRE-HEP database

        Parameter:
            inspireId: the ID of the paper in the INSPIRE database
        """
        self._runInThread(
            Thread_paperStats,
            mwstr.paperStats,
            inspireId,
            minProgress=0.0,
            stopFlag=True,
        )
        if self.lastPaperStats is None:
            infoMessage(mwstr.paperStError)
            return False
        self.lastPaperStats["fig"] = pBStats.plotStats(paper=True)
        pSP = PaperStatsPlots(
            self.lastPaperStats["fig"],
            title=mwstr.paperStStats % inspireId,
            parent=self,
        )
        pSP.show()
        self.done()

    def inspireLoadAndInsert(self, doReload=True):
        """Ask a search string, then use inspire to import the entries
        which correspond, if unique results are obtained

        Parameter:
            doReload (default True): if True, reload
                the main entries list at the end
        """
        queryStr, out = askGenericText(
            mwstr.importInspAskStr,
            mwstr.queryStr,
            self,
        )
        if not out:
            return False
        if queryStr == "":
            pBGUILogger.warning(mwstr.emptyCantProceed)
            return False
        self.loadedAndInserted = []
        self.statusBarMessage(mwstr.importInspStarting)
        if "," in queryStr:
            try:
                queryStr = ast.literal_eval("[" + queryStr.strip() + "]")
            except (ValueError, SyntaxError):
                pBGUILogger.exception(mwstr.listSyntax)
                return False

        self._runInThread(
            Thread_loadAndInsert,
            mwstr.importInspTit,
            queryStr,
            minProgress=0.0,
            stopFlag=True,
            addMessage=mwstr.searching % queryStr,
        )

        if self.loadedAndInserted == []:
            infoMessage(mwstr.importInspNoRes)
            return False
        if doReload:
            self.reloadMainContent()
        return True

    def askCatsForEntries(self, entriesList):
        """Open a selection dialog for asking the categories
        for a given list of entries

        Parameter:
            entriesList: the list of entries to be used
        """
        for entry in entriesList:
            selectCats = CatsTreeWindow(
                parent=self,
                askCats=True,
                askForBib=entry,
                previous=[a[0] for a in pBDB.cats.getByEntry(entry)],
            )
            selectCats.exec()
            if selectCats.result in ("Ok", "Exps"):
                pBDB.catBib.insert(self.selectedCats, entry)
                self.statusBarMessage(mwstr.catInserted % entry)
            if selectCats.result == "Exps":
                selectExps = ExpsListWindow(parent=self, askExps=True, askForBib=entry)
                selectExps.exec()
                if selectExps.result == "Ok":
                    pBDB.bibExp.insert(entry, self.selectedExps)
                    self.statusBarMessage(mwstr.expInserted % entry)

    def inspireLoadAndInsertWithCats(self):
        """Extend `self.inspireLoadAndInsert` to ask the categories
        to associate the entry with, if the import was successful
        """
        if (
            self.inspireLoadAndInsert(doReload=False)
            and len(self.loadedAndInserted) > 0
        ):
            for key in self.loadedAndInserted:
                pBDB.catBib.delete(pbConfig.params["defaultCategories"], key)
            self.askCatsForEntries(self.loadedAndInserted)
            self.reloadMainContent()

    def advancedImport(self):
        """Advanced import dialog.
        Open a dialog where you can select which method to use
        and insert the search string,
        then import a selection among the obtained results
        """
        adIm = AdvancedImportDialog()
        adIm.exec()
        method = adIm.comboMethod.currentText().lower().replace("-", "")
        if method == "inspirehep":
            method = "inspire"
        if method == "adsnasa":
            if not self.checkAdsToken():
                return False
        string = adIm.searchStr.text().strip()
        db = bibtexparser.bibdatabase.BibDatabase()
        if adIm.result == True and string != "":
            QApplication.setOverrideCursor(Qt.WaitCursor)
            cont = physBiblioWeb.webSearch[method].retrieveUrlAll(string)
            if cont.strip() != "":
                elements = pBDB.bibs.readEntries(cont)
            else:
                elements = []
            found = {}
            for el in elements:
                if not isinstance(el["ID"], str) or el["ID"].strip() == "":
                    db.entries = [el]
                    entry = pbWriter.write(db)
                    pBLogger.warning(dbstr.Bibs.laiEmptyKey % entry)
                else:
                    try:
                        el["arxiv"] = el["eprint"]
                    except KeyError:
                        pass
                    exist = len(pBDB.bibs.getByBibkey(el["ID"], saveQuery=False)) > 0
                    for f in ("arxiv", "doi"):
                        try:
                            exist = exist or (
                                el[f].strip() != ""
                                and len(
                                    pBDB.bibs.getAll(params={f: el[f]}, saveQuery=False)
                                )
                                > 0
                            )
                        except KeyError:
                            pBLogger.debug("KeyError '%s', entry: %s" % (f, el["ID"]))
                    found[el["ID"]] = {"bibpars": el, "exist": exist}
            QApplication.restoreOverrideCursor()
            if len(found) == 0:
                infoMessage(mwstr.noRes)
                return False
            if method == "adsnasa":
                self.statusBarMessage(physBiblioWeb.webSearch[method].getLimitInfo())

            selImpo = AdvancedImportSelect(found, self)
            selImpo.exec()
            if selImpo.result == True:
                newFound = {}
                for ch in sorted(selImpo.selected):
                    if selImpo.selected[ch]:
                        newFound[ch] = found[ch]
                found = newFound
                inserted = []
                for key in sorted(found):
                    el = found[key]
                    db.entries = [el["bibpars"]]
                    entry = pbWriter.write(db)
                    data = pBDB.bibs.prepareInsert(entry)
                    if data == {"bibkey": ""}:
                        pBGUILogger.warning(mwstr.elementFailedBibtex % key)
                        continue
                    if method == "adsnasa":
                        data["ads"] = key
                    if not pBDB.bibs.insert(data):
                        pBGUILogger.warning(mwstr.elementFailed % key)
                        continue
                    try:
                        if method == "inspire":
                            eid = pBDB.bibs.updateInspireID(key)
                            pBDB.bibs.updateInfoFromOAI(eid)
                        pBLogger.info(mwstr.elementInserted % key)
                        inserted.append(key)
                    except Exception:
                        pBLogger.warning(mwstr.failedComplete % key, exc_info=True)
                self.statusBarMessage(mwstr.elementImported % (inserted))
                if selImpo.askCats.isChecked():
                    for key in inserted:
                        pBDB.catBib.delete(pbConfig.params["defaultCategories"], key)
                    self.askCatsForEntries(inserted)
            self.reloadMainContent()
        else:
            return False

    def cleanAllBibtexsAsk(self):
        """Use a thread to clean and reformat the bibtex entries.
        Ask the index of the entry where to start from,
        before proceeding
        """
        text, out = askGenericText(
            mwstr.cleBibStartAsk,
            mwstr.cleBibStartTit,
            self,
        )
        if not out:
            return
        try:
            startFrom = int(text)
        except ValueError:
            if askYesNo(
                mwstr.invalidIntAsk,
                mwstr.invalidEntry,
            ):
                startFrom = 0
            else:
                return
        self.cleanAllBibtexs(startFrom)

    def cleanAllBibtexs(self, startFrom=0, useEntries=None):
        """Use a thread to clean and reformat the bibtex entries

        Parameters:
            startFrom (default 0): the list index of the entry
                where to start from
            useEntries (default None): if not None, it must be a list
                of entries to be processed, otherwise the full database
                content will be used
        """
        self.statusBarMessage(mwstr.cleBibStart)
        self._runInThread(
            Thread_cleanAllBibtexs,
            mwstr.cleBib,
            startFrom,
            useEntries=useEntries,
            minProgress=0.0,
            stopFlag=True,
        )

    def findBadBibtexs(self, startFrom=0, useEntries=None):
        """Use a thread to scan the database for bad bibtex entries.
        If the user wants, a dialog for fixing each of them one by one
        may appear at the end of the search

        Parameters:
            startFrom (default 0): the list index of the entry
                where to start from
            useEntries (default None): if not None, it must be a list
                of entries to be processed, otherwise the full database
                content will be used
        """
        self.statusBarMessage(mwstr.cheBibStart)
        self.badBibtexs = []
        self._runInThread(
            Thread_findBadBibtexs,
            mwstr.cheBib,
            startFrom,
            useEntries=useEntries,
            minProgress=0.0,
            stopFlag=True,
        )
        if len(self.badBibtexs) > 0:
            if askYesNo(mwstr.cheBibFound % len(self.badBibtexs)):
                for bibkey in self.badBibtexs:
                    editBibtex(self, bibkey)
            else:
                infoMessage(mwstr.cheBibSumNoAct % ", ".join(self.badBibtexs))
        else:
            infoMessage(mwstr.cheBibNoInv)

    def infoFromArxiv(self, useEntries=None):
        """Use arXiv to obtain more information
        on a specific selection of entries

        Parameter:
            useEntries (default None): if not None, it must be a list
                of entries to be completed, otherwise the full database
                content will be used
        """
        iterator = useEntries
        if useEntries is None:
            pBDB.bibs.fetchAll(doFetch=False)
            iterator = pBDB.bibs.fetchCursor()
        askFieldsWin = FieldsFromArxiv()
        askFieldsWin.exec()
        if askFieldsWin.result:
            self.statusBarMessage(mwstr.arxInStart)
            self._runInThread(
                Thread_fieldsArxiv,
                mwstr.arxIn,
                [e["bibkey"] for e in iterator],
                askFieldsWin.output,
                minProgress=0.0,
                stopFlag=True,
            )

    def browseDailyArxiv(self):
        """Browse daily news from arXiv after showing
        a dialog for asking the category,
        and import the selection of entries
        """
        bDA = DailyArxivDialog()
        bDA.exec()
        cat = bDA.comboCat.currentText().lower()
        if bDA.result and cat != "":
            sub = bDA.comboSub.currentText()
            try:
                physBiblioWeb.webSearch["arxiv"].categories[cat]
            except KeyError:
                pBGUILogger.warning(mwstr.arxBrNoCat % cat)
                return False
            QApplication.setOverrideCursor(Qt.WaitCursor)
            content = physBiblioWeb.webSearch["arxiv"].arxivDaily(
                cat if sub == "--" else "%s.%s" % (cat, sub)
            )
            found = {}
            for el in content:
                el["type"] = ""
                if el["replacement"]:
                    el["type"] += "[replacement]"
                if el["cross"]:
                    el["type"] += "[cross-listed]"
                found[el["eprint"]] = {
                    "bibpars": el,
                    "exist": len(pBDB.bibs.getByBibkey(el["eprint"], saveQuery=False))
                    > 0,
                }
            if len(found) == 0:
                infoMessage(mwstr.noRes)
                return False

            QApplication.restoreOverrideCursor()
            selImpo = DailyArxivSelect(found, self)
            selImpo.abstractFormulas = AbstractFormulas
            selImpo.exec()
            if selImpo.result == True:
                newFound = {}
                for ch in sorted(selImpo.selected):
                    if selImpo.selected[ch]:
                        newFound[ch] = found[ch]
                found = newFound
                self._runInThread(
                    Thread_importDailyArxiv, mwstr.arxIm, found, stopFlag=True
                )
                inserted, failed = self.importArXivResults
                self.statusBarMessage(
                    mwstr.elementImported.replace("\n", " ") % (inserted)
                )
                if selImpo.askCats.isChecked():
                    for key in inserted:
                        pBDB.catBib.delete(pbConfig.params["defaultCategories"], key)
                    self.askCatsForEntries(inserted)
            self.reloadMainContent()
        else:
            return False

    def sendMessage(self, message):
        """Show a simple infoMessage

        Parameter:
            message: the message content
        """
        infoMessage(message)

    def done(self):
        """Send a "done" message to the status bar"""
        self.statusBarMessage(mwstr.doneDD)

    def checkAdsToken(self):
        """Check that the ADS token is stored in the configuration"""
        while (not isinstance(pbConfig.params["ADSToken"], str)) or pbConfig.params[
            "ADSToken"
        ].strip() == "":
            newkey, out = askGenericText(
                mwstr.askADSTokenText,
                mwstr.askADSTokenTitle,
            )
            if not out:
                return False
            if newkey.strip() != "":
                pBDB.config.insert("ADSToken", newkey)
                pBDB.commit()
                pbConfig.readConfig()
                return True
        return True

    def checkDuplicates(
        self,
    ):
        """Use database.Entries.checkDuplicates in a separate thread"""
        # self.statusBarMessage(mwstr.updateStartFrom % startFrom)
        self._runInThread(
            Thread_duplicates,
            mwstr.duplT,
            minProgress=0.0,
            stopFlag=True,
        )
        # self.refreshMainContent()

if __name__ == "__main__":
    try:
        myApp = QApplication(sys.argv)
        myApp.setAttribute(Qt.AA_X11InitThreads)
        myWindow = MainWindow()
        myWindow.show()
        sys.exit(myApp.exec())
    except NameError:
        print("NameError:", sys.exc_info()[1])
    except SystemExit:
        print(mwstr.closeMainW)
    except Exception:
        print(sys.exc_info()[1])


"""
Set of functions to compute the gradients Duk of u^k (or its adjoint operator)
on a staggered grid and setting `p^kt += b^k * Duk^t` (or
`u^k -= b^k * div(p^k)`). The "boundary" of p is left untouched.

Example in two dimensions (x are the grid points of u), k fixed:

       |                |
... -- x -- -- v1 -- -- x -- ...
       |                |          v1 and w1 -- the partial x-derivatives at top and bottom
       |                |          v2 and w2 -- the partial y-derivatives at left and right
       v2      Du       w2         Du -- the gradient at the center of the box
       |                |          (Du)_1 -- mean value of v1 and w1
       |                |          (Du)_2 -- mean value of v2 and w2
... -- x -- -- w1 -- -- x -- ...
       |                |

In one dimension there is no averaging, in three dimensions each
derivative is the mean value of four finite differences etc.

The adjoint operator (reading from p and writing to u) is understood to be the
negative divergence -div(p^k) with Dirichlet boundary.
"""

import numpy as np
import itertools

from numba import jit

def staggered_diff_avgskips(imagedims):
    d_image = len(imagedims)
    skips = tuple(int(np.prod(imagedims[(d_image-t):])) for t in range(d_image))
    navgskips =  1 << (d_image - 1)
    avgskips = np.zeros([d_image, navgskips], dtype=np.int64, order='C')
    for t in range(d_image):
        for m, p in enumerate(itertools.product([0, 1], repeat=(d_image - 1))):
            avgskips[t,m] = np.inner(p[:t] + (0,) + p[t:], skips)
    return avgskips

def gradient(pgrad, u, b, avgskips):
    staggered_diff(pgrad, u, b, avgskips, adjoint=False)

def divergence(p, ugrad, b, avgskips):
    staggered_diff(p, ugrad, b, avgskips, adjoint=True)

@jit
def staggered_diff(p, u, b, avgskips, adjoint=False):
    """
    Computes `p^kt += b^k * Duk^t`.

    Args:
        p : array where the result is written as `p^kt += b^k * Duk^t`.
        u : function to be derived
        b : factor such that `p^kt += b^k * Duk^t`
        avgskips : output of staggered_diff_avgskips(u.shape[1:])
        adjoint : (optional) if True, apply the adjoint operator (reading from p
                  and writing to u), i.e. `u^k -= b^k * div(p^k)`.
    """
    l_labels, d_image, n_image = p.shape
    navgskips =  1 << (d_image - 1)
    imagedims = u.shape[1:]

    skips = (1,)
    for t in range(1,d_image):
        skips += (skips[-1]*imagedims[d_image-t],)

    u_flat = u.reshape(l_labels, n_image)
    coords = np.zeros(d_image, dtype=np.int64)

    for k in range(l_labels):
        for t in range(d_image):
            coords *= 0
            for i in range(n_image):
                # ignore boundary points
                in_range = True
                for dc in reversed(range(d_image)):
                    if coords[dc] >= imagedims[dc] - 1:
                        in_range = False
                        break

                if in_range:
                    # regular case
                    pk = p[k]
                    uk = u_flat[k]
                    bk = b[k]/navgskips

                    for avgskip in avgskips[t]:
                        base = i + avgskip
                        if adjoint:
                            uk[base + skips[t]] += bk * pk[t,i]
                            uk[base] -= bk * pk[t,i]
                        else:
                            pk[t,i] += bk * uk[base + skips[t]]
                            pk[t,i] -= bk * uk[base]

                # advance coordinates
                for dd in reversed(range(d_image)):
                    coords[dd] += 1
                    if coords[dd] >= imagedims[dd]:
                        coords[dd] = 0
                    else:
                        break


from sys import exit
import random
import pygame
import numpy as np

class SnakeBlock():
    def __init__(self, x, y, game_window, map_size, block_size, id):
        self.id = id
        self.size = 0
        self.x = x
        self.y = y
        self.game_window = game_window
        self.map_size = map_size
        self.block_size = block_size

    def draw(self):
        pygame.draw.rect(self.game_window, (255, 255, 255),
                         (self.x, self.y, self.block_size, self.block_size), 0)

class Snake():
    def __init__(self, game_window, x, y, map_size, block_size):
        self.game_window = game_window
        self.map_size = map_size
        self.block_size = block_size
        self.headX = x
        self.headY = y
        self.tailX = x
        self.tailY = y
        self.body = [SnakeBlock(self.headX, self.headY,
                                self.game_window, self.map_size, self.block_size, 1)]
        self.turns = []
        self.size = 1
        self.steps_between_foods = 0

    def move(self, dir, foodX, foodY):
        def food_dir(dir, foodX, foodY):
            if dir == 'up' and self.headY > foodY:
                return True
            if dir == 'down' and self.headY < foodY:
                return True
            if dir == 'left' and self.headX > foodX:
                return True
            if dir == 'right' and self.headX < foodX:
                return True
            return False
        self.steps_between_foods = self.steps_between_foods + 1
        eat = False
        snake_collide = False
        reward = 0
        if dir == "none":
            return (snake_collide, eat, reward)
        if dir == "up":
            self.headY = self.headY - self.block_size
        if dir == "down":
            self.headY = self.headY + self.block_size
        if dir == "right":
            self.headX = self.headX + self.block_size
        if dir == "left":
            self.headX = self.headX - self.block_size

        if self.headX == foodX and self.headY == foodY:
            self.body.append(SnakeBlock(self.body[self.size - 1].x, self.body[self.size - 1].y,
                                        self.game_window, self.map_size, self.block_size, self.size + 1))
            self.size += 1
            reward = 0.5
            eat = True
            self.steps_between_foods = 0
        else:
            if self.collide():
                snake_collide = True
                reward = -0.5
            else:
                if food_dir(dir, foodX, foodY):
                    reward = 0.1
                else:
                    pass
                    reward = -0.1

        for i in reversed(range(self.size)):
            if i == 0:
                self.body[0].x = self.headX
                self.body[0].y = self.headY
            else:
                self.body[i].x = self.body[i - 1].x
                self.body[i].y = self.body[i - 1].y
        return (snake_collide, eat, reward)

    def collide(self):
        collideFlag = False
        if self.headX < 0 or self.headX >= self.map_size[0] * self.block_size or \
                self.headY < 0 or self.headY >= self.map_size[1] * self.block_size:
            collideFlag = True

        for block1 in self.body:
            for block2 in self.body:
                if block1.id != block2.id:
                    if block1.x == block2.x and block1.y == block2.y:
                        collideFlag = True
        return collideFlag

    def draw(self):
        for block in self.body:
            block.draw()

    def getSnakePlaces(self):
        places = [[], []]
        for block in self.body:
            places[0].append(block.x)
            places[1].append(block.y)
        return places

class Food():
    def __init__(self, game_window, map_size, block_size):
        self.game_window = game_window
        self.map_size = map_size
        self.block_size = block_size
        self.x = 0
        self.y = 0

    def draw(self):
        pygame.draw.rect(self.game_window, (255, 0, 0),
                         (self.x, self.y, self.block_size, self.block_size), 0)

    def new(self, takenPlaces):
        self.x = random.randint(0, self.map_size[0] - 1) * self.block_size
        self.y = random.randint(0, self.map_size[1] - 1) * self.block_size
        while self.x in takenPlaces[0] and self.y in takenPlaces[1]:
            self.x = random.randint(0, self.map_size[0] - 1) * self.block_size
            self.y = random.randint(0, self.map_size[1] - 1) * self.block_size

class Map():
    def __init__(self, game_window, map_size, block_size):
        self.game_window = game_window
        self.map_size = map_size
        self.block_size = block_size
        self.food = Food(self.game_window, self.map_size, self.block_size)
        self.food.new([[], []])

    def draw(self):
        # draw grid

        x = 0
        y = 0

        for i in range(self.map_size[0]):
            x = x + self.block_size
            pygame.draw.line(self.game_window, (255, 255, 255),
                             (x, 0), (x, self.block_size * self.map_size[1]))

        for i in range(self.map_size[1]):
            y = y + self.block_size
            pygame.draw.line(self.game_window, (255, 255, 255),
                             (0, y), (self.block_size * self.map_size[0], y))
        # draw food
        self.food.draw()

    def newFood(self, takenPlaces):
        self.food.new(takenPlaces)

class SnakeGame():
    def __init__(self, map_size, block_size):
        self.block_size = block_size
        self.map_size = map_size
        self.width = map_size[0] * block_size
        self.height = map_size[0] * block_size
        self.score = 0
        self.state = np.zeros(12, dtype=int)

        pygame.init()
        self.game_window = pygame.display.set_mode(
            (self.width, self.height))

        self.board = Map(self.game_window, self.map_size, self.block_size)
        self.snake = Snake(self.game_window, self.width // 2,
                           self.height // 2, self.map_size, self.block_size)
        self.board.newFood(self.snake.getSnakePlaces())

        playFlag = True
        direction = "none"

    def move(self, dir):
        snake_collide, food_eaten, reward = self.snake.move(
            dir, self.board.food.x, self.board.food.y)

        if food_eaten:
            self.board.newFood(self.snake.getSnakePlaces())
            foodEaten = False
            self.score = self.score + 1

        # if collide
        if self.snake.steps_between_foods > 500:
            return (True, reward)
        return (snake_collide, reward)

    def draw_stats(self):
        x = self.width + 50
        y = (self.height // 4) * 3

        for i in range(3):
            x = x + self.block_size
            pygame.draw.line(self.game_window, (255, 255, 255),
                             (x, y), (x, y+self.block_size * 3))

        for i in range(4):
            y = y + self.block_size
            pygame.draw.line(self.game_window, (255, 255, 255),
                             (x - 4*self.block_size, y), (x, y))

    def draw(self):
        self.game_window.fill((0, 0, 0))
        # self.draw_stats()
        self.board.draw()
        self.snake.draw()
        pygame.display.update()
        pass

    def show_text(self, text, pos=(200, 200)):
        font = pygame.font.SysFont("comicsansms", 30)
        render = font.render("generation: " + text, True, (255, 255, 255))
        self.game_window.blit(render, pos)
        pygame.display.flip()

    def update_state(self):
        def blocked(pos):
            if pos[0] < 0 or pos[0] >= self.map_size[0] * self.block_size or \
                    pos[1] < 0 or pos[1] >= self.map_size[1] * self.block_size:
                return True
            for block in self.snake.body:
                if (block.x, block.y) == pos:
                    return True
            return False

        def head_new_pos(head_pos, dir):
            if dir == "up":
                return (head_pos[0], head_pos[1] - self.snake.block_size)
            if dir == "down":
                return (head_pos[0], head_pos[1] + self.snake.block_size)
            if dir == "right":
                return (head_pos[0] + self.snake.block_size, head_pos[1])
            if dir == "left":
                return (head_pos[0] - self.snake.block_size, head_pos[1])

        def get_food_state():
            food_state = np.zeros(4, dtype=int)

            if self.snake.headX > self.board.food.x:
                food_state[0] = 1
            if self.snake.headY > self.board.food.y:
                food_state[1] = 1
            if self.snake.headX < self.board.food.x:
                food_state[2] = 1
            if self.snake.headY < self.board.food.y:
                food_state[3] = 1

            # if self.snake.headX < self.board.food.x and self.snake.headY > self.board.food.y:
            #     food_state[0] = 1
            # if self.snake.headX == self.board.food.x and self.snake.headY > self.board.food.y:
            #     food_state[1] = 1
            # if self.snake.headX > self.board.food.x and self.snake.headY > self.board.food.y:
            #     food_state[2] = 1
            # if self.snake.headX < self.board.food.x and self.snake.headY == self.board.food.y:
            #     food_state[3] = 1
            # if self.snake.headX > self.board.food.x and self.snake.headY == self.board.food.y:
            #     food_state[4] = 1
            # if self.snake.headX < self.board.food.x and self.snake.headY < self.board.food.y:
            #     food_state[5] = 1
            # if self.snake.headX == self.board.food.x and self.snake.headY < self.board.food.y:
            #     food_state[6] = 1
            # if self.snake.headX > self.board.food.x and self.snake.headY < self.board.food.y:
            #     food_state[7] = 1
            return food_state

        def get_blocked_state():
            head_pos = (self.snake.headX, self.snake.headY)
            blocked_state = np.zeros(4, dtype=int)
            if blocked(head_new_pos(head_pos, "left")):
                blocked_state[0] = 1
            if blocked(head_new_pos(head_pos, "up")):
                blocked_state[1] = 1
            if blocked(head_new_pos(head_pos, "right")):
                blocked_state[2] = 1
            if blocked(head_new_pos(head_pos, "down")):
                blocked_state[3] = 1
            return blocked_state

        # food_pos = (self.board.food.x, self.board.food.y)
        # head_pos = (self.snake.headX, self.snake.headY)
        # neck_pos = (self.snake.body[-1].x, self.snake.body[-1].y)

        self.state = np.zeros(8, dtype=int)

        self.state[:4] = get_food_state()
        self.state[4:8] = get_blocked_state()

    def get_state(self):
        self.update_state()
        return self.state

def main():
    pygame.init()
    # parameters:
    block_size = 40
    map_size = (16, 16)
    playFlag = True
    direction = "none"
    clock = pygame.time.Clock()

    game = SnakeGame(map_size, block_size)

    while playFlag:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                exit(0)
            keys = pygame.key.get_pressed()

        if keys[pygame.K_UP]:
            direction = "up"
        if keys[pygame.K_DOWN]:
            direction = "down"
        if keys[pygame.K_RIGHT]:
            direction = "right"
        if keys[pygame.K_LEFT]:
            direction = "left"
        snake_collide, reward = game.move(direction)
        if snake_collide:
            playFlag = False
        print(reward)
        game.draw()
        clock.tick(8)

if __name__ == "__main__":
    main()