# -----------------------------------------------------------------------------
# Copyright (C) 2026 Iliyas Jorio.
# This file is part of GitFourchette, distributed under the GNU GPL v3.
# For full terms, see the included LICENSE file.
# -----------------------------------------------------------------------------

"""
Translated tables of enums, exception names, preference keys.
"""

from __future__ import annotations

import os
import re
import textwrap
from enum import Enum
from collections.abc import Callable

from gitfourchette.appconsts import *
from gitfourchette.localization import *

_userCommandsGuideUrl = "https://gitfourchette.org/guide/commands"


def retranslate(newLanguageCode: str):
    """Flush cached translations."""
    # Bump table cookie value, forcing _getTable to regenerate all tables.
    global _tableCookieValue
    _tableCookieValue = newLanguageCode


def enum(enumValue: Enum) -> str:
    """Translate an enum value."""
    table = _getTable(_enumTable)
    try:
        return table[type(enumValue)][enumValue]
    except KeyError:
        return enumValue.name


def exceptionName(exc: BaseException) -> str:
    """Translate an exception name."""
    table = _getTable(_exceptionNameTable)
    name = type(exc).__name__
    return table.get(name, name)


def prefKey(key: str, default: str | None = None) -> str:
    """Translate a PrefsFile field."""
    table = _getTable(_prefKeyTable)
    try:
        return table[key]
    except KeyError:
        return str(key) if default is None else default


def prefKeyNoDefault(key: str) -> str:
    """Translate a PrefsFile field (return empty string if no translation
    available)."""
    return prefKey(key, "")


# -------------------------------------------------------------------------
# Table caching
# -------------------------------------------------------------------------

_tableAttr = "_cachedLocalizationTable"
_tableCookieKey = "_TrTablesLanguageCheck"
_tableCookieValue = ""


def _getTable[T: dict](initializer: Callable[[], T]) -> T:
    # See if we've stashed a table in this initializer's attributes previously
    table: T | None = getattr(initializer, _tableAttr, None)

    # No table, or table cookie is stale?
    if table is None or table[_tableCookieKey] != _tableCookieValue:
        # Generate the table
        table = initializer()

        # Set fresh cookie
        table[_tableCookieKey] = _tableCookieValue

        # Stash the table inside the initializer
        setattr(initializer, _tableAttr, table)

    assert table is not None
    return table


# -------------------------------------------------------------------------
# Table initializers
# -------------------------------------------------------------------------

def _exceptionNameTable() -> dict[str, str]:
    return {
        "ConnectionRefusedError": _("Connection refused"),
        "FileNotFoundError": _("File not found"),
        "GitError": _("Git error"),
        "InterruptedError": _("Operation interrupted"),
        "NotImplementedError": _("Unsupported feature"),
        "PermissionError": _("Permission denied"),
    }


