#!/usr/bin/env python

# created based on implementation from HandyGCCS https://github.com/ShadowBlip/HandyGCCS/blob/10bf0da2bbe06b4e6c608e157f26628b6d848042/src/handycon/utilities.py#L351

import asyncio
import evdev
import os
import signal
import subprocess
import sys
import threading

from time import sleep

POWER_BUTTONS = ["LNXPWRBN/button/input0", "PNP0C0C/button/input0"]
KEYBOARDS = ["AT Translated Set 2 keyboard", "InputPlumber Keyboard"]

monitored_devices = []

user = ""
home_path = ""
longpresstimer = None
loop = None
press_type = None
running = False


# Main daemon loop. Runs init functions and starts the run loop.
def main():
    global loop
    global running

    print("steam-powerbuttond: Starting")

    __init__()
    get_user()

    if monitored_devices != []:
        running = True
        # Run asyncio loop to capture all events.
        loop = asyncio.get_event_loop()

        # Attach the event loop of each device to the asyncio loop.
        for device in monitored_devices:
            print(f"Setting up read loop for device: {device.name}")
            asyncio.ensure_future(monitor_device(device))

        # Establish signaling to handle gracefull shutdown.
        sigs = [signal.SIGHUP, signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]
        for s in sigs:
            loop.add_signal_handler(s, lambda s=s: asyncio.create_task(exit()))
        exit_code = 0
        try:
            print("Starting loop")
            loop.run_forever()
        except KeyboardInterrupt:
            print("Keyboard interrupt.")
            exit_code = 1
        except Exception as err:
            print(f"{err} | Hit exception condition.")
            exit_code = 2
        finally:
            running = False
            loop.stop()
            sys.exit(exit_code)


# Identifies system hardware to monitor.
def __init__():
    print("steam-powerbuttond: Searching for devices")

    devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
    for device in devices:
        # Grab power buttons devices to prevent the events from propegating to
        # the system.
        if device.name == "Power Button" and device.phys in POWER_BUTTONS:
            monitored_devices.append(device)
            device.grab()
            print(f"steam-powerbuttond: Found device: {device.name}")
        # Don't grab keyboards, they should be allowed to send events to the
        # system. We just want to look for the LEFTMETA they send after a few
        # moments of holding the power button.
        elif device.name in KEYBOARDS:
            print(f"steam-powerbuttond: Found device: {device.name}")
            monitored_devices.append(device)
        # Ignore everything else.
        else:
            device.close()

    print(f"steam-powerputtond: Found devices: {monitored_devices}")


# Gets the current (first) user so we can send steam a command from the
# users home directory.
def get_user():
    global user
    global home_path
    command = "who | awk '{print $1}' | sort | head -1"

    while not user:
        users = subprocess.Popen(
            command,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=True,
        )
        for first_user in users.stdout:
            name = first_user.decode().strip()
            if name is not None:
                user = name
                home_path = f"/home/{user}"
            break
        sleep(1)
    print(f"steam-powerbuttond: Found user: {user}")


# Checks monitored devices for output events. Power buttons provide KEY_POWER
# events and some keyboards produce KEY_LEFTMETA when power is held for more
# than 1 second.
async def monitor_device(device):
    global longpresstimer
    global press_type
    global running

    print(f"steam-powerbuttond: Starting to monitor device: {device.name}")

    while running:
        async for event in device.async_read_loop():
            # Power Buttons
            if (
                event.type == evdev.ecodes.EV_KEY
                and event.code == evdev.ecodes.KEY_POWER
            ):
                if event.value >= 1:
                    print("Detected power event, short press action queued")
                    if not press_type:
                        press_type = "short"
                        longpresstimer = threading.Timer(1.0, longpress_deck)
                        longpresstimer.start()

                else:
                    if longpresstimer is not None:
                        longpresstimer.cancel()
                        longpresstimer = None
                    if press_type:
                        press()
            # Keyboard Devices
            if (
                event.type == evdev.ecodes.EV_KEY
                and event.code == evdev.ecodes.KEY_LEFTMETA
            ):
                if event.value >= 1:
                    print("Detected left meta, long press action queued.")
                    press_type = "long"
                else:
                    press_type = None
                    print("No power event while left meta pressed, action cleared.")

        print("steam-powerbuttond: Can't find device for power button!")


# The Steam deck (and perhaps a few other devices) dont use the
# KEY_LEFTMETA method with an on-release type power button, instead
# they have a momentary power button that emits a 1 when pressed
# and a 0 when released. This timer callback supports that method.
def longpress_deck():
    global longpresstimer
    global press_type

    longpresstimer = None
    press_type = "long"
    press()


# Determines if steam is running in game mode or not, used to
# support legacy modes of suspend in desktop or other sessions.
def is_game_mode():
    global home_path
    # Get the currently running Steam PID.
    steampid_path = home_path + "/.steam/steam.pid"
    pid = None
    print("steam-powerbuttond: Checking if user is in game mode.")
    try:
        with open(steampid_path) as f:
            pid = f.read().strip()
    except Exception as e:
        print(f"steam-powerbuttond: failed to get steam PID: {e}")
        return False

    steam_cmd_path = f"/proc/{pid}/cmdline"
    if not os.path.exists(steam_cmd_path):
        # Steam not running.
        print("steam-powerbuttond: Steam is not running.")
        return False

    try:
        with open(steam_cmd_path, "rb") as f:
            steam_cmd = f.read()
    except Exception as e:
        print(f"steam-powerbuttond: failed to get steam cmdline: {e}")
        return False

    # Use this and line to determine if Steam is running in DeckUI mode.
    is_deck_ui = b"-gamepadui" in steam_cmd
    if not is_deck_ui:
        print("steam-powerbuttond: Game mode not detected.")
        return False
    print("steam-powerbuttond: Game mode was detected.")
    return True


# Runs the given steam command as determined by long/short press logic.
def run_steam_command(command):
    global home_path
    global user

    print(f"steam-powerbuttond: Running steam command {command}.")
    steam_path = home_path + "/.steam/root/ubuntu12_32/steam"
    try:
        result = subprocess.run(
            ["su", user, "-c", f"{steam_path} -ifrunning {command}"]
        )
        return result.returncode == 0
    except Exception:
        print("steam-powerbuttond: failed to run steam command: {e}")
        return False


# Determines which suspend/shutdown method to use based on long/short press.
def press():
    global press_type
    print(f"steam-powerbuttond: Detected {press_type} press.")
    if press_type == "long":
        if is_game_mode():
            run_steam_command("steam://longpowerpress")
        else:
            os.system("systemctl shutdown")
    elif press_type == "short":
        if is_game_mode():
            run_steam_command("steam://shortpowerpress")
        else:
            os.system("systemctl suspend")
    press_type = None


# Gracefull shutdown.
async def exit():
    global loop
    global running

    print("Receved exit signal. Restoring devices.")

    running = False

    for device in monitored_devices:
        try:
            device.ungrab()
            print(f"{device.name} was released.")
        except OSError:
            print(f"{device.name} was not grabbed.")
        device.close()
    print("Devices restored.")

    # Kill all tasks. They are infinite loops so we will wait forver.
    for task in [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]:
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            print(f"Failed to stop task {task}")
    if loop is not None:
        loop.stop()


if __name__ == "__main__":
    main()
