#!/usr/bin/python3
import os

HOME = os.environ.get("HOME", "/root")
CONFIG = os.environ.get("XDG_CONFIG_HOME", os.path.join(HOME, ".config"))


def seed(path, text, what, done="Player 1 on the controller"):
    if os.path.exists(path):
        return False
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path + ".tmp", "w", encoding="utf-8") as f:
        f.write(text)
    os.replace(path + ".tmp", path)
    print(f"droiddeck-pad-defaults: {what}: {done} ({path})")
    return True


RPCS3_PLAYER1 = """Player 1 Input:
  Handler: SDL
  Device: Xbox 360 Controller 1
  Config:
    Left Stick Left: LS X-
    Left Stick Down: LS Y-
    Left Stick Right: LS X+
    Left Stick Up: LS Y+
    Right Stick Left: RS X-
    Right Stick Down: RS Y-
    Right Stick Right: RS X+
    Right Stick Up: RS Y+
    Start: Start
    Select: Back
    PS Button: Guide
    Square: West
    Cross: South
    Circle: East
    Triangle: North
    Left: Left
    Down: Down
    Right: Right
    Up: Up
    R1: RB
    R2: RT
    R3: RS
    L1: LB
    L2: LT
    L3: LS
    Left Stick Deadzone: 8000
    Right Stick Deadzone: 8000
    Left Stick Anti-Deadzone: 4259
    Right Stick Anti-Deadzone: 4259
    Left Trigger Threshold: 0
    Right Trigger Threshold: 0
    Color Value B: 20
    LED battery indicator brightness: 10
  Buddy Device: ""
"""


def rpcs3():
    seed(os.path.join(CONFIG, "rpcs3/input_configs/global/Default.yml"), RPCS3_PLAYER1, "RPCS3")
    rpcs3_shader_mode()


RPCS3_INTERPRETER = "Async Recompiler with Shader Interpreter"
RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)"


def rpcs3_shader_mode():
    d = os.path.join(CONFIG, "rpcs3")
    marker = os.path.join(d, ".droiddeck-shader-mode")
    if os.path.exists(marker):
        return
    cfg = os.path.join(d, "config.yml")
    if not os.path.isfile(cfg):
        if seed(cfg, f"Video:\n  Shader Mode: {RPCS3_RECOMPILER}\n", "RPCS3", f"shader mode {RPCS3_RECOMPILER}"):
            open(marker, "w").close()
        return
    with open(cfg, encoding="utf-8", errors="replace") as f:
        text = f.read()
    if f"Shader Mode: {RPCS3_INTERPRETER}" in text:
        text = text.replace(f"Shader Mode: {RPCS3_INTERPRETER}", f"Shader Mode: {RPCS3_RECOMPILER}", 1)
        with open(cfg + ".tmp", "w", encoding="utf-8") as f:
            f.write(text)
        os.replace(cfg + ".tmp", cfg)
        print(f"droiddeck-pad-defaults: RPCS3: shader mode {RPCS3_RECOMPILER} ({cfg})")
    open(marker, "w").close()



def ini_section(lines, name):
    """(start, end) line indices of [name]'s body, or None."""
    start = None
    for i, l in enumerate(lines):
        t = l.strip()
        if t.startswith("[") and t.endswith("]"):
            if start is not None:
                return start, i
            if t[1:-1].strip() == name:
                start = i + 1
    return (start, len(lines)) if start is not None else None


