#!/usr/bin/env python3
import os
import re
import shlex
import subprocess
from typing import Any, Dict

import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib


APP_DIR = os.path.dirname(os.path.abspath(__file__))
BUS_NAME = "org.mpris.MediaPlayer2.noism"
OBJECT_PATH = "/org/mpris/MediaPlayer2"
MEDIAKEY = os.path.join(APP_DIR, "noism-mediakey")
RUNTIME_ROOT = os.path.join(os.environ.get("XDG_RUNTIME_DIR", os.path.expanduser("~/.cache")), "noism")
STATE_FILE = os.path.join(RUNTIME_ROOT, "state")


def decode_shell_value(raw: str) -> str:
    if not raw:
        return ""
    parts = shlex.split(raw, posix=True)
    return parts[0] if parts else ""


def read_state() -> Dict[str, str]:
    state: Dict[str, str] = {}
    if not os.path.exists(STATE_FILE):
        return state

    with open(STATE_FILE, "r", encoding="utf-8") as handle:
        for line in handle:
            line = line.rstrip("\n")
            if "=" not in line:
                continue
            key, raw = line.split("=", 1)
            state[key] = decode_shell_value(raw)

    if state.get("NOISM_APP_DIR") != APP_DIR:
        return {}

    return state


def pid_running(pid: str) -> bool:
    return pid.isdigit() and os.path.exists(f"/proc/{pid}")


def player_active(state: Dict[str, str]) -> bool:
    shell_pid = state.get("NOISM_SHELL_PID", "")
    playback = state.get("NOISM_PLAY_STATE", "")
    if playback not in {"playing", "paused"}:
        return False
    return pid_running(shell_pid)


