///|
/// Common transaction interpretations for synchronous digital interfaces.
pub enum ProtocolMode {
  ValidReady
  ValidOnly
  RequestResponse
  Pulse
  Level
} derive(Debug, Eq)

///|
/// Signal identifiers and timing rules used by a protocol extractor.
pub struct ProtocolChannel {
  name : String
  mode : ProtocolMode
  clock_id : String?
  valid_id : String?
  ready_id : String?
  request_id : String?
  response_id : String?
  payload_id : String?
  reset_id : String?
  active_high : Bool
  minimum_gap : Int64
} derive(Debug, Eq)

///|
/// A protocol event observed at one simulation timestamp.
pub struct ProtocolEvent {
  channel : String
  time : Int64
  kind : String
  payload : Value?
  accepted : Bool
  sequence : Int
} derive(Debug, Eq)

///|
/// A paired request/response or accepted transfer.
pub struct ProtocolTransaction {
  channel : String
  index : Int
  start : Int64
  end : Int64
  latency : Int64
  payload : Value?
  completed : Bool
  accepted : Bool
} derive(Debug, Eq)

///|
/// Aggregate protocol health metrics.
pub struct ProtocolStats {
  channel : String
  events : Int
  transactions : Int
  completed : Int
  incomplete : Int
  accepted : Int
  rejected : Int
  minimum_latency : Int64?
  maximum_latency : Int64?
  mean_latency : Double?
  throughput : Double
  backpressure_cycles : Int
} derive(Debug, Eq)

///|
/// Return a channel descriptor for a valid/ready transfer.
pub fn valid_ready_channel(
  name : String,
  clock_id : String,
  valid_id : String,
  ready_id : String,
  payload_id : String?,
) -> ProtocolChannel {
  {
    name,
    mode: ValidReady,
    clock_id: Some(clock_id),
    valid_id: Some(valid_id),
    ready_id: Some(ready_id),
    request_id: None,
    response_id: None,
    payload_id,
    reset_id: None,
    active_high: true,
    minimum_gap: 0L,
  }
}

///|
/// Return a request/response channel descriptor.
pub fn request_response_channel(
  name : String,
  request_id : String,
  response_id : String,
  payload_id : String?,
) -> ProtocolChannel {
  {
    name,
    mode: RequestResponse,
    clock_id: None,
    valid_id: None,
    ready_id: None,
    request_id: Some(request_id),
    response_id: Some(response_id),
    payload_id,
    reset_id: None,
    active_high: true,
    minimum_gap: 0L,
  }
}

///|
/// Return a one-signal pulse channel descriptor.
pub fn pulse_channel(name : String, signal_id : String) -> ProtocolChannel {
  {
    name,
    mode: Pulse,
    clock_id: None,
    valid_id: Some(signal_id),
    ready_id: None,
    request_id: None,
    response_id: None,
    payload_id: None,
    reset_id: None,
    active_high: true,
    minimum_gap: 0L,
  }
}

///|
/// Attach a reset signal to a channel without changing its mode.
pub fn ProtocolChannel::with_reset(
  self : ProtocolChannel,
  reset_id : String,
) -> ProtocolChannel {
  { ..self, reset_id: Some(reset_id) }
}

///|
/// Attach a minimum spacing rule for burst analysis.
pub fn ProtocolChannel::with_minimum_gap(
  self : ProtocolChannel,
  gap : Int64,
) -> ProtocolChannel {
  { ..self, minimum_gap: Int64::max(0L, gap) }
}

///|
/// Return all supported protocol modes for CLI selection.
pub fn protocol_mode_catalog() -> Array[ProtocolMode] {
  [ValidReady, ValidOnly, RequestResponse, Pulse, Level]
}

///|
/// Extract all events for a configured channel.
pub fn protocol_events(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
  match channel.mode {
    ValidReady => extract_valid_ready(trace, channel)
    ValidOnly => extract_valid_only(trace, channel)
    RequestResponse => extract_request_response_events(trace, channel)
    Pulse => extract_pulse_events(trace, channel)
    Level => extract_level_events(trace, channel)
  }
}

