# -*- coding: utf-8 -*-

# Copyright (c) 2003 - 2023 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Package implementing various functions/classes needed everywhere within eric.
"""

import codecs
import contextlib
import glob
import json
import os
import re
import shlex
import sys
import warnings

from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32

import chardet

from PyQt6 import sip
from PyQt6.Qsci import QSCINTILLA_VERSION_STR, QsciScintilla
from PyQt6.QtCore import (
    PYQT_VERSION_STR,
    QByteArray,
    QCoreApplication,
    QCryptographicHash,
    QProcess,
    qVersion,
)

from eric7 import Preferences
from eric7.EricWidgets.EricApplication import ericApp

# TODO: remove these with release 23.4
# imports from eric7.SystemUtilities are for backward compatibility
from eric7.Globals import (  # __IGNORE_FLAKES_WARNING__
    desktopName,
    getConfig,
    getConfigDir,
    getPythonExecutable,
    getPythonLibraryDirectory,
    getPythonScriptsDirectory,
    sessionType,
    setConfigDir,
    toBool,
)
from eric7.SystemUtilities import DesktopUtilities
from eric7.SystemUtilities.FileSystemUtilities import (  # __IGNORE_FLAKES_WARNING__
    absolutePath,
    absoluteUniversalPath,
    compactPath,
    direntries,
    findVolume,
    fromNativeSeparators,
    getDirs,
    getExecutablePath,
    getExecutablePaths,
    getWindowsExecutablePath,
    isDrive,
    isExecutable,
    isinpath,
    joinext,
    normabsjoinpath,
    normcaseabspath,
    normcasepath,
    normjoinpath,
    relativeUniversalPath,
    samefilepath,
    samepath,
    splitPath,
    startswithPath,
    toNativeSeparators,
)
from eric7.SystemUtilities.OSUtilities import (  # __IGNORE_FLAKES_WARNING__
    getEnvironmentEntry,
    hasEnvironmentEntry,
    getHomeDir,
    getRealName,
    getUserName,
    getRealName,
    isLinuxPlatform,
    isMacPlatform,
    isWindowsPlatform,
    win32_getRealName,
    win32_GetUserName,
    win32_Kill,
)
from eric7.SystemUtilities.PythonUtilities import (  # __IGNORE_FLAKES_WARNING__
    determinePythonVersion,
    getPythonLibPath,
    getPythonVersion,
)
from eric7.SystemUtilities.QtUtilities import (  # __IGNORE_FLAKES_WARNING__
    checkPyside,
    generatePyQtToolPath,
    generatePySideToolPath,
    generateQtToolName,
    getPyQt6ModulesDirectory,
    getPyQtToolsPath,
    getQtBinariesPath,
    getQtMacBundle,
    prepareQtMacBundle,
    qVersionTuple,
)
from eric7.UI.Info import Program, Version


def __showwarning(message, category, filename, lineno, file=None, line=""):
    """
    Module function to raise a SyntaxError for a SyntaxWarning.

    @param message warning object
    @param category type object of the warning
    @param filename name of the file causing the warning (string)
    @param lineno line number causing the warning (integer)
    @param file file to write the warning message to (ignored)
    @param line line causing the warning (ignored)
    @raise err exception of type SyntaxError
    """
    if category is SyntaxWarning:
        err = SyntaxError(str(message))
        err.filename = filename
        err.lineno = lineno
        raise err


warnings.showwarning = __showwarning

configDir = None

codingBytes_regexps = [
    (5, re.compile(rb"""coding[:=]\s*([-\w_.]+)""")),
    (1, re.compile(rb"""<\?xml.*\bencoding\s*=\s*['"]([-\w_.]+)['"]\?>""")),
]
coding_regexps = [
    (5, re.compile(r"""coding[:=]\s*([-\w_.]+)""")),
    (1, re.compile(r"""<\?xml.*\bencoding\s*=\s*['"]([-\w_.]+)['"]\?>""")),
]

