"""
wivexx_activity.py — what is the user DOING right now?

Reads the FOREGROUND window (process name + title) and, optionally, which app is
producing audio, then classifies into a coarse activity so the mascot can wear a
fitting accessory:

    "music"    →  headphones   (Spotify, YouTube Music, SoundCloud, a music app…)
    "gaming"   →  controller   (Minecraft, GTA, Valorant, … or any fullscreen game)
    "video"    →  popcorn       (YouTube, Netflix, Crunchyroll, Twitch, VLC, …)
    "coding"   →  glasses       (VS Code, PyCharm, terminals, …)
    "call"     → ️ headset       (Discord, Zoom, Teams, Google Meet, Skype, …)
    "design"   →  paintbrush    (Photoshop, Lightroom, Premiere, Blender, Figma, …)
    "shopping" → ️ bag           (Amazon, eBay, AliExpress, Etsy, … in the browser)
    ""         → nothing

Windows-only, cheap (a couple of Win32 calls), best-effort. The keyword lists are
easy to extend — that's the whole point.
"""

import ctypes
import os
from ctypes import wintypes

_logger = None


def set_logger(logger):
    global _logger
    _logger = logger


# ── app / title keyword maps (all lowercase) ──────────────────────────────────
_BROWSERS = {
"chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "opera.exe",
"opera_gx.exe", "vivaldi.exe", "arc.exe", "zen.exe", "librewolf.exe",
}

_MUSIC_PROCS = {
"spotify.exe", "music.exe", "applemusic.exe", "itunes.exe", "tidal.exe",
"deezer.exe", "foobar2000.exe", "aimp.exe", "amazonmusic.exe", "amazon music.exe",
"musicbee.exe", "qobuz.exe", "winamp.exe", "youtube music.exe",
}
_MUSIC_TITLES = (
"youtube music", "soundcloud", "spotify", "apple music", "tidal", "deezer",
"amazon music", "bandcamp", "now playing", "audiomack",
)

_VIDEO_PROCS = {
"vlc.exe", "mpv.exe", "mpc-hc.exe", "mpc-hc64.exe", "mpc-be64.exe",
"wmplayer.exe", "potplayer.exe", "potplayermini64.exe", "netflix.exe",
"kmplayer.exe", "smplayer.exe",
}
_VIDEO_TITLES = (
"youtube", "netflix", "crunchyroll", "twitch", "disney+", "disneyplus",
"hbo max", "prime video", "hulu", "vimeo", "funimation", "peacock",
"paramount+", "apple tv", "plex", "jellyfin", "dailymotion", "- vlc",
"rumble", "kick", "hidive", "tubi",
)

_CODING_PROCS = {
"code.exe", "code - insiders.exe", "cursor.exe", "pycharm64.exe", "pycharm.exe",
"idea64.exe", "webstorm64.exe", "clion64.exe", "rider64.exe", "goland64.exe",
"sublime_text.exe", "devenv.exe", "windowsterminal.exe", "powershell.exe",
"pwsh.exe", "cmd.exe", "wt.exe", "neovim.exe", "nvim.exe", "gvim.exe",
"atom.exe", "notepad++.exe",
}

# creative / editing apps →  paintbrush
_DESIGN_PROCS = {
"photoshop.exe", "lightroom.exe", "adobe lightroom.exe", "illustrator.exe",
"indesign.exe", "premiere.exe", "adobe premiere pro.exe", "afterfx.exe",
"animate.exe", "audition.exe", "blender.exe", "gimp.exe", "gimp-2.10.exe",
"krita.exe", "inkscape.exe", "figma.exe", "affinity photo.exe",
"affinity designer.exe", "affinity publisher.exe", "designer.exe",
"photo.exe", "publisher.exe", "capture one.exe", "captureone.exe",
"coreldrw.exe", "clipstudiopaint.exe", "csp.exe", "paint.net.exe", "pdn.exe",
"mspaint.exe", "resolve.exe", "vegas.exe", "vegas_pro.exe", "vegaspro.exe",
"sai2.exe", "sai.exe", "medibangpaintpro.exe", "rawtherapee.exe",
"darktable.exe", "cinema 4d.exe", "cinema4d.exe", "maya.exe", "3dsmax.exe",
}
# browser-based design tools (matched on tab title only)
_DESIGN_WEB = ("figma", "canva", "photopea", "pixlr", "framer")

