///|
/// Errors raised by checked ISO-TP operations.
pub suberror IsoTpError {
  InvalidFrameSize
  PayloadTooLarge
  TruncatedPacket
  InvalidFirstFrame
  UnexpectedConsecutive
  SequenceMismatch
  LengthMismatch
  UnsupportedFlowStatus
} derive(Debug)

///|
/// ISO-TP transfer parameters for classic CAN and CAN-FD.
pub struct IsoTpConfig {
  frame_bytes : Int
  block_size : Byte
  separation_time : Byte
}

///|
/// Create an ISO-TP configuration. Data capacity is normally 8 or 64 bytes.
pub fn isotp_config(
  frame_bytes? : Int = 8,
  block_size? : Byte = 0,
  separation_time? : Byte = 0,
) -> IsoTpConfig raise IsoTpError {
  if frame_bytes < 2 || frame_bytes > 64 {
    raise InvalidFrameSize
  }
  { frame_bytes, block_size, separation_time }
}

///|
pub fn IsoTpConfig::frame_bytes(self : IsoTpConfig) -> Int {
  self.frame_bytes
}

///|
pub fn IsoTpConfig::block_size(self : IsoTpConfig) -> Byte {
  self.block_size
}

///|
pub fn IsoTpConfig::separation_time(self : IsoTpConfig) -> Byte {
  self.separation_time
}

///|
/// Segment a payload while respecting frame size and the 12-bit classic length.
pub fn isotp_segment_checked(
  payload : Array[Byte],
  config : IsoTpConfig,
) -> Array[Array[Byte]] raise IsoTpError {
  if payload.length() > 4095 {
    raise PayloadTooLarge
  }
  let single_capacity = config.frame_bytes - 1
  if payload.length() <= single_capacity && payload.length() <= 15 {
    return [[payload.length().to_byte()] + payload]
  }
  let first_capacity = config.frame_bytes - 2
  if first_capacity <= 0 {
    raise InvalidFrameSize
  }
  let packets : Array[Array[Byte]] = []
  packets.push(
    [
      (0x10 | ((payload.length() >> 8) & 0x0F)).to_byte(),
      (payload.length() & 0xFF).to_byte(),
    ] +
    payload[0:first_capacity].to_owned(),
  )
  let mut offset = first_capacity
  let mut sequence : Byte = 1
  let consecutive_capacity = config.frame_bytes - 1
  while offset < payload.length() {
    let count = if payload.length() - offset > consecutive_capacity {
      consecutive_capacity
    } else {
      payload.length() - offset
    }
    packets.push(
      [(0x20 | sequence.to_int()).to_byte()] +
      payload[offset:offset + count].to_owned(),
    )
    offset += count
    sequence = ((sequence.to_int() + 1) & 0x0F).to_byte()
  }
  packets
}

///|
/// Decode a packet while enforcing its declared payload length.
pub fn isotp_decode_checked(
  packet : Array[Byte],
  config : IsoTpConfig,
) -> IsoTpPacket raise IsoTpError {
  if packet.is_empty() || packet.length() > config.frame_bytes {
    raise TruncatedPacket
  }
  let pci = packet[0].to_int()
  let kind = pci >> 4
  if kind == 0 {
    let length = pci & 0x0F
    if length > packet.length() - 1 {
      raise TruncatedPacket
    }
    Single(payload=packet[1:1 + length].to_owned())
  } else if kind == 1 {
    if packet.length() < 2 {
      raise TruncatedPacket
    }
    let length = ((pci & 0x0F) << 8) | packet[1].to_int()
    if length <= config.frame_bytes - 2 {
      raise InvalidFirstFrame
    }
    First(total_length=length, payload=packet[2:].to_owned())
  } else if kind == 2 {
    if packet.length() < 2 {
      raise TruncatedPacket
    }
    Consecutive(sequence=(pci & 0x0F).to_byte(), payload=packet[1:].to_owned())
  } else if kind == 3 {
    if packet.length() < 3 {
      raise TruncatedPacket
    }
    let status = (pci & 0x0F).to_byte()
    if status > 2 {
      raise UnsupportedFlowStatus
    }
    FlowControl(status~, block_size=packet[1], separation_time=packet[2])
  } else {
    raise TruncatedPacket
  }
}

