#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
import struct
import threading
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional

import can

TORQUE_CMD_ID = 0x280
CMD_A1 = 0xA1


def _clamp_int16(x: int) -> int:
    return max(-32768, min(32767, int(x)))


def _pack_i16_le(x: int) -> Tuple[int, int]:
    x = _clamp_int16(x)
    u = x & 0xFFFF
    return (u & 0xFF, (u >> 8) & 0xFF)


def build_torque_broadcast(t1: int, t2: int, t3: int, t4: int) -> can.Message:
    d0, d1 = _pack_i16_le(t1)
    d2, d3 = _pack_i16_le(t2)
    d4, d5 = _pack_i16_le(t3)
    d6, d7 = _pack_i16_le(t4)
    return can.Message(
        arbitration_id=TORQUE_CMD_ID,
        is_extended_id=False,
        data=[d0, d1, d2, d3, d4, d5, d6, d7],
    )


def _arb_id_single(motor_id: int) -> int:
    return 0x140 + int(motor_id)


def parse_a1(data8: bytes) -> Dict:
    # A1 回包（你实测）
    # [0]=0xA1 [1]=temp(int8) [2..3]=iq/power(int16 LE) [4..5]=speed(int16 LE, dps) [6..7]=encoder(uint16 LE)
    if len(data8) != 8 or data8[0] != CMD_A1:
        raise ValueError("not A1")
    temperature = struct.unpack("<b", data8[1:2])[0]
    iq_or_power = struct.unpack("<h", data8[2:4])[0]
    speed_dps = struct.unpack("<h", data8[4:6])[0]
    encoder = struct.unpack("<H", data8[6:8])[0]
    return dict(
        temperature_C=temperature,
        iq_or_power_raw=iq_or_power,
        speed_dps=speed_dps,
        encoder=encoder,
    )


def dump_frame(tag: str, mid: int, fr) -> str:
    if fr.t <= 0:
        return f"{tag} M{mid}: (no reply yet)"
    d = fr.data
    arb = fr.arb
    cmd = d[0]
    if cmd == CMD_A1:
        s = parse_a1(d)
        return (f"{tag} M{mid}: arb=0x{arb:X}  "
                f"T={s['temperature_C']}C  iq/p={s['iq_or_power_raw']}  "
                f"spd={s['speed_dps']}dps  enc={s['encoder']}")
    return f"{tag} M{mid}: arb=0x{arb:X} cmd=0x{cmd:02X} raw={[hex(x) for x in d]}"


@dataclass
class RxFrame:
    t: float = 0.0
    arb: int = 0
    data: bytes = b""


class RxCache4Ch(can.Listener):
    """
    一个 Notifier 监听一个 “设备Bus(包含两个channel)”，按 msg.channel 分流到 ch0/ch1 的缓存。
    """
    def __init__(self, ch_to_motor_ids: Dict[int, List[int]]):
        super().__init__()
        self.ch_to_motor_ids = {int(ch): set(mids) for ch, mids in ch_to_motor_ids.items()}
        self._lock = threading.Lock()
        self.latest: Dict[Tuple[int, int], RxFrame] = {}
        for ch, mids in self.ch_to_motor_ids.items():
            for mid in mids:
                self.latest[(ch, mid)] = RxFrame()

    def on_message_received(self, msg: can.Message):
        # python-can Message.channel：对 canalystii 多通道会填 0/1
        ch = getattr(msg, "channel", None)
        if ch is None:
            # 没有 channel 信息就无法分流，直接忽略或当作 ch0
            ch = 0
        try:
            ch = int(ch)
        except Exception:
            ch = 0

        if ch not in self.ch_to_motor_ids:
            return

        arb = msg.arbitration_id
        if arb < 0x141 or arb > 0x160:
            return
        mid = arb - 0x140
        if mid not in self.ch_to_motor_ids[ch]:
            return

        with self._lock:
            self.latest[(ch, mid)] = RxFrame(time.time(), arb, bytes(msg.data))

    def snapshot(self) -> Dict[Tuple[int, int], RxFrame]:
        with self._lock:
            return dict(self.latest)


