"""Module with many classes that are further extended
in the other physbiblio.gui modules.

This file is part of the physbiblio package.
"""
import time
import traceback
from queue import Empty
from weakref import WeakValueDictionary

import PySide6
from PySide6.QtCore import (
    QAbstractItemModel,
    QAbstractTableModel,
    QModelIndex,
    QObject,
    QSortFilterProxyModel,
    Qt,
    QThread,
    QUrl,
    Signal,
)
from PySide6.QtGui import QAction, QDesktopServices, QGuiApplication, QPainter, QPixmap
from PySide6.QtWidgets import (
    QAbstractItemView,
    QComboBox,
    QDialog,
    QGridLayout,
    QLabel,
    QLineEdit,
    QMenu,
    QStyle,
    QTableView,
    QTableWidget,
    QTableWidgetItem,
    QVBoxLayout,
)
from shiboken6 import VoidPtr

try:
    import physbiblio.gui.resourcesPyside6
    from physbiblio.database import catString, pBDB
    from physbiblio.errors import PBErrorManagerClass, pBLogger
    from physbiblio.pdf import pBPDF
    from physbiblio.strings.gui import CommonClassesStrings as ccstr
    from physbiblio.view import ViewEntry
except ImportError:
    print("Could not find physbiblio and its modules!")
    print(traceback.format_exc())


def PtrKey(ptr):
    """Retrieve a key to identify the VoidPtr.
    This is needed because VoidPtr is not hashable
    """
    return str(ptr).split()[1]


class PBDialog(QDialog):
    """Extend QDialog with centerWindow"""

    def centerWindow(self):
        """Use the `QGuiApplication` to get the relevant information
        and center the dialog in the screen.
        """
        qr = self.frameGeometry()
        cp = QGuiApplication.primaryScreen().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def cleanLayout(self):
        """Delete the previous table widget and other layout items"""
        if self.layout() is None:
            return
        while True:
            o = self.layout().takeAt(0)
            if o is None:
                break
            o.widget().deleteLater()


class PBLabel(QLabel):
    """Extension of `QLabel` with text interaction flags
    which enable text selection with the mouse
    """

    def __init__(self, *args, **kwargs):
        """Extend `QLabel.__init__` and call `setTextInteractionFlags`
        to allow text selection with the mouse
        """
        QLabel.__init__(self, *args, **kwargs)
        self.setTextFormat(Qt.RichText)
        self.setOpenExternalLinks(True)
        self.setTextInteractionFlags(
            Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse
        )


class PBLabelRight(PBLabel):
    """Class that creates a right-aligned QLabel"""

    def __init__(self, label):
        """The constructor.

        Parameter:
            label: the text label to be passed to QLabel
        """
        super(PBLabelRight, self).__init__(label)
        self.setAlignment(Qt.AlignRight | Qt.AlignVCenter)


class PBLabelCenter(PBLabel):
    """Class that creates a center-aligned QLabel"""

    def __init__(self, label):
        """The constructor.

        Parameter:
            label: the text label to be passed to QLabel
        """
        super(PBLabelCenter, self).__init__(label)
        self.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)


class PBComboBox(QComboBox):
    """Personalize QComboBox for faster construction"""

    def __init__(self, parent, fields, current=None):
        """Constructor.

        Parameters:
            parent: the parent widget
            fields: the list of (string or printable) contents
                to be added to the QComboBox
            current (default None): the value to be set as the initial value
        """
        super(PBComboBox, self).__init__(parent)
        for f in fields:
            self.addItem("%s" % f)
        if current is not None:
            try:
                self.setCurrentIndex(fields.index(current))
            except ValueError:
                pass


class PBAndOrCombo(PBComboBox):
    """Shortcut for generating a QComboBox with and/or"""

    def __init__(self, parent, current=None):
        """Constructor.

        Parameters:
            parent: the parent widget
            current (default None): the value to be set
                as selected at the beginning
        """
        super(PBAndOrCombo, self).__init__(
            parent, [ccstr.andC, ccstr.orC], current=current
        )


