"""
/***************************************************************************
Python Console for QGIS
                             -------------------
begin                : 2012-09-10
copyright            : (C) 2012 by Salvatore Larosa
email                : lrssvtml (at) gmail (dot) com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
Some portions of code were taken from https://code.google.com/p/pydee/
"""

from __future__ import annotations

import codecs
import importlib
import os
import pyclbr
import re
import sys
import tempfile
from typing import Optional, TYPE_CHECKING
from functools import partial
from operator import itemgetter
from pathlib import Path

from qgis.core import Qgis, QgsApplication, QgsBlockingNetworkRequest, QgsSettings
from qgis.gui import QgsCodeEditorPython, QgsCodeEditorWidget, QgsMessageBar

from qgis.PyQt.Qsci import QsciScintilla
from qgis.PyQt.QtCore import (
    pyqtSignal,
    QByteArray,
    QCoreApplication,
    QDir,
    QEvent,
    QFileInfo,
    QJsonDocument,
    QSize,
    Qt,
    QUrl,
)
from qgis.PyQt.QtGui import QKeySequence, QColor, QPalette
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtWidgets import (
    QAction,
    QApplication,
    QFileDialog,
    QFrame,
    QGridLayout,
    QLabel,
    QMenu,
    QMessageBox,
    QShortcut,
    QSizePolicy,
    QSpacerItem,
    QTabWidget,
    QToolButton,
    QTreeWidgetItem,
    QWidget,
)
from qgis.utils import OverrideCursor, iface

if TYPE_CHECKING:
    from .console import PythonConsoleWidget


