#!/usr/bin/env python3
"""
NexusOS network status icons generator.

Generates flat symbolic-style SVGs for nm-tray and any other Qt/GTK app
that requests network-* status icons. Output goes to
scalable/status/ of the NexusOS-icons theme.

Design language:
  * 16×16 viewBox, scalable SVG (no rasterization needed — Qt5 + GTK3
    both handle SVG fine in scalable/ dirs)
  * Light off-white stroke for base shape (wifi fan, ethernet plug,
    globe) — readable on the dark NexusOS surfaces
  * Lime brand accent for connected / active states
  * Purple lock for encryption
  * Red for error, orange for acquiring / no-route

Run:    python3 build_status.py
"""

from pathlib import Path
import math

ROOT = Path(__file__).resolve().parent
OUT = ROOT.parent / "NexusOS-icons" / "scalable" / "status"

# Palette — mirrors gtk-3.0/colors.css.
BASE  = "#f2f2f2"   # text_primary
MUTED = "#6e7173"   # text_disabled
GOOD  = "#8cc63f"   # brand_green
LOCK  = "#f2f2f2"   # text_primary (white) — neutral attribute indicator
                    # while green is reserved for active signal strength.
ERROR = "#da4453"   # error_color
WARN  = "#f67400"   # warning_color

SW = 1.6
# Wifi fan arc geometry: half-angle from vertical. Smaller half-angle ⇒
# narrower sweep but allows bigger radius (taller fan) before endpoints
# run off the sides at 16x16. 40° (80° total sweep) is a sweet spot.
ARC_HALF_ANGLE = 40
S_ARC = math.sin(math.radians(ARC_HALF_ANGLE))   # x offset coefficient
C_ARC = math.cos(math.radians(ARC_HALF_ANGLE))   # y offset coefficient


def svg(body: str) -> str:
    return (
        '<svg xmlns="http://www.w3.org/2000/svg" '
        'viewBox="0 0 16 16" width="16" height="16">\n'
        + body.rstrip()
        + "\n</svg>\n"
    )


# ── wifi fan ────────────────────────────────────────────────────────
# Fan sits low in the viewBox with arcs reaching almost the top edge so
# the rendered icon matches the height of neighboring tray icons (shield,
# bluetooth, etc.). Outermost r=11.5 with the narrower 80° arc sweep
# keeps endpoints inside x=[0.61, 15.39] — fits with stroke-cap margin.
WIFI_CX, WIFI_CY = 8, 13.0
WIFI_R = [3.0, 5.5, 8.5, 11.5]
DOT_R = 1.3


def _arc(r: float, color: str, opacity: float = 1.0) -> str:
    x0 = WIFI_CX - r * S_ARC
    x1 = WIFI_CX + r * S_ARC
    y  = WIFI_CY - r * C_ARC
    return (
        f'  <path d="M {x0:.2f} {y:.2f} A {r} {r} 0 0 1 {x1:.2f} {y:.2f}" '
        f'fill="none" stroke="{color}" stroke-width="{SW}" '
        f'stroke-linecap="round" opacity="{opacity}"/>'
    )


def wifi_fan(level: int, active_color: str = GOOD) -> str:
    """level 0 (none) .. 4 (excellent). Filled arcs use brand_green by
    default so signal strength reads as the primary visual element."""
    parts = []
    for i, r in enumerate(WIFI_R):
        if i < level:
            parts.append(_arc(r, active_color, 1.0))
        else:
            parts.append(_arc(r, MUTED, 0.35))
    if level > 0:
        parts.append(
            f'  <circle cx="{WIFI_CX}" cy="{WIFI_CY}" r="{DOT_R}" '
            f'fill="{active_color}"/>'
        )
    else:
        parts.append(
            f'  <circle cx="{WIFI_CX}" cy="{WIFI_CY}" r="{DOT_R}" '
            f'fill="{MUTED}" opacity="0.45"/>'
        )
    return "\n".join(parts)


# ── ethernet plug (RJ45) ────────────────────────────────────────────
# Sized to fill ~75% of the viewBox vertically (y=2→14) and horizontally
# (x=2→14) so the rendered icon matches the height of neighboring tray
# icons (shield, notification, bluetooth) instead of looking like a tiny
# crate in the middle of the slot.
def ethernet_plug(color: str = GOOD, opacity: float = 1.0) -> str:
    return (
        f'  <g stroke="{color}" stroke-width="{SW}" stroke-linecap="round" '
        f'stroke-linejoin="round" fill="none" opacity="{opacity}">\n'
        # Pentagonal RJ45 outline
        '    <path d="M 2 2 L 14 2 L 14 10.5 L 12 14 L 4 14 L 2 10.5 Z"/>\n'
        # Three pins, spread wider to match the bigger body
        '    <line x1="5"  y1="4.5" x2="5"  y2="8.5"/>\n'
        '    <line x1="8"  y1="4.5" x2="8"  y2="8.5"/>\n'
        '    <line x1="11" y1="4.5" x2="11" y2="8.5"/>\n'
        "  </g>"
    )