class PBTrueFalseCombo(PBComboBox):
    """Shortcut for generating a QComboBox with true/false"""

    def __init__(self, parent, current=None):
        """Constructor.

        Parameters:
            parent: the parent widget
            current (default None): the value to be set as selected
                at the beginning
        """
        super(PBTrueFalseCombo, self).__init__(
            parent, [ccstr.true, ccstr.false], current=current
        )


class ObjListWindow(PBDialog):
    """Create a window managing a list (of bibtexs or of experiments)"""

    def __init__(self, parent=None, gridLayout=False):
        """Init using parent class and create common definitions

        Parameters:
            parent: the parent object
            gridLayout (boolean, default False):
                if True, use a QGridLayout, otherwise a QVBoxLayout
        """
        super(ObjListWindow, self).__init__(parent)
        self.tableWidth = None
        self.proxyModel = None
        self.gridLayout = gridLayout
        self.filterInput = None
        self.proxyModel = None
        self.tableview = None
        if gridLayout:
            self.currLayout = QGridLayout(self)
        else:
            self.currLayout = QVBoxLayout(self)

    def triggeredContextMenuEvent(self, row, col, event):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def handleItemEntered(self, index):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def cellClick(self, index):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def cellDoubleClick(self, index):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def createTable(self, *args, **kwargs):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def changeFilter(self, string):
        """Change the filter of the current view.

        Parameter:
            string: the filter string to be matched
        """
        self.proxyModel.setFilterRegularExpression(str(string))

    def addFilterInput(self, placeholderText, gridPos=(1, 0)):
        """Add a `QLineEdit` to change the filter of the list.

        Parameter:
            placeholderText: the text to be shown
                when no filter is present
            gridPos (tuple): if gridLayout is active,
                the position of the `QLineEdit` in the `QGridLayout`
        """
        self.filterInput = QLineEdit("", self)
        self.filterInput.setPlaceholderText(placeholderText)
        self.filterInput.textChanged.connect(self.changeFilter)

        if self.gridLayout:
            self.currLayout.addWidget(self.filterInput, *gridPos)
        else:
            self.currLayout.addWidget(self.filterInput)
        self.filterInput.setFocus()

    def setProxyStuff(self, sortColumn, sortOrder):
        """Prepare the proxy model to filter and sort the view.

        Parameter:
            sortColumn: the index of the column to use
                for sorting at the beginning
            sortOrder: the order for sorting
                (`Qt.AscendingOrder` or `Qt.DescendingOrder`)
        """
        self.proxyModel = QSortFilterProxyModel(self)
        self.proxyModel.setSourceModel(self.tableModel)
        self.proxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.proxyModel.setFilterKeyColumn(-1)

        self.tableview = PBTableView(self)
        self.tableview.setModel(self.proxyModel)
        self.tableview.setSortingEnabled(True)
        self.tableview.setMouseTracking(True)
        self.tableview.setSelectionBehavior(QAbstractItemView.SelectRows)
        try:
            self.tableview.sortByColumn(
                self.tableModel.header.index("bibkey"), Qt.AscendingOrder
            )
        except (IndexError, ValueError):
            pass
        self.tableview.sortByColumn(sortColumn, sortOrder)
        try:
            self.proxyModel.sort(
                self.tableModel.header.index("bibkey"), Qt.AscendingOrder
            )
        except (IndexError, ValueError):
            pass
        self.proxyModel.sort(sortColumn, sortOrder)
        self.currLayout.addWidget(self.tableview)

    def finalizeTable(self, gridPos=(1, 0)):
        """Resize the table to fit the contents,
        connect functions, add to layout

        Parameter:
            gridPos (tuple): if gridLayout is active,
            the position of the `QLineEdit` in the `QGridLayout`
        """
        self.tableview.resizeColumnsToContents()

        maxh = QGuiApplication.primaryScreen().availableGeometry().height()
        maxw = QGuiApplication.primaryScreen().availableGeometry().width()
        self.setMaximumHeight(maxh)
        self.setMaximumWidth(maxw)

        hwidth = self.tableview.horizontalHeader().length()
        swidth = self.tableview.style().pixelMetric(QStyle.PM_ScrollBarExtent)
        fwidth = self.tableview.frameWidth() * 2

        if self.tableWidth is None:
            if hwidth > maxw - (swidth + fwidth):
                self.tableWidth = maxw - (swidth + fwidth)
            else:
                self.tableWidth = hwidth + swidth + fwidth
        self.tableview.setFixedWidth(self.tableWidth)

        self.setMinimumHeight(600)

        self.tableview.resizeColumnsToContents()
        self.tableview.resizeRowsToContents()

        self.tableview.entered.connect(self.handleItemEntered)
        self.tableview.clicked.connect(self.cellClick)
        self.tableview.doubleClicked.connect(self.cellDoubleClick)

        if self.gridLayout:
            self.currLayout.addWidget(self.tableview, *gridPos)
        else:
            self.currLayout.addWidget(self.tableview)

    def recreateTable(self):
        """Delete the previous table widget and other layout items,
        then create new ones
        """
        self.cleanLayout()
        self.createTable()


