#!/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 = 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"):
    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):
    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):
    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():
    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):
    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):
    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}")
