#!/usr/bin/env python3
"""On-demand camera feed for v4l2loopback.

Holds /dev/video50 open permanently so v4l2loopback stays in capture mode.
Writes black frames when idle (0% CPU, LED off). Starts the real camera
pipeline when an app opens the device, relaying frames through the same fd.
Stops the camera when all readers close. No fd gap = no stream interruption.
"""

import fcntl, os, signal, struct, subprocess, sys, threading, time

DEVICE = "/dev/video50"
W, H = 1920, 1080
FSIZE = W * H * 3 // 2
POLL = 2
STOP_DELAY = 6

BLACK = bytes(W * H) + bytes([128] * (W * H // 2))

dev = os.open(DEVICE, os.O_WRONLY)
pixfmt = struct.pack("II4sIIII4x188x", W, H, b"NV12", 1, W, FSIZE, 0)
fmt = struct.pack("I4x", 2) + pixfmt
fcntl.ioctl(dev, 0xC0D05605, fmt)
os.write(dev, BLACK)

# Restart WirePlumber so it sees the capture device
time.sleep(1)
subprocess.run(["systemctl", "--user", "restart", "wireplumber.service"],
               capture_output=True, timeout=10)

cam_proc = None
cam_lock = threading.Lock()
stop_idle = threading.Event()


def write_idle():
    while not stop_idle.is_set():
        try:
            os.write(dev, BLACK)
        except OSError:
            pass
        stop_idle.wait(2)


def relay_camera():
    global cam_proc
    env = os.environ.copy()
    env["GST_PLUGIN_PATH"] = "/usr/lib/gstreamer-1.0"
    cam_proc = subprocess.Popen(
        ["gst-launch-1.0", "-q",
         "icamerasrc", "device-name=ov08x40-uf",
         "sharpness=80", "ev=-1", "saturation=10",
         "!", "video/x-raw,format=NV12,width=1920,height=1080",
         "!", "videoflip", "method=rotate-180",
         "!", "fdsink", "fd=1"],
        stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env,
    )
    buf = b""
    while cam_proc.poll() is None:
        data = cam_proc.stdout.read(65536)
        if not data:
            break
        buf += data
        while len(buf) >= FSIZE:
            try:
                os.write(dev, buf[:FSIZE])
            except OSError:
                pass
            buf = buf[FSIZE:]
    cam_proc = None


def has_readers():
    try:
        r = subprocess.run(["fuser", DEVICE], capture_output=True, text=True, timeout=5)
        for pid in r.stdout.strip().split():
            pid = pid.strip()
            if not pid:
                continue
            if int(pid) == os.getpid():
                continue
            with cam_lock:
                if cam_proc and int(pid) == cam_proc.pid:
                    continue
            return True
    except Exception:
        pass
    return False


def start_camera():
    stop_idle.set()
    t = threading.Thread(target=relay_camera, daemon=True)
    t.start()
    return t


def stop_camera():
    with cam_lock:
        if cam_proc:
            cam_proc.terminate()
            try:
                cam_proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                cam_proc.kill()
    stop_idle.clear()
    t = threading.Thread(target=write_idle, daemon=True)
    t.start()
    return t


def shutdown(sig, frame):
    stop_idle.set()
    with cam_lock:
        if cam_proc:
            cam_proc.terminate()
    os.close(dev)
    sys.exit(0)


signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

# Start idle
idle_thread = threading.Thread(target=write_idle, daemon=True)
idle_thread.start()

active = False
idle_count = 0

while True:
    if has_readers():
        idle_count = 0
        if not active:
            start_camera()
            active = True
    else:
        if active:
            idle_count += 1
            if idle_count >= STOP_DELAY // POLL:
                stop_camera()
                active = False
                idle_count = 0
    time.sleep(POLL)