class EditObjectWindow(PBDialog):
    """Create a window for editing or creating an experiment"""

    def __init__(self, parent=None):
        """Constructor.

        Parameter:
            parent: the parent object
        """
        super(EditObjectWindow, self).__init__(parent)
        self.textValues = {}
        self.result = False
        self.currGrid = None
        self.initUI()

    def keyPressEvent(self, e):
        """Intercept press keys and exit if escape is pressed

        Parameters:
            e: the `PySide6.QtGui.QKeyEvent`
        """
        if e.key() == Qt.Key_Escape:
            self.onCancel()

    def onCancel(self):
        """Set that the result should not be considered and exit"""
        self.result = False
        self.close()

    def onOk(self):
        """Set that the result should be considered and exit"""
        self.result = True
        self.close()

    def initUI(self):
        """Instantiate the `QGridLayout`"""
        self.currGrid = QGridLayout()
        self.currGrid.setSpacing(1)
        self.setLayout(self.currGrid)


class PBThread(QThread):
    """Extend `QThread`, but further extension is needed"""

    finished = Signal()

    def __init__(self, parent=None):
        """Construct the class using `QThread.__init__`

        Parameters:
            parent: the parent object (default None)
        """
        QThread.__init__(self, parent)

    def run(self):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def setStopFlag(self):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def start(self, *args, **kwargs):
        """Wait 0.3s and then call `QThread.start`
        (pass all the arguments)
        """
        time.sleep(0.3)
        QThread.start(self, *args, **kwargs)


class WriteStream(PBThread):
    """Class used to redirect prints to a window"""

    newText = Signal(str)

    finished = Signal()

    def __init__(self, queue, parent=None, *args, **kwargs):
        """Constructor

        Parameters:
            queue: a Queue instance
            parent (optional): the parent widget
        """
        super(WriteStream, self).__init__(parent, *args, **kwargs)
        self.queue = queue
        self.running = True

    def write(self, text):
        """Write the given text

        Parameters:
            text: the text to send to the output stream
        """
        self.queue.put(text)

    def run(self):
        """Run the thread"""
        last = time.time()
        text = ""
        while self.running:
            try:
                text += self.queue.get(timeout=0.2)
            except Empty:
                pass
            if text != "" and (len(text) > 1000 or time.time() - last > 0.1):
                self.newText.emit(text)
                last = time.time()
                text = ""
        if text != "":
            self.newText.emit(text)
        self.finished.emit()