def _enumTable() -> dict[type[Enum], dict[Enum, str]]:
    from gitfourchette.gitdriver import GitConflictSides, GitStatus
    from gitfourchette.nav import NavContext
    from gitfourchette.porcelain import FileMode, NameValidationError, RepositoryState
    from gitfourchette.toolbox import toLengthVariants
    from gitfourchette.sidebar.sidebarmodel import SidebarItem
    from gitfourchette.settings import (
        WhitespaceMode,
        FileListClick,
        GraphRefBoxWidth,
        GraphRowHeight,
        QtApiNames,
        RefSort,
        TabBarClick,
    )
    from gitfourchette.themes import ThemeAccent, ThemeName
    from gitfourchette.toolbox import PatchPurpose, PathDisplayStyle, AuthorDisplayStyle
    from gitfourchette.repomodel import GpgStatus

    NVERule = NameValidationError.Rule

    table: dict[type[Enum], dict[Enum, str]] = {}

    table[AuthorDisplayStyle] = {
        AuthorDisplayStyle.FullName: _("Full name"),
        AuthorDisplayStyle.FirstName: _("First name"),
        AuthorDisplayStyle.LastName: _("Last name"),
        AuthorDisplayStyle.Initials: _("Initials"),
        AuthorDisplayStyle.FullEmail: _("Full email"),
        AuthorDisplayStyle.EmailUserName: _("Abbreviated email"),
    }

    table[FileListClick] = {
        FileListClick.Nothing: _("Do nothing"),
        FileListClick.Stage: _("Stage/unstage file"),
        FileListClick.Blame: _("Blame file"),
        FileListClick.Edit: _("Open file in external editor"),
        FileListClick.DiffTool: _("Open file in external diff tool"),
        FileListClick.Folder: _("Show file in folder"),
    }

    table[FileMode] = {
        FileMode.UNREADABLE: _p("FileMode", "deleted"),
        FileMode.BLOB: _p("FileMode", "regular file"),
        FileMode.BLOB_EXECUTABLE: _p("FileMode", "executable file"),
        FileMode.LINK: _p("FileMode", "symbolic link"),
        FileMode.TREE: _p("FileMode", "subtree"),
        FileMode.COMMIT: _p("FileMode", "subtree commit"),
    }

    table[GitConflictSides] = {
        GitConflictSides.BothDeleted: _p("ConflictSides", "deleted by both sides"),
        GitConflictSides.AddedByUs: _p("ConflictSides", "added by us"),
        GitConflictSides.DeletedByThem: _p("ConflictSides", "deleted by them"),
        GitConflictSides.AddedByThem: _p("ConflictSides", "added by them"),
        GitConflictSides.DeletedByUs: _p("ConflictSides", "deleted by us"),
        GitConflictSides.BothAdded: _p("ConflictSides", "added by both sides"),
        GitConflictSides.BothModified: _p("ConflictSides", "modified by both sides"),
    }

    table[GitStatus] = {
        GitStatus.Added: _p("FileStatus", "added"),
        GitStatus.Copied: _p("FileStatus", "copied"),
        GitStatus.Deleted: _p("FileStatus", "deleted"),
        GitStatus.Ignored: _p("FileStatus", "ignored"),
        GitStatus.Modified: _p("FileStatus", "modified"),
        GitStatus.Renamed: _p("FileStatus", "renamed"),
        GitStatus.TypeChanged: _p("FileStatus", "file type changed"),
        GitStatus.Unmerged: _p("FileStatus", "merge conflict"),
        GitStatus.Unknown: _p("FileStatus", "unreadable"),
        GitStatus.Untracked: _p("FileStatus", "untracked"),
    }

    table[GpgStatus] = {
        GpgStatus.Unsigned: _("Not signed"),
        GpgStatus.Pending: _("Signature not verified yet"),
        GpgStatus.CantCheck: _("Unable to verify signature"),
        GpgStatus.MissingKey: _("Can’t verify signature; Key not in your keyring"),
        GpgStatus.GoodTrusted: _("Good signature; Key trusted"),
        GpgStatus.GoodUntrusted: _("Good signature; Key not fully trusted"),
        GpgStatus.ExpiredSig: _("Good signature; Signature expired"),
        GpgStatus.ExpiredKey: _("Good signature; Key expired"),
        GpgStatus.RevokedKey: _("Good signature; Key revoked"),
        GpgStatus.Bad: _("Bad signature"),
        GpgStatus.ProcessError: _("Failed to start verification process"),
    }

    table[GraphRefBoxWidth] = {
        GraphRefBoxWidth.IconsOnly: _("Icons Only"),
        GraphRefBoxWidth.Standard: _("Truncate long ref names"),
        GraphRefBoxWidth.Wide: _("Show full ref names"),
    }

    table[GraphRowHeight] = {
        GraphRowHeight.Cramped: _p("row spacing", "Cramped"),
        GraphRowHeight.Tight: _p("row spacing", "Tight"),
        GraphRowHeight.Relaxed: _p("row spacing", "Relaxed"),
        GraphRowHeight.Roomy: _p("row spacing", "Roomy"),
        GraphRowHeight.Spacious: _p("row spacing", "Spacious"),
    }

    table[NavContext] = {
        NavContext.EMPTY: _p("NavContext", "Empty"),
        NavContext.UNSTAGED: _p("NavContext", "Unstaged"),
        NavContext.STAGED: _p("NavContext", "Staged"),
        NavContext.COMMITTED: _p("NavContext", "Committed"),
    }

    table[NVERule] = {
        NVERule.ILLEGAL_NAME: _("Illegal name."),
        NVERule.ILLEGAL_SUFFIX: _("Illegal suffix."),
        NVERule.ILLEGAL_PREFIX: _("Illegal prefix."),
        NVERule.CONTAINS_ILLEGAL_SEQ: _("Contains illegal character sequence."),
        NVERule.CONTAINS_ILLEGAL_CHAR: _("Contains illegal character."),
        NVERule.CANNOT_BE_EMPTY: _("Cannot be empty."),
        NVERule.NOT_WINDOWS_FRIENDLY: _("This name is discouraged for compatibility with Windows."),
        NVERule.NAME_TAKEN_BY_REF: _("This name is already taken."),
        NVERule.NAME_TAKEN_BY_FOLDER: _("This name is already taken by a folder."),
        NVERule.NOT_A_FOLDER: _("The folder in this path clashes with an existing ref that isn’t a folder."),
    }

    table[PatchPurpose] = {
        PatchPurpose.Stage: _p("PatchPurpose", "Stage"),
        PatchPurpose.Unstage: _p("PatchPurpose", "Unstage"),
        PatchPurpose.Discard: _p("PatchPurpose", "Discard"),
        PatchPurpose.Lines | PatchPurpose.Stage: _p("PatchPurpose", "Stage lines"),
        PatchPurpose.Lines | PatchPurpose.Unstage: _p("PatchPurpose", "Unstage lines"),
        PatchPurpose.Lines | PatchPurpose.Discard: _p("PatchPurpose", "Discard lines"),
        PatchPurpose.Hunk | PatchPurpose.Stage: _p("PatchPurpose", "Stage hunk"),
        PatchPurpose.Hunk | PatchPurpose.Unstage: _p("PatchPurpose", "Unstage hunk"),
        PatchPurpose.Hunk | PatchPurpose.Discard: _p("PatchPurpose", "Discard hunk"),
        PatchPurpose.File | PatchPurpose.Stage: _p("PatchPurpose", "Stage file"),
        PatchPurpose.File | PatchPurpose.Unstage: _p("PatchPurpose", "Unstage file"),
        PatchPurpose.File | PatchPurpose.Discard: _p("PatchPurpose", "Discard file"),
    }

    table[PathDisplayStyle] = {
        PathDisplayStyle.FullPaths: _("Full paths"),
        PathDisplayStyle.AbbreviateDirs: _("Abbreviate directories"),
        PathDisplayStyle.FileNameOnly: _("Filename only"),
        PathDisplayStyle.FileNameFirst: _("Filename first"),
    }

    table[QtApiNames] = {
        QtApiNames.Automatic: _p("Qt binding", "Automatic (recommended)"),
        QtApiNames.PySide6: "PySide6",
        QtApiNames.PyQt6: "PyQt6",
        QtApiNames.PyQt5: "PyQt5"
    }

    table[RefSort] = {
        RefSort.TimeDesc: _p("sort refs by date of latest commit, descending", "Date, Newest First"),
        RefSort.TimeAsc: _p("sort refs by date of latest commit, ascending", "Date, Oldest First"),
        RefSort.AlphaAsc: _p("sort refs alphabetically, ascending", "Name, A-Z"),
        RefSort.AlphaDesc: _p("sort refs alphabetically, descending", "Name, Z-A"),
        RefSort.UseGlobalPref: "",
    }

    table[RepositoryState] = {
        RepositoryState.NONE: _p("RepositoryState", "None"),
        RepositoryState.MERGE: _p("RepositoryState", "Merging"),
        RepositoryState.REVERT: _p("RepositoryState", "Reverting"),
        RepositoryState.REVERT_SEQUENCE: _p("RepositoryState", "Reverting (sequence)"),
        RepositoryState.CHERRYPICK: _p("RepositoryState", "Cherry-picking"),
        RepositoryState.CHERRYPICK_SEQUENCE: _p("RepositoryState", "Cherry-picking (sequence)"),
        RepositoryState.BISECT: _p("RepositoryState", "Bisecting"),
        RepositoryState.REBASE: _p("RepositoryState", "Rebasing"),
        RepositoryState.REBASE_INTERACTIVE: _p("RepositoryState", "Rebasing (interactive)"),
        RepositoryState.REBASE_MERGE: _p("RepositoryState", "Rebasing (merging)"),
        RepositoryState.APPLY_MAILBOX: "Apply Mailbox",  # intentionally untranslated
        RepositoryState.APPLY_MAILBOX_OR_REBASE: "Apply Mailbox or Rebase",  # intentionally untranslated
    }

    table[SidebarItem] = {
        SidebarItem.UncommittedChanges: toLengthVariants(_p("SidebarModel", "Working Directory|Workdir")),
        SidebarItem.LocalBranchesHeader: toLengthVariants(_p("SidebarModel", "Local Branches|Branches")),
        SidebarItem.StashesHeader: _p("SidebarModel", "Stashes"),
        SidebarItem.RemotesHeader: _p("SidebarModel", "Remotes"),
        SidebarItem.TagsHeader: _p("SidebarModel", "Tags"),
        SidebarItem.SubmodulesHeader: _p("SidebarModel", "Submodules"),
        SidebarItem.LocalBranch: _p("SidebarModel", "Local branch"),
        SidebarItem.DetachedHead: _p("SidebarModel", "Detached HEAD"),
        SidebarItem.UnbornHead: _p("SidebarModel", "Unborn HEAD"),
        SidebarItem.RemoteBranch: _p("SidebarModel", "Remote branch"),
        SidebarItem.Stash: _p("SidebarModel", "Stash"),
        SidebarItem.Remote: _p("SidebarModel", "Remote"),
        SidebarItem.Tag: _p("SidebarModel", "Tag"),
        SidebarItem.Submodule: _p("SidebarModel", "Submodules"),
        SidebarItem.Spacer: "---",
    }

    table[TabBarClick] = {
        TabBarClick.Nothing: _("Do nothing"),
        TabBarClick.Folder: _("Open repo folder"),
        TabBarClick.Terminal: _("Open repo in terminal"),
        TabBarClick.Close: _("Close tab"),
    }

    table[ThemeAccent] = {
        ThemeAccent.Blue: _("Blue"),
        ThemeAccent.Cyan: _("Cyan"),
        ThemeAccent.Green: _("Green"),
        ThemeAccent.Yellow: _("Yellow"),
        ThemeAccent.Orange: _("Orange"),
        ThemeAccent.Red: _("Red"),
        ThemeAccent.Pink: _("Pink"),
        ThemeAccent.Gray: _("Gray"),
        ThemeAccent.Indigo: _("Indigo"),
        ThemeAccent.Purple: _("Purple"),
    }

    table[ThemeName] = {
        ThemeName.BuiltIn: APP_DISPLAY_NAME,
    }

    table[WhitespaceMode] = {
        WhitespaceMode.Strict: _("Do not ignore whitespace changes"),
        WhitespaceMode.IgnoreAll: _("Ignore all whitespace changes"),
        WhitespaceMode.IgnoreChange: _("Ignore length changes in existing whitespace"),
        WhitespaceMode.IgnoreCrAtEol: _("Ignore line ending changes only ({0})", "LF ↔ CRLF"),
    }

    return table


