"""Run without arguments for simulated data; use --port for read-only serial I/O."""

from __future__ import annotations

import argparse
import json
import math
import struct
import sys
import time

try:
    from .hall_protocol import (BAUD_TABLE, CMD_GET_DATA, CMD_GET_INFO, CMD_RESPONSE, HOST,
                                TYPE_ANY, TYPE_ENCODER, TYPE_POSITION, Parser,
                                ProtocolError, build_frame, parse_info, parse_sample)
except ImportError:
    from hall_protocol import (BAUD_TABLE, CMD_GET_DATA, CMD_GET_INFO, CMD_RESPONSE, HOST,
                               TYPE_ANY, TYPE_ENCODER, TYPE_POSITION, Parser,
                               ProtocolError, build_frame, parse_info, parse_sample)


def make_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    source = parser.add_mutually_exclusive_group()
    source.add_argument("--demo", action="store_true", help="simulated data (the default)")
    source.add_argument("--port", help="serial port, for example COM7 or /dev/ttyUSB0")
    parser.add_argument("--id", type=int, default=1, help="device address 1..254")
    parser.add_argument("--baud", type=int, choices=BAUD_TABLE, default=115200)
    parser.add_argument("--profile", choices=("encoder", "line"), default="line",
                        help="demo profile only; serial mode discovers the device profile")
    parser.add_argument("--count", type=int, default=20, help="number of samples, 0 means until Ctrl+C")
    parser.add_argument("--interval-ms", type=float, default=100.0,
                        help="delay after each sample, not a guaranteed hardware sample period")
    parser.add_argument("--timeout", type=float, default=0.4, help="serial transaction timeout in seconds")
    return parser


def emit(sample, demo: bool) -> None:
    row = sample.to_dict()
    row["source"] = "simulated_not_hardware" if demo else "serial"
    print(json.dumps(row, ensure_ascii=False), flush=True)


def run_demo(args) -> int:
    profile = TYPE_ENCODER if args.profile == "encoder" else TYPE_POSITION
    index = 0
    while args.count == 0 or index < args.count:
        # Feed the wire parser too, so this path exercises the actual byte format.
        theta = index * 0.15
        value = int(round((math.degrees(theta) % 360) * 100)) if profile == TYPE_ENCODER else int(round(800 * math.sin(theta)))
        if profile == TYPE_POSITION:
            position_mm = value / 100.0
            adc = tuple(int(round(2048 + 1000 * math.exp(-((position_mm - spot) ** 2) / (2 * 8.0 ** 2))))
                        for spot in (-10.0, 0.0, 10.0))
        else:
            adc = tuple(int(round(2048 + 1000 * math.cos(theta + shift)))
                        for shift in (0, -2 * math.pi / 3, 2 * math.pi / 3))
        payload = b"\x00" + struct.pack("<ihHHHB", value, 1000, *adc, 0)
        wire = build_frame(HOST, args.id, profile, CMD_GET_DATA | CMD_RESPONSE, index & 255, payload)
        frame = Parser().feed(wire)[0]
        emit(parse_sample(frame.payload, profile), demo=True)
        index += 1
        if args.count == 0 or index < args.count:
            time.sleep(args.interval_ms / 1000.0)
    return 0


def transact(port, parser: Parser, dev_id: int, dev_type: int,
             cmd: int, seq: int, timeout: float):
    deadline = time.monotonic() + timeout
    wire = build_frame(dev_id, HOST, dev_type, cmd, seq)
    if port.write(wire) != len(wire):
        raise ProtocolError("serial write ended before the complete request was queued")
    # Waiting for a reply already establishes transmission completion.  Avoid
    # flush(), whose driver-level drain may outlive a transaction deadline.
    while (remaining := deadline - time.monotonic()) > 0:
        port.timeout = min(0.02, remaining)
        chunk = port.read(min(port.in_waiting or 1, 256))
        received_at = time.monotonic()
        if received_at > deadline:
            break
        for frame in parser.feed(chunk, now=received_at):
            if (frame.dst == HOST and frame.src == dev_id and
                    frame.cmd == (cmd | CMD_RESPONSE) and frame.seq == seq and
                    (dev_type == TYPE_ANY or frame.dev_type == dev_type)):
                return frame
    raise TimeoutError(f"device {dev_id}, command 0x{cmd:02x}: no matching reply; no sample emitted")


def run_serial(args) -> int:
    try:
        import serial
    except ImportError:
        print("Serial mode needs pyserial: python -m pip install pyserial", file=sys.stderr)
        return 2
    try:
        with serial.Serial(args.port, baudrate=args.baud, bytesize=8, parity="N",
                           stopbits=1, timeout=min(0.02, args.timeout),
                           write_timeout=args.timeout) as port:
            parser = Parser()
            info_frame = transact(port, parser, args.id, TYPE_ANY, CMD_GET_INFO, 1, args.timeout)
            info = parse_info(info_frame.payload)
            if info["dev_id"] != args.id or info_frame.dev_type != info["dev_type"]:
                raise ProtocolError("information payload and frame identity disagree")
            if BAUD_TABLE[info["baud_index"]] != args.baud:
                raise ProtocolError("reported baud does not match this serial connection")
            print(json.dumps({"device_info": info, "source": "serial"}, ensure_ascii=False), file=sys.stderr)
            index = 0
            while args.count == 0 or index < args.count:
                frame = transact(port, parser, args.id, info["dev_type"], CMD_GET_DATA,
                                 (index + 2) & 255, args.timeout)
                emit(parse_sample(frame.payload, info["dev_type"]), demo=False)
                index += 1
                if args.count == 0 or index < args.count:
                    time.sleep(args.interval_ms / 1000.0)
    except (serial.SerialException, OSError, TimeoutError, ProtocolError) as exc:
        print(f"Stopped: {exc}", file=sys.stderr)
        return 2
    return 0


def main() -> int:
    parser = make_parser()
    args = parser.parse_args()
    if not 1 <= args.id <= 254:
        parser.error("--id must be in 1..254")
    if (args.count < 0 or not math.isfinite(args.interval_ms) or args.interval_ms < 0 or
            not math.isfinite(args.timeout) or args.timeout <= 0):
        parser.error("timeout must be finite and positive; count and interval must be finite and nonnegative")
    try:
        return run_serial(args) if args.port else run_demo(args)
    except KeyboardInterrupt:
        return 0


if __name__ == "__main__":
    raise SystemExit(main())
