///|
/// SPI clock polarity used by a serial peripheral interface capture.
pub enum SpiPolarity {
  IdleLow
  IdleHigh
} derive(Debug, Eq)

///|
/// SPI clock phase used when sampling and changing data.
pub enum SpiPhase {
  SampleLeading
  SampleTrailing
} derive(Debug, Eq)

///|
/// Bit ordering on the wire.
pub enum SpiBitOrder {
  MsbFirst
  LsbFirst
} derive(Debug, Eq)

///|
/// Configuration for decoding one chip-select framed SPI stream.
pub struct SpiConfig {
  clock : String
  chip_select : String
  mosi : String?
  miso : String?
  polarity : SpiPolarity
  phase : SpiPhase
  order : SpiBitOrder
  word_bits : Int
  active_low : Bool
} derive(Debug)

///|
/// One decoded SPI word, retaining both wire values and timing metadata.
pub struct SpiWord {
  index : Int
  start_time : Int64
  end_time : Int64
  clock_edges : Int
  mosi_bits : String
  miso_bits : String
  mosi_value : Int64?
  miso_value : Int64?
  complete : Bool
  chip_select_active : Bool
} derive(Debug)

///|
/// Aggregate SPI decode statistics.
pub struct SpiReport {
  config : SpiConfig
  words : Array[SpiWord]
  complete_words : Int
  incomplete_words : Int
  active_edges : Int
  total_bits : Int
  first_time : Int64?
  last_time : Int64?
} derive(Debug)

///|
/// List every supported SPI polarity.
pub fn spi_polarity_catalog() -> Array[SpiPolarity] {
  [IdleLow, IdleHigh]
}

///|
/// List every supported SPI sampling phase.
pub fn spi_phase_catalog() -> Array[SpiPhase] {
  [SampleLeading, SampleTrailing]
}

///|
/// List every supported SPI bit order.
pub fn spi_bit_order_catalog() -> Array[SpiBitOrder] {
  [MsbFirst, LsbFirst]
}

///|
/// Create a conventional mode-0, MSB-first SPI configuration.
pub fn spi_mode0(clock : String, chip_select : String) -> SpiConfig {
  {
    clock,
    chip_select,
    mosi: None,
    miso: None,
    polarity: IdleLow,
    phase: SampleLeading,
    order: MsbFirst,
    word_bits: 8,
    active_low: true,
  }
}

///|
/// Attach a MOSI and MISO pair to an SPI configuration.
pub fn SpiConfig::with_data(
  self : SpiConfig,
  mosi : String,
  miso : String?,
) -> SpiConfig {
  { ..self, mosi: Some(mosi), miso }
}

///|
/// Set clock polarity and sampling phase.
pub fn SpiConfig::with_mode(
  self : SpiConfig,
  polarity : SpiPolarity,
  phase : SpiPhase,
) -> SpiConfig {
  { ..self, polarity, phase }
}

///|
/// Set wire order and word width, clamping width to a useful range.
pub fn SpiConfig::with_word_format(
  self : SpiConfig,
  order : SpiBitOrder,
  word_bits : Int,
) -> SpiConfig {
  { ..self, order, word_bits: Int::max(1, Int::min(64, word_bits)) }
}

///|
/// Configure chip-select polarity.
pub fn SpiConfig::with_active_low(
  self : SpiConfig,
  active_low : Bool,
) -> SpiConfig {
  { ..self, active_low, }
}

///|
/// Human-readable polarity label.
pub fn spi_polarity_name(value : SpiPolarity) -> String {
  match value {
    IdleLow => "idle-low"
    IdleHigh => "idle-high"
  }
}

///|
/// Human-readable phase label.
pub fn spi_phase_name(value : SpiPhase) -> String {
  match value {
    SampleLeading => "leading"
    SampleTrailing => "trailing"
  }
}

///|
/// Human-readable bit-order label.
pub fn spi_bit_order_name(value : SpiBitOrder) -> String {
  match value {
    MsbFirst => "msb-first"
    LsbFirst => "lsb-first"
  }
}

///|
/// Return the active level for a configured chip-select.
pub fn spi_chip_select_active(config : SpiConfig, value : Value?) -> Bool {
  let level = match value {
    Some(Scalar(ch)) => ch == '1'
    Some(Vector(bits)) => bits == "1"
    _ => false
  }
  if config.active_low {
    !level
  } else {
    level
  }
}