///|
/// Reassemble a complete ISO-TP transfer and verify sequence numbers.
pub fn isotp_reassemble_checked(
  packets : Array[IsoTpPacket],
) -> Array[Byte] raise IsoTpError {
  if packets.is_empty() {
    raise LengthMismatch
  }
  match packets[0] {
    Single(payload~) => {
      if packets.length() != 1 {
        raise LengthMismatch
      }
      payload
    }
    First(total_length~, payload~) => {
      let result = payload.copy()
      let mut expected : Byte = 1
      for packet in packets[1:] {
        match packet {
          Consecutive(sequence~, payload~) => {
            if sequence != expected {
              raise SequenceMismatch
            }
            for byte in payload {
              result.push(byte)
            }
            expected = ((expected.to_int() + 1) & 0x0F).to_byte()
          }
          _ => raise UnexpectedConsecutive
        }
      }
      if result.length() != total_length {
        raise LengthMismatch
      }
      result
    }
    _ => raise UnexpectedConsecutive
  }
}

///|
/// The state of an incremental receiver.
pub enum IsoTpStatus {
  Waiting(received~ : Int, total~ : Int)
  Complete(payload~ : Array[Byte])
}

///|
/// An incremental ISO-TP receiver for a single outstanding transfer.
pub struct IsoTpReceiver {
  config : IsoTpConfig
  mut total_length : Int
  payload : Array[Byte]
  mut next_sequence : Byte
  mut block_count : Int
}

///|
pub fn new_isotp_receiver(config : IsoTpConfig) -> IsoTpReceiver {
  { config, total_length: 0, payload: [], next_sequence: 1, block_count: 0 }
}

///|
/// Clear the current transfer.
pub fn IsoTpReceiver::reset(self : IsoTpReceiver) -> Unit {
  self.total_length = 0
  self.payload.clear()
  self.next_sequence = 1
  self.block_count = 0
}

///|
/// Feed one decoded packet into the receiver.
pub fn IsoTpReceiver::push(
  self : IsoTpReceiver,
  packet : IsoTpPacket,
) -> IsoTpStatus raise IsoTpError {
  match packet {
    Single(payload~) => {
      self.reset()
      Complete(payload~)
    }
    First(total_length~, payload~) => {
      if total_length <= 0 || total_length <= payload.length() {
        raise InvalidFirstFrame
      }
      self.reset()
      self.total_length = total_length
      self.payload.append(payload[:])
      Waiting(received=self.payload.length(), total=total_length)
    }
    Consecutive(sequence~, payload~) => {
      if self.total_length == 0 {
        raise UnexpectedConsecutive
      }
      if sequence != self.next_sequence {
        raise SequenceMismatch
      }
      self.next_sequence = ((sequence.to_int() + 1) & 0x0F).to_byte()
      self.block_count += 1
      for byte in payload {
        self.payload.push(byte)
      }
      if self.payload.length() > self.total_length {
        raise LengthMismatch
      }
      if self.payload.length() == self.total_length {
        let completed = self.payload.copy()
        self.reset()
        Complete(payload=completed)
      } else {
        Waiting(received=self.payload.length(), total=self.total_length)
      }
    }
    FlowControl(status~, ..) => {
      ignore(status)
      raise UnexpectedConsecutive
    }
  }
}

///|
/// Return a flow-control packet for this receiver's configured limits.
pub fn IsoTpReceiver::flow_control(self : IsoTpReceiver) -> Array[Byte] {
  [0x30, self.config.block_size, self.config.separation_time]
}

///|
/// Wrap ISO-TP packets in CAN or CAN-FD data frames.
pub fn isotp_frames(
  tx_id : UInt,
  payload : Array[Byte],
  config : IsoTpConfig,
  extended? : Bool = false,
) -> Array[Frame] raise IsoTpError {
  let result : Array[Frame] = []
  for packet in isotp_segment_checked(payload, config) {
    let frame = if config.frame_bytes > 8 {
      fd_frame(tx_id, packet, extended~) catch {
        _ => raise InvalidFrameSize
      }
    } else {
      data_frame(tx_id, packet, extended~) catch {
        _ => raise InvalidFrameSize
      }
    }
    result.push(frame)
  }
  result
}

///|
/// Decode and reassemble a sequence of CAN data frames.
pub fn isotp_reassemble_frames(
  frames : Array[Frame],
  config : IsoTpConfig,
) -> Array[Byte] raise IsoTpError {
  let packets : Array[IsoTpPacket] = []
  for frame in frames {
    packets.push(isotp_decode_checked(frame.data(), config))
  }
  isotp_reassemble_checked(packets)
}