class SenderThread(threading.Thread):
    """
    每个“逻辑channel(共4个)”一个发送线程：
    - 等主线程 tick_event
    - barrier 同步放行
    - 加 bus_lock 后 send（同一个设备的两个channel共享一个bus对象，必须加锁）
    """
    def __init__(
        self,
        name: str,
        bus: can.BusABC,
        bus_lock: threading.Lock,
        channel_index: int,         # 0 or 1
        tick_event: threading.Event,
        barrier: threading.Barrier,
        stop_event: threading.Event,
    ):
        super().__init__(name=name)
        self.bus = bus
        self.bus_lock = bus_lock
        self.channel_index = int(channel_index)
        self.tick_event = tick_event
        self.barrier = barrier
        self.stop_event = stop_event
        self.torque = (0, 0, 0, 0)

    def run(self):
        while not self.stop_event.is_set():
            if not self.tick_event.wait(timeout=0.5):
                continue
            if self.stop_event.is_set():
                break

            try:
                self.barrier.wait(timeout=0.05)
            except threading.BrokenBarrierError:
                continue

            t1, t2, t3, t4 = self.torque
            msg = build_torque_broadcast(t1, t2, t3, t4)
            msg.channel = self.channel_index  # ★ 指定发到该设备的哪个channel

            try:
                with self.bus_lock:
                    self.bus.send(msg)
            except can.CanError:
                pass

            # 等主线程清 tick_event，避免同tick重复发
            while self.tick_event.is_set() and not self.stop_event.is_set():
                time.sleep(0)


def open_device_bus(device_idx: int, bitrate: int) -> can.BusABC:
    """
    每块 CANalyst-II 只打开一次 Bus，channel=[0,1]
    """
    # canalystii 后端一般支持 device=0/1；如果你环境不支持会 TypeError，这里做兼容
    try:
        return can.Bus(interface="canalystii", channel=[0, 1], bitrate=bitrate, device=device_idx)
    except TypeError:
        # 某些版本 canalystii 不暴露 device 参数（只能看到一块设备）
        return can.Bus(interface="canalystii", channel=[0, 1], bitrate=bitrate)