///|
/// Decode all chip-select framed words in a trace.
pub fn spi_decode(trace : Trace, config : SpiConfig) -> Array[SpiWord] {
  let edge_times = spi_sample_times(trace, config)
  let result : Array[SpiWord] = []
  let mut bits_mosi : Array[String] = []
  let mut bits_miso : Array[String] = []
  let mut start : Int64? = None
  let mut previous_active = false
  for time in edge_times {
    let active = spi_chip_select_active(
      config,
      value_at(trace, config.chip_select, time),
    )
    if active && !previous_active {
      bits_mosi = []
      bits_miso = []
      start = Some(time)
    }
    if active {
      bits_mosi.push(spi_sample_text(trace, config.mosi, time))
      bits_miso.push(spi_sample_text(trace, config.miso, time))
      if bits_mosi.length() == config.word_bits {
        let first = match start {
          Some(value) => value
          None => time
        }
        result.push(
          spi_make_word(
            result.length(),
            first,
            time,
            bits_mosi,
            bits_miso,
            true,
          ),
        )
        bits_mosi = []
        bits_miso = []
        start = Some(time)
      }
    } else if previous_active && bits_mosi.length() > 0 {
      let first = match start {
        Some(value) => value
        None => time
      }
      result.push(
        spi_make_word(result.length(), first, time, bits_mosi, bits_miso, false),
      )
      bits_mosi = []
      bits_miso = []
      start = None
    }
    previous_active = active
  }
  if previous_active && bits_mosi.length() > 0 {
    let first = match start {
      Some(value) => value
      None => trace.end_time
    }
    result.push(
      spi_make_word(
        result.length(),
        first,
        trace.end_time,
        bits_mosi,
        bits_miso,
        false,
      ),
    )
  }
  result
}

///|
/// Decode and summarize SPI activity.
pub fn spi_report(trace : Trace, config : SpiConfig) -> SpiReport {
  let words = spi_decode(trace, config)
  let complete = words.filter(fn(item) { item.complete }).length()
  let bits = words.fold(init=0, fn(total, item) { total + item.clock_edges })
  let first = words.fold(init=None, fn(current, item) {
    match current {
      Some(value) => Some(Int64::min(value, item.start_time))
      None => Some(item.start_time)
    }
  })
  let last = words.fold(init=None, fn(current, item) {
    match current {
      Some(value) => Some(Int64::max(value, item.end_time))
      None => Some(item.end_time)
    }
  })
  {
    config,
    words,
    complete_words: complete,
    incomplete_words: words.length() - complete,
    active_edges: bits,
    total_bits: bits,
    first_time: first,
    last_time: last,
  }
}

///|
/// Return the number of complete words in a report.
pub fn SpiReport::word_count(self : SpiReport) -> Int {
  self.words.length()
}

///|
/// Return the number of incomplete words in a report.
pub fn SpiReport::incomplete_count(self : SpiReport) -> Int {
  self.words.filter(fn(item) { !item.complete }).length()
}

///|
/// Produce a stable CSV table for decoded words.
pub fn spi_words_csv(words : Array[SpiWord]) -> String {
  let out = StringBuilder()
  out.write_string(
    "index,start,end,edges,mosi,miso,mosi_value,miso_value,complete\n",
  )
  for item in words {
    out.write_string(item.index.to_string())
    out.write_char(',')
    out.write_string(item.start_time.to_string())
    out.write_char(',')
    out.write_string(item.end_time.to_string())
    out.write_char(',')
    out.write_string(item.clock_edges.to_string())
    out.write_char(',')
    out.write_string(item.mosi_bits)
    out.write_char(',')
    out.write_string(item.miso_bits)
    out.write_char(',')
    out.write_string(optional_spi_int(item.mosi_value))
    out.write_char(',')
    out.write_string(optional_spi_int(item.miso_value))
    out.write_char(',')
    out.write_string(item.complete.to_string())
    out.write_char('\n')
  }
  out.to_string()
}

///|
/// Render a report for command-line inspection.
pub fn SpiReport::to_text(self : SpiReport) -> String {
  "SPI " +
  self.config.clock +
  " words=" +
  self.words.length().to_string() +
  " complete=" +
  self.complete_words.to_string() +
  " incomplete=" +
  self.incomplete_count().to_string() +
  " bits=" +
  self.total_bits.to_string() +
  " mode=" +
  spi_polarity_name(self.config.polarity) +
  "/" +
  spi_phase_name(self.config.phase) +
  " order=" +
  spi_bit_order_name(self.config.order)
}