supportedCodecs = [
    "utf-8",
    "iso-8859-1",
    "iso-8859-2",
    "iso-8859-3",
    "iso-8859-4",
    "iso-8859-5",
    "iso-8859-6",
    "iso-8859-7",
    "iso-8859-8",
    "iso-8859-9",
    "iso-8859-10",
    "iso-8859-11",
    "iso-8859-13",
    "iso-8859-14",
    "iso-8859-15",
    "iso-8859-16",
    "latin-1",
    "koi8-r",
    "koi8-t",
    "koi8-u",
    "utf-7",
    "utf-16",
    "utf-16-be",
    "utf-16-le",
    "utf-32",
    "utf-32-be",
    "utf-32-le",
    "cp037",
    "cp273",
    "cp424",
    "cp437",
    "cp500",
    "cp720",
    "cp737",
    "cp775",
    "cp850",
    "cp852",
    "cp855",
    "cp856",
    "cp857",
    "cp858",
    "cp860",
    "cp861",
    "cp862",
    "cp863",
    "cp864",
    "cp865",
    "cp866",
    "cp869",
    "cp874",
    "cp875",
    "cp932",
    "cp949",
    "cp950",
    "cp1006",
    "cp1026",
    "cp1125",
    "cp1140",
    "windows-1250",
    "windows-1251",
    "windows-1252",
    "windows-1253",
    "windows-1254",
    "windows-1255",
    "windows-1256",
    "windows-1257",
    "windows-1258",
    "gb2312",
    "hz",
    "gb18030",
    "gbk",
    "iso-2022-jp",
    "iso-2022-jp-1",
    "iso-2022-jp-2",
    "iso-2022-jp-2004",
    "iso-2022-jp-3",
    "iso-2022-jp-ext",
    "iso-2022-kr",
    "mac-cyrillic",
    "mac-greek",
    "mac-iceland",
    "mac-latin2",
    "mac-roman",
    "mac-turkish",
    "ascii",
    "big5-tw",
    "big5-hkscs",
]


class CodingError(Exception):
    """
    Class implementing an exception, which is raised, if a given coding is
    incorrect.
    """

    def __init__(self, coding):
        """
        Constructor

        @param coding coding to include in the message (string)
        """
        self.errorMessage = QCoreApplication.translate(
            "CodingError", "The coding '{0}' is wrong for the given text."
        ).format(coding)

    def __repr__(self):
        """
        Special method returning a representation of the exception.

        @return string representing the error message
        """
        return str(self.errorMessage)

    def __str__(self):
        """
        Special method returning a string representation of the exception.

        @return string representing the error message
        """
        return str(self.errorMessage)


def get_codingBytes(text):
    """
    Function to get the coding of a bytes text.

    @param text bytes text to inspect (bytes)
    @return coding string
    """
    lines = text.splitlines()
    for coding in codingBytes_regexps:
        coding_re = coding[1]
        head = lines[: coding[0]]
        for line in head:
            m = coding_re.search(line)
            if m:
                return str(m.group(1), "ascii").lower()
    return None


def get_coding(text):
    """
    Function to get the coding of a text.

    @param text text to inspect (string)
    @return coding string
    """
    lines = text.splitlines()
    for coding in coding_regexps:
        coding_re = coding[1]
        head = lines[: coding[0]]
        for line in head:
            m = coding_re.search(line)
            if m:
                return m.group(1).lower()
    return None


def readEncodedFile(filename):
    """
    Function to read a file and decode its contents into proper text.

    @param filename name of the file to read (string)
    @return tuple of decoded text and encoding (string, string)
    """
    with open(filename, "rb") as f:
        text = f.read()
    return decode(text)


def readEncodedFileWithHash(filename):
    """
    Function to read a file, calculate a hash value and decode its contents
    into proper text.

    @param filename name of the file to read (string)
    @return tuple of decoded text, encoding and hash value (string, string,
        string)
    """
    with open(filename, "rb") as f:
        text = f.read()
    hashStr = str(
        QCryptographicHash.hash(
            QByteArray(text), QCryptographicHash.Algorithm.Md5
        ).toHex(),
        encoding="ASCII",
    )
    return decode(text) + (hashStr,)