class NoismMpris(dbus.service.Object):
    def __init__(self, bus: dbus.SessionBus) -> None:
        self._bus_name = dbus.service.BusName(BUS_NAME, bus=bus, allow_replacement=True, replace_existing=True)
        super().__init__(self._bus_name, OBJECT_PATH)
        self._last_root: Dict[str, Any] = {}
        self._last_player: Dict[str, Any] = {}

    def _run_media_key(self, action: str) -> None:
        subprocess.run([MEDIAKEY, action], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    def _root_properties(self) -> Dict[str, Any]:
        return {
            "CanQuit": dbus.Boolean(False),
            "CanRaise": dbus.Boolean(False),
            "HasTrackList": dbus.Boolean(False),
            "Identity": dbus.String("Noism"),
            "DesktopEntry": dbus.String("noism"),
            "SupportedUriSchemes": dbus.Array([], signature="s"),
            "SupportedMimeTypes": dbus.Array([], signature="s"),
        }

    def _track_id_for_slug(self, slug: str) -> dbus.ObjectPath:
        safe = re.sub(r"[^A-Za-z0-9_]", "_", slug or "noism")
        return dbus.ObjectPath(f"/org/mpris/MediaPlayer2/track/{safe}")

    def _category_for_slug(self, slug: str) -> str:
        norm = slug.replace("-", "_")
        if "noise" in norm or "hiss" in norm:
            return "Noise"
        if "sea" in norm or "wave" in norm or "ocean" in norm:
            return "Ocean"
        if "wind" in norm or "breeze" in norm:
            return "Wind"
        if "rain" in norm or "drop" in norm or "splash" in norm or "umbrella" in norm or "storm" in norm:
            return "Rain"
        return "Other"

    def _label_for_slug(self, slug: str) -> str:
        words = slug.replace("-", " ").split()
        return " ".join(word.capitalize() for word in words) if words else "Noism"

    def _player_properties(self) -> Dict[str, Any]:
        state = read_state()
        active = player_active(state)
        playback = state.get("NOISM_PLAY_STATE", "")
        if playback == "playing":
            playback_status = "Playing"
        elif playback == "paused":
            playback_status = "Paused"
        else:
            playback_status = "Stopped"

        slug = state.get("NOISM_ACTIVE_TRACK", "noism")
        title = self._label_for_slug(slug)
        album = self._category_for_slug(slug) if slug != "noism" else "Noism"

        metadata = {
            "mpris:trackid": self._track_id_for_slug(slug),
            "xesam:title": dbus.String(title),
            "xesam:album": dbus.String(album),
            "xesam:artist": dbus.Array([dbus.String("Noism")], signature="s"),
        }

        return {
            "PlaybackStatus": dbus.String(playback_status),
            "LoopStatus": dbus.String("None"),
            "Rate": dbus.Double(1.0),
            "Shuffle": dbus.Boolean(False),
            "Metadata": dbus.Dictionary(metadata, signature="sv"),
            "Volume": dbus.Double(1.0),
            "Position": dbus.Int64(0),
            "MinimumRate": dbus.Double(1.0),
            "MaximumRate": dbus.Double(1.0),
            "CanGoNext": dbus.Boolean(False),
            "CanGoPrevious": dbus.Boolean(False),
            "CanPlay": dbus.Boolean(True),
            "CanPause": dbus.Boolean(active),
            "CanSeek": dbus.Boolean(False),
            "CanControl": dbus.Boolean(True),
        }

    def _emit_if_changed(self) -> bool:
        if not player_active(read_state()):
            self._loop.quit()
            return False

        root = self._root_properties()
        player = self._player_properties()
        if root != self._last_root:
            self.PropertiesChanged("org.mpris.MediaPlayer2", root, [])
            self._last_root = root
        if player != self._last_player:
            self.PropertiesChanged("org.mpris.MediaPlayer2.Player", player, [])
            self._last_player = player
        return True

    @dbus.service.method("org.mpris.MediaPlayer2", in_signature="", out_signature="")
    def Raise(self) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2", in_signature="", out_signature="")
    def Quit(self) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="", out_signature="")
    def Next(self) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="", out_signature="")
    def Previous(self) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="", out_signature="")
    def Pause(self) -> None:
        self._run_media_key("pause")

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="", out_signature="")
    def PlayPause(self) -> None:
        self._run_media_key("play-pause")

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="", out_signature="")
    def Stop(self) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="", out_signature="")
    def Play(self) -> None:
        state = read_state()
        if player_active(state) and state.get("NOISM_PLAY_STATE", "") == "playing":
            self._run_media_key("play-pause")
            return None
        self._run_media_key("play")

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="x", out_signature="")
    def Seek(self, _offset: int) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="ox", out_signature="")
    def SetPosition(self, _track_id: str, _position: int) -> None:
        return None

    @dbus.service.method("org.mpris.MediaPlayer2.Player", in_signature="s", out_signature="")
    def OpenUri(self, _uri: str) -> None:
        return None

    @dbus.service.method("org.freedesktop.DBus.Properties", in_signature="ss", out_signature="v")
    def Get(self, interface_name: str, property_name: str) -> Any:
        props = self._get_all(interface_name)
        if property_name not in props:
            raise dbus.exceptions.DBusException(
                "org.freedesktop.DBus.Error.InvalidArgs", f"Unknown property {property_name}"
            )
        return props[property_name]

    @dbus.service.method("org.freedesktop.DBus.Properties", in_signature="s", out_signature="a{sv}")
    def GetAll(self, interface_name: str) -> Dict[str, Any]:
        return dbus.Dictionary(self._get_all(interface_name), signature="sv")

    @dbus.service.method("org.freedesktop.DBus.Properties", in_signature="ssv", out_signature="")
    def Set(self, interface_name: str, property_name: str, _value: Any) -> None:
        if interface_name == "org.mpris.MediaPlayer2.Player" and property_name == "Volume":
            return None
        raise dbus.exceptions.DBusException(
            "org.freedesktop.DBus.Error.PropertyReadOnly", f"{interface_name}.{property_name} is read-only"
        )

    def _get_all(self, interface_name: str) -> Dict[str, Any]:
        if interface_name == "org.mpris.MediaPlayer2":
            return self._root_properties()
        if interface_name == "org.mpris.MediaPlayer2.Player":
            return self._player_properties()
        raise dbus.exceptions.DBusException(
            "org.freedesktop.DBus.Error.InvalidArgs", f"Unknown interface {interface_name}"
        )

    @dbus.service.signal("org.freedesktop.DBus.Properties", signature="sa{sv}as")
    def PropertiesChanged(self, interface_name: str, changed_properties: Dict[str, Any], invalidated: Any) -> None:
        return None


def main() -> None:
    DBusGMainLoop(set_as_default=True)
    if not player_active(read_state()):
        return

    bus = dbus.SessionBus()
    loop = GLib.MainLoop()
    service = NoismMpris(bus)
    service._loop = loop
    service._emit_if_changed()
    GLib.timeout_add_seconds(1, service._emit_if_changed)
    loop.run()


if __name__ == "__main__":
    main()