///|
/// Render a report as deterministic JSON.
pub fn SpiReport::to_json(self : SpiReport) -> String {
  let words = self.words.map(spi_word_json).join(",")
  "{\"clock\":\"" +
  escape_text(self.config.clock) +
  "\",\"chip_select\":\"" +
  escape_text(self.config.chip_select) +
  "\",\"complete_words\":" +
  self.complete_words.to_string() +
  ",\"incomplete_words\":" +
  self.incomplete_count().to_string() +
  ",\"total_bits\":" +
  self.total_bits.to_string() +
  ",\"words\":[" +
  words +
  "]}"
}

///|
/// Select the clock edge on which data is sampled for the configured mode.
fn spi_sample_times(trace : Trace, config : SpiConfig) -> Array[Int64] {
  let result : Array[Int64] = []
  let mut previous : Char? = None
  let leading = match config.polarity {
    IdleLow => '0'
    IdleHigh => '1'
  }
  let active_edge_is_rising = match config.polarity {
    IdleLow => true
    IdleHigh => false
  }
  for change in trace.changes {
    if change.id != config.clock {
      continue
    }
    match change.value {
      Scalar(current) => {
        let is_edge = match previous {
          Some(before) if active_edge_is_rising =>
            before == '0' && current == '1'
          Some(before) => before == '1' && current == '0'
          None => false
        }
        let sample = match config.phase {
          SampleLeading => is_edge
          SampleTrailing => {
            let opposite = match previous {
              Some(before) if active_edge_is_rising =>
                before == '1' && current == '0'
              Some(before) => before == '0' && current == '1'
              None => false
            }
            opposite
          }
        }
        if sample && current != leading {
          result.push(change.time)
        }
        previous = Some(current)
      }
      _ => previous = None
    }
  }
  result
}

///|
/// Sample an optional data signal as a single wire token.
fn spi_sample_text(trace : Trace, signal_id : String?, time : Int64) -> String {
  match signal_id {
    Some(id) =>
      match value_at(trace, id, time) {
        Some(value) =>
          match value {
            Scalar(ch) => ch.to_string()
            Vector(bits) if bits.length() > 0 => bits
            Vector(_) => "?"
            Real(number) => number.to_string()
          }
        None => "?"
      }
    None => "?"
  }
}

///|
/// Build one word and decode numeric fields only when all bits are known.
fn spi_make_word(
  index : Int,
  start_time : Int64,
  end_time : Int64,
  mosi : Array[String],
  miso : Array[String],
  complete : Bool,
) -> SpiWord {
  let mosi_text = mosi.join("")
  let miso_text = miso.join("")
  {
    index,
    start_time,
    end_time,
    clock_edges: mosi.length(),
    mosi_bits: mosi_text,
    miso_bits: miso_text,
    mosi_value: spi_bits_int(mosi),
    miso_value: spi_bits_int(miso),
    complete,
    chip_select_active: true,
  }
}

///|
/// Parse a sequence of one-bit wire tokens as an unsigned integer.
fn spi_bits_int(bits : Array[String]) -> Int64? {
  let mut result = 0L
  if bits.length() == 0 {
    return None
  }
  for bit in bits {
    result *= 2L
    if bit == "1" {
      result += 1L
    } else if bit != "0" {
      return None
    }
  }
  Some(result)
}

///|
/// Encode an optional SPI integer for CSV.
fn optional_spi_int(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Encode one SPI word.
fn spi_word_json(item : SpiWord) -> String {
  "{\"index\":" +
  item.index.to_string() +
  ",\"start\":" +
  item.start_time.to_string() +
  ",\"end\":" +
  item.end_time.to_string() +
  ",\"edges\":" +
  item.clock_edges.to_string() +
  ",\"mosi\":\"" +
  escape_text(item.mosi_bits) +
  "\",\"miso\":\"" +
  escape_text(item.miso_bits) +
  "\",\"mosi_value\":" +
  optional_spi_json(item.mosi_value) +
  ",\"miso_value\":" +
  optional_spi_json(item.miso_value) +
  ",\"complete\":" +
  item.complete.to_string() +
  "}"
}

///|
/// Encode an optional integer as JSON null or a number.
fn optional_spi_json(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => "null"
  }
}