def decode(text):
    """
    Function to decode some byte text into a string.

    @param text byte text to decode (bytes)
    @return tuple of decoded text and encoding (string, string)
    """
    with contextlib.suppress(UnicodeError, LookupError):
        if text.startswith(BOM_UTF8):
            # UTF-8 with BOM
            return str(text[len(BOM_UTF8) :], "utf-8"), "utf-8-bom"
        elif text.startswith(BOM_UTF16):
            # UTF-16 with BOM
            return str(text[len(BOM_UTF16) :], "utf-16"), "utf-16"
        elif text.startswith(BOM_UTF32):
            # UTF-32 with BOM
            return str(text[len(BOM_UTF32) :], "utf-32"), "utf-32"
        coding = get_codingBytes(text)
        if coding:
            return str(text, coding), coding

    # Assume UTF-8
    with contextlib.suppress(UnicodeError, LookupError):
        return str(text, "utf-8"), "utf-8-guessed"

    guess = None
    if Preferences.getEditor("AdvancedEncodingDetection"):
        # Try the universal character encoding detector
        try:
            guess = chardet.detect(text)
            if guess and guess["confidence"] > 0.95 and guess["encoding"] is not None:
                codec = guess["encoding"].lower()
                return str(text, codec), "{0}-guessed".format(codec)
        except (UnicodeError, LookupError):
            pass
        except ImportError:
            pass

    # Try default encoding
    with contextlib.suppress(UnicodeError, LookupError):
        codec = Preferences.getEditor("DefaultEncoding")
        return str(text, codec), "{0}-default".format(codec)

    if (
        Preferences.getEditor("AdvancedEncodingDetection")
        and guess
        and guess["encoding"] is not None
    ):
        # Use the guessed one even if confidence level is low
        with contextlib.suppress(UnicodeError, LookupError):
            codec = guess["encoding"].lower()
            return str(text, codec), "{0}-guessed".format(codec)

    # Assume UTF-8 loosing information
    return str(text, "utf-8", "ignore"), "utf-8-ignore"


def readEncodedFileWithEncoding(filename, encoding):
    """
    Function to read a file and decode its contents into proper text.

    @param filename name of the file to read (string)
    @param encoding encoding to be used to read the file (string)
    @return tuple of decoded text and encoding (string, string)
    """
    with open(filename, "rb") as f:
        text = f.read()
    if encoding:
        with contextlib.suppress(UnicodeError, LookupError):
            return str(text, encoding), "{0}-selected".format(encoding)

        # Try default encoding
        with contextlib.suppress(UnicodeError, LookupError):
            codec = Preferences.getEditor("DefaultEncoding")
            return str(text, codec), "{0}-default".format(codec)

        # Assume UTF-8 loosing information
        return str(text, "utf-8", "ignore"), "utf-8-ignore"
    else:
        return decode(text)


def writeEncodedFile(filename, text, origEncoding, forcedEncoding=""):
    """
    Function to write a file with properly encoded text.

    @param filename name of the file to read
    @type str
    @param text text to be written
    @type str
    @param origEncoding type of the original encoding
    @type str
    @param forcedEncoding encoding to be used for writing, if no coding
        line is present
    @type str
    @return encoding used for writing the file
    @rtype str
    """
    etext, encoding = encode(text, origEncoding, forcedEncoding=forcedEncoding)

    with open(filename, "wb") as f:
        f.write(etext)

    return encoding


def encode(text, origEncoding, forcedEncoding=""):
    """
    Function to encode text into a byte text.

    @param text text to be encoded
    @type str
    @param origEncoding type of the original encoding
    @type str
    @param forcedEncoding encoding to be used for writing, if no coding line
        is present
    @type str
    @return tuple of encoded text and encoding used
    @rtype tuple of (bytes, str)
    @exception CodingError raised to indicate an invalid encoding
    """
    encoding = None
    if origEncoding == "utf-8-bom":
        etext, encoding = BOM_UTF8 + text.encode("utf-8"), "utf-8-bom"
    else:
        # Try declared coding spec
        coding = get_coding(text)
        if coding:
            try:
                etext, encoding = text.encode(coding), coding
            except (UnicodeError, LookupError):
                # Error: Declared encoding is incorrect
                raise CodingError(coding)
        else:
            if forcedEncoding:
                with contextlib.suppress(UnicodeError, LookupError):
                    etext, encoding = (text.encode(forcedEncoding), forcedEncoding)
                    # if forced encoding is incorrect, ignore it

            if encoding is None:
                # Try the original encoding
                if origEncoding and origEncoding.endswith(
                    ("-selected", "-default", "-guessed", "-ignore")
                ):
                    coding = (
                        origEncoding.replace("-selected", "")
                        .replace("-default", "")
                        .replace("-guessed", "")
                        .replace("-ignore", "")
                    )
                    with contextlib.suppress(UnicodeError, LookupError):
                        etext, encoding = text.encode(coding), coding

                if encoding is None:
                    # Try configured default
                    with contextlib.suppress(UnicodeError, LookupError):
                        codec = Preferences.getEditor("DefaultEncoding")
                        etext, encoding = text.encode(codec), codec

                    if encoding is None:
                        # Try saving as ASCII
                        with contextlib.suppress(UnicodeError):
                            etext, encoding = text.encode("ascii"), "ascii"

                        if encoding is None:
                            # Save as UTF-8 without BOM
                            etext, encoding = text.encode("utf-8"), "utf-8"

    return etext, encoding