///|
/// Convert protocol events into completed and incomplete transactions.
pub fn protocol_transactions(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolTransaction] {
  let events = protocol_events(trace, channel)
  let transactions : Array[ProtocolTransaction] = []
  let mut index = 0
  let pending : Array[ProtocolEvent] = []
  for event in events {
    match channel.mode {
      RequestResponse =>
        if event.kind == "request" {
          pending.push(event)
        } else if event.kind == "response" {
          match take_first_event(pending) {
            Some(request) => {
              transactions.push({
                channel: channel.name,
                index,
                start: request.time,
                end: event.time,
                latency: Int64::max(0L, event.time - request.time),
                payload: event.payload,
                completed: true,
                accepted: event.accepted,
              })
              index += 1
            }
            None => {
              transactions.push({
                channel: channel.name,
                index,
                start: event.time,
                end: event.time,
                latency: 0L,
                payload: event.payload,
                completed: false,
                accepted: false,
              })
              index += 1
            }
          }
        }
      _ => {
        transactions.push({
          channel: channel.name,
          index,
          start: event.time,
          end: event.time,
          latency: 0L,
          payload: event.payload,
          completed: true,
          accepted: event.accepted,
        })
        index += 1
      }
    }
  }
  for request in pending {
    transactions.push({
      channel: channel.name,
      index,
      start: request.time,
      end: request.time,
      latency: 0L,
      payload: request.payload,
      completed: false,
      accepted: false,
    })
    index += 1
  }
  transactions
}

///|
/// Calculate deterministic health metrics for a channel.
pub fn protocol_stats(
  trace : Trace,
  channel : ProtocolChannel,
) -> ProtocolStats {
  let events = protocol_events(trace, channel)
  let transactions = protocol_transactions(trace, channel)
  let completed = transactions.count_if(fn(item) { item.completed })
  let accepted = transactions.count_if(fn(item) { item.accepted })
  let latencies = transactions
    .filter(fn(item) { item.completed })
    .map(fn(item) { item.latency })
  let total_latency = latencies.fold(init=0L, fn(total, latency) {
    total + latency
  })
  let duration = Int64::max(1L, trace.duration())
  let minimum : Int64? = if latencies.length() == 0 {
    None
  } else {
    Some(
      latencies.fold(init=latencies[0], fn(value, item) {
        Int64::min(value, item)
      }),
    )
  }
  let maximum : Int64? = if latencies.length() == 0 {
    None
  } else {
    Some(
      latencies.fold(init=latencies[0], fn(value, item) {
        Int64::max(value, item)
      }),
    )
  }
  {
    channel: channel.name,
    events: events.length(),
    transactions: transactions.length(),
    completed,
    incomplete: transactions.length() - completed,
    accepted,
    rejected: transactions.length() - accepted,
    minimum_latency: minimum,
    maximum_latency: maximum,
    mean_latency: if latencies.length() == 0 {
      None
    } else {
      Some(total_latency.to_double() / latencies.length().to_double())
    },
    throughput: completed.to_double() / duration.to_double(),
    backpressure_cycles: protocol_backpressure(trace, channel),
  }
}

///|
/// Count timestamps where valid is asserted but ready is not.
pub fn protocol_backpressure(trace : Trace, channel : ProtocolChannel) -> Int {
  match (channel.valid_id, channel.ready_id) {
    (Some(valid), Some(ready)) => {
      let mut count = 0
      for time in trace_times(trace) {
        if active_at(trace, valid, time, channel.active_high) &&
          !active_at(trace, ready, time, channel.active_high) {
          count += 1
        }
      }
      count
    }
    _ => 0
  }
}

///|
/// Return events as CSV for regression artifacts.
pub fn protocol_events_csv(events : Array[ProtocolEvent]) -> String {
  let rows : Array[String] = ["channel,time,kind,payload,accepted,sequence"]
  for event in events {
    rows.push(
      csv_cell(event.channel) +
      "," +
      event.time.to_string() +
      "," +
      csv_cell(event.kind) +
      "," +
      optional_value_csv(event.payload) +
      "," +
      event.accepted.to_string() +
      "," +
      event.sequence.to_string(),
    )
  }
  rows.join("\n")
}