class Editor(QgsCodeEditorPython):

    trigger_find = pyqtSignal()

    def __init__(
        self,
        editor_tab: EditorTab,
        console_widget: PythonConsoleWidget,
        tab_widget: EditorTabWidget,
    ):
        super().__init__(editor_tab)
        self.editor_tab: EditorTab = editor_tab
        self.console_widget: PythonConsoleWidget = console_widget
        self.tab_widget: EditorTabWidget = tab_widget
        self.code_editor_widget: QgsCodeEditorWidget | None = None

        self.setMinimumHeight(120)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)

        # Disable default scintilla shortcuts
        ctrl, shift = self.SCMOD_CTRL << 16, self.SCMOD_SHIFT << 16
        self.SendScintilla(
            QsciScintilla.SCI_CLEARCMDKEY, ord("T") + ctrl
        )  # Switch current line with the next one
        self.SendScintilla(
            QsciScintilla.SCI_CLEARCMDKEY, ord("D") + ctrl
        )  # Duplicate current line / selection
        self.SendScintilla(
            QsciScintilla.SCI_CLEARCMDKEY, ord("L") + ctrl
        )  # Delete current line
        self.SendScintilla(QsciScintilla.SCI_CLEARCMDKEY, ord("L") + ctrl + shift)

        # New QShortcut = ctrl+space/ctrl+alt+space for Autocomplete
        self.newShortcutCS = QShortcut(
            QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Space), self
        )
        self.newShortcutCS.setContext(Qt.ShortcutContext.WidgetShortcut)
        self.redoScut = QShortcut(
            QKeySequence(Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_Z), self
        )
        self.redoScut.setContext(Qt.ShortcutContext.WidgetShortcut)
        self.redoScut.activated.connect(self.redo)
        self.newShortcutCS.activated.connect(self.autoComplete)
        self.runScut = QShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_E), self)
        self.runScut.setContext(Qt.ShortcutContext.WidgetShortcut)
        self.runScut.activated.connect(self.runSelectedCode)  # spellok
        self.runScriptScut = QShortcut(
            QKeySequence(Qt.Modifier.SHIFT | Qt.Modifier.CTRL | Qt.Key.Key_E), self
        )
        self.runScriptScut.setContext(Qt.ShortcutContext.WidgetShortcut)
        self.runScriptScut.activated.connect(self.runScriptCode)

        self.syntaxCheckScut = QShortcut(
            QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_4), self
        )
        self.syntaxCheckScut.setContext(Qt.ShortcutContext.WidgetShortcut)
        self.syntaxCheckScut.activated.connect(self.syntaxCheck)
        self.modificationChanged.connect(self.editor_tab.modified)
        self.modificationAttempted.connect(self.fileReadOnly)

    def set_code_editor_widget(self, widget: QgsCodeEditorWidget):
        self.code_editor_widget = widget
        self.code_editor_widget.loadedExternalChanges.connect(
            self.loaded_external_changes
        )

    def settingsEditor(self):
        # Set Python lexer
        self.initializeLexer()

    def contextMenuEvent(self, e):
        menu = QMenu(self)
        menu.addAction(
            QCoreApplication.translate("PythonConsole", "Hide Editor"), self.hideEditor
        )
        menu.addSeparator()

        syntaxCheckAction = QAction(
            QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.svg"),
            QCoreApplication.translate("PythonConsole", "Check Syntax"),
            menu,
        )
        syntaxCheckAction.triggered.connect(self.syntaxCheck)
        syntaxCheckAction.setShortcut("Ctrl+4")
        menu.addAction(syntaxCheckAction)

        runSelected = QAction(
            QgsApplication.getThemeIcon("console/mIconRunConsole.svg"),  # spellok
            QCoreApplication.translate("PythonConsole", "Run Selected"),
            menu,
        )
        runSelected.triggered.connect(self.runSelectedCode)  # spellok
        runSelected.setShortcut("Ctrl+E")  # spellok
        menu.addAction(runSelected)  # spellok

        pyQGISHelpAction = QAction(
            QgsApplication.getThemeIcon("console/iconHelpConsole.svg"),
            QCoreApplication.translate(
                "PythonConsole", "Search Selection in PyQGIS Documentation"
            ),
            menu,
        )
        pyQGISHelpAction.triggered.connect(self.searchSelectedTextInPyQGISDocs)
        menu.addAction(pyQGISHelpAction)

        start_action = QAction(
            QgsApplication.getThemeIcon("mActionStart.svg"),
            QCoreApplication.translate("PythonConsole", "Run Script"),
            menu,
        )
        start_action.triggered.connect(self.runScriptCode)
        start_action.setShortcut("Ctrl+Shift+E")
        menu.addAction(start_action)

        menu.addSeparator()
        undoAction = QAction(
            QgsApplication.getThemeIcon("mActionUndo.svg"),
            QCoreApplication.translate("PythonConsole", "Undo"),
            menu,
        )
        undoAction.triggered.connect(self.undo)
        undoAction.setShortcut(QKeySequence.StandardKey.Undo)
        menu.addAction(undoAction)

        redoAction = QAction(
            QgsApplication.getThemeIcon("mActionRedo.svg"),
            QCoreApplication.translate("PythonConsole", "Redo"),
            menu,
        )
        redoAction.triggered.connect(self.redo)
        redoAction.setShortcut("Ctrl+Shift+Z")
        menu.addAction(redoAction)

        menu.addSeparator()
        find_action = QAction(
            QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg"),
            QCoreApplication.translate("PythonConsole", "Find Text"),
            menu,
        )
        find_action.triggered.connect(self.trigger_find)
        menu.addAction(find_action)

        cutAction = QAction(
            QgsApplication.getThemeIcon("mActionEditCut.svg"),
            QCoreApplication.translate("PythonConsole", "Cut"),
            menu,
        )
        cutAction.triggered.connect(self.cut)
        cutAction.setShortcut(QKeySequence.StandardKey.Cut)
        menu.addAction(cutAction)

        copyAction = QAction(
            QgsApplication.getThemeIcon("mActionEditCopy.svg"),
            QCoreApplication.translate("PythonConsole", "Copy"),
            menu,
        )
        copyAction.triggered.connect(self.copy)
        copyAction.setShortcut(QKeySequence.StandardKey.Copy)
        menu.addAction(copyAction)

        pasteAction = QAction(
            QgsApplication.getThemeIcon("mActionEditPaste.svg"),
            QCoreApplication.translate("PythonConsole", "Paste"),
            menu,
        )
        pasteAction.triggered.connect(self.paste)
        pasteAction.setShortcut(QKeySequence.StandardKey.Paste)
        menu.addAction(pasteAction)

        selectAllAction = QAction(
            QCoreApplication.translate("PythonConsole", "Select All"), menu
        )
        selectAllAction.triggered.connect(self.selectAll)
        selectAllAction.setShortcut(QKeySequence.StandardKey.SelectAll)
        menu.addAction(selectAllAction)

        menu.addSeparator()
        toggle_comment_action = QAction(
            QgsApplication.getThemeIcon(
                "console/iconCommentEditorConsole.svg",
                self.palette().color(QPalette.ColorRole.WindowText),
            ),
            QCoreApplication.translate("PythonConsole", "Toggle Comment"),
            menu,
        )
        toggle_comment_action.triggered.connect(self.toggleComment)
        toggle_comment_action.setShortcut("Ctrl+:")
        menu.addAction(toggle_comment_action)

        menu.addSeparator()
        gist_menu = QMenu(self)
        gist_menu.setTitle(
            QCoreApplication.translate("PythonConsole", "Share on GitHub")
        )
        gist_menu.setIcon(QgsApplication.getThemeIcon("console/iconCodepadConsole.svg"))
        gist_menu.addAction(
            QCoreApplication.translate("PythonConsole", "Secret Gist"),
            partial(self.shareOnGist, False),
        )
        gist_menu.addAction(
            QCoreApplication.translate("PythonConsole", "Public Gist"),
            partial(self.shareOnGist, True),
        )
        menu.addMenu(gist_menu)
        showCodeInspection = menu.addAction(
            QgsApplication.getThemeIcon("console/iconClassBrowserConsole.svg"),
            QCoreApplication.translate("PythonConsole", "Hide/Show Object Inspector"),
            self.objectListEditor,
        )
        menu.addSeparator()
        menu.addAction(
            QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"),
            QCoreApplication.translate("PythonConsole", "Options…"),
            self.console_widget.openSettings,
        )
        syntaxCheckAction.setEnabled(False)
        pasteAction.setEnabled(False)
        pyQGISHelpAction.setEnabled(False)
        cutAction.setEnabled(False)
        runSelected.setEnabled(False)  # spellok
        copyAction.setEnabled(False)
        selectAllAction.setEnabled(False)
        undoAction.setEnabled(False)
        redoAction.setEnabled(False)
        showCodeInspection.setEnabled(False)
        if self.hasSelectedText():
            runSelected.setEnabled(True)  # spellok
            copyAction.setEnabled(True)
            cutAction.setEnabled(True)
            pyQGISHelpAction.setEnabled(True)
        if not self.text() == "":
            selectAllAction.setEnabled(True)
            syntaxCheckAction.setEnabled(True)
        if self.isUndoAvailable():
            undoAction.setEnabled(True)
        if self.isRedoAvailable():
            redoAction.setEnabled(True)
        if QApplication.clipboard().text():
            pasteAction.setEnabled(True)
        if QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool):
            showCodeInspection.setEnabled(True)
        menu.exec(self.mapToGlobal(e.pos()))

    def objectListEditor(self):
        listObj = self.console_widget.listClassMethod
        if listObj.isVisible():
            listObj.hide()
            self.console_widget.objectListButton.setChecked(False)
        else:
            listObj.show()
            self.console_widget.objectListButton.setChecked(True)

    def shareOnGist(self, is_public):
        self.code_editor_widget.shareOnGist(is_public)

    def hideEditor(self):
        self.console_widget.splitterObj.hide()
        self.console_widget.showEditorButton.setChecked(False)

    def createTempFile(self):
        name = tempfile.NamedTemporaryFile(delete=False).name
        # Need to use newline='' to avoid adding extra \r characters on Windows
        with open(name, "w", encoding="utf-8", newline="") as f:
            f.write(self.text())
        return name

    def runScriptCode(self):
        autoSave = QgsSettings().value("pythonConsole/autoSaveScript", False, type=bool)
        filename = self.code_editor_widget.filePath()
        filename_override = None
        msgEditorBlank = QCoreApplication.translate(
            "PythonConsole", "Empty scripts cannot be run!"
        )
        if not filename:
            if not self.isModified():
                self.showMessage(msgEditorBlank)
                return

        deleteTempFile = False
        if self.syntaxCheck():
            if filename and self.isModified() and autoSave:
                self.save(filename)
            elif not filename or self.isModified():
                # Create a new temp file if the file isn't already saved.
                filename = self.createTempFile()
                filename_override = self.tab_widget.tabText(
                    self.tab_widget.currentIndex()
                )
                if filename_override.startswith("*"):
                    filename_override = filename_override[1:]
                deleteTempFile = True

            self.console_widget.shell.runFile(filename, filename_override)

            if deleteTempFile:
                Path(filename).unlink()

    def runSelectedCode(self):  # spellok
        cmd = self.selectedText()
        self.console_widget.shell.insertFromDropPaste(cmd)
        self.console_widget.shell.entered()
        self.setFocus()

    def getTextFromEditor(self):
        text = self.text()
        textList = text.split("\n")
        return textList

    def goToLine(self, objName, linenr):
        self.SendScintilla(QsciScintilla.SCI_GOTOLINE, linenr - 1)
        self.SendScintilla(
            QsciScintilla.SCI_SETTARGETSTART,
            self.SendScintilla(QsciScintilla.SCI_GETCURRENTPOS),
        )
        self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, len(self.text()))
        pos = self.SendScintilla(
            QsciScintilla.SCI_SEARCHINTARGET, len(objName), objName
        )
        index = pos - self.SendScintilla(QsciScintilla.SCI_GETCURRENTPOS)
        # line, _ = self.getCursorPosition()
        self.setSelection(linenr - 1, index, linenr - 1, index + len(objName))
        self.ensureLineVisible(linenr)
        self.setFocus()

    def syntaxCheck(self):
        self.code_editor_widget.clearWarnings()
        source = self.text().encode("utf-8")
        try:
            compile(source, "", "exec")
        except SyntaxError as detail:
            eline = detail.lineno or 1
            eline -= 1
            ecolumn = detail.offset or 1
            edescr = detail.msg
            self.code_editor_widget.addWarning(eline, edescr)
            self.setCursorPosition(eline, ecolumn - 1)
            self.ensureLineVisible(eline)
            return False

        return True

    def loaded_external_changes(self):
        self.tab_widget.listObject(self.tab_widget.currentWidget())

    def fileReadOnly(self):
        msgText = QCoreApplication.translate(
            "PythonConsole",
            'The file <b>"{0}"</b> is read only, please save to different file first.',
        ).format(self.code_editor_widget.filePath())
        self.showMessage(msgText)

    def save(self, filename: str | None = None):
        if self.isReadOnly():
            return

        if QgsSettings().value("pythonConsole/formatOnSave", False, type=bool):
            self.reformatCode()

        index = self.tab_widget.indexOf(self.editor_tab)
        if not filename and not self.code_editor_widget.filePath():
            saveTr = QCoreApplication.translate(
                "PythonConsole", "Python Console: Save file"
            )
            folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath())
            path, filter = QFileDialog().getSaveFileName(
                self,
                saveTr,
                os.path.join(
                    folder, self.tab_widget.tabText(index).replace("*", "") + ".py"
                ),
                "Script file (*.py)",
            )
            # If the user didn't select a file, abort the save operation
            if not path:
                self.code_editor_widget.setFilePath(None)
                return
            filename = path

        self.code_editor_widget.save(filename)

        msgText = QCoreApplication.translate(
            "PythonConsole", "Script was correctly saved."
        )
        self.showMessage(msgText)

        # Save the new contents
        # Need to use newline='' to avoid adding extra \r characters on Windows
        with open(
            self.code_editor_widget.filePath(), "w", encoding="utf-8", newline=""
        ) as f:
            f.write(self.text())
        self.tab_widget.setTabTitle(
            index, Path(self.code_editor_widget.filePath()).name
        )
        self.tab_widget.setTabToolTip(index, self.code_editor_widget.filePath())
        self.setModified(False)
        self.console_widget.saveFileButton.setEnabled(False)
        self.console_widget.updateTabListScript(
            self.code_editor_widget.filePath(), action="append"
        )
        self.tab_widget.listObject(self.editor_tab)
        QgsSettings().setValue(
            "pythonConsole/lastDirPath",
            Path(self.code_editor_widget.filePath()).parent.as_posix(),
        )

    def event(self, e):
        """Used to override the Application shortcuts when the editor has focus"""

        if e.type() == QEvent.Type.ShortcutOverride:
            ctrl = e.modifiers() == Qt.KeyboardModifier.ControlModifier
            ctrl_shift = e.modifiers() == (
                Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
            )
            if (
                (ctrl and e.key() == Qt.Key.Key_W)
                or (ctrl_shift and e.key() == Qt.Key.Key_W)
                or (ctrl and e.key() == Qt.Key.Key_S)
                or (ctrl_shift and e.key() == Qt.Key.Key_S)
                or (ctrl and e.key() == Qt.Key.Key_T)
                or (ctrl and e.key() == Qt.Key.Key_Tab)
            ):
                e.accept()
                return True

        return super().event(e)

    def keyPressEvent(self, e):
        ctrl = e.modifiers() == Qt.KeyboardModifier.ControlModifier
        ctrl_shift = e.modifiers() == (
            Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
        )

        # Ctrl+W: close current tab
        if ctrl and e.key() == Qt.Key.Key_W:
            self.editor_tab.close()

        # Ctrl+Shift+W: close all tabs
        if ctrl_shift and e.key() == Qt.Key.Key_W:
            self.tab_widget.closeAll()

        # Ctrl+S: save current tab
        if ctrl and e.key() == Qt.Key.Key_S:
            self.save()

        # Ctrl+Shift+S: save current tab as
        if ctrl_shift and e.key() == Qt.Key.Key_S:
            self.tab_widget.saveAs()

        # Ctrl+T: open new tab
        if ctrl and e.key() == Qt.Key.Key_T:
            self.tab_widget.newTabEditor()

        super().keyPressEvent(e)

    def showMessage(
        self, text: str, title: str | None = None, level=Qgis.MessageLevel.Info
    ):
        self.editor_tab.showMessage(text, level, title=title)