class PBTableView(QTableView):
    """Extension of `QTableView`, used to define the contextMenuEvent"""

    def contextMenuEvent(self, event):
        """Connect the context menu event to the parent function

        Parameter:
            event: the `PySide6.QtGui.QContextMenuEvent`
        """
        self.parent().triggeredContextMenuEvent(
            self.rowAt(event.y()), self.columnAt(event.x()), event
        )


class PBTableModel(QAbstractTableModel):
    """Extension of `QAbstractTableModel`,
    used for experiments and bibtex entries
    """

    def __init__(self, parent, header, ask=False, previous=[], *args):
        """Constructor, based on `QAbstractTableModel.__init__`

        Parameters:
            parent: the parent widget
            header: the list of column names which constitute the table
            ask (boolean, default False):
                when True, allow to select the lines with a checkbox
            previous: the list of lines which must be selected at the beginning
        """
        QAbstractTableModel.__init__(self, parent, *args)
        self.header = header
        self.parentObj = parent
        self.previous = previous
        self.ask = ask
        self.selectedElements = {}
        self.painter = None

    def parent(self):
        """Return the parent object"""
        return self.parentObj

    def changeAsk(self, new=None):
        """Change the value of `self.ask` to show/hide checkboxes

        Parameters:
            new: `None` to just swap the current value of `self.ask`,
                `True` or `False` to set it to the desired value
        """
        self.layoutAboutToBeChanged.emit()
        if new is None:
            self.ask = not self.ask
        elif new in (True, False):
            self.ask = new
        self.layoutChanged.emit()

    def getIdentifier(self, element):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def prepareSelected(self):
        """Fill the dictionary `self.selectedElements`
        according to previous selection
        """
        self.layoutAboutToBeChanged.emit()
        self.selectedElements = {}
        try:
            for bib in self.dataList:
                self.selectedElements[self.getIdentifier(bib)] = False
        except AttributeError:
            pBLogger.exception(ccstr.dataListNotDef)
        for prevK in self.previous:
            try:
                if self.selectedElements[prevK] == False:
                    self.selectedElements[prevK] = True
            except (KeyError, IndexError):
                pBLogger.exception(ccstr.invalidIdentif % (prevK))
        self.layoutChanged.emit()

    def selectAll(self):
        """Select all the available rows"""
        self.layoutAboutToBeChanged.emit()
        for key in self.selectedElements.keys():
            self.selectedElements[key] = True
        self.layoutChanged.emit()

    def unselectAll(self):
        """Unselect all the available rows"""
        self.layoutAboutToBeChanged.emit()
        for key in self.selectedElements.keys():
            self.selectedElements[key] = False
        self.layoutChanged.emit()

    def addImage(self, imagePath, height):
        """Create a cell containing an image

        Parameters:
            imagePath: the path of the image
            height: the height to give to the image

        Output:
            a QPixmap
        """
        return QPixmap(imagePath).scaledToHeight(height)

    def addImages(self, imagePaths, outHeight, height=48):
        """Create a cell containing multiple images, using a `QPainter`

        Parameters:
            imagePaths: a list of image paths
                for creating the single `QPixmap`s
            outHeight: the final height for rescaling the image
            height (default 48): the height and width
                to be used when painting

        Output:
            a `QPixmap`
        """
        width = len(imagePaths) * height
        pm = QPixmap(width, height)
        pm.fill(Qt.transparent)
        self.painter = QPainter(pm)
        for i, img in enumerate(imagePaths):
            self.painter.drawPixmap(i * height, 0, QPixmap(img))
        self.painter.end()
        return pm.scaledToHeight(outHeight)

    def rowCount(self, parent=None):
        """Count the rows of the given model based on the header

        Parameter:
            parent: `QModelIndex` (required by the parent signature)

        Output:
            the number od rows or zero (if error occurred)
        """
        try:
            return len(self.dataList)
        except (TypeError, AttributeError):
            return 0

    def columnCount(self, parent=None):
        """Count the columns of the given model based on the header

        Parameter:
            parent: `QModelIndex` (required by the parent signature)

        Output:
            the number of columns or zero (if error occurred)
        """
        try:
            return len(self.header)
        except TypeError:
            return 0

    def data(self, index, role):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def flags(self, index):
        """Determine the flags of a given item

        Parameter:
            index: a `QModelIndex`

        Output:
            None if the index is not valid
            if `self.ask` and first column, show checkboxes:
            Qt.ItemIsUserCheckable | Qt.ItemIsEditable |
                Qt.ItemIsEnabled | Qt.ItemIsSelectable
            all the other cases: Qt.ItemIsEnabled | Qt.ItemIsSelectable
        """
        if not index.isValid():
            return Qt.NoItemFlags
        if index.column() == 0 and self.ask:
            return (
                Qt.ItemIsUserCheckable
                | Qt.ItemIsEditable
                | Qt.ItemIsEnabled
                | Qt.ItemIsSelectable
            )
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def headerData(self, col, orientation, role):
        """Obtain column name if correctly asked

        Parameters:
            col: the column index in `self.header`
            orientation: int from `Qt.Orientation`
            role: int from `Qt.ItemDataRole`

        Output:
            the column name or `None`
        """
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.header[col]
        return None

    def setData(self, index, value, role):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()