def decodeString(text):
    """
    Function to decode a string containing Unicode encoded characters.

    @param text text containing encoded chars (string)
    @return decoded text (string)
    """
    buf = b""
    index = 0
    while index < len(text):
        if text[index] == "\\":
            qb = QByteArray.fromHex(text[index : index + 4].encode())
            buf += bytes(qb)
            index += 4
        else:
            buf += codecs.encode(text[index], "utf-8")
            index += 1
    buf = buf.replace(b"\x00", b"")
    return decodeBytes(buf)


def decodeBytes(buffer):
    """
    Function to decode some byte text into a string.

    @param buffer byte buffer to decode (bytes)
    @return decoded text (string)
    """
    # try UTF with BOM
    with contextlib.suppress(UnicodeError, LookupError):
        if buffer.startswith(BOM_UTF8):
            # UTF-8 with BOM
            return str(buffer[len(BOM_UTF8) :], encoding="utf-8")
        elif buffer.startswith(BOM_UTF16):
            # UTF-16 with BOM
            return str(buffer[len(BOM_UTF16) :], encoding="utf-16")
        elif buffer.startswith(BOM_UTF32):
            # UTF-32 with BOM
            return str(buffer[len(BOM_UTF32) :], encoding="utf-32")

    # try UTF-8
    with contextlib.suppress(UnicodeError):
        return str(buffer, encoding="utf-8")

    # try codec detection
    try:
        guess = chardet.detect(buffer)
        if guess and guess["encoding"] is not None:
            codec = guess["encoding"].lower()
            return str(buffer, encoding=codec)
    except (UnicodeError, LookupError):
        pass
    except ImportError:
        pass

    return str(buffer, encoding="utf-8", errors="ignore")


def readStringFromStream(stream):
    """
    Module function to read a string from the given stream.

    @param stream data stream opened for reading (QDataStream)
    @return string read from the stream (string)
    """
    data = stream.readString()
    if data is None:
        data = b""
    return data.decode("utf-8")


def normalizeCode(codestring):
    """
    Function to normalize the given code.

    @param codestring code to be normalized (string)
    @return normalized code (string)
    """
    codestring = codestring.replace("\r\n", "\n").replace("\r", "\n")

    if codestring and codestring[-1] != "\n":
        codestring += "\n"

    return codestring


_escape = re.compile("[&<>\"'\u0080-\uffff]")

_escape_map = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#x27;",
}


def escape_entities(m, escmap=_escape_map):
    """
    Function to encode html entities.

    @param m the match object
    @param escmap the map of entities to encode
    @return the converted text (string)
    """
    char = m.group()
    text = escmap.get(char)
    if text is None:
        text = "&#{0:d};".format(ord(char))
    return text


def html_encode(text, pattern=_escape):
    """
    Function to correctly encode a text for html.

    @param text text to be encoded (string)
    @param pattern search pattern for text to be encoded (string)
    @return the encoded text (string)
    """
    if not text:
        return ""
    text = pattern.sub(escape_entities, text)
    return text


_uescape = re.compile("[\u0080-\uffff]")