class EditorTab(QWidget):

    search_bar_toggled = pyqtSignal(bool)

    def __init__(
        self,
        tab_widget: EditorTabWidget,
        console_widget: PythonConsoleWidget,
        filename: str | None,
        read_only: bool,
    ):
        super().__init__(tab_widget)
        self.tab_widget: EditorTabWidget = tab_widget

        self._editor = Editor(
            editor_tab=self, console_widget=console_widget, tab_widget=tab_widget
        )

        self._editor_code_widget = QgsCodeEditorWidget(self._editor)
        self._editor.set_code_editor_widget(self._editor_code_widget)
        self._editor_code_widget.searchBarToggled.connect(self.search_bar_toggled)

        self._editor.trigger_find.connect(self._editor_code_widget.triggerFind)

        if filename:
            if QFileInfo(filename).exists():
                self._editor_code_widget.loadFile(filename)
                if read_only:
                    self._editor.setReadOnly(True)

        self.tabLayout = QGridLayout(self)
        self.tabLayout.setContentsMargins(0, 0, 0, 0)
        self.tabLayout.addWidget(self._editor_code_widget)

    def set_file_path(self, path: str):
        self._editor_code_widget.setFilePath(path)

    def file_path(self) -> str | None:
        return self._editor_code_widget.filePath()

    def open_in_external_editor(self):
        self._editor_code_widget.openInExternalEditor()

    def modified(self, modified):
        self.tab_widget.tabModified(self, modified)

    def search_bar_visible(self) -> bool:
        """
        Returns True if the tab's search bar is visible
        """
        return self._editor_code_widget.isSearchBarVisible()

    def trigger_find(self):
        """
        Triggers a find operation using the default behavior
        """
        self._editor_code_widget.triggerFind()

    def hide_search_bar(self):
        """
        Hides the search bar
        """
        self._editor_code_widget.hideSearchBar()

    def close(self):
        self.tab_widget._removeTab(self, tab2index=True)

    def __getattr__(self, name):
        """Forward all missing attribute requests to the editor."""
        try:
            return super().__getattr__(name)
        except AttributeError:
            return getattr(self._editor, name)

    def __setattr__(self, name, value):
        """Forward all missing attribute requests to the editor."""
        try:
            return super().__setattr__(name, value)
        except AttributeError:
            return setattr(self._editor, name, value)

    def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""):
        self._editor_code_widget.messageBar().pushMessage(title, text, level, timeout)


