import time
import struct
from collections import deque
from typing import NamedTuple, cast
from collections.abc import Callable, Generator
from enum import IntEnum
from functools import partial

from opendbc.car.carlog import carlog


class SERVICE_TYPE(IntEnum):
  DIAGNOSTIC_SESSION_CONTROL = 0x10
  ECU_RESET = 0x11
  SECURITY_ACCESS = 0x27
  COMMUNICATION_CONTROL = 0x28
  TESTER_PRESENT = 0x3E
  ACCESS_TIMING_PARAMETER = 0x83
  SECURED_DATA_TRANSMISSION = 0x84
  CONTROL_DTC_SETTING = 0x85
  RESPONSE_ON_EVENT = 0x86
  LINK_CONTROL = 0x87
  READ_DATA_BY_IDENTIFIER = 0x22
  READ_MEMORY_BY_ADDRESS = 0x23
  READ_SCALING_DATA_BY_IDENTIFIER = 0x24
  READ_DATA_BY_PERIODIC_IDENTIFIER = 0x2A
  DYNAMICALLY_DEFINE_DATA_IDENTIFIER = 0x2C
  WRITE_DATA_BY_IDENTIFIER = 0x2E
  WRITE_MEMORY_BY_ADDRESS = 0x3D
  CLEAR_DIAGNOSTIC_INFORMATION = 0x14
  READ_DTC_INFORMATION = 0x19
  INPUT_OUTPUT_CONTROL_BY_IDENTIFIER = 0x2F
  ROUTINE_CONTROL = 0x31
  REQUEST_DOWNLOAD = 0x34
  REQUEST_UPLOAD = 0x35
  TRANSFER_DATA = 0x36
  REQUEST_TRANSFER_EXIT = 0x37

class SESSION_TYPE(IntEnum):
  DEFAULT = 1
  PROGRAMMING = 2
  EXTENDED_DIAGNOSTIC = 3
  SAFETY_SYSTEM_DIAGNOSTIC = 4

class RESET_TYPE(IntEnum):
  HARD = 1
  KEY_OFF_ON = 2
  SOFT = 3
  ENABLE_RAPID_POWER_SHUTDOWN = 4
  DISABLE_RAPID_POWER_SHUTDOWN = 5

class ACCESS_TYPE(IntEnum):
  REQUEST_SEED = 1
  SEND_KEY = 2

class CONTROL_TYPE(IntEnum):
  ENABLE_RX_ENABLE_TX = 0
  ENABLE_RX_DISABLE_TX = 1
  DISABLE_RX_ENABLE_TX = 2
  DISABLE_RX_DISABLE_TX = 3

class MESSAGE_TYPE(IntEnum):
  NORMAL = 1
  NETWORK_MANAGEMENT = 2
  NORMAL_AND_NETWORK_MANAGEMENT = 3

class TIMING_PARAMETER_TYPE(IntEnum):
  READ_EXTENDED_SET = 1
  SET_TO_DEFAULT_VALUES = 2
  READ_CURRENTLY_ACTIVE = 3
  SET_TO_GIVEN_VALUES = 4

class DTC_SETTING_TYPE(IntEnum):
  ON = 1
  OFF = 2

class RESPONSE_EVENT_TYPE(IntEnum):
  STOP_RESPONSE_ON_EVENT = 0
  ON_DTC_STATUS_CHANGE = 1
  ON_TIMER_INTERRUPT = 2
  ON_CHANGE_OF_DATA_IDENTIFIER = 3
  REPORT_ACTIVATED_EVENTS = 4
  START_RESPONSE_ON_EVENT = 5
  CLEAR_RESPONSE_ON_EVENT = 6
  ON_COMPARISON_OF_VALUES = 7

class LINK_CONTROL_TYPE(IntEnum):
  VERIFY_BAUDRATE_TRANSITION_WITH_FIXED_BAUDRATE = 1
  VERIFY_BAUDRATE_TRANSITION_WITH_SPECIFIC_BAUDRATE = 2
  TRANSITION_BAUDRATE = 3

class BAUD_RATE_TYPE(IntEnum):
  PC9600 = 1
  PC19200 = 2
  PC38400 = 3
  PC57600 = 4
  PC115200 = 5
  CAN125000 = 16
  CAN250000 = 17
  CAN500000 = 18
  CAN1000000 = 19

class DATA_IDENTIFIER_TYPE(IntEnum):
  BOOT_SOFTWARE_IDENTIFICATION = 0xF180
  APPLICATION_SOFTWARE_IDENTIFICATION = 0xF181
  APPLICATION_DATA_IDENTIFICATION = 0xF182
  BOOT_SOFTWARE_FINGERPRINT = 0xF183
  APPLICATION_SOFTWARE_FINGERPRINT = 0xF184
  APPLICATION_DATA_FINGERPRINT = 0xF185
  ACTIVE_DIAGNOSTIC_SESSION = 0xF186
  VEHICLE_MANUFACTURER_SPARE_PART_NUMBER = 0xF187
  VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER = 0xF188
  VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER = 0xF189
  SYSTEM_SUPPLIER_IDENTIFIER = 0xF18A
  ECU_MANUFACTURING_DATE = 0xF18B
  ECU_SERIAL_NUMBER = 0xF18C
  SUPPORTED_FUNCTIONAL_UNITS = 0xF18D
  VEHICLE_MANUFACTURER_KIT_ASSEMBLY_PART_NUMBER = 0xF18E
  VIN = 0xF190
  VEHICLE_MANUFACTURER_ECU_HARDWARE_NUMBER = 0xF191
  SYSTEM_SUPPLIER_ECU_HARDWARE_NUMBER = 0xF192
  SYSTEM_SUPPLIER_ECU_HARDWARE_VERSION_NUMBER = 0xF193
  SYSTEM_SUPPLIER_ECU_SOFTWARE_NUMBER = 0xF194
  SYSTEM_SUPPLIER_ECU_SOFTWARE_VERSION_NUMBER = 0xF195
  EXHAUST_REGULATION_OR_TYPE_APPROVAL_NUMBER = 0xF196
  SYSTEM_NAME_OR_ENGINE_TYPE = 0xF197
  REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER = 0xF198
  PROGRAMMING_DATE = 0xF199
  CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER = 0xF19A
  CALIBRATION_DATE = 0xF19B
  CALIBRATION_EQUIPMENT_SOFTWARE_NUMBER = 0xF19C
  ECU_INSTALLATION_DATE = 0xF19D
  ODX_FILE = 0xF19E
  ENTITY = 0xF19F

