#!/usr/bin/env python3

import os
import subprocess
import sys
from pathlib import Path


DMENU_ARGS = [
    "dmenu",
    "-i",
    "-fn", "JetBrainsMono Nerd Font-10",
    "-nb", "#121212",
    "-nf", "#e0e0e0",
    "-sb", "#222222",
    "-sf", "#e0e0e0",
    "-l", "10",
    "-p", "Launch:",
]

DESKTOP_DIRS = [
    Path.home() / ".local/share/applications",
    Path.home() / ".local/share/flatpak/exports/share/applications",
    Path("/var/lib/flatpak/exports/share/applications"),
    Path("/usr/share/applications"),
    Path("/usr/local/share/applications"),
]

INVALID_EXEC_PREFIXES = ("wine", "wscript", "rundll32", "winhlp32", "hh ")


def parse_desktop_file(path: Path):
    """
    Returns (name, exec_cmd) or (None, None) if the file should be skipped.
    Handles: NoDisplay=true, Hidden=true, missing Name/Exec, field codes, env wrappers.
    """
    props = {}
    in_desktop_entry = False

    try:
        with path.open(encoding="utf-8", errors="replace") as f:
            for line in f:
                line = line.rstrip("\n")
                if line.startswith("["):
                    in_desktop_entry = line == "[Desktop Entry]"
                    continue
                if not in_desktop_entry or "=" not in line:
                    continue
                key, _, value = line.partition("=")
                key = key.strip()
                if key not in props:
                    props[key] = value.strip()
    except OSError:
        return None, None

    if props.get("NoDisplay", "").lower() == "true":
        return None, None
    if props.get("Hidden", "").lower() == "true":
        return None, None
    if props.get("Type", "") != "Application":
        return None, None

    name = props.get("Name")
    exec_raw = props.get("Exec")

    if not name or not exec_raw:
        return None, None

    exec_cmd = clean_exec(exec_raw)

    if not exec_cmd:
        return None, None

    lower = exec_cmd.lower()
    if any(lower.startswith(p) for p in INVALID_EXEC_PREFIXES):
        return None, None

    terminal = props.get("Terminal", "").lower() == "true"
    if terminal:
        term = os.environ.get("TERMINAL", "xterm")
        exec_cmd = f"{term} -e {exec_cmd}"

    return name, exec_cmd


def clean_exec(exec_raw: str) -> str:
    tokens = exec_raw.split()
    cleaned = []
    for token in tokens:
        if token.startswith("%") and len(token) == 2 and token[1].isalpha():
            continue
        cleaned.append(token)
    return " ".join(cleaned).strip()


def collect_apps():
    seen_stems = {}
    for directory in DESKTOP_DIRS:
        if not directory.is_dir():
            continue
        for desktop_file in sorted(directory.glob("*.desktop")):
            stem = desktop_file.stem.lower()
            if stem not in seen_stems:
                seen_stems[stem] = desktop_file

    apps = {}
    for desktop_file in seen_stems.values():
        name, cmd = parse_desktop_file(desktop_file)
        if name and cmd and name not in apps:
            apps[name] = cmd
    return apps


def run_dmenu(app_names: list[str]) -> str | None:
    names_input = "\n".join(sorted(app_names, key=str.casefold)).encode()
    try:
        result = subprocess.run(
            DMENU_ARGS,
            input=names_input,
            capture_output=True,
        )
    except FileNotFoundError:
        sys.exit("error: dmenu not found in PATH")

    if result.returncode != 0:
        return None

    return result.stdout.decode().strip()


def launch(cmd: str):
    subprocess.Popen(
        cmd,
        shell=True,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        start_new_session=True,
    )


def main():
    apps = collect_apps()

    if not apps:
        sys.exit("error: no applications found")

    selected = run_dmenu(list(apps.keys()))

    if not selected:
        return

    cmd = apps.get(selected)
    if not cmd:
        sys.exit(f"error: no command found for '{selected}'")

    launch(cmd)


if __name__ == "__main__":
    main()
