///|
/// APB signal mapping for transaction extraction.
pub struct ApbConfig {
  pclk : String
  psel : String
  penable : String
  pwrite : String
  paddr : String
  pwdata : String?
  prdata : String?
  pready : String?
  pslverr : String?
} derive(Debug, Eq)

///|
/// One decoded APB transfer.
pub struct ApbTransfer {
  index : Int
  setup_time : Int64
  access_time : Int64
  complete_time : Int64
  address : Int64?
  write : Bool
  write_data : Int64?
  read_data : Int64?
  ready : Bool
  error : Bool
  wait_cycles : Int
} derive(Debug, Eq)

///|
/// APB decode summary.
pub struct ApbReport {
  transfers : Int
  completed : Int
  reads : Int
  writes : Int
  errors : Int
  maximum_wait : Int
  mean_wait : Double?
  addresses : Int
} derive(Debug, Eq)

///|
/// Construct an APB4 mapping.
pub fn apb_config(
  pclk : String,
  psel : String,
  penable : String,
  pwrite : String,
  paddr : String,
) -> ApbConfig {
  {
    pclk,
    psel,
    penable,
    pwrite,
    paddr,
    pwdata: None,
    prdata: None,
    pready: None,
    pslverr: None,
  }
}

///|
/// Add data and response signals to an APB mapping.
pub fn ApbConfig::with_data(
  self : ApbConfig,
  pwdata : String,
  prdata : String,
) -> ApbConfig {
  { ..self, pwdata: Some(pwdata), prdata: Some(prdata) }
}

///|
/// Add optional APB response signals.
pub fn ApbConfig::with_response(
  self : ApbConfig,
  pready : String,
  pslverr : String,
) -> ApbConfig {
  { ..self, pready: Some(pready), pslverr: Some(pslverr) }
}

///|
/// Decode APB transfers by observing setup, access, and completion phases.
pub fn apb_decode(trace : Trace, config : ApbConfig) -> Array[ApbTransfer] {
  let clocks = clock_edge_times(trace, clock_config(config.pclk))
  let transfers : Array[ApbTransfer] = []
  let mut active : ApbTransfer? = None
  for time in clocks {
    let selected = signal_is_one(trace, config.psel, time)
    let enabled = signal_is_one(trace, config.penable, time)
    let ready = match config.pready {
      Some(id) => signal_is_one(trace, id, time)
      None => true
    }
    let error = match config.pslverr {
      Some(id) => signal_is_one(trace, id, time)
      None => false
    }
    if selected && !enabled && active is None {
      active = Some({
        index: transfers.length(),
        setup_time: time,
        access_time: time,
        complete_time: time,
        address: optional_value_int(value_at(trace, config.paddr, time)),
        write: signal_is_one(trace, config.pwrite, time),
        write_data: optional_int_signal(trace, config.pwdata, time),
        read_data: None,
        ready: false,
        error: false,
        wait_cycles: 0,
      })
    } else if selected && enabled {
      match active {
        Some(item) => {
          let updated = if ready {
            {
              ..item,
              access_time: if item.access_time == item.setup_time {
                time
              } else {
                item.access_time
              },
              complete_time: time,
              ready: true,
              error,
              read_data: optional_int_signal(trace, config.prdata, time),
            }
          } else {
            {
              ..item,
              access_time: if item.access_time == item.setup_time {
                time
              } else {
                item.access_time
              },
              wait_cycles: item.wait_cycles + 1,
            }
          }
          active = Some(updated)
          if ready {
            transfers.push(updated)
            active = None
          }
        }
        None => ()
      }
    } else if !selected {
      match active {
        Some(item) => {
          transfers.push({ ..item, complete_time: time })
          active = None
        }
        None => ()
      }
    }
  }
  match active {
    Some(item) => transfers.push(item)
    None => ()
  }
  transfers
}