# https://www.hardcoded.net/articles/using_qtreeview_with_qabstractitemmodel
class TreeNode(QObject):
    """Create an object that will work as a tree node"""

    # map `void*` -> `TreeNode` instance
    # see https://bugreports.qt.io/browse/PYSIDE-883
    _instances = WeakValueDictionary()

    @classmethod
    def cast(cls, obj):
        """Convert a VoidPtr to TreeNode, if possible and if necessary.

        Parameters:
            cls: the class instance
            obj: an index, from which the VoidPtr is extracted
                by internalPointer

        Output:
            should be a TreeNode instance
        """
        return obj.internalPointer()

    def __init__(self, parent, row):
        """Constructor, set basic properties

        Parameters:
            parent: the parent node
            row: the content of the data row
        """
        super(TreeNode, self).__init__()
        self.parentObj = parent
        self.row = row
        self.subnodes = self._getChildren()

    def parent(self):
        """Return the parent object"""
        return self.parentObj

    def _getChildren(self):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()


class TreeModel(QAbstractItemModel):
    """Model for a tree structure."""

    def __init__(self):
        """Class constructor. Calls `_getRootNodes`
        to build the tree structure
        """
        QAbstractItemModel.__init__(self)
        self.rootNodes = self._getRootNodes()

    def _getRootNodes(self):
        """Not implemented: requires a subclass"""
        raise NotImplementedError()

    def index(self, row, column, parent=QModelIndex()):
        """Retrieve the `QModelIndex` of the requested object

        Parameters:
            row (int): the row index
            column (int): the column index
            parent: the parent `QModelIndex`

        Output:
            A `QModelIndex` instance
        """
        if not isinstance(parent, QModelIndex):
            pBLogger.debug(ccstr.invalidParentTM % (parent, "index"), exc_info=True)
            return QModelIndex()
        if not parent.isValid():
            try:
                return self.createIndex(row, column, self.rootNodes[row])
            except IndexError:
                return QModelIndex()
        parentNode = TreeNode.cast(parent)
        if parentNode is None:
            return QModelIndex()
        try:
            return self.createIndex(row, column, parentNode.subnodes[row])
        except IndexError:
            return QModelIndex()

    def parent(self, index):
        """Retrieve the `QModelIndex` of the parent of
        the item with the given index, if it exists,
        or an invalid `QModelIndex` instead

        Parameters:
            index: the `QModelIndex` of the node

        Output:
            A `QModelIndex` instance
        """
        if not isinstance(index, QModelIndex):
            pBLogger.debug(ccstr.invalidIndexTMP % index, exc_info=True)
            return QModelIndex()
        if not index.isValid():
            return QModelIndex()
        node = TreeNode.cast(index)
        if node is None:
            return QModelIndex()
        nodeParent = node.parent()
        if nodeParent is None:
            return QModelIndex()
        return self.createIndex(nodeParent.row, 0, nodeParent)

    def rowCount(self, parent=QModelIndex()):
        """Count the rows in a given tree branch

        Parameter:
            parent: the `QModelIndex` of the branch parent

        Output:
            the line number
        """
        if not isinstance(parent, QModelIndex):
            pBLogger.debug(ccstr.invalidParentTM % (parent, "rowCount"), exc_info=True)
            return len(self.rootNodes)
        if not parent.isValid():
            return len(self.rootNodes)
        node = TreeNode.cast(parent)
        if node is None:
            return len(self.rootNodes)
        return len(node.subnodes)