def escape_uentities(m):
    """
    Function to encode html entities.

    @param m the match object
    @return the converted text (string)
    """
    char = m.group()
    text = "&#{0:d};".format(ord(char))
    return text


def html_uencode(text, pattern=_uescape):
    """
    Function to correctly encode a unicode text for html.

    @param text text to be encoded (string)
    @param pattern search pattern for text to be encoded (string)
    @return the encoded text (string)
    """
    if not text:
        return ""
    text = pattern.sub(escape_uentities, text)
    return text


_uunescape = re.compile(r"&#\d+;")


def unescape_uentities(m):
    """
    Function to decode html entities.

    @param m the match object
    @return the converted text (string)
    """
    char = m.group()
    ordinal = int(char[2:-1])
    return chr(ordinal)


def html_udecode(text, pattern=_uunescape):
    """
    Function to correctly decode a html text to a unicode text.

    @param text text to be decoded (string)
    @param pattern search pattern for text to be decoded (string)
    @return the decoded text (string)
    """
    if not text:
        return ""
    text = pattern.sub(unescape_uentities, text)
    return text


def convertLineEnds(text, eol):
    """
    Function to convert the end of line characters.

    @param text text to be converted (string)
    @param eol new eol setting (string)
    @return text with converted eols (string)
    """
    if eol == "\r\n":
        regexp = re.compile(r"""(\r(?!\n)|(?<!\r)\n)""")
        return regexp.sub(lambda m, eol="\r\n": eol, text)
    elif eol == "\n":
        regexp = re.compile(r"""(\r\n|\r)""")
        return regexp.sub(lambda m, eol="\n": eol, text)
    elif eol == "\r":
        regexp = re.compile(r"""(\r\n|\n)""")
        return regexp.sub(lambda m, eol="\r": eol, text)
    else:
        return text


def linesep():
    """
    Function to return the line separator used by the editor.

    @return line separator used by the editor (string)
    """
    eolMode = Preferences.getEditor("EOLMode")
    if eolMode == QsciScintilla.EolMode.EolUnix:
        return "\n"
    elif eolMode == QsciScintilla.EolMode.EolMac:
        return "\r"
    else:
        return "\r\n"


def extractFlags(text):
    """
    Function to extract eric specific flags out of the given text.

    Flags are contained in comments and are introduced by 'eflag:'.
    The rest of the line is interpreted as 'key = value'. value is
    analyzed for being an integer or float value. If that fails, it
    is assumed to be a string. If a key does not contain a '='
    character, it is assumed to be a boolean flag. Flags are expected
    at the very end of a file. The search is ended, if a line without
    the 'eflag:' marker is found.

    @param text text to be scanned (string)
    @return dictionary of string, boolean, complex, float and int
    """
    flags = {}
    lines = text.rstrip().splitlines() if isinstance(text, str) else text
    for line in reversed(lines):
        try:
            index = line.index("eflag:")
        except ValueError:
            # no flag found, don't look any further
            break

        flag = line[index + 6 :].strip()
        if "=" in flag:
            key, value = flag.split("=", 1)
            key = key.strip()
            value = value.strip()

            if value.lower() in ["true", "false", "yes", "no", "ok"]:
                # it is a flag
                flags[key] = value.lower() in ["true", "yes", "ok"]
                continue

            try:
                # interpret as int first
                value = int(value)
            except ValueError:
                with contextlib.suppress(ValueError):
                    # interpret as float next
                    value = float(value)

            flags[key] = value
        else:
            # treat it as a boolean
            if flag[0] == "-":
                # false flags start with '-'
                flags[flag[1:]] = False
            else:
                flags[flag] = True

    return flags


def extractFlagsFromFile(filename):
    """
    Function to extract eric specific flags out of the given file.

    @param filename name of the file to be scanned (string)
    @return dictionary of string, boolean, complex, float and int
    """
    try:
        source, encoding = readEncodedFile(filename)
    except (UnicodeError, OSError):
        return {}

    return extractFlags(source)


