///|
/// Reusable stimulus patterns for deterministic waveform fixtures.
pub enum SynthPattern {
  ConstantLow
  ConstantHigh
  Toggle
  PulseTrain
  Counter
  RampReal
} derive(Debug, Eq)

///|
/// A signal stimulus declaration.
pub struct SynthSignal {
  id : String
  name : String
  width : Int
  pattern : SynthPattern
  period : Int64
  phase : Int64
  seed : Int64
} derive(Debug)

///|
/// A complete synthetic trace plan.
pub struct SynthPlan {
  end_time : Int64
  signals : Array[SynthSignal]
  timescale : String
} derive(Debug)

///|
/// List deterministic stimulus patterns.
pub fn synth_pattern_catalog() -> Array[SynthPattern] {
  [ConstantLow, ConstantHigh, Toggle, PulseTrain, Counter, RampReal]
}

///|
/// Create an empty synthetic plan.
pub fn synth_plan(end_time : Int64) -> SynthPlan {
  { end_time: Int64::max(0L, end_time), signals: [], timescale: "1ns" }
}

///|
/// Add a signal stimulus to a plan.
pub fn SynthPlan::with_signal(
  self : SynthPlan,
  id : String,
  name : String,
  width : Int,
  pattern : SynthPattern,
  period : Int64,
) -> SynthPlan {
  let signals = self.signals.copy()
  signals.push({
    id,
    name,
    width: Int::max(1, width),
    pattern,
    period: Int64::max(1L, period),
    phase: 0L,
    seed: 0L,
  })
  { ..self, signals, }
}

///|
/// Add a phase offset and initial seed to the last signal.
pub fn SynthPlan::with_last_signal_seed(
  self : SynthPlan,
  phase : Int64,
  seed : Int64,
) -> SynthPlan {
  if self.signals.length() == 0 {
    return self
  }
  let signals = self.signals.copy()
  let index = signals.length() - 1
  let signal = signals[index]
  signals[index] = { ..signal, phase, seed }
  { ..self, signals, }
}

///|
/// Generate a valid trace from a synthetic plan.
pub fn synthesize_trace(plan : SynthPlan) -> Trace {
  let signals = plan.signals.map(fn(item) {
    { id: item.id, name: item.name, width: item.width, kind: "wire", scope: [] }
  })
  let changes : Array[Change] = []
  for signal in plan.signals {
    let mut time = Int64::max(0L, signal.phase)
    while time <= plan.end_time {
      let value = synth_value(signal, time)
      changes.push({ time, id: signal.id, value })
      time += signal.period
    }
  }
  let ordered = sort_trace_changes({
    header: {
      date: None,
      version: Some("synth"),
      timescale: Some(plan.timescale),
      comment: None,
    },
    signals,
    changes,
    end_time: plan.end_time,
  })
  ordered
}

///|
/// Generate a pulse fixture with clock and payload signals.
pub fn synthesize_pulse_fixture(end_time : Int64, period : Int64) -> Trace {
  synthesize_trace(
    synth_plan(end_time)
    .with_signal("clk", "clk", 1, Toggle, period)
    .with_signal("pulse", "pulse", 1, PulseTrain, period * 4L),
  )
}

///|
/// Render a plan as a compact text description.
pub fn SynthPlan::to_text(self : SynthPlan) -> String {
  "end=" +
  self.end_time.to_string() +
  " signals=" +
  self.signals.length().to_string() +
  " timescale=" +
  self.timescale
}

///|
/// Render a synthetic plan as JSON.
pub fn SynthPlan::to_json(self : SynthPlan) -> String {
  let signals = self.signals.map(synth_signal_json).join(",")
  "{\"end_time\":" +
  self.end_time.to_string() +
  ",\"timescale\":\"" +
  self.timescale +
  "\",\"signals\":[" +
  signals +
  "]}"
}

///|
/// Produce one value at a synthetic timestamp.
fn synth_value(signal : SynthSignal, time : Int64) -> Value {
  let phase_time = Int64::max(0L, time - signal.phase)
  match signal.pattern {
    ConstantLow => Scalar('0')
    ConstantHigh => Scalar('1')
    Toggle =>
      Scalar(if phase_time / signal.period % 2L == 0L { '0' } else { '1' })
    PulseTrain =>
      Scalar(if phase_time % signal.period == 0L { '1' } else { '0' })
    Counter =>
      Vector(
        synth_counter_bits(
          signal.seed + phase_time / signal.period,
          signal.width,
        ),
      )
    RampReal =>
      Real(
        signal.seed.to_double() +
        phase_time.to_double() / signal.period.to_double(),
      )
  }
}

///|
/// Render a counter value with a fixed width.
fn synth_counter_bits(value : Int64, width : Int) -> String {
  let result : Array[String] = []
  for index in 0..> index) & 1L
    result.insert(0, if bit == 1L { "1" } else { "0" })
  }
  result.join("")
}

///|
/// Encode one synthetic signal.
fn synth_signal_json(item : SynthSignal) -> String {
  "{\"id\":\"" +
  escape_text(item.id) +
  "\",\"name\":\"" +
  escape_text(item.name) +
  "\",\"width\":" +
  item.width.to_string() +
  ",\"pattern\":\"" +
  synth_pattern_name(item.pattern) +
  "\",\"period\":" +
  item.period.to_string() +
  "}"
}

///|
/// Name a synthetic pattern.
pub fn synth_pattern_name(value : SynthPattern) -> String {
  match value {
    ConstantLow => "constant-low"
    ConstantHigh => "constant-high"
    Toggle => "toggle"
    PulseTrain => "pulse-train"
    Counter => "counter"
    RampReal => "ramp-real"
  }
}