///|
/// UART parity modes supported by the waveform decoder.
pub enum UartParity {
  NoParity
  EvenParity
  OddParity
} derive(Debug, Eq)

///|
/// Sampling configuration for an asynchronous serial line.
pub struct UartConfig {
  signal_id : String
  bit_period : Int64
  data_bits : Int
  stop_bits : Int
  parity : UartParity
  idle_high : Bool
  sample_offset : Int64
  lsb_first : Bool
} derive(Debug, Eq)

///|
/// A decoded UART frame and its validation status.
pub struct UartFrame {
  index : Int
  start : Int64
  end : Int64
  value : Int64
  data_bits : String
  parity_bit : Char?
  stop_ok : Bool
  parity_ok : Bool
  framing_ok : Bool
  error : String?
} derive(Debug, Eq)

///|
/// Aggregate UART decode metrics.
pub struct UartReport {
  signal_id : String
  frames : Int
  valid_frames : Int
  invalid_frames : Int
  parity_errors : Int
  framing_errors : Int
  minimum_gap : Int64?
  maximum_gap : Int64?
  mean_gap : Double?
  bytes_per_time : Double
} derive(Debug, Eq)

///|
/// Return all supported parity modes.
pub fn uart_parity_catalog() -> Array[UartParity] {
  [NoParity, EvenParity, OddParity]
}

///|
/// Create a conventional 8-N-1 UART configuration.
pub fn uart_8n1(signal_id : String, bit_period : Int64) -> UartConfig {
  {
    signal_id,
    bit_period: Int64::max(1L, bit_period),
    data_bits: 8,
    stop_bits: 1,
    parity: NoParity,
    idle_high: true,
    sample_offset: bit_period / 2L,
    lsb_first: true,
  }
}

///|
/// Update the data width while keeping timing parameters intact.
pub fn UartConfig::with_data_bits(
  self : UartConfig,
  data_bits : Int,
) -> UartConfig {
  { ..self, data_bits: Int::max(5, Int::min(9, data_bits)) }
}

///|
/// Update parity handling.
pub fn UartConfig::with_parity(
  self : UartConfig,
  parity : UartParity,
) -> UartConfig {
  { ..self, parity, }
}

///|
/// Update stop-bit count.
pub fn UartConfig::with_stop_bits(
  self : UartConfig,
  stop_bits : Int,
) -> UartConfig {
  { ..self, stop_bits: Int::max(1, Int::min(2, stop_bits)) }
}

///|
/// Update the sample offset within the first data bit.
pub fn UartConfig::with_sample_offset(
  self : UartConfig,
  offset : Int64,
) -> UartConfig {
  {
    ..self,
    sample_offset: Int64::max(0L, Int64::min(self.bit_period - 1L, offset)),
  }
}

///|
/// Decode all falling start edges that have enough samples for a frame.
pub fn uart_decode(trace : Trace, config : UartConfig) -> Array[UartFrame] {
  let starts = uart_start_edges(trace, config)
  let frames : Array[UartFrame] = []
  for index, start in starts {
    frames.push(decode_uart_frame(trace, config, index, start))
  }
  frames
}

///|
/// Calculate aggregate decode metrics.
pub fn uart_report(trace : Trace, config : UartConfig) -> UartReport {
  let frames = uart_decode(trace, config)
  let valid = frames.count_if(fn(frame) { frame.framing_ok && frame.parity_ok })
  let parity_errors = frames.count_if(fn(frame) { !frame.parity_ok })
  let framing_errors = frames.count_if(fn(frame) { !frame.framing_ok })
  let gaps : Array[Int64] = []
  for index in 1.. String {
  let rows : Array[String] = [
    "index,start,end,value,data_bits,parity,stop_ok,parity_ok,framing_ok,error",
  ]
  for frame in frames {
    rows.push(
      frame.index.to_string() +
      "," +
      frame.start.to_string() +
      "," +
      frame.end.to_string() +
      "," +
      frame.value.to_string() +
      "," +
      frame.data_bits +
      "," +
      optional_char_text(frame.parity_bit) +
      "," +
      frame.stop_ok.to_string() +
      "," +
      frame.parity_ok.to_string() +
      "," +
      frame.framing_ok.to_string() +
      "," +
      optional_string_text(frame.error),
    )
  }
  rows.join("\n")
}

///|
/// Render a UART report as text.
pub fn UartReport::to_text(self : UartReport) -> String {
  "uart=" +
  self.signal_id +
  " frames=" +
  self.frames.to_string() +
  " valid=" +
  self.valid_frames.to_string() +
  " invalid=" +
  self.invalid_frames.to_string() +
  " parity_errors=" +
  self.parity_errors.to_string() +
  " framing_errors=" +
  self.framing_errors.to_string() +
  " mean_gap=" +
  optional_double_text(self.mean_gap) +
  " bytes_per_time=" +
  self.bytes_per_time.to_string()
}

///|
/// Render a UART report as JSON.
pub fn UartReport::to_json(self : UartReport) -> String {
  "{\"signal\":\"" +
  csv_cell(self.signal_id) +
  "\",\"frames\":" +
  self.frames.to_string() +
  ",\"valid_frames\":" +
  self.valid_frames.to_string() +
  ",\"invalid_frames\":" +
  self.invalid_frames.to_string() +
  ",\"parity_errors\":" +
  self.parity_errors.to_string() +
  ",\"framing_errors\":" +
  self.framing_errors.to_string() +
  ",\"minimum_gap\":" +
  optional_int64_json(self.minimum_gap) +
  ",\"maximum_gap\":" +
  optional_int64_json(self.maximum_gap) +
  ",\"mean_gap\":" +
  optional_double_json(self.mean_gap) +
  ",\"bytes_per_time\":" +
  self.bytes_per_time.to_string() +
  "}"
}

///|
/// Find falling transitions from idle to the start bit.
fn uart_start_edges(trace : Trace, config : UartConfig) -> Array[Int64] {
  let starts : Array[Int64] = []
  let changes = trace.changes.filter(fn(change) {
    change.id == config.signal_id
  })
  for index in 1..= config.bit_period {
        starts.push(changes[index].time)
      }
    }
  }
  starts
}