# ── globe (generic network) ─────────────────────────────────────────
def globe(color: str = BASE, opacity: float = 1.0) -> str:
    return (
        f'  <g stroke="{color}" stroke-width="{SW}" stroke-linecap="round" '
        f'stroke-linejoin="round" fill="none" opacity="{opacity}">\n'
        '    <circle cx="8" cy="8" r="5.5"/>\n'
        '    <ellipse cx="8" cy="8" rx="2.5" ry="5.5"/>\n'
        '    <line x1="2.5" y1="8" x2="13.5" y2="8"/>\n'
        "  </g>"
    )


# ── key (vpn) ───────────────────────────────────────────────────────
def vpn_key(color: str = GOOD, opacity: float = 1.0) -> str:
    """Horizontal key: round bow on the left, shaft to the right with
    two teeth hanging down. Stroke-only so it doesn't visually dominate
    the way the filled globe did."""
    return (
        f'  <g stroke="{color}" stroke-width="1.7" stroke-linecap="round" '
        f'stroke-linejoin="round" fill="none" opacity="{opacity}">\n'
        # Bow (handle) — outline circle on the left
        '    <circle cx="4.6" cy="7" r="2.7"/>\n'
        # Shaft
        '    <path d="M 7.3 7 H 14"/>\n'
        # Two teeth pointing down
        '    <path d="M 11.7 7 V 9.6 M 13.6 7 V 8.7"/>\n'
        "  </g>"
    )


# ── state overlays / badges ─────────────────────────────────────────
# nm-tray's MultiIconDelegate stacks state icons on top of the signal-
# level base, so each overlay icon is *just the badge* — no wifi fan,
# no globe. That way stacking signal-good + encrypted gives fan + lock
# instead of fan + fan + lock, and adding -disconnected on top of an
# unsaved network adds a single X instead of another fan.

def standalone_lock(color: str = LOCK) -> str:
    """Centered padlock filling most of the 16x16 box. This is the icon
    nm-tray actually loads for encryption — it requests `security-high`
    / `security-high-symbolic` (NOT `network-wireless-encrypted`) and
    overlays it on top of a `network-wireless-signal-*` base via its
    MultiIconDelegate, scaling our full-size lock into a corner badge.

    Keyhole detail intentionally omitted: at the corner-badge render
    size nm-tray uses, it's invisible anyway, and a dark keyhole on a
    white body reads as visual noise rather than as a lock detail."""
    return (
        # Body: 8.5w x 6h, centered horizontally, sitting low
        f'  <rect x="3.75" y="7.5" width="8.5" height="6.5" rx="1.0" '
        f'fill="{color}"/>\n'
        # Shackle: pronounced U above the body
        f'  <path d="M 5.5 7.5 V 4.5 a 2.5 2.5 0 0 1 5 0 V 7.5" '
        f'fill="none" stroke="{color}" stroke-width="1.8" '
        f'stroke-linecap="round" stroke-linejoin="round"/>'
    )


def badge_lock() -> str:
    """Lock badge: body 4.4w × 3.8h + shackle 3w × 2h on top, lower-right
    corner. Made deliberately larger and with a thicker shackle stroke
    than a typical badge — at 16-24px render, a small thin-stroke lock
    blurs into a shield-like rounded shape. The big-shackle proportions
    here keep the lock recognizable down to ~18px icon size."""
    return (
        '  <g transform="translate(9.5 9.0)">\n'
        f'    <rect x="0" y="3.0" width="4.4" height="3.8" rx="0.45" '
        f'fill="{LOCK}"/>\n'
        f'    <path d="M 0.7 3.0 V 1.1 a 1.5 1.5 0 0 1 3.0 0 V 3.0" '
        f'fill="none" stroke="{LOCK}" stroke-width="1.15" '
        f'stroke-linecap="round" stroke-linejoin="round"/>\n'
        "  </g>"
    )


def badge_x(color: str = ERROR) -> str:
    return (
        f'  <g stroke="{color}" stroke-width="1.4" stroke-linecap="round" '
        'transform="translate(10 10)">\n'
        '    <line x1="0" y1="0" x2="4.5" y2="4.5"/>\n'
        '    <line x1="4.5" y1="0" x2="0" y2="4.5"/>\n'
        "  </g>"
    )


def badge_dots(color: str = WARN) -> str:
    return (
        f'  <g fill="{color}">\n'
        '    <circle cx="10.5" cy="13" r="0.9"/>\n'
        '    <circle cx="13"   cy="13" r="0.9"/>\n'
        '    <circle cx="15.5" cy="13" r="0.9"/>\n'
        "  </g>"
    )