def main():
    bitrate = 1000000
    hz = 500
    dt = 1.0 / hz

    # ========= 你需要按实际接线修改：每个“设备-channel”上有哪些电机ID =========
    # 逻辑4路：dev0-ch0, dev0-ch1, dev1-ch0, dev1-ch1
    motor_map = {
        ("dev0", 0): [1, 2, 3, 4],
        ("dev0", 1): [1, 2, 3, 4],
        ("dev1", 0): [1, 2, 3, 4],
        ("dev1", 1): [1, 2, 3, 4],
    }

    # ========= 打开两块设备（每块一次性打开2路channel） =========
    bus0 = open_device_bus(device_idx=0, bitrate=bitrate)
    bus1 = open_device_bus(device_idx=1, bitrate=bitrate)

    # 过滤：只收我们关心的 arb_id（两设备相同过滤即可）
    def set_filters_for(bus: can.BusABC, all_motor_ids: List[int]):
        try:
            bus.set_filters([{"can_id": _arb_id_single(i), "can_mask": 0x7FF, "extended": False} for i in all_motor_ids])
        except Exception:
            pass

    all_ids_dev0 = sorted(set(motor_map[("dev0", 0)] + motor_map[("dev0", 1)]))
    all_ids_dev1 = sorted(set(motor_map[("dev1", 0)] + motor_map[("dev1", 1)]))
    set_filters_for(bus0, all_ids_dev0)
    set_filters_for(bus1, all_ids_dev1)

    # ========= 接收缓存：每个设备一个 Notifier，一个 cache（按 channel 分流） =========
    cache0 = RxCache4Ch({0: motor_map[("dev0", 0)], 1: motor_map[("dev0", 1)]})
    cache1 = RxCache4Ch({0: motor_map[("dev1", 0)], 1: motor_map[("dev1", 1)]})
    notifier0 = can.Notifier(bus0, [cache0], timeout=0.01)
    notifier1 = can.Notifier(bus1, [cache1], timeout=0.01)

    # ========= 发送线程：4个逻辑channel =========
    stop_event = threading.Event()
    tick_event = threading.Event()
    barrier = threading.Barrier(4 + 1)  # 4个sender + 主线程

    bus0_lock = threading.Lock()
    bus1_lock = threading.Lock()

    sender_dev0_ch0 = SenderThread("sender-dev0-ch0", bus0, bus0_lock, 0, tick_event, barrier, stop_event)
    sender_dev0_ch1 = SenderThread("sender-dev0-ch1", bus0, bus0_lock, 1, tick_event, barrier, stop_event)
    sender_dev1_ch0 = SenderThread("sender-dev1-ch0", bus1, bus1_lock, 0, tick_event, barrier, stop_event)
    sender_dev1_ch1 = SenderThread("sender-dev1-ch1", bus1, bus1_lock, 1, tick_event, barrier, stop_event)

    senders = [sender_dev0_ch0, sender_dev0_ch1, sender_dev1_ch0, sender_dev1_ch1]
    for th in senders:
        th.start()

    # ========= 你的控制输出：这里演示固定 torque =========
    def torque_for(tag: str, ch: int) -> Tuple[int, int, int, int]:
        # TODO：替换为你的控制算法输出
        return (30, 30, 30, 30)

    k = 0
    next_tick = time.perf_counter()

    try:
        while True:
            # 1) 写入4路 torque（主线程写共享变量）
            sender_dev0_ch0.torque = torque_for("dev0", 0)
            sender_dev0_ch1.torque = torque_for("dev0", 1)
            sender_dev1_ch0.torque = torque_for("dev1", 0)
            sender_dev1_ch1.torque = torque_for("dev1", 1)

            # 2) 同步tick发布 + barrier放行（尽量同时发送）
            tick_event.set()
            try:
                barrier.wait(timeout=0.05)
            except threading.BrokenBarrierError:
                pass
            tick_event.clear()

            # 3) 打印（每100ms一次）
            if k % int(hz * 0.1) == 0:
                print(f"\n==== tick {k}  t={time.time():.3f} ====")

                snap0 = cache0.snapshot()
                print("[dev0-ch0]")
                for mid in motor_map[("dev0", 0)]:
                    print("  " + dump_frame("[dev0-ch0]", mid, snap0[(0, mid)]))
                print("[dev0-ch1]")
                for mid in motor_map[("dev0", 1)]:
                    print("  " + dump_frame("[dev0-ch1]", mid, snap0[(1, mid)]))

                snap1 = cache1.snapshot()
                print("[dev1-ch0]")
                for mid in motor_map[("dev1", 0)]:
                    print("  " + dump_frame("[dev1-ch0]", mid, snap1[(0, mid)]))
                print("[dev1-ch1]")
                for mid in motor_map[("dev1", 1)]:
                    print("  " + dump_frame("[dev1-ch1]", mid, snap1[(1, mid)]))

            k += 1

            # 4) 500Hz 固定周期（绝对时间推进）
            next_tick += dt
            now = time.perf_counter()
            if next_tick > now:
                time.sleep(next_tick - now)
            else:
                next_tick = now

    except KeyboardInterrupt:
        pass
    finally:
        # 停止发送线程
        stop_event.set()
        tick_event.set()
        for th in senders:
            th.join(timeout=0.5)

        # 停止 Notifier（避免你之前那种 interpreter shutdown 崩溃）
        try:
            notifier0.stop()
            notifier1.stop()
        except Exception:
            pass

        # 安全清零力矩（给每个设备两个channel都发一次）
        try:
            for ch in [0, 1]:
                m = build_torque_broadcast(0, 0, 0, 0)
                m.channel = ch
                with bus0_lock:
                    bus0.send(m)
                with bus1_lock:
                    bus1.send(m)
        except Exception:
            pass

        try:
            bus0.shutdown()
            bus1.shutdown()
        except Exception:
            pass


if __name__ == "__main__":
    main()