class NamedElement(object):
    """Basic object for the tree structure of categories"""

    def __init__(self, idCat, name, subelements):
        """Class constructor, set some element properties

        Parameters:
            idCat: the category id
            name: the name of the category
            subelements: the list of children elements
        """
        self.idCat = idCat
        self.name = name
        self.text = catString(idCat, pBDB)
        self.subelements = subelements


class NamedNode(TreeNode):
    """Extend `TreeNode` to work with `NamedElement`"""

    def __init__(self, element, parent, row):
        """Define `self.element` and call `TreeNode.__init__`

        Parameters:
            element: the `NamedElement` of the object
            parent: the parent node
            row: the row index
        """
        self.element = element
        TreeNode.__init__(self, parent, row)

    def _getChildren(self):
        """Return a list of `NamedNode`s, containing the children nodes.
        Overrides `TreeNode._getChildren`
        """
        return [
            NamedNode(elem, self, index)
            for index, elem in enumerate(self.element.subelements)
        ]


# http://gaganpreet.in/blog/2013/07/04/qtreeview-and-custom-filter-models/
class LeafFilterProxyModel(QSortFilterProxyModel):
    """Class to override the following behaviour:
        If a parent item doesn't match the filter,
        none of its children will be shown.

    This Model matches items which are descendants
    or ascendants of matching items.
    """

    def filterAcceptsRow(self, row_num, source_parent):
        """Overriding the parent function

        Parameters:
            row_num: the row number
            source_parent: the parent node in the tree
        """
        # Check if the current row matches
        if self.filterAcceptsRowItself(row_num, source_parent):
            return True

        # Traverse up all the way to root and check if any of them match
        if self.filterAcceptsAnyParent(source_parent):
            return True

        # Finally, check if any of the children match
        return self.hasAcceptedChildren(row_num, source_parent)

    def filterAcceptsRowItself(self, row_num, parent):
        """New name for the original `filterAcceptsRow`,
        which has been overridden

        Parameters:
            row_num: the row number
            parent: the parent node in the tree
        """
        return super(LeafFilterProxyModel, self).filterAcceptsRow(row_num, parent)

    def filterAcceptsAnyParent(self, parent):
        """Traverse to the root node and check if any of the
        ancestors match the filter

        Parameter:
            parent: the parent node in the tree
        """
        while parent.isValid():
            if self.filterAcceptsRowItself(parent.row(), parent.parent()):
                return True
            parent = parent.parent()
        return False

    def hasAcceptedChildren(self, row_num, parent):
        """Starting from the current node as root, traverse all
        the descendants and test if any of the children match

        Parameters:
            row_num: the row number
            parent: the parent node in the tree
        """
        model = self.sourceModel()
        source_index = model.index(row_num, 0, parent)

        children_count = model.rowCount(source_index)
        for i in range(children_count):
            if self.filterAcceptsRow(i, source_index):
                return True
        return False


