"""
System volume — the real ALSA mixer level behind the Quick Menu slider and the
Display & Sound screen. One element, surfaced in both.

dArkOS is ALSA-based, so we drive `amixer`. The simple-control name varies by
board (Master / PCM / Playback / Speaker / …), so we SCAN `amixer scontrols`
and use the first known one. amixer runs as the audio-group user — no sudo
(unlike the backlight). Tolerant: no amixer binary / no control → None / False
(dev box / Mac), so callers keep their demo value and nothing is changed.

Lives in gui.library (like backlight) because both the launcher and Control
share that package.
"""

from __future__ import annotations

import re
import subprocess
from typing import Optional

# Priority order of ALSA simple-control names to try (first present wins).
_CANDIDATES = ("Master", "PCM", "Playback", "Speaker", "Headphone", "Digital",
               "DAC", "HP")
_PCT_RE = re.compile(r"\[(\d{1,3})%\]")
_CTRL_RE = re.compile(r"Simple mixer control '([^']+)'")


# ── pure helpers (testable, no device) ────────────────────────────────

def parse_pct(amixer_get_output: str) -> Optional[int]:
    """First [NN%] in `amixer get <ctrl>` output → 0..100, or None."""
    m = _PCT_RE.search(amixer_get_output or "")
    return max(0, min(100, int(m.group(1)))) if m else None


def parse_controls(scontrols_output: str) -> list[str]:
    """`amixer scontrols` → list of simple-control names, in listed order."""
    return _CTRL_RE.findall(scontrols_output or "")


def pick_control(available: list[str]) -> Optional[str]:
    """First known candidate present, else the first control, else None."""
    for name in _CANDIDATES:
        if name in available:
            return name
    return available[0] if available else None


# ── tolerant live layer (device; never raises) ───────────────────────

def _run(*args: str, timeout: float = 5) -> Optional[str]:
    """`amixer <args>` stdout on success, else None (binary missing / error)."""
    try:
        r = subprocess.run(["amixer", *args], capture_output=True, text=True,
                           timeout=timeout)
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return None
    return r.stdout if r.returncode == 0 else None


def control() -> Optional[str]:
    """The mixer control we drive, or None when there's no usable mixer."""
    out = _run("scontrols")
    return pick_control(parse_controls(out)) if out is not None else None


def present() -> bool:
    return control() is not None


def read() -> Optional[int]:
    """Current volume 0..100, or None when there's no mixer."""
    c = control()
    if not c:
        return None
    return parse_pct(_run("get", c) or "")


def set_pct(pct: int) -> bool:
    """Set the volume (0..100). True on success, False otherwise."""
    c = control()
    if not c:
        return False
    try:
        pct_i = max(0, min(100, int(pct)))
    except (TypeError, ValueError):
        return False
    try:
        r = subprocess.run(["amixer", "set", c, f"{pct_i}%"],
                           capture_output=True, text=True, timeout=5)
        return r.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False