class TRANSMISSION_MODE_TYPE(IntEnum):
  SEND_AT_SLOW_RATE = 1
  SEND_AT_MEDIUM_RATE = 2
  SEND_AT_FAST_RATE = 3
  STOP_SENDING = 4

class DYNAMIC_DEFINITION_TYPE(IntEnum):
  DEFINE_BY_IDENTIFIER = 1
  DEFINE_BY_MEMORY_ADDRESS = 2
  CLEAR_DYNAMICALLY_DEFINED_DATA_IDENTIFIER = 3

class ISOTP_FRAME_TYPE(IntEnum):
  SINGLE = 0
  FIRST = 1
  CONSECUTIVE = 2
  FLOW = 3

class DynamicSourceDefinition(NamedTuple):
  data_identifier: int
  position: int
  memory_size: int
  memory_address: int

class DTC_GROUP_TYPE(IntEnum):
  EMISSIONS = 0x000000
  ALL = 0xFFFFFF

class DTC_REPORT_TYPE(IntEnum):
  NUMBER_OF_DTC_BY_STATUS_MASK = 0x01
  DTC_BY_STATUS_MASK = 0x02
  DTC_SNAPSHOT_IDENTIFICATION = 0x03
  DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER = 0x04
  DTC_SNAPSHOT_RECORD_BY_RECORD_NUMBER = 0x05
  DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER = 0x06
  NUMBER_OF_DTC_BY_SEVERITY_MASK_RECORD = 0x07
  DTC_BY_SEVERITY_MASK_RECORD = 0x08
  SEVERITY_INFORMATION_OF_DTC = 0x09
  SUPPORTED_DTC = 0x0A
  FIRST_TEST_FAILED_DTC = 0x0B
  FIRST_CONFIRMED_DTC = 0x0C
  MOST_RECENT_TEST_FAILED_DTC = 0x0D
  MOST_RECENT_CONFIRMED_DTC = 0x0E
  MIRROR_MEMORY_DTC_BY_STATUS_MASK = 0x0F
  MIRROR_MEMORY_DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER = 0x10
  NUMBER_OF_MIRROR_MEMORY_DTC_BY_STATUS_MASK = 0x11
  NUMBER_OF_EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK = 0x12
  EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK = 0x13
  DTC_FAULT_DETECTION_COUNTER = 0x14
  DTC_WITH_PERMANENT_STATUS = 0x15

class DTC_STATUS_MASK_TYPE(IntEnum):
  TEST_FAILED = 0x01
  TEST_FAILED_THIS_OPERATION_CYCLE = 0x02
  PENDING_DTC = 0x04
  CONFIRMED_DTC = 0x08
  TEST_NOT_COMPLETED_SINCE_LAST_CLEAR = 0x10
  TEST_FAILED_SINCE_LAST_CLEAR = 0x20
  TEST_NOT_COMPLETED_THIS_OPERATION_CYCLE = 0x40
  WARNING_INDICATOR_REQUESTED = 0x80
  ALL = 0xFF

class DTC_SEVERITY_MASK_TYPE(IntEnum):
  MAINTENANCE_ONLY = 0x20
  CHECK_AT_NEXT_HALT = 0x40
  CHECK_IMMEDIATELY = 0x80
  ALL = 0xE0

class CONTROL_PARAMETER_TYPE(IntEnum):
  RETURN_CONTROL_TO_ECU = 0
  RESET_TO_DEFAULT = 1
  FREEZE_CURRENT_STATE = 2
  SHORT_TERM_ADJUSTMENT = 3

class ROUTINE_CONTROL_TYPE(IntEnum):
  START = 1
  STOP = 2
  REQUEST_RESULTS = 3

class ROUTINE_IDENTIFIER_TYPE(IntEnum):
  ERASE_MEMORY = 0xFF00
  CHECK_PROGRAMMING_DEPENDENCIES = 0xFF01
  ERASE_MIRROR_MEMORY_DTCS = 0xFF02

class MessageTimeoutError(Exception):
  pass

class NegativeResponseError(Exception):
  def __init__(self, message, service_id, error_code):
    super().__init__()
    self.message = message
    self.service_id = service_id
    self.error_code = error_code

  def __str__(self):
    return self.message

class InvalidServiceIdError(Exception):
  pass

class InvalidSubFunctionError(Exception):
  pass

class InvalidSubAddressError(Exception):
  pass

_negative_response_codes = {
    0x00: 'positive response',
    0x10: 'general reject',
    0x11: 'service not supported',
    0x12: 'sub-function not supported',
    0x13: 'incorrect message length or invalid format',
    0x14: 'response too long',
    0x21: 'busy repeat request',
    0x22: 'conditions not correct',
    0x24: 'request sequence error',
    0x25: 'no response from subnet component',
    0x26: 'failure prevents execution of requested action',
    0x31: 'request out of range',
    0x33: 'security access denied',
    0x35: 'invalid key',
    0x36: 'exceed number of attempts',
    0x37: 'required time delay not expired',
    0x70: 'upload download not accepted',
    0x71: 'transfer data suspended',
    0x72: 'general programming failure',
    0x73: 'wrong block sequence counter',
    0x78: 'request correctly received - response pending',
    0x7e: 'sub-function not supported in active session',
    0x7f: 'service not supported in active session',
    0x81: 'rpm too high',
    0x82: 'rpm too low',
    0x83: 'engine is running',
    0x84: 'engine is not running',
    0x85: 'engine run time too low',
    0x86: 'temperature too high',
    0x87: 'temperature too low',
    0x88: 'vehicle speed too high',
    0x89: 'vehicle speed too low',
    0x8a: 'throttle/pedal too high',
    0x8b: 'throttle/pedal too low',
    0x8c: 'transmission not in neutral',
    0x8d: 'transmission not in gear',
    0x8f: 'brake switch(es) not closed',
    0x90: 'shifter lever not in park',
    0x91: 'torque converter clutch locked',
    0x92: 'voltage too high',
    0x93: 'voltage too low',
}