def badge_excl(color: str = ERROR) -> str:
    return (
        f'  <g fill="{color}" stroke="none">\n'
        '    <rect x="11.4" y="9" width="1.4" height="3" rx="0.5"/>\n'
        '    <circle cx="12.1" cy="13.5" r="0.85"/>\n'
        "  </g>"
    )


def badge_slash(color: str = MUTED) -> str:
    return (
        f'  <line x1="2.5" y1="13.5" x2="13.5" y2="2.5" '
        f'stroke="{color}" stroke-width="1.6" stroke-linecap="round"/>'
    )


def badge_arrow_up(color: str = GOOD) -> str:
    return (
        f'  <path d="M 12.5 14 V 9 M 10.5 11 L 12.5 9 L 14.5 11" '
        f'fill="none" stroke="{color}" stroke-width="1.4" '
        'stroke-linecap="round" stroke-linejoin="round"/>'
    )


def badge_arrow_down(color: str = GOOD) -> str:
    return (
        f'  <path d="M 3.5 9 V 14 M 1.5 12 L 3.5 14 L 5.5 12" '
        f'fill="none" stroke="{color}" stroke-width="1.4" '
        'stroke-linecap="round" stroke-linejoin="round"/>'
    )


# ── icon catalog ────────────────────────────────────────────────────
def cell_tech(label: str) -> str:
    """Wifi fan + tiny generation-tech label (2G/3G/4G/5G/E/G/H/U/1x)."""
    return svg(
        wifi_fan(3) + "\n"
        f'  <text x="11" y="13" font-family="sans-serif" font-size="4.5" '
        f'font-weight="bold" fill="{BASE}">{label}</text>'
    )


ICONS = {
    # wireless signal levels
    "network-wireless-signal-none":       svg(wifi_fan(0)),
    "network-wireless-signal-weak":       svg(wifi_fan(1)),
    "network-wireless-signal-ok":         svg(wifi_fan(2)),
    "network-wireless-signal-good":       svg(wifi_fan(3)),
    "network-wireless-signal-excellent":  svg(wifi_fan(4)),

    # wireless — nm-tray picks one icon per row, so each name is a
    # self-contained composite (signal-level fan + state badge if any).
    "network-wireless":                   svg(wifi_fan(3)),
    "network-wireless-connected":         svg(wifi_fan(4, GOOD)),
    # Disconnected: faded-empty fan. NO X badge — nm-tray overlays this
    # on top of saved-network rows, so an X here would visually pile up
    # with the encrypted-lock overlay. The dim level-0 fan already reads
    # as "no signal/disconnected" without the extra badge.
    "network-wireless-disconnected":      svg(wifi_fan(0)),
    "network-wireless-disabled":          svg(wifi_fan(0) + "\n" + badge_slash()),
    "network-wireless-acquiring":         svg(wifi_fan(2) + "\n" + badge_dots()),
    "network-wireless-error":             svg(wifi_fan(2) + "\n" + badge_excl()),
    "network-wireless-no-route":          svg(wifi_fan(3) + "\n" + badge_excl(WARN)),
    "network-wireless-offline":           svg(wifi_fan(0)),
    "network-wireless-encrypted":         svg(wifi_fan(3) + "\n" + badge_lock()),
    "network-wireless-hotspot":           svg(wifi_fan(4, GOOD) + "\n" + badge_dots(GOOD)),
    "network-wireless-hardware-disabled": svg(wifi_fan(0) + "\n" + badge_slash(ERROR)),

    # wired
    "network-wired":                      svg(ethernet_plug()),
    "network-wired-disconnected":         svg(ethernet_plug(MUTED, 0.55) + "\n" + badge_x()),
    "network-wired-acquiring":            svg(ethernet_plug() + "\n" + badge_dots()),
    "network-wired-error":                svg(ethernet_plug() + "\n" + badge_excl()),
    "network-wired-no-route":             svg(ethernet_plug() + "\n" + badge_excl(WARN)),
    "network-wired-offline":              svg(ethernet_plug(MUTED, 0.55)),

    # vpn — green key. Stroke-only so it doesn't dominate the menu row.
    "network-vpn":                        svg(vpn_key()),
    "network-vpn-connected":              svg(vpn_key()),
    "network-vpn-acquiring":              svg(vpn_key() + "\n" + badge_dots()),
    "network-vpn-disconnected":           svg(vpn_key(MUTED, 0.55) + "\n" + badge_x()),
    "network-vpn-error":                  svg(vpn_key(ERROR)),
    "network-vpn-no-route":               svg(vpn_key() + "\n" + badge_excl(WARN)),
    "network-vpn-offline":                svg(vpn_key(MUTED, 0.55)),

    # generic
    "network-error":                      svg(globe() + "\n" + badge_excl()),
    "network-idle":                       svg(globe()),
    "network-offline":                    svg(globe(MUTED, 0.55) + "\n" + badge_slash()),
    "network-no-route":                   svg(globe() + "\n" + badge_excl(WARN)),
    "network-acquiring":                  svg(globe() + "\n" + badge_dots()),
    "network-transmit":                   svg(globe() + "\n" + badge_arrow_up()),
    "network-receive":                    svg(globe() + "\n" + badge_arrow_down()),
    "network-transmit-receive":           svg(globe() + "\n" + badge_arrow_up() + "\n" + badge_arrow_down()),

    # cellular — same fan, with state badges
    "network-cellular-signal-none":       svg(wifi_fan(0)),
    "network-cellular-signal-weak":       svg(wifi_fan(1)),
    "network-cellular-signal-ok":         svg(wifi_fan(2)),
    "network-cellular-signal-good":       svg(wifi_fan(3)),
    "network-cellular-signal-excellent":  svg(wifi_fan(4)),
    "network-cellular-acquiring":         svg(wifi_fan(2) + "\n" + badge_dots()),
    "network-cellular-connected":         svg(wifi_fan(4, GOOD)),
    "network-cellular-disabled":          svg(wifi_fan(0) + "\n" + badge_slash()),
    "network-cellular-disconnected":      svg(wifi_fan(0) + "\n" + badge_x()),
    "network-cellular-error":             svg(wifi_fan(2) + "\n" + badge_excl()),
    "network-cellular-hardware-disabled": svg(wifi_fan(0) + "\n" + badge_slash(ERROR)),
    "network-cellular-no-route":          svg(wifi_fan(3) + "\n" + badge_excl(WARN)),
    "network-cellular-offline":           svg(wifi_fan(0)),

    # security indicators — nm-tray's actual encryption overlay icon
    "security-high":            svg(standalone_lock()),
    "security-high-symbolic":   svg(standalone_lock()),
    "security-medium":          svg(standalone_lock(WARN)),
    "security-medium-symbolic": svg(standalone_lock(WARN)),
    "security-low":             svg(standalone_lock(ERROR)),
    "security-low-symbolic":    svg(standalone_lock(ERROR)),

    # cellular generation-tech labels
    "network-cellular-2g":      cell_tech("2G"),
    "network-cellular-3g":      cell_tech("3G"),
    "network-cellular-4g":      cell_tech("4G"),
    "network-cellular-5g":      cell_tech("5G"),
    "network-cellular-edge":    cell_tech("E"),
    "network-cellular-gprs":    cell_tech("G"),
    "network-cellular-cdma-1x": cell_tech("1x"),
    "network-cellular-hspa":    cell_tech("H"),
    "network-cellular-umts":    cell_tech("U"),
}