def patch_ini_section(path, section, body, what, needle="SDL-", require=None, done="Player 1 on the controller"):
    """Add body's lines to [section] of an ini the emulator wrote itself, unless the section
    already binds a controller (needle). Missing file: nothing - the emulator writes its own
    defaults on first run and this runs again."""
    if not os.path.isfile(path):
        return
    with open(path, encoding="utf-8", errors="replace") as f:
        text = f.read()
    if require and require not in text:
        return
    lines = text.splitlines()
    span = ini_section(lines, section)
    if span and any(needle in l for l in lines[span[0]:span[1]]):
        return
    new = body.strip("\n").splitlines()
    if span:
        have = {l.split("=", 1)[0].strip() for l in lines[span[0]:span[1]] if "=" in l}
        new = [l for l in new if not (l.split("=", 1)[0].strip() == "Type" and "Type" in have)]
        end = span[1]
        while end > span[0] and not lines[end - 1].strip():
            end -= 1
        lines[end:end] = new
    else:
        lines += ["", f"[{section}]"] + new
    with open(path + ".tmp", "w", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")
    os.replace(path + ".tmp", path)
    print(f"droiddeck-pad-defaults: {what}: {done} ({path})")


DUCKSTATION_PAD1 = """Type = AnalogController
Up = SDL-0/DPadUp
Down = SDL-0/DPadDown
Left = SDL-0/DPadLeft
Right = SDL-0/DPadRight
Cross = SDL-0/A
Circle = SDL-0/B
Square = SDL-0/X
Triangle = SDL-0/Y
Select = SDL-0/Back
Start = SDL-0/Start
Analog = SDL-0/Guide
L1 = SDL-0/LeftShoulder
R1 = SDL-0/RightShoulder
L2 = SDL-0/+LeftTrigger
R2 = SDL-0/+RightTrigger
L3 = SDL-0/LeftStick
R3 = SDL-0/RightStick
LLeft = SDL-0/-LeftX
LRight = SDL-0/+LeftX
LUp = SDL-0/-LeftY
LDown = SDL-0/+LeftY
RLeft = SDL-0/-RightX
RRight = SDL-0/+RightX
RUp = SDL-0/-RightY
RDown = SDL-0/+RightY
LargeMotor = SDL-0/LargeMotor
SmallMotor = SDL-0/SmallMotor
"""


def ini_get(path, section, key):
    try:
        with open(path, encoding="utf-8", errors="replace") as f:
            lines = f.read().splitlines()
    except OSError:
        return None
    span = ini_section(lines, section)
    if not span:
        return None
    for l in lines[span[0]:span[1]]:
        if "=" in l and l.split("=", 1)[0].strip() == key:
            return l.split("=", 1)[1].strip()
    return None


def ini_set(path, section, key, value):
    """Sets one key (the first of that name) in [section], adding either as needed."""
    with open(path, encoding="utf-8", errors="replace") as f:
        lines = f.read().splitlines()
    span = ini_section(lines, section)
    if span is None:
        lines += ["", f"[{section}]", f"{key} = {value}"]
    else:
        for i in range(span[0], span[1]):
            if "=" in lines[i] and lines[i].split("=", 1)[0].strip() == key:
                lines[i] = f"{key} = {value}"
                break
        else:
            end = span[1]
            while end > span[0] and not lines[end - 1].strip():
                end -= 1
            lines.insert(end, f"{key} = {value}")
    with open(path + ".tmp", "w", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")
    os.replace(path + ".tmp", path)


def drop_line(path, line):
    with open(path, encoding="utf-8", errors="replace") as f:
        lines = f.read().splitlines()
    kept = [l for l in lines if l.replace(" ", "") != line.replace(" ", "")]
    if len(kept) != len(lines):
        with open(path + ".tmp", "w", encoding="utf-8") as f:
            f.write("\n".join(kept) + "\n")
        os.replace(path + ".tmp", path)


def system_folder(names, default):
    for name in names:
        for cand in (name, name.upper()):
            d = os.path.join(ROMS, cand)
            if os.path.isdir(d):
                return d
    return os.path.join(ROMS, default)


def bios_folder(base):
    for name in ("bios", "BIOS", "firmware"):
        d = os.path.join(base, name)
        if os.path.isdir(d):
            return d
    d = os.path.join(base, "bios")
    if os.path.isdir(ROMS):
        try:
            os.makedirs(d, exist_ok=True)
        except OSError:
            pass
    return d


DUCKSTATION_PAD1_GUIDE_FREE = "\n".join(l for l in DUCKSTATION_PAD1.splitlines() if not l.startswith("Analog ")) + "\n"


def duckstation():
    ps1 = system_folder(("psx", "ps1"), "psx")
    d = os.path.join(CONFIG, "duckstation")
    ini = os.path.join(d, "settings.ini")
    legacy = os.path.join(HOME, ".local/share/duckstation/settings.ini")
    if not os.path.isfile(ini) and os.path.isfile(legacy):
        ini = legacy
    if not os.path.isfile(ini):
        bios = bios_folder(ps1)
        seed(ini, "[Main]\nSetupWizardIncomplete = false\nInhibitScreensaver = false\n\n"
                  f"[BIOS]\nSearchDirectory = {bios}\n\n"
                  f"[GameList]\nRecursivePaths = {ps1}\n\n"
                  "[GPU]\nRenderer = Vulkan\n\n"
                  "[Hotkeys]\nOpenPauseMenu = Keyboard/Escape\nOpenPauseMenu = SDL-0/Guide\n\n"
                  "[Pad1]\n" + DUCKSTATION_PAD1_GUIDE_FREE, "DuckStation", "first-time setup, BIOS from " + bios)
        return
    patch_ini_section(ini, "Pad1", DUCKSTATION_PAD1_GUIDE_FREE, "DuckStation")
    drop_line(ini, "Analog = SDL-0/Guide")
    patch_ini_section(ini, "Hotkeys", "OpenPauseMenu = SDL-0/Guide\n", "DuckStation", needle="SDL-0/Guide",
                      done="the guide button opens the pause menu")
    patch_ini_section(ini, "GameList", f"RecursivePaths = {ps1}\n", "DuckStation", needle=ROMS,
                      done="games listed from the ROMs folder")
    if ini_get(ini, "Main", "SetupWizardIncomplete") == "true":
        ini_set(ini, "Main", "SetupWizardIncomplete", "false")
        print("droiddeck-pad-defaults: DuckStation: first-time setup already done here")
    if (ini_get(ini, "Main", "InhibitScreensaver") or "true") == "true":
        ini_set(ini, "Main", "InhibitScreensaver", "false")
        print("droiddeck-pad-defaults: DuckStation: no screensaver inhibit (no such service here)")
    marker = os.path.join(os.path.dirname(ini), ".droiddeck-setup")
    if os.path.exists(marker):
        return
    current = ini_get(ini, "BIOS", "SearchDirectory") or "bios"
    own = current if os.path.isabs(current) else os.path.join(os.path.dirname(ini), current)
    if not (os.path.isdir(own) and os.listdir(own)):
        ini_set(ini, "BIOS", "SearchDirectory", bios_folder(ps1))
        print(f"droiddeck-pad-defaults: DuckStation: BIOS from {bios_folder(ps1)}")
    if (ini_get(ini, "GPU", "Renderer") or "Automatic") == "Automatic":
        ini_set(ini, "GPU", "Renderer", "Vulkan")
        print("droiddeck-pad-defaults: DuckStation: renderer Vulkan")
    open(marker, "w").close()


PCSX2_PAD1 = """Type = DualShock2
Up = SDL-0/DPadUp
Right = SDL-0/DPadRight
Down = SDL-0/DPadDown
Left = SDL-0/DPadLeft
Triangle = SDL-0/FaceNorth
Circle = SDL-0/FaceEast
Cross = SDL-0/FaceSouth
Square = SDL-0/FaceWest
Select = SDL-0/Back
Start = SDL-0/Start
L1 = SDL-0/LeftShoulder
R1 = SDL-0/RightShoulder
L2 = SDL-0/+LeftTrigger
R2 = SDL-0/+RightTrigger
L3 = SDL-0/LeftStick
R3 = SDL-0/RightStick
Analog = SDL-0/Guide
LUp = SDL-0/-LeftY
LRight = SDL-0/+LeftX
LDown = SDL-0/+LeftY
LLeft = SDL-0/-LeftX
RUp = SDL-0/-RightY
RRight = SDL-0/+RightX
RDown = SDL-0/+RightY
RLeft = SDL-0/-RightX
LargeMotor = SDL-0/LargeMotor
SmallMotor = SDL-0/SmallMotor
"""


def pcsx2():
    patch_ini_section(os.path.join(CONFIG, "PCSX2/inis/PCSX2.ini"), "Pad1", PCSX2_PAD1, "PCSX2",
                      require="SettingsVersion")



DOLPHIN_GC = """[GCPad1]
Device = SDL/0/Xbox 360 Controller
Buttons/A = `Button S`
Buttons/B = `Button W`
Buttons/X = `Button E`
Buttons/Y = `Button N`
Buttons/Z = `Shoulder R`
Buttons/Start = `Start`
Main Stick/Up = `Left Y+`
Main Stick/Down = `Left Y-`
Main Stick/Left = `Left X-`
Main Stick/Right = `Left X+`
C-Stick/Up = `Right Y+`
C-Stick/Down = `Right Y-`
C-Stick/Left = `Right X-`
C-Stick/Right = `Right X+`
Triggers/L = `Trigger L`
Triggers/R = `Trigger R`
Triggers/L-Analog = `Trigger L`
Triggers/R-Analog = `Trigger R`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
D-Pad/Right = `Pad E`
Rumble/Motor = `Motor`
"""
DOLPHIN_WII = """[Wiimote1]
Device = SDL/0/Xbox 360 Controller
Source = 1
Buttons/A = `Button S`
Buttons/B = `Trigger R`
Buttons/1 = `Button W`
Buttons/2 = `Button N`
Buttons/- = `Back`
Buttons/+ = `Start`
Buttons/Home = `Shoulder R`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
D-Pad/Right = `Pad E`
IR/Up = `Right Y+`
IR/Down = `Right Y-`
IR/Left = `Right X-`
IR/Right = `Right X+`
Shake/X = `Button E`
Shake/Y = `Button E`
Shake/Z = `Button E`
Rumble/Motor = `Motor`
Extension = Nunchuk
Nunchuk/Buttons/C = `Shoulder L`
Nunchuk/Buttons/Z = `Trigger L`
Nunchuk/Stick/Up = `Left Y+`
Nunchuk/Stick/Down = `Left Y-`
Nunchuk/Stick/Left = `Left X-`
Nunchuk/Stick/Right = `Left X+`
"""


ARMSX2_PAD1 = "\n".join(l for l in PCSX2_PAD1.splitlines() if not l.startswith("Analog ")) + "\n"
ROMS = "/root/ROMs"


def ps2_folder():
    for name in ("ps2", "PS2"):
        d = os.path.join(ROMS, name)
        if os.path.isdir(d):
            return d
    return os.path.join(ROMS, "ps2")


def ps2_bios_folder():
    base = ps2_folder()
    for name in ("bios", "BIOS", "firmware"):
        d = os.path.join(base, name)
        if os.path.isdir(d):
            return d
    d = os.path.join(base, "bios")
    if os.path.isdir(ROMS):
        try:
            os.makedirs(d, exist_ok=True)
        except OSError:
            pass
    return d


def armsx2():
    root = os.path.join(CONFIG, "ARMSX2")
    ini = os.path.join(root, "inis/PCSX2.ini")
    old = os.path.join(CONFIG, "PCSX2")
    if not os.path.isfile(ini) and os.path.isfile(os.path.join(old, "inis/PCSX2.ini")):
        import shutil
        for sub in ("inis", "memcards", "bios", "gamesettings", "cheats", "patches", "covers",
                    "inputprofiles", "textures"):
            src = os.path.join(old, sub)
            if os.path.isdir(src):
                shutil.copytree(src, os.path.join(root, sub), dirs_exist_ok=True)
        print(f"droiddeck-pad-defaults: ARMSX2: settings and memory cards copied from PCSX2 ({root})")
    elif not os.path.isfile(ini):
        bios, games = ps2_bios_folder(), ps2_folder()
        seed(ini, "[UI]\nSettingsVersion = 1\nSetupWizardIncomplete = false\n\n"
                  f"[Folders]\nBios = {bios}\n\n"
                  f"[GameList]\nRecursivePaths = {games}\n\n"
                  "[EmuCore/GS]\nRenderer = 14\n\n"
                  "[Hotkeys]\nOpenPauseMenu = Keyboard/Escape\nOpenPauseMenu = SDL-0/Guide\n\n"
                  "[Pad1]\n" + ARMSX2_PAD1, "ARMSX2", "first-time setup, BIOS from " + bios)
        armsx2_bios(ini)
        return
    patch_ini_section(ini, "Pad1", ARMSX2_PAD1, "ARMSX2", require="SettingsVersion")
    with open(ini, encoding="utf-8", errors="replace") as f:
        lines = f.read().splitlines()
    kept = [l for l in lines if l.replace(" ", "") != "Analog=SDL-0/Guide"]
    if len(kept) != len(lines):
        with open(ini + ".tmp", "w", encoding="utf-8") as f:
            f.write("\n".join(kept) + "\n")
        os.replace(ini + ".tmp", ini)
    patch_ini_section(ini, "Hotkeys", "OpenPauseMenu = SDL-0/Guide\n", "ARMSX2", needle="SDL-0/Guide",
                      require="SettingsVersion", done="the guide button opens the pause menu")
    patch_ini_section(ini, "GameList", f"RecursivePaths = {ps2_folder()}\n", "ARMSX2", needle=ROMS,
                      require="SettingsVersion", done="games listed from the ROMs folder")
    if ini_get(ini, "UI", "SetupWizardIncomplete") == "true":
        ini_set(ini, "UI", "SetupWizardIncomplete", "false")
        print("droiddeck-pad-defaults: ARMSX2: first-time setup already done here")
    armsx2_bios(ini)


def armsx2_bios(ini):
    """PCSX2 boots the one BIOS file named in [Filenames] BIOS (inside [Folders] Bios); its first-time
    setup is what names it. With that skipped, the first dump in the folder is named here - a US one
    first - while none is named or the named one is gone. A choice made in ARMSX2 is kept."""
    folder = ini_get(ini, "Folders", "Bios") or ps2_bios_folder()
    if not os.path.isabs(folder):
        folder = os.path.join(os.path.dirname(os.path.dirname(ini)), folder)
    current = ini_get(ini, "Filenames", "BIOS")
    if current and os.path.isfile(os.path.join(folder, current)):
        return
    try:
        dumps = sorted(f for f in os.listdir(folder)
                       if os.path.isfile(os.path.join(folder, f)) and 4 << 20 <= os.path.getsize(os.path.join(folder, f)) <= 8 << 20)
    except OSError:
        return
    if not dumps:
        return
    us = [f for f in dumps if any(t in f.lower() for t in ("usa", "(us)", "(us,", "ntsc-u"))]
    pick = (us or dumps)[0]
    ini_set(ini, "Filenames", "BIOS", pick)
    print(f"droiddeck-pad-defaults: ARMSX2: BIOS {pick}")


DOLPHIN_GUIDE = "`SDL/0/Xbox 360 Controller:Guide`"
DOLPHIN_HOTKEYS = """[Hotkeys]
Device = XInput2/0/Virtual core pointer
General/Open = @(Ctrl+O)
General/Toggle Pause = F10
General/Stop = Escape
General/Toggle Fullscreen = @(Alt+Return) | `SDL/0/Xbox 360 Controller:Guide`
General/Take Screenshot = F9
General/Open Achievements = @(Alt+A)
Emulation Speed/Disable Emulation Speed Limit = Tab
Stepping/Step Into = F11
Stepping/Step Over = @(Shift+F10)
Stepping/Step Out = @(Shift+F11)
Breakpoint/Toggle Breakpoint = @(Shift+F9)
Wii/Connect Wii Remote 1 = @(Alt+F5)
Wii/Connect Wii Remote 2 = @(Alt+F6)
Wii/Connect Wii Remote 3 = @(Alt+F7)
Wii/Connect Wii Remote 4 = @(Alt+F8)
Wii/Connect Balance Board = @(Alt+F9)
Load State/Load State Slot 1 = F1
Load State/Load State Slot 2 = F2
Load State/Load State Slot 3 = F3
Load State/Load State Slot 4 = F4
Load State/Load State Slot 5 = F5
Load State/Load State Slot 6 = F6
Load State/Load State Slot 7 = F7
Load State/Load State Slot 8 = F8
Save State/Save State Slot 1 = @(Shift+F1)
Save State/Save State Slot 2 = @(Shift+F2)
Save State/Save State Slot 3 = @(Shift+F3)
Save State/Save State Slot 4 = @(Shift+F4)
Save State/Save State Slot 5 = @(Shift+F5)
Save State/Save State Slot 6 = @(Shift+F6)
Save State/Save State Slot 7 = @(Shift+F7)
Save State/Save State Slot 8 = @(Shift+F8)
Other State Hotkeys/Undo Load State = F12
Other State Hotkeys/Undo Save State = @(Shift+F12)
GBA Core/Load ROM = @(`Ctrl`+`Shift`+`O`)
GBA Core/Unload ROM = @(`Ctrl`+`Shift`+`W`)
GBA Core/Reset = @(`Ctrl`+`Shift`+`R`)
GBA Volume/Volume Down = `KP_Subtract`
GBA Volume/Volume Up = `KP_Add`
GBA Volume/Volume Toggle Mute = `M`
GBA Window Size/1x = `KP_1`
GBA Window Size/2x = `KP_2`
GBA Window Size/3x = `KP_3`
GBA Window Size/4x = `KP_4`
USB Emulation Devices/Show Skylanders Portal = @(Ctrl+P)
USB Emulation Devices/Show Infinity Base = @(Ctrl+I)
"""

DOLPHIN_APPIMAGE = "/opt/appimages/dolphin.AppImage"
DOLPHIN_DATA = "/usr/share/dolphin-emu"


def dolphin_data():
    """The Dolphin build in the catalog finds its own data - the GameCube fonts, the per-game fixes
    (GameSettings: GUT.ini puts Ultimate Spider-Man on the MMU), its icons and shaders - only at the
    fixed /usr/share/dolphin-emu it was built for; there is no setting or variable to move it. Here it
    runs from a copy unpacked under /tmp (no FUSE), so it found none of it: a GameCube game stopped
    on "Windows-1252 fonts are not loaded", another on an invalid write the missing game fix would
    have prevented. So the folder is unpacked from the AppImage to that path, once per AppImage."""
    import shutil, subprocess, tempfile
    try:
        st = os.stat(DOLPHIN_APPIMAGE)
    except OSError:
        return
    stamp = "%d:%d" % (st.st_size, int(st.st_mtime))
    marker = os.path.join(DOLPHIN_DATA, ".droiddeck-from")
    try:
        if open(marker).read() == stamp and os.path.isfile(os.path.join(DOLPHIN_DATA, "Sys/GC/font_western.bin")):
            return
    except OSError:
        pass
    work = tempfile.mkdtemp(prefix="dolphin-data-", dir="/tmp")
    try:
        subprocess.run([DOLPHIN_APPIMAGE, "--appimage-extract", "usr/share/dolphin-emu/*"], cwd=work,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)
        src = os.path.join(work, "squashfs-root/usr/share/dolphin-emu")
        if not os.path.isfile(os.path.join(src, "Sys/GC/font_western.bin")):
            print("droiddeck-pad-defaults: Dolphin: could not unpack its data from the AppImage")
            return
        new = DOLPHIN_DATA + ".new"
        shutil.rmtree(new, ignore_errors=True)
        shutil.move(src, new)
        with open(os.path.join(new, ".droiddeck-from"), "w") as f:
            f.write(stamp)
        shutil.rmtree(DOLPHIN_DATA, ignore_errors=True)
        os.rename(new, DOLPHIN_DATA)
        print("droiddeck-pad-defaults: Dolphin: its data (fonts, per-game fixes) in " + DOLPHIN_DATA)
    except (OSError, subprocess.SubprocessError) as e:
        print("droiddeck-pad-defaults: Dolphin: unpacking its data failed: %s" % e)
    finally:
        shutil.rmtree(work, ignore_errors=True)


def dolphin():
    if not os.path.isfile(DOLPHIN_APPIMAGE):
        return
    dolphin_data()
    d = os.path.join(CONFIG, "dolphin-emu")
    seed(os.path.join(d, "GCPadNew.ini"), DOLPHIN_GC, "Dolphin (GameCube)")
    seed(os.path.join(d, "WiimoteNew.ini"), DOLPHIN_WII, "Dolphin (Wii)")
    wii_pad = os.path.join(d, "WiimoteNew.ini")
    if ini_get(wii_pad, "Wiimote1", "Buttons/Home") == "`Guide`":
        ini_set(wii_pad, "Wiimote1", "Buttons/Home", "`Shoulder R`")
        print("droiddeck-pad-defaults: Dolphin (Wii): Home on the right shoulder button (guide = Dolphin's window)")
    hotkeys = os.path.join(d, "Hotkeys.ini")
    if not os.path.isfile(hotkeys):
        seed(hotkeys, DOLPHIN_HOTKEYS, "Dolphin", "the guide button shows Dolphin's window (Toggle Fullscreen)")
    else:
        current = ini_get(hotkeys, "Hotkeys", "General/Toggle Fullscreen")
        if current is not None and DOLPHIN_GUIDE not in current:
            ini_set(hotkeys, "Hotkeys", "General/Toggle Fullscreen",
                    (current + " | " if current else "") + DOLPHIN_GUIDE)
            print("droiddeck-pad-defaults: Dolphin: the guide button shows Dolphin's window (Toggle Fullscreen)")
    ini = os.path.join(d, "Dolphin.ini")
    gc = system_folder(("gamecube", "gc", "ngc"), "gamecube")
    wii = system_folder(("wii",), "wii")
    if not os.path.isfile(ini):
        os.makedirs(d, exist_ok=True)
        with open(ini, "w", encoding="utf-8") as f:
            f.write("[Analytics]\nEnabled = False\nPermissionAsked = True\n"
                    "[Core]\nGFXBackend = Vulkan\n"
                    "[General]\nISOPaths = 2\nISOPath0 = %s\nISOPath1 = %s\nRecursiveISOPaths = True\n"
                    "[Interface]\nSkipNKitWarning = True\n" % (gc, wii))
        print("droiddeck-pad-defaults: Dolphin: first-time setup (Vulkan, games from %s and %s)" % (gc, wii))
        return
    try:
        count = int(ini_get(ini, "General", "ISOPaths") or "0")
    except ValueError:
        count = 0
    paths = [ini_get(ini, "General", "ISOPath%d" % i) for i in range(count)]
    changed = []
    if any((p or "").rstrip("/") == ROMS for p in paths):
        paths = [p for p in paths if p and p.rstrip("/") != ROMS]
        for i, p in enumerate(paths):
            ini_set(ini, "General", "ISOPath%d" % i, p)
        for i in range(len(paths), count):
            ini_set(ini, "General", "ISOPath%d" % i, "")
        count = len(paths)
        ini_set(ini, "General", "ISOPaths", str(count))
        changed.append("not the whole ROMs folder")
    for folder in (gc, wii):
        if folder not in paths:
            ini_set(ini, "General", "ISOPath%d" % count, folder)
            count += 1
            paths.append(folder)
            changed.append(folder)
    if changed:
        ini_set(ini, "General", "ISOPaths", str(count))
    if ini_get(ini, "General", "RecursiveISOPaths") != "True":
        ini_set(ini, "General", "RecursiveISOPaths", "True")
        changed.append("subfolders")
    if changed:
        print("droiddeck-pad-defaults: Dolphin: games from " + ", ".join(changed))
    marker = os.path.join(d, ".droiddeck-setup")
    if os.path.exists(marker):
        return
    if not ini_get(ini, "Core", "GFXBackend"):
        ini_set(ini, "Core", "GFXBackend", "Vulkan")
    ini_set(ini, "Interface", "SkipNKitWarning", "True")
    if ini_get(ini, "Analytics", "PermissionAsked") != "True":
        ini_set(ini, "Analytics", "Enabled", "False")
        ini_set(ini, "Analytics", "PermissionAsked", "True")
    open(marker, "w").close()
    print("droiddeck-pad-defaults: Dolphin: Vulkan, no NKit warning")


CEMU_MAP = [(1, 1), (2, 0), (3, 3), (4, 2), (5, 9), (6, 10), (7, 42), (8, 43), (9, 6), (10, 4),
            (11, 11), (12, 12), (13, 13), (14, 14), (15, 7), (16, 8), (17, 45), (18, 39), (19, 44),
            (20, 38), (21, 47), (22, 41), (23, 46), (24, 40), (27, 5)]
CEMU_XML = """<?xml version="1.0" encoding="UTF-8"?>
<emulated_controller>
\t<type>Wii U GamePad</type>
\t<toggle_display>0</toggle_display>
\t<controller>
\t\t<api>SDLController</api>
\t\t<uuid>0_03005e615e0400008e02000010010000</uuid>
\t\t<display_name>Xbox 360 Controller</display_name>
\t\t<rumble>0.5</rumble>
\t\t<mappings>
""" + "".join(f"\t\t\t<entry>\n\t\t\t\t<mapping>{m}</mapping>\n\t\t\t\t<button>{b}</button>\n\t\t\t</entry>\n" for m, b in CEMU_MAP) + """\t\t</mappings>
\t</controller>
</emulated_controller>
"""


CEMU_SETTINGS = """<?xml version="1.0" encoding="UTF-8"?>
<content>
    <mlc_path></mlc_path>
    <GamePaths>
        <Entry>{games}</Entry>
    </GamePaths>
    <Graphic>
        <api>1</api>
    </Graphic>
    <Audio>
        <api>3</api>
        <TVVolume>100</TVVolume>
        <PadVolume>100</PadVolume>
        <TVDevice>default</TVDevice>
        <PadDevice>default</PadDevice>
    </Audio>
</content>
"""


def cemu():
    seed(os.path.join(CONFIG, "Cemu/controllerProfiles/controller0.xml"), CEMU_XML, "Cemu")
    games = system_folder(["wiiu", "wii u"], "wiiu")
    seed(os.path.join(CONFIG, "Cemu/settings.xml"), CEMU_SETTINGS.format(games=games), "Cemu",
         done=f"games in {games}, Vulkan, Cubeb audio on the default device")


MELONDS_GUIDE = 8
MELONDS_JOY = f"""[Instance0.Joystick]
A = 1
B = 0
X = 3
Y = 2
L = 4
R = 5
Select = 6
Start = 7
Up = 17891585
Down = 16843012
Left = 1114376
Right = 65794
HK_FullscreenToggle = {MELONDS_GUIDE}
"""

MELONDS_WINDOW = """[Instance0.Window0]
ScreenLayout = 2
ScreenSizing = 1
IntegerScaling = false
ScreenFilter = false
"""
MELONDS_WINDOW_DEFAULTS = {"ScreenLayout": ("0", "3"), "ScreenSizing": ("0",), "ScreenFilter": ("true",)}


def melonds_geometry():
    import base64, struct
    w, h = 512, 288
    try:
        screen_w = int(os.environ.get("BL_WIDTH", "1920"))
    except ValueError:
        screen_w = 1920
    rect = (0, 0, w - 1, h - 1)
    blob = struct.pack(">IHH4i4iiBBi4i", 0x1D9D0CB, 3, 0, *rect, *rect, 0, 0, 0, screen_w, *rect)
    return base64.b64encode(blob).decode()


def toml_get(lines, table, key):
    """The raw value of key in [table], or None."""
    inside = False
    for l in lines:
        t = l.strip()
        if t.startswith("["):
            inside = t == f"[{table}]"
        elif inside and "=" in t and t.split("=", 1)[0].strip() == key:
            return t.split("=", 1)[1].strip()
    return None


def toml_set(lines, table, key, value):
    """key = value in [table]; the table is added at the end when missing."""
    start = None
    for i, l in enumerate(lines):
        t = l.strip()
        if t.startswith("["):
            if start is not None:
                break
            if t == f"[{table}]":
                start = i
        elif start is not None and "=" in t and t.split("=", 1)[0].strip() == key:
            lines[i] = f"{key} = {value}"
            return
    if start is None:
        lines += ["", f"[{table}]", f"{key} = {value}"]
        return
    end = start + 1
    while end < len(lines) and not lines[end].strip().startswith("["):
        end += 1
    while end > start + 1 and not lines[end - 1].strip():
        end -= 1
    lines.insert(end, f"{key} = {value}")


def toml_str(path):
    return '"' + path.replace("\\", "\\\\").replace('"', '\\"') + '"'


DS_BIOS7_MD5 = "df692a80a5b1bc90728bc3dfc76cd948"
DS_BIOS9_MD5 = "a392174eb3e572fed6447e956bde4b25"


def nds_firmware_files():
    import hashlib
    base = system_folder(("nds", "ds"), "nds")
    roots = [os.path.join(base, n) for n in ("bios", "BIOS", "firmware") if os.path.isdir(os.path.join(base, n))]
    found = {}
    for root in roots:
        for d, _, files in os.walk(root):
            if d[len(root):].count(os.sep) > 2:
                continue
            for name in sorted(files):
                f = os.path.join(d, name)
                try:
                    size = os.path.getsize(f)
                except OSError:
                    continue
                low = name.lower()
                if size == 16384 or size == 4096:
                    with open(f, "rb") as h:
                        md5 = hashlib.md5(h.read()).hexdigest()
                    if md5 == DS_BIOS7_MD5:
                        found.setdefault("DS.BIOS7Path", f)
                    elif md5 == DS_BIOS9_MD5:
                        found.setdefault("DS.BIOS9Path", f)
                elif size in (262144, 524288) and "dsi" not in low:
                    found.setdefault("DS.FirmwarePath", f)
                elif size == 65536:
                    if "arm7" in low or "bios7" in low:
                        found.setdefault("DSi.BIOS7Path", f)
                    elif "arm9" in low or "bios9" in low:
                        found.setdefault("DSi.BIOS9Path", f)
                elif size == 131072:
                    found.setdefault("DSi.FirmwarePath", f)
                elif size >= 200 * 1024 * 1024 and "nand" in low:
                    found.setdefault("DSi.NANDPath", f)
    return found


def melonds_bios(lines):
    found = nds_firmware_files()
    changed = []
    for key, f in found.items():
        table, k = key.split(".", 1)
        if (toml_get(lines, table, k) or '""') == '""':
            toml_set(lines, table, k, toml_str(f))
            changed.append(key)
    for table, flag, keys in (("Emu", "ExternalBIOSEnable", ("DS.BIOS7Path", "DS.BIOS9Path", "DS.FirmwarePath")),
                              ("DSi", "ExternalBIOSEnable", ("DSi.BIOS7Path", "DSi.BIOS9Path", "DSi.FirmwarePath", "DSi.NANDPath"))):
        if all(k in found for k in keys) and any(k in changed for k in keys):
            toml_set(lines, table, flag, "true")
    return changed


def melonds():
    path = os.path.join(CONFIG, "melonDS/melonDS.toml")
    marker = os.path.join(os.path.dirname(path), ".droiddeck-display-2")
    if not os.path.isfile(path):
        seed(path, "[Instance0]\nJoystickID = 0\n\n" + MELONDS_JOY + "\n" + MELONDS_WINDOW +
             f'Geometry = "{melonds_geometry()}"\n', "melonDS",
             "Player 1 on the controller, big top screen")
        open(marker, "w").close()
    with open(path, encoding="utf-8", errors="replace") as f:
        text = f.read()
    try:
        import tomllib
        joy = tomllib.loads(text).get("Instance0", {}).get("Joystick", {})
    except Exception:
        return
    lines = text.splitlines()
    done = []
    if not any(isinstance(v, int) and v != -1 and k != "HK_FullscreenToggle" for k, v in joy.items()):
        out, skipping = [], False
        for line in lines:
            t = line.strip()
            if t.startswith("["):
                skipping = t == "[Instance0.Joystick]"
            if not skipping:
                out.append(line)
        lines = "\n".join(out).rstrip("\n").splitlines() + [""] + MELONDS_JOY.rstrip("\n").splitlines()
        done.append("Player 1 on the controller")
    elif joy.get("HK_FullscreenToggle", -1) == -1 and MELONDS_GUIDE not in joy.values():
        toml_set(lines, "Instance0.Joystick", "HK_FullscreenToggle", str(MELONDS_GUIDE))
        done.append("guide button toggles full screen")
    if not os.path.exists(marker):
        for l in MELONDS_WINDOW.splitlines()[1:]:
            k, v = (x.strip() for x in l.split("=", 1))
            if toml_get(lines, "Instance0.Window0", k) in (None,) + MELONDS_WINDOW_DEFAULTS.get(k, ()):
                toml_set(lines, "Instance0.Window0", k, v)
        toml_set(lines, "Instance0.Window0", "Geometry", f'"{melonds_geometry()}"')
        done.append("big top screen, 1x window")
        open(marker, "w").close()
    bios = melonds_bios(lines)
    if bios:
        done.append("BIOS " + ", ".join(bios))
    if done:
        with open(path + ".tmp", "w", encoding="utf-8") as f:
            f.write("\n".join(lines).rstrip("\n") + "\n")
        os.replace(path + ".tmp", path)
        print(f"droiddeck-pad-defaults: melonDS: {'; '.join(done)} ({path})")


PPSSPP_CONTROLS = """[ControlMapping]
Up = 1-19,10-19
Down = 1-20,10-20
Left = 1-21,10-21
Right = 1-22,10-22
Cross = 1-54,10-189
Circle = 1-52,10-190
Square = 1-29,10-191
Triangle = 1-47,10-188
Start = 1-62,10-197
Select = 1-66,10-196
L = 1-45,10-193
R = 1-51,10-192
An.Up = 1-37,10-4003
An.Down = 1-39,10-4002
An.Left = 1-38,10-4001
An.Right = 1-40,10-4000
RightAn.Up = 10-4007
RightAn.Down = 10-4006
RightAn.Left = 10-4005
RightAn.Right = 10-4004
Pause = 1-111,10-4
"""


def ppsspp():
    seed(os.path.join(CONFIG, "ppsspp/PSP/SYSTEM/controls.ini"), PPSSPP_CONTROLS, "PPSSPP")


RETROARCH_PROFILE = """input_driver = "sdl2"
input_device = "Xbox 360 Controller"
input_vendor_id = "1118"
input_product_id = "654"
input_b_btn = "0"
input_a_btn = "1"
input_y_btn = "2"
input_x_btn = "3"
input_select_btn = "4"
input_menu_toggle_btn = "5"
input_start_btn = "6"
input_l3_btn = "7"
input_r3_btn = "8"
input_l_btn = "9"
input_r_btn = "10"
input_up_btn = "11"
input_down_btn = "12"
input_left_btn = "13"
input_right_btn = "14"
input_l2_axis = "+4"
input_r2_axis = "+5"
input_l_x_plus_axis = "+0"
input_l_x_minus_axis = "-0"
input_l_y_plus_axis = "+1"
input_l_y_minus_axis = "-1"
input_r_x_plus_axis = "+2"
input_r_x_minus_axis = "-2"
input_r_y_plus_axis = "+3"
input_r_y_minus_axis = "-3"
"""


def retroarch():
    d = os.path.join(CONFIG, "retroarch")
    seed(os.path.join(d, "autoconfig/sdl2/Xbox 360 Controller.cfg"), RETROARCH_PROFILE, "RetroArch (profile)")
    cfg = os.path.join(d, "retroarch.cfg")
    if not os.path.isfile(cfg):
        seed(cfg, 'input_joypad_driver = "sdl2"\ninput_autodetect_enable = "true"\n', "RetroArch")
        return
    with open(cfg, encoding="utf-8", errors="replace") as f:
        lines = f.read().splitlines()
    changed = False
    for i, l in enumerate(lines):
        if l.replace(" ", "").startswith('input_joypad_driver="udev"'):
            lines[i] = 'input_joypad_driver = "sdl2"'
            changed = True
    if changed:
        with open(cfg + ".tmp", "w", encoding="utf-8") as f:
            f.write("\n".join(lines) + "\n")
        os.replace(cfg + ".tmp", cfg)
        print(f"droiddeck-pad-defaults: RetroArch: joypad driver udev -> sdl2 ({cfg})")


SWITCH_PAD = "engine:sdl,port:0,guid:030000005e0400008e02000010010000,"
SWITCH_PLAYER1 = {
    "button_a": "button:1", "button_b": "button:0", "button_x": "button:3", "button_y": "button:2",
    "button_lstick": "button:9", "button_rstick": "button:10", "button_l": "button:4", "button_r": "button:5",
    "button_zl": "axis:2,threshold:0.5,invert:+", "button_zr": "axis:5,threshold:0.5,invert:+",
    "button_plus": "button:7", "button_minus": "button:6", "button_home": "button:8",
    "button_dleft": "hat:0,direction:left", "button_dup": "hat:0,direction:up",
    "button_dright": "hat:0,direction:right", "button_ddown": "hat:0,direction:down",
    "lstick": "axis_x:0,axis_y:1,offset_x:0,offset_y:0,invert_x:+,invert_y:+",
    "rstick": "axis_x:3,axis_y:4,offset_x:0,offset_y:0,invert_x:+,invert_y:+",
}


def switch_emulators():
    for name in sorted(os.listdir(CONFIG)) if os.path.isdir(CONFIG) else ():
        cfg = os.path.join(CONFIG, name, "qt-config.ini")
        marker = os.path.join(CONFIG, name, ".droiddeck-pad")
        if os.path.exists(marker) or not os.path.isfile(cfg):
            continue
        with open(cfg, encoding="utf-8", errors="replace") as f:
            lines = f.read().splitlines()
        span = ini_section(lines, "Controls")
        if not span:
            continue
        current = {l.split("=", 1)[0]: l.split("=", 1)[1] for l in lines[span[0]:span[1]] if "=" in l}
        if "engine:keyboard" not in current.get("player_0_button_a", ""):
            continue
        wanted = {"player_0_connected": "true", "player_0_type": "0"}
        for key, binding in SWITCH_PLAYER1.items():
            wanted[f"player_0_{key}"] = f'"{SWITCH_PAD}{binding}"'
        body = [l for l in lines[span[0]:span[1]]
                if l.split("=", 1)[0].removesuffix("\\default") not in wanted]
        while body and not body[-1].strip():
            body.pop()
        for key, value in wanted.items():
            body += [f"{key}\\default=false", f"{key}={value}"]
        lines[span[0]:span[1]] = body + [""]
        with open(cfg + ".tmp", "w", encoding="utf-8") as f:
            f.write("\n".join(lines) + "\n")
        os.replace(cfg + ".tmp", cfg)
        open(marker, "w").close()
        print(f"droiddeck-pad-defaults: {name}: Player 1 on the controller ({cfg})")


for step in (rpcs3, duckstation, pcsx2, armsx2, dolphin, cemu, melonds, ppsspp, retroarch, switch_emulators):
    try:
        step()
    except Exception as e:
        print(f"droiddeck-pad-defaults: {step.__name__}: {e}")
