#!/usr/bin/python3
"""Shift'ni ikki marta bosishni kuzatadi va faol foydalanuvchi seansida yangi-alifbo-ogir'ni ishga tushiradi.

Tizim xizmati (root) sifatida ishlaydi: Wayland'da klaviaturani faqat /dev/input orqali o'qish mumkin.
Ruxsat foydalanuvchi dasturlariga berilmaydi — klaviatura hodisalarini faqat shu xizmat ko'radi
va undan faqat Shift bosilishlari va "boshqa tugma bosildi" degan fakt ishlatiladi.
"""

import glob
import os
import pwd
import re
import select
import struct
import subprocess
import sys
import time

sys.path.insert(0, "/usr/share/ozbek-yangi-alifbo")
from ikki_shift import IkkiShift  # noqa: E402

EV_KEY = 1
KEY_A, KEY_LEFTSHIFT, KEY_RIGHTSHIFT = 30, 42, 54
HODISA = struct.Struct("llHHi")
QAYTA_QIDIRISH = 5.0  # klaviatura ulanishi/uzilishini sezish uchun, sekund
VIRTUAL_NOMI = "Yangi alifbo"  # yangi-alifbo-ogir'ning o'z uinput qurilmasi


def klaviaturalar() -> list[str]:
    """/proc/bus/input/devices dan haqiqiy klaviaturalarning /dev/input/eventN yo'llari."""
    with open("/proc/bus/input/devices", encoding="utf-8", errors="replace") as f:
        bloklar = f.read().split("\n\n")
    yollar = []
    for blok in bloklar:
        nomi = re.search(r'^N: Name="(.*)"', blok, re.M)
        ishlovchi = re.search(r"^H: Handlers=(.*)$", blok, re.M)
        tugmalar = re.search(r"^B: KEY=(.*)$", blok, re.M)
        if not (nomi and ishlovchi and tugmalar) or nomi.group(1) == VIRTUAL_NOMI:
            continue
        hodisa = re.search(r"\bevent\d+\b", ishlovchi.group(1))
        # KEY bitmap: so'zlar katta tartibdan kichikka; A va Shift tugmasi borlar — klaviatura
        bitlar = int("".join(soz.zfill(16) for soz in tugmalar.group(1).split()), 16)
        if hodisa and "kbd" in ishlovchi.group(1) and bitlar >> KEY_A & 1 and bitlar >> KEY_LEFTSHIFT & 1:
            yollar.append(f"/dev/input/{hodisa.group(0)}")
    return yollar


def faol_seans() -> dict[str, str] | None:
    """seat0'dagi faol grafik seans: foydalanuvchi nomi va muhit o'zgaruvchilari."""
    def loginctl(*args: str) -> str:
        return subprocess.run(["loginctl", *args], capture_output=True, text=True).stdout.strip()

    seans = loginctl("show-seat", "seat0", "-p", "ActiveSession", "--value")
    if not seans:
        return None
    xossalar = dict(
        qator.split("=", 1)
        for qator in loginctl("show-session", seans, "-p", "Name", "-p", "User", "-p", "Type", "-p", "Display").splitlines()
        if "=" in qator
    )
    if xossalar.get("Type") not in ("wayland", "x11"):
        return None
    uid = xossalar["User"]
    runtime = f"/run/user/{uid}"
    muhit = {
        "HOME": pwd.getpwuid(int(uid)).pw_dir,
        "XDG_RUNTIME_DIR": runtime,
        "DBUS_SESSION_BUS_ADDRESS": f"unix:path={runtime}/bus",
    }
    if xossalar["Type"] == "wayland":
        soketlar = sorted(os.path.basename(s) for s in glob.glob(f"{runtime}/wayland-*") if not s.endswith(".lock"))
        if not soketlar:
            return None
        muhit["WAYLAND_DISPLAY"] = soketlar[0]
    else:
        muhit["DISPLAY"] = xossalar.get("Display") or ":0"
        for xauth in (f"{runtime}/gdm/Xauthority", os.path.join(muhit["HOME"], ".Xauthority")):
            if os.path.exists(xauth):
                muhit["XAUTHORITY"] = xauth
                break
    return {"nomi": xossalar["Name"], **muhit}


def ogir() -> None:
    seans = faol_seans()
    if seans is None:
        return
    nomi = seans.pop("nomi")
    muhit = [f"{kalit}={qiymat}" for kalit, qiymat in seans.items()]
    # Fonda: keyingi Shift'lar kuzatilishda davom etsin
    subprocess.Popen(["runuser", "-u", nomi, "--", "env", *muhit, "/usr/bin/yangi-alifbo-ogir"],
                     stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def main() -> None:
    aniqlovchi = IkkiShift()
    ochiq: dict[str, int] = {}
    oxirgi_qidiruv = 0.0
    while True:
        if time.monotonic() - oxirgi_qidiruv > QAYTA_QIDIRISH:
            oxirgi_qidiruv = time.monotonic()
            for yol in klaviaturalar():
                if yol not in ochiq:
                    try:
                        ochiq[yol] = os.open(yol, os.O_RDONLY | os.O_NONBLOCK)
                    except OSError:
                        pass
        tayyor, _, _ = select.select(list(ochiq.values()), [], [], QAYTA_QIDIRISH)
        for fd in tayyor:
            try:
                malumot = os.read(fd, HODISA.size * 64)
            except OSError:
                # Klaviatura uzildi
                yol = next(y for y, f in ochiq.items() if f == fd)
                os.close(fd)
                del ochiq[yol]
                continue
            for i in range(0, len(malumot) - HODISA.size + 1, HODISA.size):
                sek, mikro, turi, kod, qiymat = HODISA.unpack_from(malumot, i)
                if turi != EV_KEY or qiymat == 2:  # 2 — avto-takror
                    continue
                shiftmi = kod in (KEY_LEFTSHIFT, KEY_RIGHTSHIFT)
                if aniqlovchi.hodisa(shiftmi, qiymat == 1, sek + mikro / 1e6):
                    ogir()


if __name__ == "__main__":
    main()