# live calls / meetings → ️ headset
_CALL_PROCS = {
"discord.exe", "discordcanary.exe", "discordptb.exe", "zoom.exe",
"teams.exe", "ms-teams.exe", "msteams.exe", "skype.exe", "webex.exe",
"webexmta.exe", "ciscowebexstart.exe", "gotomeeting.exe", "bluejeans.exe",
"ringcentral.exe",
}
_CALL_TITLES = (
"google meet", "zoom meeting", "- zoom", "microsoft teams call",
"whereby", "jitsi meet", "is presenting", "is sharing your screen",
"in a call", "voice connected",
)

# shopping sites → ️ bag (browser tab titles)
_SHOP_TITLES = (
"amazon", "ebay", "aliexpress", "alibaba", "etsy", "mercado libre",
"mercadolibre", "walmart", "best buy", "shein", "temu", "wish.com",
"newegg", "asos", "shopify", "wallapop", "depop", "rakuten", "zalando",
"shopping cart", "add to cart", "your cart", "checkout", "carrito",
)

# A solid list of popular game executables — extend freely.
_GAME_PROCS = {
"javaw.exe", "minecraft.exe", "minecraftlauncher.exe", "minecraft.windows.exe",
"gta5.exe", "gtav.exe", "gta-v.exe", "rdr2.exe", "valorant.exe",
"valorant-win64-shipping.exe", "leagueclient.exe", "league of legends.exe",
"csgo.exe", "cs2.exe", "fortniteclient-win64-shipping.exe", "r5apex.exe",
"overwatch.exe", "eldenring.exe", "destiny2.exe", "rocketleague.exe",
"dota2.exe", "wow.exe", "factorio.exe", "terraria.exe", "stardewvalley.exe",
"stardew valley.exe", "hades.exe", "hades2.exe", "celeste.exe",
"amongus.exe", "fallguys_client_game.exe", "roblox.exe", "robloxplayerbeta.exe",
"genshinimpact.exe", "yuanshen.exe", "deadbydaylight-win64-shipping.exe",
"phasmophobia.exe", "palworld-win64-shipping.exe", "helldivers2.exe",
"cyberpunk2077.exe", "witcher3.exe", "starfield.exe", "bg3.exe",
"bg3_dx11.exe", "ets2.exe", "marvel-win64-shipping.exe", "thefinals.exe",
"deltaforce.exe", "apex_legends.exe", "warframe.x64.exe", "rust.exe",
"satisfactory.exe", "valheim.exe", "fc24.exe", "fc25.exe", "nba2k24.exe",
"forzahorizon5.exe", "seaofthieves.exe", "pubg.exe", "tslgame.exe",
"isaac-ng.exe", "noita.exe", "vampire survivors.exe", "balatro.exe",
}
_GAME_TITLE_HINTS = (
"minecraft", "grand theft auto", "valorant", "league of legends",
"counter-strike", "fortnite", "elden ring", "roblox", "genshin",
"cyberpunk", "baldur's gate", "the witcher", "apex legends", "rocket league",
"stardew valley", "terraria", "overwatch", "call of duty",
)
# things that are fullscreen but NOT games
_FULLSCREEN_EXCLUDE = {
"explorer.exe", "searchhost.exe", "shellexperiencehost.exe",
"applicationframehost.exe", "powerpnt.exe", "wivexx.exe", "pythonw.exe",
"python.exe", "lockapp.exe", "", "dwm.exe",
}


def _proc_name(pid: int) -> str:
    """Executable file name for a PID via Win32 (no psutil dependency)."""
    if not pid:
        return ""
    try:
        k = ctypes.windll.kernel32
        PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
        h = k.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
        if not h:
            return ""
        try:
            buf = ctypes.create_unicode_buffer(520)
            size = wintypes.DWORD(520)
            if k.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
                return os.path.basename(buf.value or "")
        finally:
            k.CloseHandle(h)
    except Exception:
        pass
    return ""


def _foreground_info():
    """(proc_name_lower, title_lower, hwnd) for the focused window."""
    try:
        u = ctypes.windll.user32
        hwnd = u.GetForegroundWindow()
        if not hwnd:
            return ("", "", 0)
        n = u.GetWindowTextLengthW(hwnd)
        buf = ctypes.create_unicode_buffer(n + 2)
        u.GetWindowTextW(hwnd, buf, n + 2)
        title = (buf.value or "").lower()
        pid = wintypes.DWORD()
        u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        return (_proc_name(pid.value).lower(), title, hwnd)
    except Exception:
        return ("", "", 0)