///|
/// Decode one frame by sampling the center of each serial bit.
fn decode_uart_frame(
  trace : Trace,
  config : UartConfig,
  index : Int,
  start : Int64,
) -> UartFrame {
  let data : Array[Char] = []
  let mut numeric : Int64 = 0L
  let mut ones = 0
  for bit in 0.. None
    _ => Some(uart_sample(trace, config.signal_id, parity_time))
  }
  let stop_start = if config.parity == NoParity {
    config.data_bits + 1
  } else {
    config.data_bits + 2
  }
  let stop_ok = uart_stop_ok(trace, config, start, stop_start)
  let parity_ok = uart_parity_ok(config.parity, ones, parity_bit)
  let start_level = if config.idle_high { '0' } else { '1' }
  let framing_ok = uart_sample(
      trace,
      config.signal_id,
      start + config.sample_offset,
    ) ==
    start_level &&
    stop_ok
  let data_bits = data.map(fn(bit) { bit.to_string() }).join("")
  let error = if !framing_ok {
    Some("framing")
  } else if !parity_ok {
    Some("parity")
  } else {
    None
  }
  {
    index,
    start,
    end: start + (stop_start + config.stop_bits).to_int64() * config.bit_period,
    value: numeric,
    data_bits,
    parity_bit,
    stop_ok,
    parity_ok,
    framing_ok,
    error,
  }
}

///|
/// Return a binary sample or unknown marker.
fn uart_sample(trace : Trace, signal_id : String, time : Int64) -> Char {
  match value_at(trace, signal_id, time) {
    Some(Scalar(value)) =>
      if value == '0' || value == '1' {
        value
      } else {
        'x'
      }
    Some(Vector(bits)) =>
      if bits.length() > 0 {
        bits.get_char(0).unwrap_or('x')
      } else {
        'x'
      }
    _ => 'x'
  }
}

///|
/// Return whether a value represents the configured logical level.
fn uart_level(value : Value, active_high : Bool) -> Bool {
  match value {
    Scalar('1') => active_high
    Scalar('0') => !active_high
    _ => false
  }
}

///|
/// Check all stop-bit samples.
fn uart_stop_ok(
  trace : Trace,
  config : UartConfig,
  start : Int64,
  stop_start : Int,
) -> Bool {
  let expected = if config.idle_high { '1' } else { '0' }
  for bit in 0.. Bool {
  match mode {
    NoParity => true
    EvenParity =>
      match parity {
        Some(bit) => {
          let parity_one = if bit == '1' { 1 } else { 0 }
          (ones + parity_one) % 2 == 0
        }
        None => false
      }
    OddParity =>
      match parity {
        Some(bit) => {
          let parity_one = if bit == '1' { 1 } else { 0 }
          (ones + parity_one) % 2 == 1
        }
        None => false
      }
  }
}

///|
/// Return a minimum element for a timestamp array.
fn optional_minimum_i64(values : Array[Int64]) -> Int64? {
  if values.length() == 0 {
    None
  } else {
    Some(
      values.fold(init=values[0], fn(value, item) { Int64::min(value, item) }),
    )
  }
}

///|
/// Return a maximum element for a timestamp array.
fn optional_maximum_i64(values : Array[Int64]) -> Int64? {
  if values.length() == 0 {
    None
  } else {
    Some(
      values.fold(init=values[0], fn(value, item) { Int64::max(value, item) }),
    )
  }
}

///|
/// Render an optional character.
fn optional_char_text(value : Char?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Render an optional string.
fn optional_string_text(value : String?) -> String {
  match value {
    Some(item) => item
    None => ""
  }
}