# ── genmon panel applet PNGs ─────────────────────────────────────────
# bin/panel/network-applet.sh (the XFCE genmon network applet) loads PNGs
# directly from assets/panel-icons/ by path — it does NOT go through the
# icon theme. Rasterize the relevant status icons there too so the applet
# stays in sync with the canonical SVG design above (otherwise the PNGs
# drift into a different look, e.g. solid filled cones vs. arc bars).
PANEL_PNG_DIR = ROOT.parent.parent / "panel-icons"   # assets/panel-icons
PANEL_PNG_SIZE = 22
PANEL_PNG_NAMES = [
    "network-wireless-signal-none",
    "network-wireless-signal-weak",
    "network-wireless-signal-ok",
    "network-wireless-signal-good",
    "network-wireless-signal-excellent",
    "network-wired",
    "network-offline",
]


def _rasterize_panel_pngs() -> int:
    import subprocess
    if not PANEL_PNG_DIR.is_dir():
        return 0
    n = 0
    for name in PANEL_PNG_NAMES:
        svg_path = OUT / f"{name}.svg"
        if not svg_path.exists():
            continue
        subprocess.run(
            ["inkscape", str(svg_path), "--export-type=png",
             f"--export-filename={PANEL_PNG_DIR / (name + '.png')}",
             f"--export-width={PANEL_PNG_SIZE}",
             f"--export-height={PANEL_PNG_SIZE}",
             "--export-background-opacity=0"],
            check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
        n += 1
    return n


def main() -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    # Sweep any leftover Papirus fallback symlinks from the same dir.
    cleaned = 0
    for entry in OUT.iterdir():
        if entry.is_symlink():
            entry.unlink()
            cleaned += 1
    for name, body in ICONS.items():
        (OUT / f"{name}.svg").write_text(body)
    print(f"Cleared {cleaned} symlinks, wrote {len(ICONS)} SVGs to {OUT}")
    n = _rasterize_panel_pngs()
    print(f"Rasterized {n} genmon panel PNGs to {PANEL_PNG_DIR}")


if __name__ == "__main__":
    main()