class PBDDTableWidget(QTableWidget):
    """Drag and drop extension of QTableWidget"""

    def __init__(self, parent, header):
        """Set some properties and settings.

        Parameters:
            header: the title of the column
        """
        super(PBDDTableWidget, self).__init__(parent)
        self.setColumnCount(1)
        self.setHorizontalHeaderLabels([header])
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setDragDropOverwriteMode(False)
        self.lastDropRow = None

    # Override this method to get the correct row index for insertion
    def dropMimeData(self, row, col, mimeData, action):
        """Overridden method to get the row index for insertion

        Parameters:
            row: the index of the dropped row
            col, mimeData, action: not used params
                (see the signature of `QTableWidget.dropMimeData`)
        """
        self.lastDropRow = row
        return True

    def dropEvent(self, event):
        """Accept dropEvent and move the selected item
        to the new position

        Parameter:
            event: a `QDropEvent`
        """
        # The QTableWidget from which selected rows will be moved
        sender = event.source()

        # Default dropEvent method fires dropMimeData
        # with appropriate parameters (we're interested in the row index).
        super(PBDDTableWidget, self).dropEvent(event)
        # Now we know where to insert selected row(s)
        dropRow = self.lastDropRow

        selectedRows = sender.getselectedRowsFast()

        # Allocate space for transfer
        for _ in selectedRows:
            self.insertRow(dropRow)

        # if sender == receiver (self),
        # after creating new empty rows selected rows might change
        # their locations
        sel_rows_offsets = [
            0 if self != sender or srow < dropRow else len(selectedRows)
            for srow in selectedRows
        ]
        selectedRows = [
            row + offset for row, offset in zip(selectedRows, sel_rows_offsets)
        ]

        # copy content of selected rows into empty ones
        for i, srow in enumerate(selectedRows):
            for j in range(self.columnCount()):
                item = sender.item(srow, j)
                if item:
                    source = QTableWidgetItem(item)
                    self.setItem(dropRow + i, j, source)

        # delete selected rows
        for srow in reversed(selectedRows):
            sender.removeRow(srow)

        event.accept()

    def getselectedRowsFast(self):
        """Return the list of selected rows

        Output:
            a list with the row indexes of the selected rows
        """
        selectedRows = []
        for item in self.selectedItems():
            row = item.row()
            text = item.text()
            if row not in selectedRows and text != "bibkey":
                selectedRows.append(row)
        return sorted(selectedRows)


class PBMenu(QMenu):
    """Extend `QMenu` for faster menu build"""

    def __init__(self, parent=None):
        """Construct the element defining some basic properties

        Parameter:
            parent: the parent widget
        """
        super(PBMenu, self).__init__(parent)
        self.possibleActions = []
        self.result = False

    def fillMenu(self):
        """Add actions, separators or submenus according to the content
        of `self.possibleActions` (recursively for submenus)
        """
        for act in self.possibleActions:
            if act is None:
                self.addSeparator()
            elif isinstance(act, list):
                submenu = PBMenu()
                submenu.setTitle(act[0])
                submenu.possibleActions = act[1]
                submenu.fillMenu()
                self.addMenu(submenu)
            elif isinstance(act, dict):
                submenu = PBMenu()
                submenu.setTitle(act["title"])
                submenu.possibleActions = act["actions"]
                submenu.fillMenu()
                try:
                    submenu.setToolTipsVisible(act["toolTipsVisible"])
                except KeyError:
                    pass
                self.addMenu(submenu)
            elif isinstance(act, QAction):
                self.addAction(act)

    def keyPressEvent(self, e):
        """Intercept press keys and exit if escape is pressed

        Parameters:
            e: the `PySide6.QtGui.QKeyEvent`
        """
        if e.key() == Qt.Key_Escape:
            self.close()