def _prefKeyTable() -> dict[str, str]:
    from gitfourchette.toolbox.textutils import paragraphs, tquo, escape
    from gitfourchette.exttools.usercommand import UserCommand

    sshAuthSock = os.environ.get("SSH_AUTH_SOCK", "")
    if sshAuthSock:
        sshAuthSockHelp = paragraphs(
            _("Note: Per {k}, your system is providing an ssh-agent ({v}). "
              "It’s recommended to use this one."),
            _("If your system’s agent isn’t saving any passphrases, "
              "make sure you’ve enabled {c} in your SSH configuration."))
    else:
        sshAuthSockHelp = _("Note: Per {k}, no ssh-agent seems to be running on your system.")
    sshAuthSockHelp = "<blockquote>" + sshAuthSockHelp.format(
        k="SSH_AUTH_SOCK", v=escape(sshAuthSock), c="AddKeysToAgent")

    return {
        "general": _p("Prefs", "General"),
        "diff": _p("Prefs", "Code"),
        "imageDiff": _p("Prefs", "Images"),
        "tabs": _p("Prefs", "Tabs"),
        "graph": _p("Prefs", "Commit History"),
        "trash": _p("Prefs", "Trash"),
        "git": _p("Prefs", "Git Integration"),
        "external": _p("Prefs", "External Tools"),
        "advanced": _p("Prefs", "Advanced"),
        "userCommands": _p("Prefs", "Custom Commands"),

        "language": _("Language"),
        "qtStyle": _("Qt style"),
        "shortHashChars": _("Shorten hashes to # characters"),
        "shortTimeFormat": _("Date/time format"),
        "shortTimeFormat_help": _timeFormatGuide(),
        "pathDisplayStyle": _("Path display style"),
        "authorDisplayStyle": _("Author display style"),
        "maxRecentRepos": _("Remember up to # recent repositories"),
        "showStatusBar": _("Show status bar"),
        "showToolBar": _("Show toolbar"),
        "showMenuBar": _("Show menu bar"),
        "showMenuBar_help": _("When the menu bar is hidden, press the Alt key to show it again."),
        "resetDontShowAgain": _("Restore all “don’t show this again” messages"),
        "pygmentsPlugins": _("Allow third-party Pygments plugins"),
        "pygmentsPlugins_help": "<p>" + _("Let {app} load third-party Pygments plugins installed on your system. "
                                          "These plugins extend syntax highlighting with new languages "
                                          "and color schemes. <b>May incur significant slowdowns.</b>"),
        "refSort": _("Sort branches && tags by"),
        "refSort_help": paragraphs(
            _("The default sorting mode for local branches, remote branches, and tags in the sidebar."),
            _("You can fine-tune this setting in each repo by right-clicking Branches, Remotes, or Tags "
              "in the sidebar. (Note that changing the default setting here will clear per-repo tweaks.)")),

        "font": _("Font"),
        "tabSpaces": _("One tab is # spaces"),
        "contextLines": _("Show up to # context lines"),
        "contextLines_help": _("Amount of unmodified lines to show around red or green lines in a diff."),
        "largeFileThresholdKB": _("Load diffs up to # KB"),
        "imageFileThresholdKB": _("Load images up to # KB"),
        "syntaxHighlighting": _("Syntax highlighting"),
        "wordWrap": _("Word wrap"),
        "showStrayCRs": _("Show alien line endings (CRLF)"),
        "showWhitespace": _("Show whitespace symbols"),
        "whitespaceMode": _("Whitespace diffs"),
        "whitespaceMode_help": paragraphs(
            _("How whitespace changes are shown in diffs."),
            _("This setting only affects how diffs are displayed. "
              "Operations that handle patches (export, apply, revert, etc.) still honor all whitespace changes.")),
        "colorblind": _("“-/+” colors"),
        "colorblind_help": _("Background colors for deleted (-) and added (+) lines."),
        "renderSvg": _("SVG files"),
        "renderSvg_false": _("Display as text"),
        "renderSvg_true": _("Display as images"),

        "tabCloseButton": _("Show tab close button"),
        "expandingTabs": _("Expand tabs to available width"),
        "autoHideTabs": _("Auto-hide tabs if only one repo is open"),

        "chronologicalOrder": _("Sort commits"),
        "chronologicalOrder_true": _("Chronologically"),
        "chronologicalOrder_false": _("Topologically"),
        "chronologicalOrder_help": paragraphs(
            _("<b>Chronological mode</b> lets you stay on top of the latest activity in the repository. "
              "The most recent commits always show up at the top of the graph. "
              "However, the graph can get messy when multiple branches receive commits in the same timeframe."),
            _("<b>Topological mode</b> makes the graph easier to read. It attempts to present sequences of "
              "commits within a branch in a linear fashion. Since this is not a strictly chronological "
              "mode, you may have to do more scrolling to see the latest changes in various branches."),
        ),
        "graphRowHeight": _("Row spacing"),
        "flattenLanes": _("Avoid gaps between branches in the graph"),
        "authorDiffAsterisk": _("Mark author/committer signature differences"),
        "authorDiffAsterisk_help": paragraphs(
            _("The commit history displays information about a commit’s <b>author</b>—"
              "their name and the date at which they made the commit. But in some cases, a commit "
              "might have been revised by someone else than the original author—"
              "this person is called the <b>committer</b>."),
            _("If you tick this option, an asterisk (*) will appear after the author’s name "
              "and/or date if they differ from the committer’s for any given commit."),
            _("Note that you can always hover over the author’s name or date to obtain "
              "detailed information about the author and the committer."),
        ),
        "maxCommits": _("Load up to # commits in the history"),
        "maxCommits_help": _("Set to 0 to always load the full commit history."),
        "alternatingRowColors": _("Draw rows using alternating background colors"),
        "refBoxMaxWidth": _("Ref indicators"),
        "refBoxMaxWidth_help": _("You can always hover over an indicator to display the full name of the ref."),
        "verifyGpgOnTheFly": _("Verify signed commits on the fly"),
        "verifyGpgOnTheFly_help": _(
            "As commits scroll into view, call {0} automatically to verify their signatures. "
            "The verification status is materialized by a seal icon next to the author’s name:", tquo("git verify-commit")
        ) + _gpgStatusReferenceTable() + "<br>" + _("(No seal = Commit isn’t signed)"),

        "maxTrashFiles": _("The trash keeps up to # discarded patches"),
        "maxTrashFileKB": _("Patches bigger than # KB won’t be salvaged"),
        "trash_HEADER": _(
            "When you discard changes from the working directory, {app} keeps a temporary copy in a hidden "
            "“trash” folder. This gives you a last resort to rescue changes that you have discarded by mistake. "
            "You can look around this trash folder via <i>“Help &rarr; Open Trash”</i>."),

        "verbosity": _("Logging verbosity"),
        "autoRefresh": _("Auto-refresh when app regains focus"),
        "autoRefresh_help": paragraphs(
            _("When you return to {app} from another application, it automatically "
              "scans for changes in the working directory and local branches. "
              "This keeps the interface in sync with the state of your repo on disk."),
            _("If you turn this off, you will need to hit {key} to "
              "perform this refresh manually.", key="F5"),
            "<b>" + _("We strongly recommend to keep this setting enabled.") + "</b>"),
        "autoFetchMinutes": _("Auto-fetch remotes every # minutes"),
        "animations": _("Animation effects in sidebar"),
        "smoothScroll": _("Smooth scrolling (where applicable)"),
        "forceQtApi": _("Preferred Qt binding"),
        "forceQtApi_help": paragraphs(
            _("After restarting, {app} will use this Qt binding if available."),
            _("You can also pass the name of a Qt binding via the “QT_API” environment variable."),
        ),
        "condensedFonts": _("Use condensed fonts"),
        "condensedFonts_help": "<p>" + _(
            "When a branch name or author name is too long to fit in its allotted space, "
            "condense the font before truncating the text."),

        "externalEditor": _("Text editor"),
        "externalDiff": _("Diff tool"),
        "externalDiff_help":
            "<p style='white-space: pre'>" + _("Argument placeholders:") + "\n" + _tokenReferenceTable({
                "$L": _("Old / Left"),
                "$R": _("New / Right"),
            }),
        "externalMerge": _("Merge tool"),
        "externalMerge_help":
            "<p style='white-space: pre'>" + _("Argument placeholders:") + "\n" + _tokenReferenceTable({
                "$B": _("Ancestor / Base / Center"),
                "$L": _("Ours / Local / Left"),
                "$R": _("Theirs / Remote / Right"),
                "$M": _("Merged / Output / Result"),
            }),
        "terminal": _("Terminal"),
        "terminal_help": paragraphs(
            _("Argument placeholders:"),
            _tokenReferenceTable({"$COMMAND": _("Command to execute after launching the terminal")}),
            _("The {0} placeholder is mandatory. It is automatically substituted for a wrapper script that "
              "enters your working directory and optionally starts one of your Custom Commands.",
              "$COMMAND")),
        "gitPath": "git",
        "ownAskpass": _("Have OpenSSH ask for passphrases via {app}", app=APP_DISPLAY_NAME),
        "ownAskpass_help": paragraphs(
            _("Tick this to have OpenSSH use {app} to ask for passphrases."),
            _("Untick this if you’ve set up another program in the {0} environment variable (such as {1}).", tquo("SSH_ASKPASS"), tquo("ksshaskpass"))),
        "ownSshAgent": "ssh-agent",
        "ownSshAgent_false": _("Use ssh-agent provided by the system") + " " + "" if sshAuthSock else _("(not detected)"),
        "ownSshAgent_true": _("Have {app} manage its own ssh-agent", app=APP_DISPLAY_NAME),
        "ownSshAgent_help": paragraphs(
            _("“ssh-agent” can save your SSH credentials so you don’t have to retype the same passphrase over and over. "
              "Some Linux distributions set up an ssh-agent for you."),
            _("You can also have {app} start its own instance of ssh-agent "
              "for the duration of your session and have it remember passphrases."),
            sshAuthSockHelp,
        ),
        "lfsAware": _("Parse LFS pointers"),
        "lfsAware_help": paragraphs(
            _("Tick this to display the real contents from LFS files."),
            _("Untick to display the raw text in LFS pointers."),
        ),

        "mouseShortcuts": _("Mouse Shortcuts"),
        "mouseShortcuts_HEADER": _("Tip: If your mouse has side buttons, you can use them to navigate back/forward in the repo."),
        "tabBarClicks": _("Repository tabs:"),
        "doubleClickTabBar": _("Double-click tab"),
        "middleClickTabBar": _("Middle-click tab"),
        "fileListClicks": _("File lists:"),
        "doubleClickFileList": _("Double-click file"),
        "middleClickFileList": _("Middle-click file"),
        "diffViewClicks": _("Diff view:"),
        "middleClickStageLines": _("Middle-click selection"),
        "middleClickStageLines_true": _("Stage/unstage selected lines"),
        "middleClickStageLines_false": _("Do nothing"),

        "userCommands_guide": _userCommandsGuide(),
        "commands": "",
        "confirmCommands": _("Ask for confirmation before running any command"),
        "confirmCommands_help": _(
            "If you untick this, you can still force a prompt to appear for "
            "specific commands by prepending them with {0}. For example: {1}",
            tquo(f"<tt>{UserCommand.AlwaysConfirmPrefix}</tt>"),
            "<pre>?git stash</pre>"),
    }


