#!/usr/bin/env python3
"""Apply the Settings colour scheme and the primary monitor to the Alt+Tab switcher.

Reads accentColor, darkMode, borderRadius and primaryMonitor from
~/.config/quickshell/default/settings.txt (command-line values win, because
Settings.qml writes that file asynchronously) and writes the matching keys
into ~/.config/snappy-switcher/config.ini. Keys in config.ini override the
theme file, so the light/dark theme supplies the base colours and this script
adds accent border, accent-tinted selection, corner radius and the monitor.
A running daemon picks the change up live via --reload-config.
"""
import argparse
import os
from pathlib import Path
import re
import subprocess
import sys

config_home = Path(os.environ.get('XDG_CONFIG_HOME', Path.home() / '.config'))
SETTINGS = config_home / 'quickshell/default/settings.txt'
CONFIG = config_home / 'snappy-switcher/config.ini'
THEMES = config_home / 'snappy-switcher/themes'
HEX = re.compile(r'^#[0-9a-fA-F]{6}$')
OUTPUT = re.compile(r'^[A-Za-z0-9_.:+-]{1,63}$')


def read_settings():
    if not SETTINGS.is_file():
        return {}
    return dict(line.split('=', 1) for line in SETTINGS.read_text().splitlines() if '=' in line)


def hypr_primary():
    """Fallback when Settings has no primary monitor: the one Hyprland rules use."""
    try:
        sys.path.insert(0, str(config_home / 'hypr/lua/bin'))
        from hypr_state import read_state
        return read_state(config_home / 'hypr/lua').get('primary_monitor') or ''
    except Exception:
        return ''


def theme_color(theme, key, default):
    path = THEMES / theme
    if not path.is_file():
        path = Path('/usr/share/snappy-switcher/themes') / theme
    try:
        for line in path.read_text().splitlines():
            name, _, value = line.partition('=')
            if name.strip() == key and HEX.match(value.strip()):
                return value.strip()
    except OSError:
        pass
    return default


def mix(base, accent, amount):
    b = [int(base[i:i + 2], 16) for i in (1, 3, 5)]
    a = [int(accent[i:i + 2], 16) for i in (1, 3, 5)]
    return '#' + ''.join('%02x' % round(x + (y - x) * amount) for x, y in zip(b, a))


def set_key(lines, section, key, value):
    """Set key in [section], inserting the key (and section) when missing."""
    header = '[' + section + ']'
    start = next((i for i, l in enumerate(lines) if l.strip() == header), None)
    if start is None:
        lines += ['', header, f'{key} = {value}']
        return
    end = next((i for i in range(start + 1, len(lines)) if lines[i].lstrip().startswith('[')), len(lines))
    for i in range(start + 1, end):
        name = lines[i].split('=', 1)[0].strip()
        if '=' in lines[i] and name == key and not lines[i].lstrip().startswith(('#', ';')):
            lines[i] = f'{key} = {value}'
            return
    insert = end
    while insert > start + 1 and not lines[insert - 1].strip():
        insert -= 1
    lines.insert(insert, f'{key} = {value}')


def readable_on(accent):
    """Dark or white text, whichever contrasts better on the accent."""
    def channel(v):
        v /= 255
        return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4
    r, g, b = (channel(int(accent[i:i + 2], 16)) for i in (1, 3, 5))
    lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
    dark_contrast = (lum + 0.05) / (0.0072 + 0.05)   # #141414 has relative luminance of about 0.0072
    white_contrast = 1.05 / (lum + 0.05)
    return '#141414' if dark_contrast >= white_contrast else '#ffffff'


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('--accent')
    parser.add_argument('--dark', choices=['true', 'false'])
    parser.add_argument('--radius', type=int)
    parser.add_argument('--monitor')
    parser.add_argument('--no-reload', action='store_true')
    args = parser.parse_args()
    if not CONFIG.is_file():
        return 0
    settings = read_settings()
    accent = args.accent or settings.get('accentColor') or '#FE6F2D'
    if not HEX.match(accent):
        accent = '#FE6F2D'
    dark = (args.dark or settings.get('darkMode', 'true')).strip().lower() != 'false'
    try:
        radius = args.radius if args.radius is not None else int(settings.get('borderRadius', '10'))
    except ValueError:
        radius = 10
    radius = max(0, min(radius, 40))
    monitor = args.monitor if args.monitor is not None else (settings.get('primaryMonitor') or hypr_primary())
    monitor = monitor.strip() if OUTPUT.match(monitor.strip()) else ''

    theme = 'quickshell-dark.ini' if dark else 'quickshell-light.ini'
    card_bg = theme_color(theme, 'card_bg', '#2a2a2a' if dark else '#f0f0f0')
    selected = mix(card_bg, accent, 0.30 if dark else 0.22)

    original = CONFIG.read_text()
    lines = original.splitlines()
    set_key(lines, 'general', 'monitor', monitor)
    set_key(lines, 'theme', 'name', theme)
    set_key(lines, 'theme', 'border_color', accent.lower())
    set_key(lines, 'theme', 'card_selected', selected)
    set_key(lines, 'theme', 'corner_radius', str(radius))
    set_key(lines, 'theme', 'badge_bg', accent.lower())
    set_key(lines, 'theme', 'badge_bg_selected', accent.lower())
    set_key(lines, 'theme', 'badge_text_color', readable_on(accent))
    text = '\n'.join(lines) + '\n'
    if text == original:
        return 0
    temporary = CONFIG.with_name(CONFIG.name + '.tmp')
    temporary.write_text(text)
    os.replace(temporary, CONFIG)
    if not args.no_reload and subprocess.run(['pgrep', '-u', str(os.getuid()), '-x', 'snappy-switcher'],
                                              capture_output=True).returncode == 0:
        subprocess.run(['snappy-switcher', '--reload-config'], capture_output=True, timeout=5)
    return 0


if __name__ == '__main__':
    sys.exit(main())