class EditorTabWidget(QTabWidget):

    search_bar_toggled = pyqtSignal(bool)

    def __init__(self, console_widget: PythonConsoleWidget):
        super().__init__(parent=None)
        self.console_widget: PythonConsoleWidget = console_widget

        self.idx = -1
        # Layout for top frame (restore tabs)
        self.layoutTopFrame = QGridLayout(self)
        self.layoutTopFrame.setContentsMargins(0, 0, 0, 0)
        spacerItem = QSpacerItem(
            20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding
        )
        self.layoutTopFrame.addItem(spacerItem, 1, 0, 1, 1)
        self.topFrame = QFrame(self)
        self.topFrame.setStyleSheet("background-color: rgb(255, 255, 230);")
        self.topFrame.setFrameShape(QFrame.Shape.StyledPanel)
        self.topFrame.setMinimumHeight(24)
        self.layoutTopFrame2 = QGridLayout(self.topFrame)
        self.layoutTopFrame2.setContentsMargins(0, 0, 0, 0)
        label = QCoreApplication.translate(
            "PythonConsole", "Click on button to restore all tabs from last session."
        )
        self.label = QLabel(label)

        self.restoreTabsButton = QToolButton()
        toolTipRestore = QCoreApplication.translate("PythonConsole", "Restore tabs")
        self.restoreTabsButton.setToolTip(toolTipRestore)
        self.restoreTabsButton.setIcon(
            QgsApplication.getThemeIcon("console/iconRestoreTabsConsole.svg")
        )
        self.restoreTabsButton.setIconSize(QSize(24, 24))
        self.restoreTabsButton.setAutoRaise(True)
        self.restoreTabsButton.setCursor(Qt.CursorShape.PointingHandCursor)
        self.restoreTabsButton.setStyleSheet(
            "QToolButton:hover{border: none } \
                                              QToolButton:pressed{border: none}"
        )

        self.clButton = QToolButton()
        toolTipClose = QCoreApplication.translate("PythonConsole", "Close")
        self.clButton.setToolTip(toolTipClose)
        self.clButton.setIcon(QgsApplication.getThemeIcon("/mIconClose.svg"))
        self.clButton.setIconSize(QSize(18, 18))
        self.clButton.setCursor(Qt.CursorShape.PointingHandCursor)
        self.clButton.setStyleSheet(
            "QToolButton:hover{border: none } \
                                     QToolButton:pressed{border: none}"
        )
        self.clButton.setAutoRaise(True)

        sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
        self.topFrame.setSizePolicy(sizePolicy)
        self.layoutTopFrame.addWidget(self.topFrame, 0, 0, 1, 1)
        self.layoutTopFrame2.addWidget(self.label, 0, 1, 1, 1)
        self.layoutTopFrame2.addWidget(self.restoreTabsButton, 0, 0, 1, 1)
        self.layoutTopFrame2.addWidget(self.clButton, 0, 2, 1, 1)

        self.topFrame.hide()
        self.restoreTabsButton.clicked.connect(self.restoreTabs)
        self.clButton.clicked.connect(self.closeRestore)

        # Fixes #7653
        if sys.platform != "darwin":
            self.setDocumentMode(True)

        self.setMovable(True)
        self.setTabsClosable(True)
        self.setTabPosition(QTabWidget.TabPosition.North)

        # Menu button list tabs
        self.fileTabMenu = QMenu()
        self.fileTabMenu.aboutToShow.connect(self.showFileTabMenu)
        self.fileTabMenu.triggered.connect(self.showFileTabMenuTriggered)
        self.fileTabButton = QToolButton()
        txtToolTipMenuFile = QCoreApplication.translate(
            "PythonConsole", "List all tabs"
        )
        self.fileTabButton.setToolTip(txtToolTipMenuFile)
        self.fileTabButton.setIcon(
            QgsApplication.getThemeIcon("console/iconFileTabsMenuConsole.svg")
        )
        self.fileTabButton.setIconSize(QSize(24, 24))
        self.fileTabButton.setAutoRaise(True)
        self.fileTabButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
        self.fileTabButton.setMenu(self.fileTabMenu)
        self.setCornerWidget(self.fileTabButton, Qt.Corner.TopRightCorner)
        self.tabCloseRequested.connect(self._removeTab)
        self.currentChanged.connect(self._currentWidgetChanged)

        # New Editor button
        self.newTabButton = QToolButton()
        txtToolTipNewTab = QCoreApplication.translate("PythonConsole", "New Editor")
        self.newTabButton.setToolTip(txtToolTipNewTab)
        self.newTabButton.setAutoRaise(True)
        self.newTabButton.setIcon(
            QgsApplication.getThemeIcon("console/iconNewTabEditorConsole.svg")
        )
        self.newTabButton.setIconSize(QSize(24, 24))
        self.setCornerWidget(self.newTabButton, Qt.Corner.TopLeftCorner)
        self.newTabButton.clicked.connect(self.newTabEditor)

    def _currentWidgetChanged(self, tab):
        if QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool):
            self.listObject(tab)
        self.changeLastDirPath(tab)
        self.enableSaveIfModified(tab)

        if self.currentWidget():
            self.search_bar_toggled.emit(self.currentWidget().search_bar_visible())

    def toggle_search_bar(self, visible: bool):
        """
        Toggles whether the search bar should be visible
        """
        if visible and not self.currentWidget().search_bar_visible():
            self.currentWidget().trigger_find()
        elif not visible and self.currentWidget().search_bar_visible():
            self.currentWidget().hide_search_bar()

    def contextMenuEvent(self, e):
        tabBar = self.tabBar()
        self.idx = tabBar.tabAt(e.pos())
        if self.widget(self.idx):
            cW = self.widget(self.idx)
            menu = QMenu(self)
            menu.addSeparator()
            menu.addAction(
                QCoreApplication.translate("PythonConsole", "New Editor"),
                self.newTabEditor,
            )
            menu.addSeparator()
            closeTabAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Close Tab"), cW.close
            )
            closeAllTabAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Close All"), self.closeAll
            )
            closeOthersTabAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Close Others"),
                self.closeOthers,
            )
            menu.addSeparator()
            saveAction = menu.addAction(
                QCoreApplication.translate("PythonConsole", "Save"), cW.save
            )
            menu.addAction(
                QCoreApplication.translate("PythonConsole", "Save As"), self.saveAs
            )
            closeTabAction.setEnabled(False)
            closeAllTabAction.setEnabled(False)
            closeOthersTabAction.setEnabled(False)
            saveAction.setEnabled(False)
            if self.count() > 1:
                closeTabAction.setEnabled(True)
                closeAllTabAction.setEnabled(True)
                closeOthersTabAction.setEnabled(True)
            if self.widget(self.idx).isModified():
                saveAction.setEnabled(True)
            menu.exec(self.mapToGlobal(e.pos()))

    def closeOthers(self):
        idx = self.idx
        countTab = self.count()
        for i in list(range(countTab - 1, idx, -1)) + list(range(idx - 1, -1, -1)):
            self._removeTab(i)

    def closeAll(self):
        countTab = self.count()
        for i in range(countTab - 1, 0, -1):
            self._removeTab(i)

        self.newTabEditor(
            tabName=QCoreApplication.translate("PythonConsole", "Untitled-0")
        )
        self._removeTab(0)

    def saveAs(self):
        self.console_widget.saveAsScriptFile(self.idx)

    def enableSaveIfModified(self, tab):
        tabWidget = self.widget(tab)
        if tabWidget:
            self.console_widget.saveFileButton.setEnabled(tabWidget.isModified())

    def enableToolBarEditor(self, enable):
        if self.topFrame.isVisible():
            enable = False
        self.console_widget.toolBarEditor.setEnabled(enable)

    def newTabEditor(self, tabName=None, filename: str | None = None):
        read_only = False
        if filename:
            read_only = not QFileInfo(filename).isWritable()
            try:
                fn = codecs.open(filename, "rb", encoding="utf-8")
                fn.read()
                fn.close()
            except OSError as error:
                IOErrorTr = QCoreApplication.translate(
                    "PythonConsole", "The file {0} could not be opened. Error: {1}\n"
                ).format(filename, error.strerror)
                print("## Error: ")
                sys.stderr.write(IOErrorTr)
                return

        nr = self.count()
        if not tabName:
            tabName = QCoreApplication.translate(
                "PythonConsole", "Untitled-{0}"
            ).format(nr)
        tab = EditorTab(
            tab_widget=self,
            console_widget=self.console_widget,
            filename=filename,
            read_only=read_only,
        )
        self.iconTab = QgsApplication.getThemeIcon("console/iconTabEditorConsole.svg")
        self.addTab(tab, self.iconTab, tabName + " (ro)" if read_only else tabName)
        self.setCurrentWidget(tab)
        if filename:
            self.setTabToolTip(self.currentIndex(), filename)
        else:
            self.setTabToolTip(self.currentIndex(), tabName)

        tab.search_bar_toggled.connect(self._tab_search_bar_toggled)

    def _tab_search_bar_toggled(self, visible: bool):
        if self.sender() != self.currentWidget():
            return

        self.search_bar_toggled.emit(visible)

    def tabModified(self, tab, modified):
        index = self.indexOf(tab)
        s = self.tabText(index)
        self.setTabTitle(index, f"*{s}" if modified else re.sub(r"^(\*)", "", s))
        self.console_widget.saveFileButton.setEnabled(modified)

    def setTabTitle(self, tab, title):
        self.setTabText(tab, title)

    def _removeTab(self, tab, tab2index=False):
        if tab2index:
            tab = self.indexOf(tab)
        editorTab = self.widget(tab)
        if editorTab.isModified():
            txtSaveOnRemove = QCoreApplication.translate(
                "PythonConsole", "Python Console: Save File"
            )
            txtMsgSaveOnRemove = QCoreApplication.translate(
                "PythonConsole",
                "The file <b>'{0}'</b> has been modified, save changes?",
            ).format(self.tabText(tab))
            res = QMessageBox.question(
                self,
                txtSaveOnRemove,
                txtMsgSaveOnRemove,
                QMessageBox.StandardButton.Save
                | QMessageBox.StandardButton.Discard
                | QMessageBox.StandardButton.Cancel,
            )
            if res == QMessageBox.StandardButton.Cancel:
                return
            if res == QMessageBox.StandardButton.Save:
                editorTab.save()
            if editorTab.code_editor_widget.filePath():
                self.console_widget.updateTabListScript(
                    editorTab.code_editor_widget.filePath(), action="remove"
                )
            self.removeTab(tab)
            if self.count() < 1:
                self.newTabEditor()
        else:
            if editorTab.code_editor_widget.filePath():
                self.console_widget.updateTabListScript(
                    editorTab.code_editor_widget.filePath(), action="remove"
                )
            if self.count() <= 1:
                self.removeTab(tab)
                self.newTabEditor()
            else:
                self.removeTab(tab)

        editorTab.deleteLater()
        self.currentWidget()._editor.setFocus(Qt.FocusReason.TabFocusReason)

    def restoreTabsOrAddNew(self):
        """
        Restore tabs if they are found in the settings. If none are found it will add a new empty tab.
        """
        # Restore scripts from the previous session
        tabScripts = QgsSettings().value("pythonConsole/tabScripts", [])
        self.restoreTabList = tabScripts

        if self.restoreTabList:
            self.restoreTabs()
        else:
            self.newTabEditor(filename=None)

    def restoreTabs(self):
        for script in self.restoreTabList:
            pathFile = script
            if QFileInfo(pathFile).exists():
                tabName = pathFile.split("/")[-1]
                self.newTabEditor(tabName, pathFile)
            else:
                errOnRestore = QCoreApplication.translate(
                    "PythonConsole", "Unable to restore the file: \n{0}\n"
                ).format(pathFile)
                print("## Error: ")
                s = errOnRestore
                sys.stderr.write(s)
                self.console_widget.updateTabListScript(pathFile, action="remove")
        if self.count() < 1:
            self.newTabEditor(filename=None)
        self.topFrame.close()
        self.enableToolBarEditor(True)
        self.currentWidget()._editor.setFocus(Qt.FocusReason.TabFocusReason)

    def closeRestore(self):
        self.console_widget.updateTabListScript(None)
        self.topFrame.close()
        self.newTabEditor(filename=None)
        self.enableToolBarEditor(True)

    def showFileTabMenu(self):
        self.fileTabMenu.clear()
        for index in range(self.count()):
            action = self.fileTabMenu.addAction(
                self.tabIcon(index), self.tabText(index)
            )
            action.setData(index)

    def showFileTabMenuTriggered(self, action):
        index = action.data()
        if index is not None:
            self.setCurrentIndex(index)

    def listObject(self, tab):
        self.console_widget.listClassMethod.clear()
        if isinstance(tab, EditorTab):
            tabWidget = self.widget(self.indexOf(tab))
        else:
            tabWidget = self.widget(tab)
        if tabWidget:
            if tabWidget.file_path():
                pathFile, file = os.path.split(tabWidget.file_path())
                module, ext = os.path.splitext(file)
                found = False
                if pathFile not in sys.path:
                    sys.path.append(pathFile)
                    found = True
                try:
                    importlib.reload(pyclbr)  # NOQA
                    dictObject = {}
                    readModule = pyclbr.readmodule(module)
                    readModuleFunction = pyclbr.readmodule_ex(module)
                    for name, class_data in sorted(
                        list(readModule.items()), key=lambda x: x[1].lineno
                    ):
                        if os.path.normpath(class_data.file) == os.path.normpath(
                            tabWidget.file_path()
                        ):
                            superClassName = []
                            for superClass in class_data.super:
                                if superClass == "object":
                                    continue
                                if isinstance(superClass, str):
                                    superClassName.append(superClass)
                                else:
                                    superClassName.append(superClass.name)
                            classItem = QTreeWidgetItem()
                            if superClassName:
                                super = ", ".join([i for i in superClassName])
                                classItem.setText(0, name + " [" + super + "]")
                                classItem.setToolTip(0, name + " [" + super + "]")
                            else:
                                classItem.setText(0, name)
                                classItem.setToolTip(0, name)
                            if sys.platform.startswith("win"):
                                classItem.setSizeHint(0, QSize(18, 18))
                            classItem.setText(1, str(class_data.lineno))
                            iconClass = QgsApplication.getThemeIcon(
                                "console/iconClassTreeWidgetConsole.svg"
                            )
                            classItem.setIcon(0, iconClass)
                            dictObject[name] = class_data.lineno
                            for meth, lineno in sorted(
                                list(class_data.methods.items()), key=itemgetter(1)
                            ):
                                methodItem = QTreeWidgetItem()
                                methodItem.setText(0, meth + " ")
                                methodItem.setText(1, str(lineno))
                                methodItem.setToolTip(0, meth)
                                iconMeth = QgsApplication.getThemeIcon(
                                    "console/iconMethodTreeWidgetConsole.svg"
                                )
                                methodItem.setIcon(0, iconMeth)
                                if sys.platform.startswith("win"):
                                    methodItem.setSizeHint(0, QSize(18, 18))
                                classItem.addChild(methodItem)
                                dictObject[meth] = lineno
                            self.console_widget.listClassMethod.addTopLevelItem(
                                classItem
                            )
                    for func_name, data in sorted(
                        list(readModuleFunction.items()), key=lambda x: x[1].lineno
                    ):
                        if isinstance(data, pyclbr.Function) and os.path.normpath(
                            data.file
                        ) == os.path.normpath(tabWidget.file_path()):
                            funcItem = QTreeWidgetItem()
                            funcItem.setText(0, func_name + " ")
                            funcItem.setText(1, str(data.lineno))
                            funcItem.setToolTip(0, func_name)
                            iconFunc = QgsApplication.getThemeIcon(
                                "console/iconFunctionTreeWidgetConsole.svg"
                            )
                            funcItem.setIcon(0, iconFunc)
                            if sys.platform.startswith("win"):
                                funcItem.setSizeHint(0, QSize(18, 18))
                            dictObject[func_name] = data.lineno
                            self.console_widget.listClassMethod.addTopLevelItem(
                                funcItem
                            )
                    if found:
                        sys.path.remove(pathFile)
                except:
                    msgItem = QTreeWidgetItem()
                    msgItem.setText(
                        0, QCoreApplication.translate("PythonConsole", "Check Syntax")
                    )
                    msgItem.setText(1, "syntaxError")
                    iconWarning = QgsApplication.getThemeIcon(
                        "console/iconSyntaxErrorConsole.svg"
                    )
                    msgItem.setIcon(0, iconWarning)
                    self.console_widget.listClassMethod.addTopLevelItem(msgItem)

    def refreshSettingsEditor(self):
        objInspectorEnabled = QgsSettings().value(
            "pythonConsole/enableObjectInsp", False, type=bool
        )
        listObj = self.console_widget.objectListButton
        if self.console_widget.listClassMethod.isVisible():
            listObj.setChecked(objInspectorEnabled)
        listObj.setEnabled(objInspectorEnabled)
        if objInspectorEnabled:
            cW = self.currentWidget()
            if cW and not self.console_widget.listClassMethod.isVisible():
                with OverrideCursor(Qt.CursorShape.WaitCursor):
                    self.listObject(cW)

    def changeLastDirPath(self, tab):
        tabWidget = self.widget(tab)
        if tabWidget and tabWidget.file_path():
            QgsSettings().setValue(
                "pythonConsole/lastDirPath",
                Path(tabWidget.file_path()).parent.as_posix(),
            )

    def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""):
        currWidget = self.currentWidget()
        currWidget.showMessage(text, level, timeout, title)