///|
/// Return transactions as CSV for spreadsheet and CI consumers.
pub fn protocol_transactions_csv(
  transactions : Array[ProtocolTransaction],
) -> String {
  let rows : Array[String] = [
    "channel,index,start,end,latency,payload,completed,accepted",
  ]
  for item in transactions {
    rows.push(
      csv_cell(item.channel) +
      "," +
      item.index.to_string() +
      "," +
      item.start.to_string() +
      "," +
      item.end.to_string() +
      "," +
      item.latency.to_string() +
      "," +
      optional_value_csv(item.payload) +
      "," +
      item.completed.to_string() +
      "," +
      item.accepted.to_string(),
    )
  }
  rows.join("\n")
}

///|
/// Render a protocol statistic record as JSON.
pub fn ProtocolStats::to_json(self : ProtocolStats) -> String {
  "{\"channel\":\"" +
  csv_cell(self.channel) +
  "\",\"events\":" +
  self.events.to_string() +
  ",\"transactions\":" +
  self.transactions.to_string() +
  ",\"completed\":" +
  self.completed.to_string() +
  ",\"incomplete\":" +
  self.incomplete.to_string() +
  ",\"accepted\":" +
  self.accepted.to_string() +
  ",\"rejected\":" +
  self.rejected.to_string() +
  ",\"minimum_latency\":" +
  optional_int64_json(self.minimum_latency) +
  ",\"maximum_latency\":" +
  optional_int64_json(self.maximum_latency) +
  ",\"mean_latency\":" +
  optional_double_json(self.mean_latency) +
  ",\"throughput\":" +
  self.throughput.to_string() +
  ",\"backpressure_cycles\":" +
  self.backpressure_cycles.to_string() +
  "}"
}

///|
/// Render a human-readable protocol summary.
pub fn ProtocolStats::to_text(self : ProtocolStats) -> String {
  "protocol=" +
  self.channel +
  " events=" +
  self.events.to_string() +
  " transactions=" +
  self.transactions.to_string() +
  " completed=" +
  self.completed.to_string() +
  " incomplete=" +
  self.incomplete.to_string() +
  " accepted=" +
  self.accepted.to_string() +
  " rejected=" +
  self.rejected.to_string() +
  " latency=" +
  optional_double_text(self.mean_latency) +
  " throughput=" +
  self.throughput.to_string() +
  " backpressure=" +
  self.backpressure_cycles.to_string()
}

///|
/// Extract a valid/ready event at every timestamp where either control changes.
fn extract_valid_ready(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
  match (channel.valid_id, channel.ready_id) {
    (Some(valid), Some(ready)) => {
      let events : Array[ProtocolEvent] = []
      let mut sequence = 0
      for time in trace_times(trace) {
        let valid_active = active_at(trace, valid, time, channel.active_high)
        let ready_active = active_at(trace, ready, time, channel.active_high)
        if valid_active {
          events.push({
            channel: channel.name,
            time,
            kind: if ready_active {
              "transfer"
            } else {
              "stall"
            },
            payload: optional_signal_value(trace, channel.payload_id, time),
            accepted: ready_active,
            sequence,
          })
          sequence += 1
        }
      }
      events
    }
    _ => []
  }
}

///|
/// Extract a transfer for each active level of a valid-only channel.
fn extract_valid_only(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
  match channel.valid_id {
    Some(valid) => {
      let events : Array[ProtocolEvent] = []
      let mut sequence = 0
      for time in trace_times(trace) {
        if active_at(trace, valid, time, channel.active_high) {
          events.push({
            channel: channel.name,
            time,
            kind: "valid",
            payload: optional_signal_value(trace, channel.payload_id, time),
            accepted: true,
            sequence,
          })
          sequence += 1
        }
      }
      events
    }
    None => []
  }
}

