#!/usr/bin/env python3
"""
I2C Doctor — USB/I2C Bridge host script.

Requires: pip install pyserial

Usage (interactive CLI):
    python3 bridge.py --port /dev/tty.usbserial-* [--baud 115200]

Usage (as library):
    from bridge import I2CBridge
    with I2CBridge('/dev/tty.usbserial-0001') as b:
        print(b.scan())
        data = b.read_reg(0x48, 0x00, 2)
        b.write_reg(0x48, 0x01, bytes([0x60]))

Protocol (ASCII, newline-terminated):
    SCAN                 -> OK:3C 48\r\n (space-separated hex addresses, empty if none)
    PROBE XX             -> OK\r\n / ERR:NACK\r\n
    READ XX N            -> OK:AA BB\r\n (addr hex, count decimal — no register)
    READ XX XX N         -> OK:AA BB\r\n (addr hex, reg hex, count decimal)
    WRITE XX YY..        -> OK\r\n       (addr hex, data hex bytes — no register)
    READ_REG XX XX N     -> OK:AA BB\r\n (addr hex, reg hex, count decimal)
    WRITE_REG XX XX YY.. -> OK\r\n       (addr hex, reg hex, data hex bytes)
    SPEED N              -> OK\r\n       (speed decimal)
"""

import argparse
import time
import serial

class I2CBridge:
    """USB to I2C bridge for the I2C Doctor device."""

    def __init__(self, port: str, baud: int = 115200, timeout: float = 5.0):
        self.ser = serial.Serial(port, baud, timeout=timeout)
        time.sleep(0.2)
        self.ser.reset_input_buffer()

    def _cmd(self, cmd: str) -> str:
        self.ser.reset_input_buffer()
        self.ser.write((cmd + '\n').encode())
        resp = self.ser.readline().decode(errors='replace').strip()
        return resp

    def scan(self) -> list:
        """Scan the I2C bus. Returns list of found 7-bit addresses (int)."""
        resp = self._cmd('SCAN')
        if not resp.startswith('OK:'):
            raise IOError(f'SCAN failed: {resp!r}')
        part = resp[3:].strip()
        return [int(x, 16) for x in part.split()] if part else []

    def probe(self, addr: int) -> bool:
        """Probe address `addr` (7-bit int). Returns True if ACK received."""
        resp = self._cmd(f'PROBE {addr:02X}')
        return resp == 'OK'

    def read(self, addr: int, length: int) -> bytes:
        """Read `length` bytes from device at `addr` (7-bit) without register address."""
        resp = self._cmd(f'READ {addr:02X} {length}')
        if not resp.startswith('OK:'):
            raise IOError(f'READ 0x{addr:02X} failed: {resp!r}')
        return bytes(int(x, 16) for x in resp[3:].split())

    def write(self, addr: int, data: bytes) -> None:
        """Write `data` to device at `addr` (7-bit) without register address."""
        hex_bytes = ' '.join(f'{b:02X}' for b in data)
        resp = self._cmd(f'WRITE {addr:02X} {hex_bytes}')
        if resp != 'OK':
            raise IOError(f'WRITE 0x{addr:02X} failed: {resp!r}')

    def read_reg(self, addr: int, reg: int, length: int) -> bytes:
        """Read `length` bytes from register `reg` of device at `addr` (7-bit)."""
        resp = self._cmd(f'READ_REG {addr:02X} {reg:02X} {length}')
        if not resp.startswith('OK:'):
            raise IOError(f'READ_REG 0x{addr:02X} reg 0x{reg:02X} failed: {resp!r}')
        return bytes(int(x, 16) for x in resp[3:].split())

    def write_reg(self, addr: int, reg: int, data: bytes) -> None:
        """Write `data` to register `reg` of device at `addr` (7-bit)."""
        hex_bytes = ' '.join(f'{b:02X}' for b in data)
        resp = self._cmd(f'WRITE_REG {addr:02X} {reg:02X} {hex_bytes}')
        if resp != 'OK':
            raise IOError(f'WRITE_REG 0x{addr:02X} reg 0x{reg:02X} failed: {resp!r}')

    def speed(self, speed: int):
        """Set I2C speed."""
        resp = self._cmd(f'SPEED {speed}')
        return resp == 'OK'

    def close(self) -> None:
        self.ser.close()

    def __enter__(self):
        return self

    def __exit__(self, *_):
        self.close()

def _interactive(bridge: I2CBridge) -> None:
    try:
        import readline  # enables arrow-key history on macOS/Linux
        readline.set_history_length(100)
    except ImportError:
        pass
    print("I2C Doctor Bridge — ready. Commands: scan, probe, read, write, read_reg, write_reg, help, quit")
    while True:
        try:
            line = input('i2c> ').strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break
        if not line:
            continue
        parts = line.split()
        cmd = parts[0].lower()
        try:
            if cmd in ('quit', 'exit', 'q'):
                break
            elif cmd == 'help':
                print('  scan                                scan bus, list found addresses')
                print('  probe     <addr_hex>                check if address ACKs')
                print('  read      <addr_hex> <n>            read n bytes (no register addr)')
                print('  read      <addr_hex> <reg_hex> <n>  read n bytes from register')
                print('  read_reg  <addr_hex> <reg_hex> <n>  read n bytes from register')
                print('  write     <addr_hex> <..>           write bytes (no register addr)')
                print('  write_reg <addr_hex> <reg_hex> <..> write bytes to register')
                print('  speed     <kbit/s>                  speed in kbit/s (10, 50, 100, 200, 400, 1000)')
            elif cmd == 'scan':
                addrs = bridge.scan()
                if addrs:
                    print('Found:', ', '.join(f'0x{a:02X}' for a in addrs))
                else:
                    print('No devices found')
            elif cmd == 'probe' and len(parts) == 2:
                print('ACK' if bridge.probe(int(parts[1], 16)) else 'NACK')
            elif cmd == 'read' and len(parts) == 4:
                data = bridge.read_reg(int(parts[1], 16), int(parts[2], 16), int(parts[3]))
                print(' '.join(f'{b:02X}' for b in data))
            elif cmd == 'read' and len(parts) == 3:
                data = bridge.read(int(parts[1], 16), int(parts[2]))
                print(' '.join(f'{b:02X}' for b in data))
            elif cmd == 'read_reg' and len(parts) == 4:
                data = bridge.read_reg(int(parts[1], 16), int(parts[2], 16), int(parts[3]))
                print(' '.join(f'{b:02X}' for b in data))
            elif cmd == 'write' and len(parts) >= 3:
                bridge.write(int(parts[1], 16), bytes(int(x, 16) for x in parts[2:]))
                print('OK')
            elif cmd == 'write_reg' and len(parts) >= 4:
                bridge.write_reg(int(parts[1], 16), int(parts[2], 16), bytes(int(x, 16) for x in parts[3:]))
                print('OK')
            elif cmd == 'speed' and len(parts) == 2:
                bridge.speed(int(parts[1], 10))
                print('OK')
            else:
                print(f"Unknown or malformed command: '{line}'. Type 'help'.")
        except (IOError, ValueError) as e:
            print(f'Error: {e}')

def main() -> None:
    parser = argparse.ArgumentParser(description='I2C Doctor USB Bridge')
    parser.add_argument('--port', required=True, help='Serial port (e.g. /dev/tty.usbserial-*, /dev/ttyUSB0, COM3)')
    parser.add_argument('--baud', type=int, default=115200, help='Baud rate (default: 115200)')
    args = parser.parse_args()
    with I2CBridge(args.port, args.baud) as bridge:
        _interactive(bridge)

if __name__ == '__main__':
    main()