class GUIViewEntry(ViewEntry):
    """Extends the ViewEntry class to work with QtGui.QDesktopServices"""

    def openLink(self, key, arg="", fileArg=None):
        """Use `QDesktopServices` to open an url using
        the system default applications

        Parameters:
            key: the entry key or the link (if `arg` == "link")
            arg:
                if `arg` == "file", `fileArg` must be the file name
                if `arg` == "link", `key` must be the link to be opened
                for any other values, the link will be generated using
                    the `physbiblio.view.viewWntry.getLink` method
            fileArg: the file name if `arg` == "file", or
                the argument passed to `physbiblio.view.viewWntry.getLink`
                if needed
        """
        if isinstance(key, list):
            for k in key:
                self.openLink(k, arg, fileArg)
        else:
            if arg == "file":
                url = QUrl.fromLocalFile(fileArg)
            elif arg == "link":
                url = QUrl(key)
            else:
                link = self.getLink(key, arg=arg, fileArg=fileArg)
                url = QUrl(link)
            if QDesktopServices.openUrl(url):
                pBLogger.debug(ccstr.openSuccess % (url.toString(), key))
            else:
                pBLogger.warning(ccstr.openFailed % key)


pBGuiView = GUIViewEntry()


class PBImportedTableModel(PBTableModel):
    """Extend `PBTableModel` to manage the selection
    during the import of entries
    """

    def __init__(self, parent, bibdict, header, idName="ID", *args):
        """Set some properties and settings

        Parameters:
            parent: the parent widget (pass to `PBTableModel.__init__`)
            bibdict: a dictionary with the info
                of the imported bibtex entries.
                It should contain dictionaries with two items:
                    "bibpars", "exist"
            header: the header names to be passed
                to `PBTableModel.__init__`
            idName: the key of the field representing
                the unique ID of the item
        """
        self.typeClass = "imports"
        self.idName = idName
        self.bibsOrder = [k for k in sorted(bibdict.keys())]
        self.dataList = [bibdict[k]["bibpars"] for k in self.bibsOrder]
        self.existList = [bibdict[k]["exist"] for k in self.bibsOrder]
        PBTableModel.__init__(self, parent, header, *args)
        self.prepareSelected()

    def getIdentifier(self, element):
        """Return the unique identifier of the given element

        Parameters:
            element: a dictionary
        """
        return element[self.idName]

    def data(self, index, role):
        """Return the data for the requested cell and role

        Parameters:
            index: a `QModelIndex`
            role: the requested role for the given cell
        """
        if not index.isValid():
            return None
        row = index.row()
        column = index.column()
        try:
            value = self.dataList[row][self.header[column]]
        except (IndexError, KeyError):
            pBLogger.debug(ccstr.missElement, exc_info=True)
            return None

        if role == Qt.CheckStateRole and column == 0 and not self.existList[row]:
            if self.selectedElements[self.dataList[row][self.idName]] == False:
                return Qt.Unchecked
            else:
                return Qt.Checked
        if (
            role in (Qt.EditRole, Qt.DisplayRole)
            and column == 0
            and self.existList[row]
        ):
            return value + ccstr.alreadyExisting
        if role == Qt.EditRole:
            return value
        if role == Qt.DisplayRole:
            return value
        return None

    def setData(self, index, value, role):
        """Set the selection data for a given index

        Parameters:
            index: a `QModelIndex`
            value: `Qt.Checked` to set the row as selected
            role: `Qt.CheckStateRole` for performing an action,
                or any other role
        """
        if not index.isValid():
            return False
        if role == Qt.CheckStateRole and index.column() == 0:
            if Qt.CheckState(value) == Qt.Checked:
                self.selectedElements[self.dataList[index.row()][self.idName]] = True
            else:
                self.selectedElements[self.dataList[index.row()][self.idName]] = False
        self.dataChanged.emit(index, index)
        return True

    def flags(self, index):
        """Return the flags for the requested row

        Parameters:
            index: a `QModelIndex`
        """
        if not index.isValid():
            return Qt.NoItemFlags
        if index.column() == 0 and not self.existList[index.row()]:
            return (
                Qt.ItemIsUserCheckable
                | Qt.ItemIsEditable
                | Qt.ItemIsEnabled
                | Qt.ItemIsSelectable
            )
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable


class ObjectWithSignal(QObject):
    """Being a QObject is necessary to create a signal and use it"""

    customSignal = Signal()
