#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
#
# pocketds-steam-default-proton11 — pin an ARM64 Proton as the default
# Steam Play compatibility tool for *all* titles on the Pocket DS.
#
# pocketds-steam ships compat-tool entries for Valve's ARM64 Protons in
# /usr/share/steam/compatibilitytools.d (pocketds-proton-experimental-arm64,
# pocketds-proton-11-arm64; proton-cachyos-arm64 comes in its own package).
# Once one of them is installed, we pin it (CachyOS first) as the global
# default by editing config.vdf directly:
#
#   InstallConfigStore.Software.Valve.Steam.CompatToolMapping."0"
#       name     = proton-cachyos-arm64 | pocketds-proton-experimental-arm64
#                  | pocketds-proton-11-arm64
#       config   = ""
#       priority = "250"
#
# AppID "0" is Valve's "fallback for any non-native title" key — same
# slot the GUI toggles when you flip the global toggle in Settings.
#
# Steam owns config.vdf and rewrites it whenever it shuts down, so this
# runs from the Steam launcher wrapper on every cold start, guarded by a
# marker so it's a no-op after the first successful pin.
#
# This script is a hand-rolled VDF reader/writer — Fedora doesn't ship
# the `vdf` PyPI module and we don't want to drag in pip on a device
# image. The Steam KeyValues format is a balanced "key" "value" /
# "key" { ... } tree with quoted strings and tab indentation; the
# parser below handles exactly what Steam emits (no escapes inside
# tokens, no conditional [$OS] suffixes, no multi-line values).

from __future__ import annotations

import os
import re
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Union

STEAMROOT = Path(os.path.expanduser("~/.local/share/Steam"))
CONFIG_VDF = STEAMROOT / "config" / "config.vdf"
MARKER = STEAMROOT / ".pocketds-default-proton11.done"

# Steam parses CompatToolMapping at next-launch. A bare `priority` of
# 250 is what the Steam UI writes when you select a tool from the
# global Steam Play dropdown; mirror that exactly so the UI doesn't
# notice an out-of-band write.
DEFAULT_PRIORITY = "250"


VDFNode = Union[str, dict]


def log(msg: str) -> None:
    print(f"pocketds-steam-default-proton11: {msg}", file=sys.stderr)


def find_proton_tool_name() -> str | None:
    """Pick the packaged ARM64 Proton entry (/usr/share/steam/
    compatibilitytools.d): Proton CachyOS (ARM64) when the
    proton-cachyos-arm64 package is installed, else the Valve build that
    is installed, Proton Experimental (ARM64) before Proton 11.0 (ARM64)."""
    if Path("/usr/share/steam/compatibilitytools.d/proton-cachyos-arm64/proton").is_file():
        return "proton-cachyos-arm64"
    for appid, tool in (("4427310", "pocketds-proton-experimental-arm64"),
                        ("4628740", "pocketds-proton-11-arm64")):
        if (STEAMROOT / "steamapps" / f"appmanifest_{appid}.acf").is_file():
            return tool
    return None


# ── minimal VDF parser/serialiser ───────────────────────────────────

_TOKEN_RE = re.compile(r'"((?:[^"\\]|\\.)*)"|([{}])')


def parse_vdf(text: str) -> dict:
    tokens = []
    for m in _TOKEN_RE.finditer(text):
        if m.group(1) is not None:
            tokens.append(("STR", m.group(1)))
        else:
            tokens.append((m.group(2), m.group(2)))
    pos = 0

    def parse_block() -> dict:
        nonlocal pos
        result: dict = {}
        while pos < len(tokens):
            kind, val = tokens[pos]
            if kind == "}":
                pos += 1
                return result
            if kind != "STR":
                raise ValueError(f"unexpected token {tokens[pos]!r}")
            key = val
            pos += 1
            if pos >= len(tokens):
                raise ValueError(f"truncated at key {key!r}")
            nkind, nval = tokens[pos]
            if nkind == "{":
                pos += 1
                result[key] = parse_block()
            elif nkind == "STR":
                result[key] = nval
                pos += 1
            else:
                raise ValueError(f"unexpected value token {tokens[pos]!r}")
        return result

    return parse_block()


def serialise(tree: dict, indent: int = 0) -> str:
    out = []
    pad = "\t" * indent
    for k, v in tree.items():
        if isinstance(v, dict):
            out.append(f'{pad}"{k}"')
            out.append(f"{pad}{{")
            out.append(serialise(v, indent + 1))
            out.append(f"{pad}}}")
        else:
            out.append(f'{pad}"{k}"\t\t"{v}"')
    return "\n".join(out)


def deep_set(tree: dict, path: list[str], value: VDFNode) -> None:
    node = tree
    for key in path[:-1]:
        if key not in node or not isinstance(node[key], dict):
            node[key] = {}
        node = node[key]
    node[path[-1]] = value


def atomic_write(target: Path, content: str) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(prefix=".pocketds-vdf.", dir=str(target.parent))
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
            f.write("\n")
        # Preserve mode of any existing file so Steam doesn't notice.
        if target.exists():
            shutil.copymode(target, tmp)
        else:
            os.chmod(tmp, 0o644)
        os.replace(tmp, target)
    except Exception:
        try:
            os.unlink(tmp)
        except FileNotFoundError:
            pass
        raise


# ── main ────────────────────────────────────────────────────────────


def steam_running() -> bool:
    uid = os.getuid()
    for proc in Path("/proc").iterdir():
        if not proc.name.isdigit():
            continue
        try:
            if proc.stat().st_uid == uid and (proc / "comm").read_text().strip() == "steam":
                return True
        except OSError:
            continue
    return False


def main() -> int:
    if MARKER.exists():
        log(f"marker present ({MARKER}) — default already pinned")
        return 0

    if steam_running():
        log("Steam is running and would overwrite config.vdf on exit — deferring to the next launch")
        return 0

    tool_name = find_proton_tool_name()
    if tool_name is None:
        log("no ARM64 Proton installed in Steam yet — nothing to pin")
        return 0

    if CONFIG_VDF.exists():
        try:
            tree = parse_vdf(CONFIG_VDF.read_text(encoding="utf-8", errors="replace"))
        except Exception as exc:
            log(f"could not parse {CONFIG_VDF}: {exc}; refusing to overwrite")
            return 1
    else:
        # Steam will fill in everything else on its first launch; we
        # just need enough scaffolding for it to read CompatToolMapping
        # without complaining.
        tree = {"InstallConfigStore": {"Software": {"Valve": {"Steam": {}}}}}

    mapping = {
        "name": tool_name,
        "config": "",
        "priority": DEFAULT_PRIORITY,
    }
    deep_set(
        tree,
        ["InstallConfigStore", "Software", "Valve", "Steam", "CompatToolMapping", "0"],
        mapping,
    )

    atomic_write(CONFIG_VDF, serialise(tree))
    MARKER.parent.mkdir(parents=True, exist_ok=True)
    MARKER.touch()
    log(f"default Steam Play tool pinned to {tool_name!r}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