def is_desktop_foreground() -> bool:
    """True when the Windows DESKTOP itself is focused (you clicked the wallpaper /
    an empty desktop spot) rather than an app window — i.e. the foreground window's
    class is Progman or WorkerW. Used to "hide the overlay orb into the wallpaper."""
    try:
        u = ctypes.windll.user32
        hwnd = u.GetForegroundWindow()
        if not hwnd:
            return False
        buf = ctypes.create_unicode_buffer(64)
        u.GetClassNameW(hwnd, buf, 64)
        return (buf.value or "") in ("Progman", "WorkerW")
    except Exception:
        return False


def _is_true_fullscreen(hwnd) -> bool:
    """True only for a window that covers the WHOLE monitor (incl. the taskbar) —
    i.e. exclusive/borderless fullscreen, NOT just a maximized window."""
    try:
        u = ctypes.windll.user32
        rect = wintypes.RECT()
        u.GetWindowRect(hwnd, ctypes.byref(rect))
        w, h = rect.right - rect.left, rect.bottom - rect.top
        sw, sh = u.GetSystemMetrics(0), u.GetSystemMetrics(1)
        return (w >= sw - 1 and h >= sh - 1 and rect.left <= 0 and rect.top <= 0)
    except Exception:
        return False


def _foreground_activity(audio_playing: bool = False, music_audio: bool = False) -> str:
    """The single best label for the FOREGROUND window (process + title + the
    fullscreen heuristic). `music_audio` = a known music app is the one producing
    sound, so background Spotify still earns headphones."""
    proc, title, hwnd = _foreground_info()
    if not proc and not title:
        return ""

    # 1) live calls / meetings (Discord, Zoom, Teams, Meet…) — you're talking to people
    if proc in _CALL_PROCS or (proc in _BROWSERS and any(c in title for c in _CALL_TITLES)):
        return "call"
    # 2) explicit games (by process or window title)
    if proc in _GAME_PROCS or any(g in title for g in _GAME_TITLE_HINTS):
        return "gaming"
    # 3) creative / editing apps (Photoshop, Premiere, Blender, Figma desktop…)
    if proc in _DESIGN_PROCS:
        return "design"
    # 4) video players / streaming sites
    if proc in _VIDEO_PROCS:
        return "video"
    if proc in _BROWSERS and "youtube music" not in title and any(v in title for v in _VIDEO_TITLES):
        return "video"
    # 5) music apps / sites
    if proc in _MUSIC_PROCS:
        return "music"
    if proc in _BROWSERS and any(m in title for m in _MUSIC_TITLES):
        return "music"
    # 6) browser-based design tools (Figma, Canva, Photopea…)
    if proc in _BROWSERS and any(d in title for d in _DESIGN_WEB):
        return "design"
    # 7) shopping sites (Amazon, eBay, AliExpress… — after video/music so
    # "Amazon Music"/"Amazon Prime Video" don't get mis-tagged)
    if proc in _BROWSERS and any(s in title for s in _SHOP_TITLES):
        return "shopping"
    # 8) coding / terminals
    if proc in _CODING_PROCS:
        return "coding"
    # 9) an unknown app running TRUE fullscreen → almost certainly a game
    if (proc not in _BROWSERS and proc not in _FULLSCREEN_EXCLUDE
            and "wivexx" not in title and hwnd and _is_true_fullscreen(hwnd)):
        return "gaming"
    # 10) music playing in the background (e.g. Spotify minimized) → headphones
    if music_audio:
        return "music"
    return ""


def detect_activities(audio_playing: bool = False, music_audio: bool = False,
                      call_audio: bool = False) -> list:
    """Everything the mascot should be wearing RIGHT NOW, so outfits can layer by
    body slot (e.g. gaming + a Discord call → controller + headset). The foreground
    window gives ONE primary task; `call` and `music` are AMBIENT layers detected
    from the audio sessions, so they stack on top of whatever's in front. Returns
    an ordered list (primary first); the renderer resolves any same-slot clashes."""
    acts = []
    fg = _foreground_activity(audio_playing, music_audio)
    if fg:
        acts.append(fg)
    if call_audio and "call" not in acts:        # a live call going in the background
        acts.append("call")
    if music_audio and "music" not in acts:      # music playing under everything else
        acts.append("music")
    return acts


def detect_activity(audio_playing: bool = False, music_audio: bool = False) -> str:
    """Back-compat: the single PRIMARY activity (first of detect_activities)."""
    acts = detect_activities(audio_playing, music_audio)
    return acts[0] if acts else ""


def music_app_name(name: str) -> bool:
    """Is this audio-session process name a known music app?"""
    return (name or "").lower() in _MUSIC_PROCS


def call_app_name(name: str) -> bool:
    """Is this audio-session process a known live-call app (Discord/Zoom/Teams…)?"""
    return (name or "").lower() in _CALL_PROCS