def get_dtc_num_as_str(dtc_num_bytes):
  # ISO 15031-6
  designator = {
    0b00: "P",
    0b01: "C",
    0b10: "B",
    0b11: "U",
  }
  d = designator[dtc_num_bytes[0] >> 6]
  n = bytes([dtc_num_bytes[0] & 0x3F]) + dtc_num_bytes[1:]
  return d + n.hex()

def get_dtc_status_names(status):
  result = list()
  for m in DTC_STATUS_MASK_TYPE:
    if m == DTC_STATUS_MASK_TYPE.ALL:
      continue
    if status & m.value:
      result.append(m.name)
  return result

class CanClient:
  def __init__(self, can_send: Callable[[int, bytes, int], None], can_recv: Callable[[], list[tuple[int, bytes, int]]],
               tx_addr: int, rx_addr: int, bus: int, sub_addr: int | None = None):
    self.tx = can_send
    self.rx = can_recv
    self.tx_addr = tx_addr
    self.rx_addr = rx_addr
    self.rx_buff: deque[bytes] = deque()
    self.sub_addr = sub_addr
    self.bus = bus

  def _recv_filter(self, bus: int, addr: int) -> bool:
    # handle functional addresses (switch to first addr to respond)
    if self.tx_addr == 0x7DF:
      is_response = addr >= 0x7E8 and addr <= 0x7EF
      if is_response:
        carlog.debug(f"switch to physical addr {hex(addr)}")
        self.tx_addr = addr - 8
        self.rx_addr = addr
      return is_response
    if self.tx_addr == 0x18DB33F1:
      is_response = addr >= 0x18DAF100 and addr <= 0x18DAF1FF
      if is_response:
        carlog.debug(f"switch to physical addr {hex(addr)}")
        self.tx_addr = 0x18DA00F1 + (addr << 8 & 0xFF00)
        self.rx_addr = addr
    return bus == self.bus and addr == self.rx_addr

  def _recv_buffer(self, drain: bool = False) -> None:
    while True:
      msgs = self.rx()
      if drain:
        carlog.debug(f"CAN-RX: drain - {len(msgs)}")
        self.rx_buff.clear()
      else:
        for rx_addr, rx_data, rx_bus in msgs or []:
          if self._recv_filter(rx_bus, rx_addr) and len(rx_data) > 0:
            rx_data = bytes(rx_data)  # convert bytearray to bytes

            carlog.debug(f"CAN-RX: {hex(rx_addr)} - 0x{bytes.hex(rx_data)}")

            # Cut off sub addr in first byte
            if self.sub_addr is not None:
              if rx_data[0] != self.sub_addr:
                raise InvalidSubAddressError(f"isotp - rx: invalid sub-address: {rx_data[0]}, expected: {self.sub_addr}")
              rx_data = rx_data[1:]

            self.rx_buff.append(rx_data)
      # break when non-full buffer is processed
      if len(msgs) < 254:
        return

  def recv(self, drain: bool = False) -> Generator[bytes, None, None]:
    # buffer rx messages in case two response messages are received at once
    # (e.g. response pending and success/failure response)
    self._recv_buffer(drain)
    try:
      while True:
        yield self.rx_buff.popleft()
    except IndexError:
      pass  # empty

  def send(self, msgs: list[bytes], delay: float = 0) -> None:
    for i, msg in enumerate(msgs):
      if delay and i != 0:
        carlog.debug(f"CAN-TX: delay - {delay}")
        time.sleep(delay)

      if self.sub_addr is not None:
        msg = bytes([self.sub_addr]) + msg

      carlog.debug(f"CAN-TX: {hex(self.tx_addr)} - 0x{bytes.hex(msg)}")
      assert len(msg) <= 8

      self.tx(self.tx_addr, msg, self.bus)
      # prevent rx buffer from overflowing on large tx
      if i % 10 == 9:
        self._recv_buffer()