def _timeFormatGuide() -> str:
    from gitfourchette.qt import QLocale, QDateTime, QDate, QTime

    locale = QLocale()
    firstDay = QDateTime(QDate(2000, 1, 1), QTime(0, 0))
    lastDay = QDateTime(QDate(2099, 12, 31), QTime(23, 59, 59))
    monday = QDateTime(QDate(2024, 12, 23), QTime(12, 0))
    sunday = QDateTime(QDate(2024, 12, 29), QTime(12, 0))

    def row(fmt: str, caption="", date1: QDateTime | None = firstDay, date2=lastDay):
        sample = ""
        if date1 is not None:
            f1 = locale.toString(date1, fmt)
            f2 = locale.toString(date2, fmt)
            sample = f1 + "–" + f2
            if caption:
                sample = f", {sample}"
        return f"\n<code>{fmt:>4} </code> {caption}{sample}"

    return (
            "<html style='white-space: pre'>"
            + _p("date/time formats", "Available formats:")
            + "<p>"
            + row("yy", _("year"))
            + row("yyyy", _("year") + f", {QDate.currentDate().year()}", None)
            + "</p><p>"
            + row("M", _("month"))
            + row("MM", _("month"))
            + row("MMM")
            + row("MMMM")
            + "</p><p>"
            + row("d", _("day"))
            + row("dd", _("day"))
            + row("ddd", "", monday, sunday)
            + row("dddd", "", monday, sunday)
            + "</p><p>"
            + row("h", _("hour") + ", 0–23/1–12", None)
            + row("hh", _("hour") + ", 00–23/01–12", None)
            + row("mm", _("minute"))
            + row("ss", _("second"))
            + "</p><p>"
            + row("a")
            + row("A")
            + "</p>")