def extractLineFlags(line, startComment="#", endComment="", flagsLine=False):
    """
    Function to extract flags starting and ending with '__' from a line
    comment.

    @param line line to extract flags from (string)
    @param startComment string identifying the start of the comment (string)
    @param endComment string identifying the end of a comment (string)
    @param flagsLine flag indicating to check for a flags only line (bool)
    @return list containing the extracted flags (list of strings)
    """
    flags = []

    if not flagsLine or (flagsLine and line.strip().startswith(startComment)):
        pos = line.rfind(startComment)
        if pos >= 0:
            comment = line[pos + len(startComment) :].strip()
            if endComment:
                endPos = line.rfind(endComment)
                if endPos >= 0:
                    comment = comment[:endPos]
            flags = [
                f.strip()
                for f in comment.split()
                if (f.startswith("__") and f.endswith("__"))
            ]
    return flags


def filterAnsiSequences(txt):
    """
    Function to filter out ANSI escape sequences (color only).

    @param txt text to be filtered
    @type str
    @return text without ANSI escape sequences
    @rtype str
    """
    ntxt = txt[:]
    while True:
        start = ntxt.find("\33[")  # find escape character
        if start == -1:
            break
        end = ntxt.find("m", start)
        if end == -1:
            break
        ntxt = ntxt[:start] + ntxt[end + 1 :]

    return ntxt


def getTestFileNames(fn):
    """
    Function to build the potential file names of a test file.

    The file names for the test file is built by prepending the string
    "test" and "test_" to the file name passed into this function and
    by appending the string "_test".

    @param fn file name basis to be used for the test file names
    @type str
    @return file names of the corresponding test file
    @rtype list of str
    """
    dn, fn = os.path.split(fn)
    fn, ext = os.path.splitext(fn)
    prefixes = ["test", "test_"]
    postfixes = ["_test"]
    return [
        os.path.join(dn, "{0}{1}{2}".format(prefix, fn, ext)) for prefix in prefixes
    ] + [
        os.path.join(dn, "{0}{1}{2}".format(fn, postfix, ext)) for postfix in postfixes
    ]


def getCoverageFileNames(fn):
    """
    Function to build a list of coverage data file names.

    @param fn file name basis to be used for the coverage data file
    @type str
    @return list of existing coverage data files
    @rtype list of str
    """
    files = []
    for filename in [fn, os.path.dirname(fn) + os.sep] + getTestFileNames(fn):
        f = getCoverageFileName(filename)
        if f:
            files.append(f)
    return files


def getCoverageFileName(fn, mustExist=True):
    """
    Function to build a file name for a coverage data file.

    @param fn file name basis to be used for the coverage data file name
    @type str
    @param mustExist flag indicating to check that the file exists (defaults
        to True)
    @type bool (optional)
    @return coverage data file name
    @rtype str
    """
    basename = os.path.splitext(fn)[0]
    filename = "{0}.coverage".format(basename)
    if mustExist:
        if os.path.isfile(filename):
            return filename
        else:
            return ""
    else:
        return filename


def getProfileFileNames(fn):
    """
    Function to build a list of profile data file names.

    @param fn file name basis to be used for the profile data file
    @type str
    @return list of existing profile data files
    @rtype list of str
    """
    files = []
    for filename in [fn, os.path.dirname(fn) + os.sep] + getTestFileNames(fn):
        f = getProfileFileName(filename)
        if f:
            files.append(f)
    return files


def getProfileFileName(fn, mustExist=True):
    """
    Function to build a file name for a profile data file.

    @param fn file name basis to be used for the profile data file name
    @type str
    @param mustExist flag indicating to check that the file exists (defaults
        to True)
    @type bool (optional)
    @return profile data file name
    @rtype str
    """
    basename = os.path.splitext(fn)[0]
    filename = "{0}.profile".format(basename)
    if mustExist:
        if os.path.isfile(filename):
            return filename
        else:
            return ""
    else:
        return filename


def parseOptionString(s):
    """
    Function used to convert an option string into a list of options.

    @param s option string
    @type str
    @return list of options
    @rtype list of str
    """
    s = re.sub(r"%[A-Z%]", _percentReplacementFunc, s)
    return shlex.split(s)


def _percentReplacementFunc(matchobj):
    """
    Protected function called for replacing % codes.

    @param matchobj match object for the code
    @type re.Match
    @return replacement string
    @rtype str
    """
    return getPercentReplacement(matchobj.group(0))