class IsoTpMessage:
  def __init__(self, can_client: CanClient, timeout: float = 1, single_frame_mode: bool = False, separation_time: float = 0):
    self._can_client = can_client
    self.timeout = timeout
    self.single_frame_mode = single_frame_mode
    self.max_len = 8 if self._can_client.sub_addr is None else 7

    # <= 127, separation time in milliseconds
    # 0xF1 to 0xF9 UF, 100 to 900 microseconds
    if 1e-4 <= separation_time <= 9e-4:
      offset = int(round(separation_time, 4) * 1e4) - 1
      separation_time = 0xF1 + offset
    elif 0 <= separation_time <= 0.127:
      separation_time = round(separation_time * 1000)
    else:
      raise Exception("Separation time not in range")

    self.flow_control_msg = bytes([
      0x30,  # flow control
      0x01 if self.single_frame_mode else 0x00,  # block size
      separation_time,
    ]).ljust(self.max_len, b"\x00")

  def send(self, dat: bytes, setup_only: bool = False) -> None:
    # throw away any stale data
    self._can_client.recv(drain=True)

    self.tx_dat = dat
    self.tx_len = len(dat)
    self.tx_idx = 0
    self.tx_done = False

    self.rx_dat = b""
    self.rx_len = 0
    self.rx_idx = 0
    self.rx_done = False

    if not setup_only:
      carlog.debug(f"ISO-TP: REQUEST - {hex(self._can_client.tx_addr)} 0x{bytes.hex(self.tx_dat)}")
    self._tx_first_frame(setup_only=setup_only)

  def _tx_first_frame(self, setup_only: bool = False) -> None:
    if self.tx_len < self.max_len:
      # single frame (send all bytes)
      if not setup_only:
        carlog.debug(f"ISO-TP: TX - single frame - {hex(self._can_client.tx_addr)}")
      msg = (bytes([self.tx_len]) + self.tx_dat).ljust(self.max_len, b"\x00")
      self.tx_done = True
    else:
      # first frame (send first 6 bytes)
      if not setup_only:
        carlog.debug(f"ISO-TP: TX - first frame - {hex(self._can_client.tx_addr)}")
      msg = (struct.pack("!H", 0x1000 | self.tx_len) + self.tx_dat[:self.max_len - 2]).ljust(self.max_len - 2, b"\x00")
    if not setup_only:
      self._can_client.send([msg])

  def recv(self, timeout=None) -> tuple[bytes | None, bool]:
    if timeout is None:
      timeout = self.timeout

    start_time = time.monotonic()
    rx_in_progress = False
    try:
      while True:
        for msg in self._can_client.recv():
          frame_type = self._isotp_rx_next(msg)
          start_time = time.monotonic()
          # Anything that signifies we're building a response
          rx_in_progress = frame_type in (ISOTP_FRAME_TYPE.FIRST, ISOTP_FRAME_TYPE.CONSECUTIVE)
          if self.tx_done and self.rx_done:
            return self.rx_dat, False
        # no timeout indicates non-blocking
        if timeout == 0:
          return None, rx_in_progress
        if time.monotonic() - start_time > timeout:
          raise MessageTimeoutError("timeout waiting for response")
    finally:
      if self.rx_dat:
        carlog.debug(f"ISO-TP: RESPONSE - {hex(self._can_client.rx_addr)} 0x{bytes.hex(self.rx_dat)}")

  def _isotp_rx_next(self, rx_data: bytes) -> ISOTP_FRAME_TYPE:
    # TODO: Handle CAN frame data optimization, which is allowed with some frame types
    # # ISO 15765-2 specifies an eight byte CAN frame for ISO-TP communication
    # assert len(rx_data) == self.max_len, f"isotp - rx: invalid CAN frame length: {len(rx_data)}"

    if rx_data[0] >> 4 == ISOTP_FRAME_TYPE.SINGLE:
      assert self.rx_dat == b"" or self.rx_done, "isotp - rx: single frame with active frame"

      # "if the first byte is 0x00, then it's a CAN-FD SF, and the second byte specifies the size of the data."
      # - https://en.wikipedia.org/wiki/CAN_FD
      if rx_data[0] & 0x0F == 0 and len(rx_data) > 8:
        self.rx_len = rx_data[1]
        offset = 2
        # TODO: update self.max_len for CAN FD
        max_len = 62 if self._can_client.sub_addr is None else 61
        assert self.rx_len <= max_len, f"isotp - rx: invalid single frame length: {self.rx_len}"
      else:
        self.rx_len = rx_data[0] & 0x0F
        offset = 1
        assert self.rx_len < self.max_len, f"isotp - rx: invalid single frame length: {self.rx_len}"

      self.rx_dat = rx_data[offset:offset + self.rx_len]
      self.rx_idx = 0
      self.rx_done = True
      carlog.debug(f"ISO-TP: RX - single frame - {hex(self._can_client.rx_addr)} idx={self.rx_idx} done={self.rx_done}")
      return ISOTP_FRAME_TYPE.SINGLE

    elif rx_data[0] >> 4 == ISOTP_FRAME_TYPE.FIRST:
      # TODO: support CAN FD first frames
      # Once a first frame is received, further frames must be consecutive
      assert self.rx_dat == b"" or self.rx_done, "isotp - rx: first frame with active frame"
      self.rx_len = ((rx_data[0] & 0x0F) << 8) + rx_data[1]
      assert self.rx_len >= self.max_len, f"isotp - rx: invalid first frame length: {self.rx_len}"
      assert len(rx_data) == self.max_len, f"isotp - rx: invalid CAN frame length: {len(rx_data)}"
      self.rx_dat = rx_data[2:]
      self.rx_idx = 0
      self.rx_done = False
      carlog.debug(f"ISO-TP: RX - first frame - {hex(self._can_client.rx_addr)} idx={self.rx_idx} done={self.rx_done}")
      carlog.debug(f"ISO-TP: TX - flow control continue - {hex(self._can_client.tx_addr)}")
      # send flow control message
      self._can_client.send([self.flow_control_msg])
      return ISOTP_FRAME_TYPE.FIRST

    elif rx_data[0] >> 4 == ISOTP_FRAME_TYPE.CONSECUTIVE:
      assert not self.rx_done, "isotp - rx: consecutive frame with no active frame"
      self.rx_idx += 1
      assert self.rx_idx & 0xF == rx_data[0] & 0xF, "isotp - rx: invalid consecutive frame index"
      rx_size = self.rx_len - len(self.rx_dat)
      self.rx_dat += rx_data[1:1 + rx_size]
      if self.rx_len == len(self.rx_dat):
        self.rx_done = True
      elif self.single_frame_mode:
        # notify ECU to send next frame
        self._can_client.send([self.flow_control_msg])
      carlog.debug(f"ISO-TP: RX - consecutive frame - {hex(self._can_client.rx_addr)} idx={self.rx_idx} done={self.rx_done}")
      return ISOTP_FRAME_TYPE.CONSECUTIVE

    elif rx_data[0] >> 4 == ISOTP_FRAME_TYPE.FLOW:
      assert not self.tx_done, "isotp - rx: flow control with no active frame"
      assert rx_data[0] != 0x32, "isotp - rx: flow-control overflow/abort"
      assert rx_data[0] == 0x30 or rx_data[0] == 0x31, "isotp - rx: flow-control transfer state indicator invalid"
      if rx_data[0] == 0x30:
        carlog.debug(f"ISO-TP: RX - flow control continue - {hex(self._can_client.tx_addr)}")
        delay_ts = rx_data[2] & 0x7F
        # scale is 1 milliseconds if first bit == 0, 100 micro seconds if first bit == 1
        delay_div = 1000. if rx_data[2] & 0x80 == 0 else 10000.
        delay_sec = delay_ts / delay_div

        # first frame = 6 bytes, each consecutive frame = 7 bytes
        num_bytes = self.max_len - 1
        start = self.max_len - 2 + self.tx_idx * num_bytes
        count = rx_data[1]
        end = start + count * num_bytes if count > 0 else self.tx_len
        tx_msgs = []
        for i in range(start, end, num_bytes):
          self.tx_idx += 1
          # consecutive tx messages
          msg = (bytes([0x20 | (self.tx_idx & 0xF)]) + self.tx_dat[i:i + num_bytes]).ljust(self.max_len, b"\x00")
          tx_msgs.append(msg)
        # send consecutive tx messages
        self._can_client.send(tx_msgs, delay=delay_sec)
        if end >= self.tx_len:
          self.tx_done = True
        carlog.debug(f"ISO-TP: TX - consecutive frame - {hex(self._can_client.tx_addr)} idx={self.tx_idx} done={self.tx_done}")
      elif rx_data[0] == 0x31:
        # wait (do nothing until next flow control message)
        carlog.debug(f"ISO-TP: TX - flow control wait - {hex(self._can_client.tx_addr)}")
      return ISOTP_FRAME_TYPE.FLOW

    # 4-15 - reserved
    else:
      raise Exception(f"isotp - rx: invalid frame type: {rx_data[0] >> 4}")