def _gpgStatusReferenceTable() -> str:
    from gitfourchette.repomodel import GpgStatus

    return _tokenReferenceTable({
        GpgStatus.Pending.iconHtml(): _("Verification pending"),
        GpgStatus.CantCheck.iconHtml(): _("Verification failed (e.g. missing key)"),
        GpgStatus.GoodTrusted.iconHtml(): _("Good signature; Key trusted"),
        GpgStatus.GoodUntrusted.iconHtml(): _("Good signature; Key not fully trusted"),
        GpgStatus.ExpiredSig.iconHtml(): _("Good signature; Key or signature expired"),
        GpgStatus.Bad.iconHtml(): _("Key revoked or bad signature"),
    })


def _tokenReferenceTable(table) -> str:
    markup = "<table>"
    for token, caption in table.items():
        markup += f"<tr><td><b>{token} </b></td><td> {caption}</td></tr>\n"
    markup += "</table>"
    return markup


def _userCommandsGuide() -> str:
    from gitfourchette.toolbox.textutils import paragraphs, linkify, stripAccelerators, tquo
    from gitfourchette.exttools.usercommand import UserCommand

    def cmdName(s: str):
        s = re.sub(r"&(.)", r"<u>&amp;\1</u>", s)
        return f"<com># {s}</com>"

    markup = textwrap.dedent("""\
    <style>
    body {background-color: palette(window); }
    pre { font-size: small; white-space: pre-wrap; margin: 0px; margin-left: 16px; }
    com { color: gray; font-style: italic; }
    tok { font-weight: bold; }
    </style><body>""")
    markup += paragraphs(_("Feel free to copy the sample below and paste it into the text box. "
                           "Then, click OK, and explore {menu} in the menu bar.",
                           menu=tquo(stripAccelerators(_("&Commands")))))
    markup += textwrap.dedent(f"""\
    <pre>
    git rebase -i <tok>$COMMIT</tok>   {cmdName(_("&Interactive Rebase"))}
    git rebase --continue   {cmdName(_("&Continue Rebase"))}
    <tok>?</tok> git rebase --abort    {cmdName(_("&Abort Rebase"))}
    git diff <tok>$COMMIT</tok> HEAD   {cmdName(_("Diff Commit With &HEAD"))}
    </pre>""")
    markup += paragraphs(_("You may use the following placeholders in your commands:"))
    markup += _tokenReferenceTable(UserCommand.tokenHelpTable())
    markup += paragraphs(linkify(_("For advanced usage tips, please visit [the user’s guide]."
                                   ), _userCommandsGuideUrl))
    return markup