///|
/// Calculate APB statistics.
pub fn apb_report(trace : Trace, config : ApbConfig) -> ApbReport {
  let transfers = apb_decode(trace, config)
  let waits = transfers.map(fn(item) { item.wait_cycles })
  let addresses : Array[Int64] = []
  for item in transfers {
    match item.address {
      Some(address) =>
        if !addresses.contains(address) {
          addresses.push(address)
        }
      None => ()
    }
  }
  let completed = transfers.count_if(fn(item) { item.ready })
  {
    transfers: transfers.length(),
    completed,
    reads: transfers.count_if(fn(item) { !item.write }),
    writes: transfers.count_if(fn(item) { item.write }),
    errors: transfers.count_if(fn(item) { item.error }),
    maximum_wait: waits.fold(init=0, fn(value, item) { Int::max(value, item) }),
    mean_wait: if waits.length() == 0 {
      None
    } else {
      Some(
        waits.fold(init=0, fn(total, item) { total + item }).to_double() /
        waits.length().to_double(),
      )
    },
    addresses: addresses.length(),
  }
}

///|
/// Render APB transfers as CSV.
pub fn apb_transfers_csv(transfers : Array[ApbTransfer]) -> String {
  let rows : Array[String] = [
    "index,setup,access,complete,address,write,write_data,read_data,ready,error,wait_cycles",
  ]
  for item in transfers {
    rows.push(
      item.index.to_string() +
      "," +
      item.setup_time.to_string() +
      "," +
      item.access_time.to_string() +
      "," +
      item.complete_time.to_string() +
      "," +
      optional_int64_text(item.address) +
      "," +
      item.write.to_string() +
      "," +
      optional_int64_text(item.write_data) +
      "," +
      optional_int64_text(item.read_data) +
      "," +
      item.ready.to_string() +
      "," +
      item.error.to_string() +
      "," +
      item.wait_cycles.to_string(),
    )
  }
  rows.join("\n")
}

///|
/// Render APB summary as text.
pub fn ApbReport::to_text(self : ApbReport) -> String {
  "apb transfers=" +
  self.transfers.to_string() +
  " completed=" +
  self.completed.to_string() +
  " reads=" +
  self.reads.to_string() +
  " writes=" +
  self.writes.to_string() +
  " errors=" +
  self.errors.to_string() +
  " maximum_wait=" +
  self.maximum_wait.to_string() +
  " mean_wait=" +
  optional_double_text(self.mean_wait) +
  " addresses=" +
  self.addresses.to_string()
}

///|
/// Render APB summary as JSON.
pub fn ApbReport::to_json(self : ApbReport) -> String {
  "{\"transfers\":" +
  self.transfers.to_string() +
  ",\"completed\":" +
  self.completed.to_string() +
  ",\"reads\":" +
  self.reads.to_string() +
  ",\"writes\":" +
  self.writes.to_string() +
  ",\"errors\":" +
  self.errors.to_string() +
  ",\"maximum_wait\":" +
  self.maximum_wait.to_string() +
  ",\"mean_wait\":" +
  optional_double_json(self.mean_wait) +
  ",\"addresses\":" +
  self.addresses.to_string() +
  "}"
}

///|
/// Read a scalar control signal at a time.
fn signal_is_one(trace : Trace, id : String, time : Int64) -> Bool {
  match value_at(trace, id, time) {
    Some(Scalar('1')) => true
    Some(Vector(bits)) => bits == "1"
    _ => false
  }
}

///|
/// Read an optional integer signal.
fn optional_int_signal(trace : Trace, id : String?, time : Int64) -> Int64? {
  match id {
    Some(signal_id) => optional_value_int(value_at(trace, signal_id, time))
    None => None
  }
}

///|
/// Convert an optional waveform value to an integer without hiding unknowns.
fn optional_value_int(value : Value?) -> Int64? {
  match value {
    Some(item) => item.to_int()
    None => None
  }
}