///|
/// Errors raised by text log parsing.
pub suberror CanTextError {
  InvalidHeader
  InvalidFieldCount
  InvalidTimestamp
  InvalidPayload
} derive(Debug)

///|
/// A human-readable CAN log record.
pub struct CanLogEntry {
  timestamp_us : UInt64
  frame : Frame
  channel : String
}

///|
/// A portable text log with stable ordering.
pub struct CanLog {
  entries : Array[CanLogEntry]
}

///|
pub fn new_can_log() -> CanLog {
  { entries: [] }
}

///|
pub fn CanLog::push(
  self : CanLog,
  timestamp_us : UInt64,
  frame : Frame,
  channel? : String = "can0",
) -> Unit {
  self.entries.push({ timestamp_us, frame, channel })
}

///|
pub fn CanLog::length(self : CanLog) -> Int {
  self.entries.length()
}

///|
pub fn CanLog::entries(self : CanLog) -> Array[CanLogEntry] {
  self.entries.copy()
}

///|
/// Encode a log with a header and one comma-separated record per line.
pub fn CanLog::to_text(self : CanLog) -> String {
  let builder = StringBuilder()
  builder.write_string("timestamp_us,channel,frame_hex\n")
  for entry in self.entries {
    builder.write_string(entry.timestamp_us.to_string())
    builder.write_string(",")
    builder.write_string(entry.channel)
    builder.write_string(",")
    builder.write_string(frame_to_hex(entry.frame))
    builder.write_string("\n")
  }
  builder.to_string()
}

///|
/// Parse a log produced by `CanLog::to_text`.
pub fn can_log_from_text(text : String) -> CanLog raise CanTextError {
  let result = new_can_log()
  let mut line_number = 0
  for raw in text.split("\n") {
    if line_number == 0 {
      if raw.trim() != "timestamp_us,channel,frame_hex" {
        raise InvalidHeader
      }
    } else {
      let line = raw.trim()
      if !line.is_empty() {
        let fields : Array[String] = []
        for part in line.split(",") {
          fields.push(part.to_owned())
        }
        if fields.length() != 3 {
          raise InvalidFieldCount
        }
        let timestamp : UInt64 = @strconv.from_str(fields[0]) catch {
          _ => raise InvalidTimestamp
        }
        let frame = frame_from_hex(fields[2]) catch {
          _ => raise InvalidPayload
        }
        result.push(timestamp, frame, channel=fields[1])
      }
    }
    line_number += 1
  }
  result
}

///|
pub fn CanLog::sorted(self : CanLog) -> CanLog {
  let result = new_can_log()
  let entries = self.entries.copy()
  entries.sort_by((left, right) => {
    if left.timestamp_us < right.timestamp_us {
      -1
    } else if left.timestamp_us > right.timestamp_us {
      1
    } else {
      compare_frames(left.frame, right.frame)
    }
  })
  for entry in entries {
    result.push(entry.timestamp_us, entry.frame, channel=entry.channel)
  }
  result
}

///|
pub fn CanLog::filter_channel(self : CanLog, channel : String) -> CanLog {
  let result = new_can_log()
  for entry in self.entries {
    if entry.channel == channel {
      result.push(entry.timestamp_us, entry.frame, channel~)
    }
  }
  result
}

///|
pub fn CanLogEntry::timestamp(self : CanLogEntry) -> UInt64 {
  self.timestamp_us
}

///|
pub fn CanLogEntry::frame(self : CanLogEntry) -> Frame {
  self.frame
}

///|
pub fn CanLogEntry::channel(self : CanLogEntry) -> String {
  self.channel
}

///|
/// Encode a single compact line for shell tools.
pub fn can_line(timestamp_us : UInt64, frame : Frame) -> String {
  "\{timestamp_us},\{frame_to_hex(frame)}"
}

///|
/// Parse a compact two-field line.
pub fn parse_can_line(line : String) -> (UInt64, Frame) raise CanTextError {
  let fields : Array[String] = []
  for part in line.trim().split(",") {
    fields.push(part.to_owned())
  }
  if fields.length() != 2 {
    raise InvalidFieldCount
  }
  let timestamp : UInt64 = @strconv.from_str(fields[0]) catch {
    _ => raise InvalidTimestamp
  }
  let frame = frame_from_hex(fields[1]) catch { _ => raise InvalidPayload }
  (timestamp, frame)
}