FUNCTIONAL_ADDRS = [0x7DF, 0x18DB33F1]


def get_rx_addr_for_tx_addr(tx_addr, rx_offset=0x8):
  if tx_addr in FUNCTIONAL_ADDRS:
    return None

  if tx_addr < 0xFFF8:
    # pseudo-standard 11 bit response addr (add 8) works for most manufacturers
    # allow override; some manufacturers use other offsets for non-OBD2 access
    return tx_addr + rx_offset

  if tx_addr > 0x10000000 and tx_addr < 0xFFFFFFFF:
    # standard 29 bit response addr (flip last two bytes)
    return (tx_addr & 0xFFFF0000) + (tx_addr << 8 & 0xFF00) + (tx_addr >> 8 & 0xFF)

  raise ValueError(f"invalid tx_addr: {tx_addr}")


class UdsClient:
  def __init__(self, panda, tx_addr: int, rx_addr: int | None = None, bus: int = 0, sub_addr: int | None = None, timeout: float = 1,
               tx_timeout: float = 1, response_pending_timeout: float = 10):
    self.bus = bus
    self.tx_addr = tx_addr
    self.rx_addr = rx_addr if rx_addr is not None else get_rx_addr_for_tx_addr(tx_addr)
    self.sub_addr = sub_addr
    self.timeout = timeout
    can_send_with_timeout = partial(panda.can_send, timeout=int(tx_timeout*1000))
    self._can_client = CanClient(can_send_with_timeout, panda.can_recv, self.tx_addr, self.rx_addr, self.bus, self.sub_addr)
    self.response_pending_timeout = response_pending_timeout

  # generic uds request
  def _uds_request(self, service_type: SERVICE_TYPE, subfunction: int | None = None, data: bytes | None = None) -> bytes:
    req = bytes([service_type])
    if subfunction is not None:
      req += bytes([subfunction])
    if data is not None:
      req += data

    # send request, wait for response
    isotp_msg = IsoTpMessage(self._can_client, timeout=self.timeout)
    isotp_msg.send(req)
    response_pending = False
    while True:
      timeout = self.response_pending_timeout if response_pending else self.timeout
      resp, _ = isotp_msg.recv(timeout)

      if resp is None:
        continue

      response_pending = False
      resp_sid = resp[0] if len(resp) > 0 else None

      # negative response
      if resp_sid == 0x7F:
        service_id = resp[1] if len(resp) > 1 else -1
        try:
          service_desc = SERVICE_TYPE(service_id).name
        except BaseException:
          service_desc = 'NON_STANDARD_SERVICE'
        error_code = resp[2] if len(resp) > 2 else -1
        try:
          error_desc = _negative_response_codes[error_code]
        except BaseException:
          error_desc = resp[3:].hex()
        # wait for another message if response pending
        if error_code == 0x78:
          response_pending = True
          carlog.debug("UDS-RX: response pending")
          continue
        raise NegativeResponseError(f'{service_desc} - {error_desc}', service_id, error_code)

      # positive response
      if service_type + 0x40 != resp_sid:
        resp_sid_hex = hex(resp_sid) if resp_sid is not None else None
        raise InvalidServiceIdError(f'invalid response service id: {resp_sid_hex}')

      if subfunction is not None:
        resp_sfn = resp[1] if len(resp) > 1 else None
        if subfunction != resp_sfn:
          resp_sfn_hex = hex(resp_sfn) if resp_sfn is not None else None
          raise InvalidSubFunctionError(f'invalid response subfunction: {resp_sfn_hex}')

      # return data (exclude service id and sub-function id)
      return resp[(1 if subfunction is None else 2):]

  # services
  def diagnostic_session_control(self, session_type: SESSION_TYPE):
    self._uds_request(SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL, subfunction=session_type)

  def ecu_reset(self, reset_type: RESET_TYPE):
    resp = self._uds_request(SERVICE_TYPE.ECU_RESET, subfunction=reset_type)
    power_down_time = None
    if reset_type == RESET_TYPE.ENABLE_RAPID_POWER_SHUTDOWN:
      power_down_time = resp[0]
      return power_down_time

  def security_access(self, access_type: ACCESS_TYPE, security_key: bytes = b'', data_record: bytes = b''):
    request_seed = access_type % 2 != 0
    if request_seed and len(security_key) != 0:
      raise ValueError('security_key not allowed')
    if not request_seed and len(security_key) == 0:
      raise ValueError('security_key is missing')
    if not request_seed and len(data_record) != 0:
      raise ValueError('data_record not allowed')
    data = security_key + data_record
    resp = self._uds_request(SERVICE_TYPE.SECURITY_ACCESS, subfunction=access_type, data=data)
    if request_seed:
      security_seed = resp
      return security_seed

  def communication_control(self, control_type: CONTROL_TYPE, message_type: MESSAGE_TYPE):
    data = bytes([message_type])
    self._uds_request(SERVICE_TYPE.COMMUNICATION_CONTROL, subfunction=control_type, data=data)

  def tester_present(self, ):
    self._uds_request(SERVICE_TYPE.TESTER_PRESENT, subfunction=0x00)

  def access_timing_parameter(self, timing_parameter_type: TIMING_PARAMETER_TYPE, parameter_values: bytes | None = None):
    write_custom_values = timing_parameter_type == TIMING_PARAMETER_TYPE.SET_TO_GIVEN_VALUES
    read_values = (timing_parameter_type == TIMING_PARAMETER_TYPE.READ_CURRENTLY_ACTIVE or
                   timing_parameter_type == TIMING_PARAMETER_TYPE.READ_EXTENDED_SET)
    if not write_custom_values and parameter_values is not None:
      raise ValueError('parameter_values not allowed')
    if write_custom_values and parameter_values is None:
      raise ValueError('parameter_values is missing')
    resp = self._uds_request(SERVICE_TYPE.ACCESS_TIMING_PARAMETER, subfunction=timing_parameter_type, data=parameter_values)
    if read_values:
      # TODO: parse response into values?
      parameter_values = resp
      return parameter_values

  def secured_data_transmission(self, data: bytes):
    # TODO: split data into multiple input parameters?
    resp = self._uds_request(SERVICE_TYPE.SECURED_DATA_TRANSMISSION, subfunction=None, data=data)
    # TODO: parse response into multiple output values?
    return resp

  def control_dtc_setting(self, dtc_setting_type: DTC_SETTING_TYPE):
    self._uds_request(SERVICE_TYPE.CONTROL_DTC_SETTING, subfunction=dtc_setting_type)

  def response_on_event(self, response_event_type: RESPONSE_EVENT_TYPE, store_event: bool, window_time: int,
                        event_type_record: int, service_response_record: int):
    if store_event:
      response_event_type |= 0x20  # type: ignore
    # TODO: split record parameters into arrays
    data = bytes([window_time, event_type_record, service_response_record])
    resp = self._uds_request(SERVICE_TYPE.RESPONSE_ON_EVENT, subfunction=response_event_type, data=data)

    if response_event_type == RESPONSE_EVENT_TYPE.REPORT_ACTIVATED_EVENTS:
      return {
        "num_of_activated_events": resp[0],
        "data": resp[1:],  # TODO: parse the reset of response
      }

    return {
      "num_of_identified_events": resp[0],
      "event_window_time": resp[1],
      "data": resp[2:],  # TODO: parse the reset of response
    }

  def link_control(self, link_control_type: LINK_CONTROL_TYPE, baud_rate_type: BAUD_RATE_TYPE | None = None):
    data: bytes | None

    if link_control_type == LINK_CONTROL_TYPE.VERIFY_BAUDRATE_TRANSITION_WITH_FIXED_BAUDRATE:
      # baud_rate_type = BAUD_RATE_TYPE
      data = bytes([cast(int, baud_rate_type)])
    elif link_control_type == LINK_CONTROL_TYPE.VERIFY_BAUDRATE_TRANSITION_WITH_SPECIFIC_BAUDRATE:
      # baud_rate_type = custom value (3 bytes big-endian)
      data = struct.pack('!I', baud_rate_type)[1:]
    else:
      data = None
    self._uds_request(SERVICE_TYPE.LINK_CONTROL, subfunction=link_control_type, data=data)

  def read_data_by_identifier(self, data_identifier_type: DATA_IDENTIFIER_TYPE):
    # TODO: support list of identifiers
    data = struct.pack('!H', data_identifier_type)
    resp = self._uds_request(SERVICE_TYPE.READ_DATA_BY_IDENTIFIER, subfunction=None, data=data)
    resp_id = struct.unpack('!H', resp[0:2])[0] if len(resp) >= 2 else None
    if resp_id != data_identifier_type:
      raise ValueError(f'invalid response data identifier: {hex(resp_id)} expected: {hex(data_identifier_type)}')
    return resp[2:]

  def read_memory_by_address(self, memory_address: int, memory_size: int, memory_address_bytes: int = 4, memory_size_bytes: int = 1):
    if memory_address_bytes < 1 or memory_address_bytes > 4:
      raise ValueError(f'invalid memory_address_bytes: {memory_address_bytes}')
    if memory_size_bytes < 1 or memory_size_bytes > 4:
      raise ValueError(f'invalid memory_size_bytes: {memory_size_bytes}')
    data = bytes([memory_size_bytes << 4 | memory_address_bytes])

    if memory_address >= 1 << (memory_address_bytes * 8):
      raise ValueError(f'invalid memory_address: {memory_address}')
    data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
    if memory_size >= 1 << (memory_size_bytes * 8):
      raise ValueError(f'invalid memory_size: {memory_size}')
    data += struct.pack('!I', memory_size)[4 - memory_size_bytes:]

    resp = self._uds_request(SERVICE_TYPE.READ_MEMORY_BY_ADDRESS, subfunction=None, data=data)
    return resp

  def read_scaling_data_by_identifier(self, data_identifier_type: DATA_IDENTIFIER_TYPE):
    data = struct.pack('!H', data_identifier_type)
    resp = self._uds_request(SERVICE_TYPE.READ_SCALING_DATA_BY_IDENTIFIER, subfunction=None, data=data)
    resp_id = struct.unpack('!H', resp[0:2])[0] if len(resp) >= 2 else None
    if resp_id != data_identifier_type:
      raise ValueError(f'invalid response data identifier: {hex(resp_id)}')
    return resp[2:]  # TODO: parse the response

  def read_data_by_periodic_identifier(self, transmission_mode_type: TRANSMISSION_MODE_TYPE, periodic_data_identifier: int):
    # TODO: support list of identifiers
    data = bytes([transmission_mode_type, periodic_data_identifier])
    self._uds_request(SERVICE_TYPE.READ_DATA_BY_PERIODIC_IDENTIFIER, subfunction=None, data=data)

  def dynamically_define_data_identifier(self, dynamic_definition_type: DYNAMIC_DEFINITION_TYPE, dynamic_data_identifier: int,
                                         source_definitions: list[DynamicSourceDefinition], memory_address_bytes: int = 4, memory_size_bytes: int = 1):
    if memory_address_bytes < 1 or memory_address_bytes > 4:
      raise ValueError(f'invalid memory_address_bytes: {memory_address_bytes}')
    if memory_size_bytes < 1 or memory_size_bytes > 4:
      raise ValueError(f'invalid memory_size_bytes: {memory_size_bytes}')

    data = struct.pack('!H', dynamic_data_identifier)
    if dynamic_definition_type == DYNAMIC_DEFINITION_TYPE.DEFINE_BY_IDENTIFIER:
      for s in source_definitions:
        data += struct.pack('!H', s.data_identifier) + bytes([s.position, s.memory_size])
    elif dynamic_definition_type == DYNAMIC_DEFINITION_TYPE.DEFINE_BY_MEMORY_ADDRESS:
      data += bytes([memory_size_bytes << 4 | memory_address_bytes])
      for s in source_definitions:
        if s.memory_address >= 1 << (memory_address_bytes * 8):
          raise ValueError(f'invalid memory_address: {s.memory_address}')
        data += struct.pack('!I', s.memory_address)[4 - memory_address_bytes:]
        if s.memory_size >= 1 << (memory_size_bytes * 8):
          raise ValueError(f'invalid memory_size: {s.memory_size}')
        data += struct.pack('!I', s.memory_size)[4 - memory_size_bytes:]
    elif dynamic_definition_type == DYNAMIC_DEFINITION_TYPE.CLEAR_DYNAMICALLY_DEFINED_DATA_IDENTIFIER:
      pass
    else:
      raise ValueError(f'invalid dynamic identifier type: {hex(dynamic_definition_type)}')
    self._uds_request(SERVICE_TYPE.DYNAMICALLY_DEFINE_DATA_IDENTIFIER, subfunction=dynamic_definition_type, data=data)

  def write_data_by_identifier(self, data_identifier_type: DATA_IDENTIFIER_TYPE, data_record: bytes):
    data = struct.pack('!H', data_identifier_type) + data_record
    resp = self._uds_request(SERVICE_TYPE.WRITE_DATA_BY_IDENTIFIER, subfunction=None, data=data)
    resp_id = struct.unpack('!H', resp[0:2])[0] if len(resp) >= 2 else None
    if resp_id != data_identifier_type:
      raise ValueError(f'invalid response data identifier: {hex(resp_id)}')

  def write_memory_by_address(self, memory_address: int, memory_size: int, data_record: bytes, memory_address_bytes: int = 4, memory_size_bytes: int = 1):
    if memory_address_bytes < 1 or memory_address_bytes > 4:
      raise ValueError(f'invalid memory_address_bytes: {memory_address_bytes}')
    if memory_size_bytes < 1 or memory_size_bytes > 4:
      raise ValueError(f'invalid memory_size_bytes: {memory_size_bytes}')
    data = bytes([memory_size_bytes << 4 | memory_address_bytes])

    if memory_address >= 1 << (memory_address_bytes * 8):
      raise ValueError(f'invalid memory_address: {memory_address}')
    data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
    if memory_size >= 1 << (memory_size_bytes * 8):
      raise ValueError(f'invalid memory_size: {memory_size}')
    data += struct.pack('!I', memory_size)[4 - memory_size_bytes:]

    data += data_record
    self._uds_request(SERVICE_TYPE.WRITE_MEMORY_BY_ADDRESS, subfunction=None, data=data)

  def clear_diagnostic_information(self, dtc_group_type: DTC_GROUP_TYPE):
    data = struct.pack('!I', dtc_group_type)[1:]  # 3 bytes
    self._uds_request(SERVICE_TYPE.CLEAR_DIAGNOSTIC_INFORMATION, subfunction=None, data=data)

  def read_dtc_information(self, dtc_report_type: DTC_REPORT_TYPE, dtc_status_mask_type: DTC_STATUS_MASK_TYPE = DTC_STATUS_MASK_TYPE.ALL,
                           dtc_severity_mask_type: DTC_SEVERITY_MASK_TYPE = DTC_SEVERITY_MASK_TYPE.ALL, dtc_mask_record: int = 0xFFFFFF,
                           dtc_snapshot_record_num: int = 0xFF, dtc_extended_record_num: int = 0xFF):
    data = b''
    # dtc_status_mask_type
    if dtc_report_type == DTC_REPORT_TYPE.NUMBER_OF_DTC_BY_STATUS_MASK or \
       dtc_report_type == DTC_REPORT_TYPE.DTC_BY_STATUS_MASK or \
       dtc_report_type == DTC_REPORT_TYPE.MIRROR_MEMORY_DTC_BY_STATUS_MASK or \
       dtc_report_type == DTC_REPORT_TYPE.NUMBER_OF_MIRROR_MEMORY_DTC_BY_STATUS_MASK or \
       dtc_report_type == DTC_REPORT_TYPE.NUMBER_OF_EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK or \
       dtc_report_type == DTC_REPORT_TYPE.EMISSIONS_RELATED_OBD_DTC_BY_STATUS_MASK:
       data += bytes([dtc_status_mask_type])
    # dtc_mask_record
    if dtc_report_type == DTC_REPORT_TYPE.DTC_SNAPSHOT_IDENTIFICATION or \
       dtc_report_type == DTC_REPORT_TYPE.DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER or \
       dtc_report_type == DTC_REPORT_TYPE.DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER or \
       dtc_report_type == DTC_REPORT_TYPE.MIRROR_MEMORY_DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER or \
       dtc_report_type == DTC_REPORT_TYPE.SEVERITY_INFORMATION_OF_DTC:
       data += struct.pack('!I', dtc_mask_record)[1:]  # 3 bytes
    # dtc_snapshot_record_num
    if dtc_report_type == DTC_REPORT_TYPE.DTC_SNAPSHOT_IDENTIFICATION or \
       dtc_report_type == DTC_REPORT_TYPE.DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER or \
       dtc_report_type == DTC_REPORT_TYPE.DTC_SNAPSHOT_RECORD_BY_RECORD_NUMBER:
       data += bytes([dtc_snapshot_record_num])
    # dtc_extended_record_num
    if dtc_report_type == DTC_REPORT_TYPE.DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER or \
       dtc_report_type == DTC_REPORT_TYPE.MIRROR_MEMORY_DTC_EXTENDED_DATA_RECORD_BY_DTC_NUMBER:
       data += bytes([dtc_extended_record_num])
    # dtc_severity_mask_type
    if dtc_report_type == DTC_REPORT_TYPE.NUMBER_OF_DTC_BY_SEVERITY_MASK_RECORD or \
       dtc_report_type == DTC_REPORT_TYPE.DTC_BY_SEVERITY_MASK_RECORD:
       data += bytes([dtc_severity_mask_type, dtc_status_mask_type])

    resp = self._uds_request(SERVICE_TYPE.READ_DTC_INFORMATION, subfunction=dtc_report_type, data=data)

    # TODO: parse response
    return resp

  def input_output_control_by_identifier(self, data_identifier_type: DATA_IDENTIFIER_TYPE, control_parameter_type: CONTROL_PARAMETER_TYPE,
                                         control_option_record: bytes = b'', control_enable_mask_record: bytes = b''):
    data = struct.pack('!H', data_identifier_type) + bytes([control_parameter_type]) + control_option_record + control_enable_mask_record
    resp = self._uds_request(SERVICE_TYPE.INPUT_OUTPUT_CONTROL_BY_IDENTIFIER, subfunction=None, data=data)
    resp_id = struct.unpack('!H', resp[0:2])[0] if len(resp) >= 2 else None
    if resp_id != data_identifier_type:
      raise ValueError(f'invalid response data identifier: {hex(resp_id)}')
    return resp[2:]

  def routine_control(self, routine_control_type: ROUTINE_CONTROL_TYPE, routine_identifier_type: ROUTINE_IDENTIFIER_TYPE, routine_option_record: bytes = b''):
    data = struct.pack('!H', routine_identifier_type) + routine_option_record
    resp = self._uds_request(SERVICE_TYPE.ROUTINE_CONTROL, subfunction=routine_control_type, data=data)
    resp_id = struct.unpack('!H', resp[0:2])[0] if len(resp) >= 2 else None
    if resp_id != routine_identifier_type:
      raise ValueError(f'invalid response routine identifier: {hex(resp_id)}')
    return resp[2:]

  def request_download(self, memory_address: int, memory_size: int, memory_address_bytes: int = 4, memory_size_bytes: int = 4, data_format: int = 0x00):
    data = bytes([data_format])

    if memory_address_bytes < 1 or memory_address_bytes > 4:
      raise ValueError(f'invalid memory_address_bytes: {memory_address_bytes}')
    if memory_size_bytes < 1 or memory_size_bytes > 4:
      raise ValueError(f'invalid memory_size_bytes: {memory_size_bytes}')
    data += bytes([memory_size_bytes << 4 | memory_address_bytes])

    if memory_address >= 1 << (memory_address_bytes * 8):
      raise ValueError(f'invalid memory_address: {memory_address}')
    data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
    if memory_size >= 1 << (memory_size_bytes * 8):
      raise ValueError(f'invalid memory_size: {memory_size}')
    data += struct.pack('!I', memory_size)[4 - memory_size_bytes:]

    resp = self._uds_request(SERVICE_TYPE.REQUEST_DOWNLOAD, subfunction=None, data=data)
    max_num_bytes_len = resp[0] >> 4 if len(resp) > 0 else 0
    if max_num_bytes_len >= 1 and max_num_bytes_len <= 4:
      max_num_bytes = struct.unpack('!I', (b"\x00" * (4 - max_num_bytes_len)) + resp[1:max_num_bytes_len + 1])[0]
    else:
      raise ValueError(f'invalid max_num_bytes_len: {max_num_bytes_len}')

    return max_num_bytes  # max number of bytes per transfer data request

  def request_upload(self, memory_address: int, memory_size: int, memory_address_bytes: int = 4, memory_size_bytes: int = 4, data_format: int = 0x00):
    data = bytes([data_format])

    if memory_address_bytes < 1 or memory_address_bytes > 4:
      raise ValueError(f'invalid memory_address_bytes: {memory_address_bytes}')
    if memory_size_bytes < 1 or memory_size_bytes > 4:
      raise ValueError(f'invalid memory_size_bytes: {memory_size_bytes}')
    data += bytes([memory_size_bytes << 4 | memory_address_bytes])

    if memory_address >= 1 << (memory_address_bytes * 8):
      raise ValueError(f'invalid memory_address: {memory_address}')
    data += struct.pack('!I', memory_address)[4 - memory_address_bytes:]
    if memory_size >= 1 << (memory_size_bytes * 8):
      raise ValueError(f'invalid memory_size: {memory_size}')
    data += struct.pack('!I', memory_size)[4 - memory_size_bytes:]

    resp = self._uds_request(SERVICE_TYPE.REQUEST_UPLOAD, subfunction=None, data=data)
    max_num_bytes_len = resp[0] >> 4 if len(resp) > 0 else 0
    if max_num_bytes_len >= 1 and max_num_bytes_len <= 4:
      max_num_bytes = struct.unpack('!I', (b"\x00" * (4 - max_num_bytes_len)) + resp[1:max_num_bytes_len + 1])[0]
    else:
      raise ValueError(f'invalid max_num_bytes_len: {max_num_bytes_len}')

    return max_num_bytes  # max number of bytes per transfer data request

  def transfer_data(self, block_sequence_count: int, data: bytes = b''):
    data = bytes([block_sequence_count]) + data
    resp = self._uds_request(SERVICE_TYPE.TRANSFER_DATA, subfunction=None, data=data)
    resp_id = resp[0] if len(resp) > 0 else None
    if resp_id != block_sequence_count:
      raise ValueError(f'invalid block_sequence_count: {resp_id}')
    return resp[1:]

  def request_transfer_exit(self):
    self._uds_request(SERVICE_TYPE.REQUEST_TRANSFER_EXIT, subfunction=None)
