///|
/// Errors raised when validating or parsing a trace log.
pub suberror TraceError {
  NonMonotonicTimestamp
  InvalidLine
  InvalidTimestamp
  InvalidFrame
} derive(Debug)

///|
/// Validate timestamps and frame invariants in capture order.
pub fn Trace::validate(self : Trace) -> Unit raise TraceError {
  let mut previous : UInt64? = None
  for entry in self.entries {
    match previous {
      Some(timestamp) =>
        if entry.timestamp_us < timestamp {
          raise NonMonotonicTimestamp
        }
      None => ()
    }
    if !frame_is_valid(entry.frame) {
      raise InvalidFrame
    }
    previous = Some(entry.timestamp_us)
  }
}

///|
/// Return a capture-order copy sorted by timestamp and arbitration id.
pub fn Trace::sorted(self : Trace) -> Trace {
  let result = new_trace()
  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.record(entry.timestamp_us, entry.frame)
  }
  result
}

///|
/// Encode a trace in a stable CSV-like text format.
pub fn Trace::to_text(self : Trace) -> String {
  let builder = StringBuilder()
  builder.write_string("timestamp_us,frame_hex\n")
  for entry in self.entries {
    builder.write_string(entry.timestamp_us.to_string())
    builder.write_string(",")
    builder.write_string(frame_to_hex(entry.frame))
    builder.write_string("\n")
  }
  builder.to_string()
}

///|
/// Parse a trace produced by `Trace::to_text`.
pub fn trace_from_text(text : String) -> Trace raise TraceError {
  let result = new_trace()
  let mut index = 0
  for raw in text.split("\n") {
    if index == 0 {
      if raw.trim() != "timestamp_us,frame_hex" {
        raise InvalidLine
      }
    } 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() != 2 {
          raise InvalidLine
        }
        let timestamp : UInt64 = @strconv.from_str(fields[0]) catch {
          _ => raise InvalidTimestamp
        }
        let frame = frame_from_hex(fields[1]) catch { _ => raise InvalidFrame }
        result.record(timestamp, frame)
      }
    }
    index += 1
  }
  result.validate()
  result
}

///|
/// Select entries matching an identifier.
pub fn Trace::filter_id(self : Trace, id : UInt) -> Trace {
  let result = new_trace()
  for entry in self.entries {
    if entry.frame.id() == id {
      result.record(entry.timestamp_us, entry.frame)
    }
  }
  result
}

///|
/// Select entries in a closed timestamp interval.
pub fn Trace::between(
  self : Trace,
  start_us : UInt64,
  end_us : UInt64,
) -> Trace {
  let result = new_trace()
  for entry in self.entries {
    if entry.timestamp_us >= start_us && entry.timestamp_us <= end_us {
      result.record(entry.timestamp_us, entry.frame)
    }
  }
  result
}

///|
/// Return the first and last capture timestamps.
pub fn Trace::time_range(self : Trace) -> (UInt64, UInt64)? {
  if self.entries.is_empty() {
    None
  } else {
    Some(
      (
        self.entries[0].timestamp_us,
        self.entries[self.entries.length() - 1].timestamp_us,
      ),
    )
  }
}

///|
/// Merge two traces and normalize their ordering.
pub fn merge_traces(left : Trace, right : Trace) -> Trace {
  let result = new_trace()
  for entry in left.entries {
    result.record(entry.timestamp_us, entry.frame)
  }
  for entry in right.entries {
    result.record(entry.timestamp_us, entry.frame)
  }
  result.sorted()
}

///|
/// Return all frames in capture order.
pub fn Trace::frames(self : Trace) -> Array[Frame] {
  self.entries.map(entry => entry.frame)
}