///|
/// Extract request and response edges from a two-signal channel.
fn extract_request_response_events(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
  match (channel.request_id, channel.response_id) {
    (Some(request), Some(response)) => {
      let events : Array[ProtocolEvent] = []
      let mut sequence = 0
      let mut request_before = false
      let mut response_before = false
      for time in trace_times(trace) {
        let request_now = active_at(trace, request, time, channel.active_high)
        let response_now = active_at(trace, response, time, channel.active_high)
        if request_now && !request_before {
          events.push({
            channel: channel.name,
            time,
            kind: "request",
            payload: optional_signal_value(trace, channel.payload_id, time),
            accepted: true,
            sequence,
          })
          sequence += 1
        }
        if response_now && !response_before {
          events.push({
            channel: channel.name,
            time,
            kind: "response",
            payload: optional_signal_value(trace, channel.payload_id, time),
            accepted: true,
            sequence,
          })
          sequence += 1
        }
        request_before = request_now
        response_before = response_now
      }
      events
    }
    _ => []
  }
}

///|
/// Extract rising pulses from one signal.
fn extract_pulse_events(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
  match channel.valid_id {
    Some(signal_id) => {
      let events : Array[ProtocolEvent] = []
      let mut previous = false
      let mut sequence = 0
      for time in trace_times(trace) {
        let current = active_at(trace, signal_id, time, channel.active_high)
        if current && !previous {
          events.push({
            channel: channel.name,
            time,
            kind: "pulse",
            payload: optional_signal_value(trace, channel.payload_id, time),
            accepted: true,
            sequence,
          })
          sequence += 1
        }
        previous = current
      }
      events
    }
    None => []
  }
}

///|
/// Emit a level event whenever a signal's sampled value changes.
fn extract_level_events(
  trace : Trace,
  channel : ProtocolChannel,
) -> Array[ProtocolEvent] {
  match channel.valid_id {
    Some(signal_id) => {
      let events : Array[ProtocolEvent] = []
      let mut previous : Value? = None
      let mut sequence = 0
      for time in trace_times(trace) {
        let current = value_at(trace, signal_id, time)
        if current != previous {
          events.push({
            channel: channel.name,
            time,
            kind: "level",
            payload: current,
            accepted: true,
            sequence,
          })
          sequence += 1
        }
        previous = current
      }
      events
    }
    None => []
  }
}

///|
/// Return all distinct timestamps in source order.
fn trace_times(trace : Trace) -> Array[Int64] {
  let times : Array[Int64] = []
  for change in trace.changes {
    if !times.contains(change.time) {
      times.push(change.time)
    }
  }
  if times.length() == 0 || times[0] != 0L {
    times.insert(0, 0L)
  }
  times
}

///|
/// Return whether a signal is active at a timestamp.
fn active_at(
  trace : Trace,
  signal_id : String,
  time : Int64,
  active_high : Bool,
) -> Bool {
  match value_at(trace, signal_id, time) {
    Some(Scalar(value)) => if active_high { value == '1' } else { value == '0' }
    Some(Vector(bits)) =>
      if bits.length() == 0 {
        false
      } else if active_high {
        bits.get_char(0) == Some('1')
      } else {
        bits.get_char(0) == Some('0')
      }
    _ => false
  }
}

///|
/// Read an optional payload signal at a timestamp.
fn optional_signal_value(
  trace : Trace,
  signal_id : String?,
  time : Int64,
) -> Value? {
  match signal_id {
    Some(id) => value_at(trace, id, time)
    None => None
  }
}

///|
/// Remove and return the first pending event without relying on queue APIs.
fn take_first_event(queue : Array[ProtocolEvent]) -> ProtocolEvent? {
  if queue.length() == 0 {
    None
  } else {
    let first = queue[0]
    for index in 1.. String {
  if text.contains(",") || text.contains("\"") || text.contains("\n") {
    "\"" + text.replace(old="\"", new="\"\"") + "\""
  } else {
    text
  }
}

///|
/// Render an optional value as a CSV field.
fn optional_value_csv(value : Value?) -> String {
  match value {
    Some(item) => csv_cell(item.display())
    None => ""
  }
}

///|
/// Render an optional floating-point value as JSON.
fn optional_double_json(value : Double?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "null"
  }
}

///|
/// Render an optional floating-point value for text output.
fn optional_double_text(value : Double?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "n/a"
  }
}