#!/usr/bin/python3
# The client's installed games, on the desktop. The client writes a menu entry per game itself
# (`steam steam://rungameid/<id>`, with the game's icon), which the desktop's `steam` shim hands to
# the app - so an entry of ours exists only for a game the client wrote none for. Plus two links in
# the home folder to the game files. Run at every desktop start; stale entries go.
#
#   droiddeck-desktop-games [steam root]
import os
import re
import sys

HOME = os.environ.get("HOME", "/root")
STEAM_ROOT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HOME, ".local/share/Steam")
LIBRARIES = ((STEAM_ROOT, "Steam Games"), ("/mnt/droiddeck-sd", "SD Games"))
# The client's own tools and runtimes live in steamapps beside the games; they are not titles.
NOT_GAMES = {228980, 1493710, 3127680, 4183110, 4427310, 4185400}
APPS_DIR = os.path.join(HOME, ".local/share/applications")
PREFIX = "droiddeck-game-"
NAME = re.compile(r'^\s*"name"\s*"([^"]*)"', re.M)
STATE = re.compile(r'^\s*"StateFlags"\s*"(\d+)"', re.M)


def games(library):
    steamapps = os.path.join(library, "steamapps")
    out = []
    if not os.path.isdir(steamapps):
        return out
    for entry in sorted(os.listdir(steamapps)):
        m = re.fullmatch(r"appmanifest_(\d+)\.acf", entry)
        if not m:
            continue
        appid = int(m.group(1))
        if appid in NOT_GAMES:
            continue
        try:
            with open(os.path.join(steamapps, entry), encoding="utf-8", errors="replace") as f:
                text = f.read()
        except OSError:
            continue
        name = NAME.search(text)
        state = STATE.search(text)
        # StateFlags 4 = fully installed; anything else is downloading, updating or broken.
        if not name or not state or (int(state.group(1)) & 4) == 0:
            continue
        out.append((appid, name.group(1)))
    return out


def icon_for(appid):
    cache = os.path.join(STEAM_ROOT, "appcache/librarycache", str(appid))
    for candidate in ("logo.png", "library_600x900.jpg", "header.jpg"):
        path = os.path.join(cache, candidate)
        if os.path.isfile(path):
            return path
    return "steam"


def client_entries():
    """App ids the client already wrote a menu entry for."""
    found = set()
    if not os.path.isdir(APPS_DIR):
        return found
    for entry in os.listdir(APPS_DIR):
        if entry.startswith(PREFIX) or not entry.endswith(".desktop"):
            continue
        try:
            with open(os.path.join(APPS_DIR, entry), encoding="utf-8", errors="replace") as f:
                m = re.search(r"steam://rungameid/(\d+)", f.read())
        except OSError:
            continue
        if m:
            found.add(int(m.group(1)))
    return found


def registered():
    try:
        with open(os.path.join(STEAM_ROOT, "steamapps", "libraryfolders.vdf"), encoding="utf-8", errors="replace") as f:
            return [path.replace("\\\\", "\\") for path in re.findall(r'"path"\s+"((?:[^"\\]|\\.)*)"', f.read())]
    except OSError:
        return []


def write_entries():
    os.makedirs(APPS_DIR, exist_ok=True)
    wanted = {}
    theirs = client_entries()
    for library in dict.fromkeys([library for library, _ in LIBRARIES] + registered()):
        for appid, name in games(library):
            if appid not in theirs:
                wanted.setdefault(appid, name)
    for entry in os.listdir(APPS_DIR):
        if entry.startswith(PREFIX) and entry.endswith(".desktop"):
            appid = entry[len(PREFIX):-len(".desktop")]
            if not appid.isdigit() or int(appid) not in wanted:
                os.unlink(os.path.join(APPS_DIR, entry))
    for appid, name in wanted.items():
        safe = name.replace("\n", " ").strip() or str(appid)
        body = (
            "[Desktop Entry]\nType=Application\nName=%s\nComment=Steam game %d - opens in Steam under gamescope\n"
            "Exec=/usr/local/bin/droiddeck-steam-launch desktop %d\nIcon=%s\nTerminal=false\n"
            "Categories=Game;\nX-DroidDeck-AppId=%d\n" % (safe, appid, appid, icon_for(appid), appid)
        )
        path = os.path.join(APPS_DIR, "%s%d.desktop" % (PREFIX, appid))
        try:
            with open(path, encoding="utf-8") as f:
                if f.read() == body:
                    continue
        except OSError:
            pass
        with open(path, "w", encoding="utf-8") as f:
            f.write(body)
    return wanted


def link_libraries():
    for library, label in LIBRARIES:
        common = os.path.join(library, "steamapps", "common")
        link = os.path.join(HOME, label)
        if os.path.isdir(common):
            if os.path.islink(link) and os.readlink(link) == common:
                continue
            if os.path.islink(link) or os.path.isfile(link):
                os.unlink(link)
            if not os.path.exists(link):
                os.symlink(common, link)
        elif os.path.islink(link):
            os.unlink(link)


def main():
    wanted = write_entries()
    link_libraries()
    print("droiddeck-desktop-games: %d entr%s of ours, %d the client's" % (
        len(wanted), "y" if len(wanted) == 1 else "ies", len(client_entries())))


if __name__ == "__main__":
    main()