def getPercentReplacement(code):
    """
    Function to get the replacement for code.

    @param code code indicator
    @type str
    @return replacement string
    @rtype str
    """
    if code in ["C", "%C"]:
        # column of the cursor of the current editor
        aw = ericApp().getObject("ViewManager").activeWindow()
        if aw is None:
            column = -1
        else:
            column = aw.getCursorPosition()[1]
        return "{0:d}".format(column)
    elif code in ["D", "%D"]:
        # directory of active editor
        aw = ericApp().getObject("ViewManager").activeWindow()
        if aw is None:
            dn = "not_available"
        else:
            fn = aw.getFileName()
            if fn is None:
                dn = "not_available"
            else:
                dn = os.path.dirname(fn)
        return dn
    elif code in ["F", "%F"]:
        # filename (complete) of active editor
        aw = ericApp().getObject("ViewManager").activeWindow()
        if aw is None:
            fn = "not_available"
        else:
            fn = aw.getFileName()
            if fn is None:
                fn = "not_available"
        return fn
    elif code in ["H", "%H"]:
        # home directory
        return getHomeDir()
    elif code in ["L", "%L"]:
        # line of the cursor of the current editor
        aw = ericApp().getObject("ViewManager").activeWindow()
        if aw is None:
            line = 0
        else:
            line = aw.getCursorPosition()[0] + 1
        return "{0:d}".format(line)
    elif code in ["P", "%P"]:
        # project path
        projectPath = ericApp().getObject("Project").getProjectPath()
        if not projectPath:
            projectPath = "not_available"
        return projectPath
    elif code in ["S", "%S"]:
        # selected text of the current editor
        aw = ericApp().getObject("ViewManager").activeWindow()
        if aw is None:
            text = "not_available"
        else:
            text = aw.selectedText()
        return text
    elif code in ["U", "%U"]:
        # username
        un = getUserName()
        if un is None:
            return code
        else:
            return un
    elif code in ["%", "%%"]:
        # the percent sign
        return "%"
    else:
        # unknown code, just return it
        return code


def getPercentReplacementHelp():
    """
    Function to get the help text for the supported %-codes.

    @returns help text (string)
    """
    return QCoreApplication.translate(
        "Utilities",
        """<p>You may use %-codes as placeholders in the string."""
        """ Supported codes are:"""
        """<table>"""
        """<tr><td>%C</td><td>column of the cursor of the current editor"""
        """</td></tr>"""
        """<tr><td>%D</td><td>directory of the current editor</td></tr>"""
        """<tr><td>%F</td><td>filename of the current editor</td></tr>"""
        """<tr><td>%H</td><td>home directory of the current user</td></tr>"""
        """<tr><td>%L</td><td>line of the cursor of the current editor"""
        """</td></tr>"""
        """<tr><td>%P</td><td>path of the current project</td></tr>"""
        """<tr><td>%S</td><td>selected text of the current editor</td></tr>"""
        """<tr><td>%U</td><td>username of the current user</td></tr>"""
        """<tr><td>%%</td><td>the percent sign</td></tr>"""
        """</table>"""
        """</p>""",
    )


def rxIndex(rx, txt):
    """
    Function to get the index (start position) of a regular expression match
    within some text.

    @param rx regular expression object as created by re.compile()
    @type re.Pattern
    @param txt text to be scanned
    @type str
    @return start position of the match or -1 indicating no match was found
    @rtype int
    """
    match = rx.search(txt)
    if match is None:
        return -1
    else:
        return match.start()


###############################################################################
## Other utility functions below
###############################################################################


