# -----------------------------------------------------------------------------
# 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.
# -----------------------------------------------------------------------------

import os
import shlex
from typing import ClassVar

from gitfourchette.exttools.toolcommands import ToolCommands
from gitfourchette.qt import *


class ToolPresets:
    DefaultDiffCommand = ""
    DefaultMergeCommand = ""
    DefaultTerminalCommand = ""
    FlatpakNamePrefix = "Flatpak: "

    Editors: ClassVar = {
        "System default": "",
        "BBEdit"        : "bbedit",
        "GVim"          : "gvim",
        "Kate"          : "kate",
        "KWrite"        : "kwrite",
        "MacVim"        : "mvim",
        "VS Code"       : "code",
    }

    DiffTools: ClassVar = {
        "Beyond Compare": "bcompare $L $R",
        "CLion"         : "clion diff $L $R",
        "DiffMerge"     : "diffmerge $L $R",
        "FileMerge"     : "assets:mac/opendiff.sh $L $R",
        "GVim"          : "gvim -f -d $L $R",
        "IntelliJ IDEA" : "idea diff $L $R",
        "KDiff3"        : "kdiff3 $L $R",
        "MacVim"        : "mvim -f -d $L $R",
        "Meld"          : "meld $L $R",
        "P4Merge"       : "p4merge $L $R",
        "PyCharm"       : "pycharm diff $L $R",
        "VS Code"       : "code --new-window --wait --diff $L $R",
        "WinMerge"      : "winmergeu /u /wl /wr $L $R",
    }

    # $B: ANCESTOR/BASE/CENTER
    # $L: OURS/LOCAL/LEFT
    # $R: THEIRS/REMOTE/RIGHT
    # $M: MERGED/OUTPUT
    MergeTools: ClassVar = {
        "Beyond Compare": "bcompare $L $R $B $M",
        "CLion"         : "clion merge $L $R $B $M",
        "DiffMerge"     : "diffmerge --merge --result=$M $L $B $R",
        "FileMerge"     : "assets:mac/opendiff.sh -ancestor $B $L $R -merge $M",
        "GVim"          : "gvim -f -d -c 'wincmd J' $M $L $B $R",
        "IntelliJ IDEA" : "idea merge $L $R $B $M",
        "KDiff3"        : "kdiff3 --merge $B $L $R --output $M",
        "MacVim"        : "mvim -f -d -c 'wincmd J' $M $L $B $R",
        "Meld"          : "meld --auto-merge $L $B $R --output=$M",
        "P4Merge"       : "p4merge $B $L $R $M",
        "PyCharm"       : "pycharm merge $L $R $B $M",
        "VS Code"       : "code --new-window --wait --merge $L $R $B $M",
        "WinMerge"      : "winmergeu /u /wl /wm /wr /am $B $L $R /o $M",
    }

    _macTerminals: ClassVar = {
        "macOS Terminal": "assets:mac/terminal.scpt $COMMAND",
        "kitty"         : "kitty --single-instance $COMMAND",  # single instance looks better in dock
        "WezTerm"       : "wezterm start $COMMAND",  # 'start' instead of '-e' to reuse app instance
        "Ghostty"       : "open -na ghostty.app --args -e $COMMAND",
    }

    _windowsTerminals: ClassVar = {
        # By default, git-bash starts bash with -i --login, which appends the
        # contents of our kicker script to the user's .bash_history.
        # To bypass this, invoke mintty manually.
        "Git Bash"      : "git-bash --command=usr/bin/mintty.exe --exec $COMMAND",
        # Can't use cmd or powershell on Windows for now, because we generate a bash script
        # that wraps the command to run.
        # "Command Prompt": "cmd /c start cmd /k $COMMAND",
        # "PowerShell"    : "cmd /c start powershell -NoExit -Command $COMMAND",
    }

    _freedesktopTerminals: ClassVar = {
        "Alacritty"     : "alacritty -e $COMMAND",
        "Contour"       : "contour $COMMAND",
        "foot"          : "foot $COMMAND",
        "Ghostty"       : "ghostty -e $COMMAND",
        "GNOME Terminal": "gnome-terminal -- $COMMAND",
        "kitty"         : "kitty $COMMAND",
        "Konsole"       : "konsole -e $COMMAND",
        "Ptyxis"        : "ptyxis -x $COMMAND",
        "st"            : "st -e $COMMAND",
        "Terminator"    : "terminator -x $COMMAND",
        "urxvt"         : "urxvt -e $COMMAND",
        "WezTerm"       : "wezterm -e $COMMAND",
        "xterm"         : "xterm -e $COMMAND",
    }

    # Filled in depending on platform
    Terminals: ClassVar[dict[str, str]] = {}

    FlatpakIDs: ClassVar = {
        "CLion"             : ("CLion",         "com.jetbrains.CLion"),
        "GVim"              : ("GVim",          "org.vim.Vim"),
        "IntelliJ IDEA CE"  : ("IntelliJ IDEA", "com.jetbrains.IntelliJ-IDEA-Community"),
        "PyCharm CE"        : ("PyCharm",       "com.jetbrains.PyCharm-Community"),
        "Kate"              : ("Kate",          "org.kde.kate"),
        "KDiff3"            : ("KDiff3",        "org.kde.kdiff3"),
        "KWrite"            : ("KWrite",        "org.kde.kwrite"),
        "Meld"              : ("Meld",          "org.gnome.meld"),
        "VS Code"           : ("VS Code",       "com.visualstudio.code"),
        "VS Code OSS"       : ("VS Code",       "com.visualstudio.code-oss"),
        # Terminals
        "Konsole"           : ("Konsole",       "org.kde.konsole"),
    }

    @classmethod
    def _filterToolPresets(cls):
        freedesktopTools = ["Kate", "KWrite"]
        macTools = ["FileMerge", "MacVim", "BBEdit"]
        winTools = ["WinMerge"]
        allPresetDicts = [cls.Editors, cls.DiffTools, cls.MergeTools, cls.Terminals]

        if MACOS:
            excludeTools = winTools + freedesktopTools
            cls.Terminals.update(cls._macTerminals)
            defaultDiffPreset = "FileMerge"
            defaultMergePreset = "FileMerge"
            defaultTerminalPreset = "macOS Terminal"
        elif WINDOWS:
            excludeTools = macTools + freedesktopTools
            cls.Terminals.update(cls._windowsTerminals)
            defaultDiffPreset = "WinMerge"
            defaultMergePreset = "WinMerge"
            defaultTerminalPreset = "Git Bash"
        else:
            excludeTools = macTools + winTools
            cls.Terminals.update(cls._freedesktopTerminals)

            terminalScores = dict.fromkeys(cls._freedesktopTerminals, 0)
            terminalScores |= {
                "Ptyxis"        : 2 if GNOME else -2,  # Fedora default
                "GNOME Terminal": 1 if GNOME else -1,
                "Konsole"       : 1 if KDE else 0,
                "xterm"         : -4 if WAYLAND else -3,
            }

            defaultDiffPreset = "Meld"
            defaultMergePreset = "Meld"
            defaultTerminalPreset = cls._findBestPreset(cls._freedesktopTerminals, terminalScores, "Konsole")

        for key in excludeTools:
            for presets in allPresetDicts:
                try:
                    del presets[key]
                except KeyError:
                    pass

        if FREEDESKTOP:
            for name, (alias, flatpakId) in cls.FlatpakIDs.items():
                k2 = cls.FlatpakNamePrefix + name
                assert any(
                    alias in presets for presets in allPresetDicts), f"missing non-flatpak preset for {alias}"
                for presets in allPresetDicts:
                    try:
                        originalCommand = presets[alias]
                    except KeyError:
                        continue
                    newCommand = ToolCommands.replaceProgramTokenInCommand(originalCommand, "flatpak", "run", flatpakId)
                    presets[k2] = newCommand

        cls.DefaultDiffCommand = cls._postProcessDefaultPreset(defaultDiffPreset, cls.DiffTools)
        cls.DefaultMergeCommand = cls._postProcessDefaultPreset(defaultMergePreset, cls.MergeTools)
        cls.DefaultTerminalCommand = cls._postProcessDefaultPreset(defaultTerminalPreset, cls.Terminals)

    @classmethod
    def _findBestPreset(cls, presets: dict[str, str], scores: dict[str, int], fallback: str) -> str:
        assert fallback in presets
        assert set(scores.keys()) == set(presets.keys()), "scores and presets must have the same keys!"

        sortedKeys = sorted(scores.keys(), key=lambda k: scores[k], reverse=True)

        for key in sortedKeys:
            program = presets[key].split()[0]
            if ToolCommands.which(program):
                return key

        return fallback

    @classmethod
    def _postProcessDefaultPreset(cls, key, presets):
        assert key in presets

        # If we're running as a Flatpak, use Flatpak as default tool as well
        if FLATPAK:
            flatpakKey = cls.FlatpakNamePrefix + key
            if flatpakKey in presets:
                key = flatpakKey

        return presets[key]

    @classmethod
    def getCommandName(cls, command: str, fallback = "", presets: dict[str, str] | None = None) -> str:
        if not command.strip():
            return fallback

        if presets is not None:
            presetName = next((k for k, v in presets.items() if v == command), "")
            if presetName:
                if presetName.startswith(cls.FlatpakNamePrefix):
                    presetName = presetName.removeprefix(cls.FlatpakNamePrefix)
                    presetName += " (Flatpak)"
                return presetName

        tokens = ToolCommands.splitCommandTokens(command)
        interestingToken = 0

        if FREEDESKTOP:
            interestingToken = ToolCommands.isFlatpakRunCommand(tokens)
            assert interestingToken >= 0

        try:
            name = tokens[interestingToken]
        except IndexError:
            return fallback

        name = os.path.basename(name)

        if MACOS:
            name = name.removesuffix(".app")

        return name

    @classmethod
    def flatpakBuiltInGit(cls) -> str:
        builtInGitPath = "/app/bin/git"
        if FLATPAK and os.path.exists(builtInGitPath):
            return ToolCommands.FlatpakSandboxedCommandPrefix + builtInGitPath
        return ""

    @classmethod
    def defaultGit(cls, hostOnly=False) -> str:
        return shlex.quote(
            (not hostOnly and cls.flatpakBuiltInGit())
            or ToolCommands.which("git")
            or "/usr/bin/git")


ToolPresets._filterToolPresets()
