#!/usr/bin/env python3
import argparse
import fcntl
import glob
import math
import os
import sys
import time

I2C_SLAVE = 0x0703
LED_ADDR = 0x45
BUS_GLOB = "/sys/devices/platform/soc/11d00000.i2c/i2c-*"

# Original breathing config
BREATHING_BASELINE = {
    0x30: 0x07,  # LCTR: all 3 channels enabled
    0x31: 0x9f,  # LCFG0: CUR=15, SYNC=1, MD=1 (pattern mode)
    0x32: 0x0f,  # LCFG1
    0x33: 0x0f,  # LCFG2
    0x34: 0xff,  # PWM0
    0x35: 0xff,  # PWM1
    0x36: 0xff,  # PWM2
    0x37: 0x66,  # LED0T0: T1=6, T2=6
    0x38: 0x66,  # LED0T1: T3=6, T4=6
    0x39: 0x00,  # LED0T2: T0=0, REPEAT=0 (infinite)
    0x04: 0x01,  # GCR2: IMAX=1 (30mA)
}


def find_toh_bus():
    matches = glob.glob(BUS_GLOB)
    if not matches:
        raise RuntimeError("TOH i2c bus not found")
    return int(os.path.basename(matches[0]).split("-")[1])


def add_target(bus, address, name="ledcli"):
    with open(f"/sys/devices/platform/soc/11d00000.i2c/i2c-{bus}/new_device", "w") as f:
        f.write(f"{name} 0x{address:02x}\n")


def delete_target(bus, address):
    with open(f"/sys/devices/platform/soc/11d00000.i2c/i2c-{bus}/delete_device", "w") as f:
        f.write(f"0x{address:02x}\n")


def wreg(fd, reg, val):
    os.write(fd, bytes([reg, val]))


def rreg(fd, reg):
    os.write(fd, bytes([reg]))
    return os.read(fd, 1)[0]


def seconds_to_nibble(seconds):
    if seconds <= 0:
        return 0
    value = round(math.sqrt((seconds * 1000.0) / 31.25))
    return max(0, min(15, value))


def nibble_to_seconds(value):
    return (31.25 * value * value) / 1000.0


def open_led(bus, name):
    add_target(bus, LED_ADDR, name)
    fd = os.open(f"/dev/i2c-{bus}", os.O_RDWR)
    fcntl.ioctl(fd, I2C_SLAVE, LED_ADDR)
    return fd


def close_led(bus, fd):
    os.close(fd)
    delete_target(bus, LED_ADDR)


def restore_breathing(bus):
    fd = open_led(bus, "ledrestore")
    try:
        wreg(fd, 0x01, 0x01)  
        for reg, val in BREATHING_BASELINE.items():
            wreg(fd, reg, val)
        wreg(fd, 0x01, 0x00)

        mismatches = []
        for reg, expected in BREATHING_BASELINE.items():
            actual = rreg(fd, reg)
            if actual != expected:
                mismatches.append((reg, expected, actual))
        if mismatches:
            print("MISMATCH after restore:", file=sys.stderr)
            for reg, expected, actual in mismatches:
                print(f"  0x{reg:02x}: expected 0x{expected:02x}, got 0x{actual:02x}", file=sys.stderr)
            return 1
        print("Restored original breathing.")
        return 0
    finally:
        close_led(bus, fd)


def apply_pattern(bus, rise, hold, decay, off, delay, repeat, brightness):
    t1 = seconds_to_nibble(rise)
    t2 = seconds_to_nibble(hold)
    t3 = seconds_to_nibble(decay)
    t4 = seconds_to_nibble(off)
    t0 = seconds_to_nibble(delay)

    fd = open_led(bus, "ledcli")
    try:
        wreg(fd, 0x01, 0x01)
        wreg(fd, 0x37, (t1 << 4) | t2)
        wreg(fd, 0x38, (t3 << 4) | t4)
        wreg(fd, 0x39, (t0 << 4) | repeat)
        
        wreg(fd, 0x01, 0x00)
        time.sleep(0.2)
        wreg(fd, 0x01, 0x01)

        wreg(fd, 0x30, 0x07)
        wreg(fd, 0x31, 0x9f)
        wreg(fd, 0x34, brightness)

        print(
            f"rise={rise:.3f}s(v{t1}~{nibble_to_seconds(t1):.3f}s) "
            f"hold={hold:.3f}s(v{t2}~{nibble_to_seconds(t2):.3f}s) "
            f"decay={decay:.3f}s(v{t3}~{nibble_to_seconds(t3):.3f}s) "
            f"off={off:.3f}s(v{t4}~{nibble_to_seconds(t4):.3f}s) "
            f"delay={delay:.3f}s(v{t0}~{nibble_to_seconds(t0):.3f}s) "
            f"repeat={repeat}{' (infinite)' if repeat == 0 else ''} "
            f"brightness={brightness}"
        )
        print("triggered")
        return 0
    finally:
        close_led(bus, fd)


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--rise", type=float, default=0.0, help="fade-in time in seconds (0-~7.03s, default 0)")
    parser.add_argument("--hold", type=float, default=0.0, help="time held at peak brightness in seconds (default 0)")
    parser.add_argument("--decay", type=float, default=0.0, help="fade-out time in seconds (default 0)")
    parser.add_argument("--off", type=float, default=0.0, help="time off between repeats in seconds (default 0)")
    parser.add_argument("--delay", type=float, default=0.0, help="initial delay before the first cycle, in seconds")
    parser.add_argument("--repeat", type=int, default=0, help="number of cycles, 0 = infinite")
    parser.add_argument("--brightness", type=int, default=255, help="peak brightness 0-255 (default 255)")
    parser.add_argument("--restore", action="store_true", help="restore the original breathing")
    args = parser.parse_args()

    if os.geteuid() != 0:
        print("must run as root", file=sys.stderr)
        return 1

    if not (0 <= args.repeat <= 15):
        print("--repeat must be 0-15", file=sys.stderr)
        return 1
    if not (0 <= args.brightness <= 255):
        print("--brightness must be 0-255", file=sys.stderr)
        return 1

    bus = find_toh_bus()

    if args.restore:
        return restore_breathing(bus)
    return apply_pattern(bus, args.rise, args.hold, args.decay, args.off, args.delay, args.repeat, args.brightness)


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