"""
/***************************************************************************
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
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: Optional[QgsCodeEditorWidget] = 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"),
            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)
        gist_menu.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)
            gist_menu.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):
        ACCESS_TOKEN = QgsSettings().value("pythonConsole/accessTokenGithub", '', type=QByteArray)
        if not ACCESS_TOKEN:
            msg_text = QCoreApplication.translate(
                'PythonConsole', 'GitHub personal access token must be generated (see Console Options)')
            self.showMessage(msg_text,
                             level=Qgis.MessageLevel.Warning)
            return

        URL = "https://api.github.com/gists"

        path = self.code_editor_widget.filePath()
        filename = os.path.basename(path) if path else None
        filename = filename if filename else "pyqgis_snippet.py"

        selected_text = self.selectedText()
        data = {"description": "Gist created by PyQGIS Console",
                "public": is_public,
                "files": {filename: {"content": selected_text}}}

        request = QgsBlockingNetworkRequest()
        net_req = QNetworkRequest()
        url = QUrl(URL)
        net_req.setUrl(url)
        net_req.setRawHeader(b"Authorization", b"token %s" % ACCESS_TOKEN)
        err = request.post(net_req, QJsonDocument(data).toJson())
        if not err:
            response = request.reply().content()
            json_doc = QJsonDocument()
            _json = json_doc.fromJson(response)
            link = _json.object()['html_url'].toString()
            QApplication.clipboard().setText(link)
            msg = QCoreApplication.translate('PythonConsole', 'URL copied to clipboard.')
            self.showMessage(msg)
        else:
            msg = QCoreApplication.translate('PythonConsole', 'Connection error: ')
            self.showMessage(msg + request.errorMessage(),
                             level=Qgis.MessageLevel.Warning)

    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',
                                                    'Hey, type something to run!')
        if filename is None:
            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: Optional[str] = None):
        if self.isReadOnly():
            return

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

        index = self.tab_widget.indexOf(self.editor_tab)
        if filename:
            self.code_editor_widget.setFilePath(filename)
        if 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
            self.code_editor_widget.setFilePath(path)

            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: Optional[str] = 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: Optional[str],
                 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) -> Optional[str]:
        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: Optional[str] = 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 IOError 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, '*{}'.format(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)