def generateVersionInfo(linesep="\n"):
    """
    Module function to generate a string with various version infos.

    @param linesep string to be used to separate lines
    @type str
    @return string with version infos
    @rtype str
    """
    try:
        sip_version_str = sip.SIP_VERSION_STR
    except AttributeError:
        sip_version_str = "sip version not available"

    sizeStr = "64-Bit" if sys.maxsize > 2**32 else "32-Bit"

    info = ["Version Numbers:"]

    info.append("  Python {0}, {1}".format(sys.version.split()[0], sizeStr))
    info.append("  Qt {0}".format(qVersion()))
    info.append("  PyQt6 {0}".format(PYQT_VERSION_STR))
    try:
        from PyQt6 import QtCharts  # __IGNORE_WARNING_I10__

        info.append("  PyQt6-Charts {0}".format(QtCharts.PYQT_CHART_VERSION_STR))
    except (ImportError, AttributeError):
        info.append("  PyQt6-Charts not installed")
    try:
        from PyQt6 import QtWebEngineCore  # __IGNORE_WARNING_I10__

        info.append(
            "  PyQt6-WebEngine {0}".format(QtWebEngineCore.PYQT_WEBENGINE_VERSION_STR)
        )
    except (ImportError, AttributeError):
        info.append("  PyQt6-WebEngine not installed")
    info.append("  PyQt6-QScintilla {0}".format(QSCINTILLA_VERSION_STR))
    info.append("  sip {0}".format(sip_version_str))
    with contextlib.suppress(ImportError):
        from PyQt6 import QtWebEngineWidgets  # __IGNORE_WARNING__

        from eric7.WebBrowser.Tools import WebBrowserTools  # __IGNORE_WARNING_I101__

        (
            chromiumVersion,
            chromiumSecurityVersion,
        ) = WebBrowserTools.getWebEngineVersions()[0:2]
        info.append("  WebEngine {0}".format(chromiumVersion))
        if chromiumSecurityVersion:
            info.append("    (Security) {0}".format(chromiumSecurityVersion))
    info.append("  {0} {1}".format(Program, Version))
    info.append("")
    info.append("Platform: {0}".format(sys.platform))
    if os.environ.get("SOMMELIER_VERSION", ""):
        info[-1] += ", ChromeOS"
    info.append(sys.version)
    desktop = DesktopUtilities.desktopName()
    if desktop:
        info.append("")
        info.append("Desktop: {0}".format(desktop))
    session = DesktopUtilities.sessionType()
    if session:
        info.append("")
        info.append("Session Type: {0}".format(session))

    return linesep.join(info)


def generatePluginsVersionInfo(linesep="\n"):
    """
    Module function to generate a string with plugins version infos.

    @param linesep string to be used to separate lines
    @type str
    @return string with plugins version infos
    @rtype str
    """
    info = []
    app = ericApp()
    if app is not None:
        with contextlib.suppress(KeyError):
            pm = app.getObject("PluginManager")
            versions = {}
            for pinfo in pm.getPluginInfos():
                versions[pinfo["module_name"]] = pinfo["version"]

            info.append("Plugins Version Numbers:")
            for pluginModuleName in sorted(versions.keys()):
                info.append(
                    "  {0} {1}".format(pluginModuleName, versions[pluginModuleName])
                )

    return linesep.join(info)


def generateDistroInfo(linesep="\n"):
    """
    Module function to generate a string with distribution infos.

    @param linesep string to be used to separate lines
    @type str
    @return string with distribution infos
    @rtype str
    """
    info = []
    if isLinuxPlatform():
        releaseList = glob.glob("/etc/*-release")
        if releaseList:
            info.append("Distribution Info:")
            for rfile in releaseList:
                try:
                    with open(rfile, "r") as f:
                        lines = f.read().splitlines()
                except OSError:
                    continue

                info.append("  {0}".format(rfile))
                info.extend(["    {0}".format(line) for line in lines])
                info.append("")

    return linesep.join(info)


def getSysPath(interpreter):
    """
    Module function to get the Python path (sys.path) of a specific
    interpreter.

    @param interpreter Python interpreter executable to get sys.path for
    @type str
    @return list containing sys.path of the interpreter; an empty list
        is returned, if the interpreter is the one used to run eric itself
    @rtype list of str
    """
    sysPath = []

    getSysPathSkript = os.path.join(os.path.dirname(__file__), "GetSysPath.py")
    args = [getSysPathSkript]
    proc = QProcess()
    proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
    proc.start(interpreter, args)
    finished = proc.waitForFinished(30000)
    if finished and proc.exitCode() == 0:
        text = proc.readAllStandardOutput()
        sysPathResult = str(text, "utf-8", "replace").strip()
        with contextlib.suppress(TypeError, ValueError):
            sysPath = json.loads(sysPathResult)
            if "" in sysPath:
                sysPath.remove("")

    return sysPath
