///|
/// Metric families included in a signal profile.
pub enum ProfileMetricKind {
  TransitionCount
  RisingCount
  FallingCount
  UnknownCount
  DutyCycle
  Frequency
  Entropy
  MeanChangeInterval
} derive(Debug, Eq)

///|
/// One measured profile metric.
pub struct ProfileMetric {
  signal_id : String
  kind : ProfileMetricKind
  value : Double?
  count : Int?
  unit : String
  quality : Int
} derive(Debug)

///|
/// A complete profile for one trace.
pub struct TraceProfile {
  trace_end : Int64
  signal_count : Int
  metrics : Array[ProfileMetric]
  quality : Int
} derive(Debug)

///|
/// List profile metric families.
pub fn profile_metric_catalog() -> Array[ProfileMetricKind] {
  [
    TransitionCount,
    RisingCount,
    FallingCount,
    UnknownCount,
    DutyCycle,
    Frequency,
    Entropy,
    MeanChangeInterval,
  ]
}

///|
/// Build signal profiles from the existing analysis primitives.
pub fn build_trace_profile(trace : Trace) -> TraceProfile {
  let metrics : Array[ProfileMetric] = []
  for signal in trace.signals {
    let summary = stats(trace, signal.id)
    metrics.push({
      signal_id: signal.id,
      kind: TransitionCount,
      value: None,
      count: Some(summary.changes),
      unit: "changes",
      quality: profile_quality(summary.changes),
    })
    metrics.push({
      signal_id: signal.id,
      kind: RisingCount,
      value: None,
      count: Some(summary.rising_edges),
      unit: "edges",
      quality: 100,
    })
    metrics.push({
      signal_id: signal.id,
      kind: FallingCount,
      value: None,
      count: Some(summary.falling_edges),
      unit: "edges",
      quality: 100,
    })
    metrics.push({
      signal_id: signal.id,
      kind: UnknownCount,
      value: None,
      count: Some(summary.unknown_values),
      unit: "samples",
      quality: if summary.changes == 0 {
        0
      } else {
        100
      },
    })
    metrics.push({
      signal_id: signal.id,
      kind: DutyCycle,
      value: duty_cycle(trace, signal.id),
      count: None,
      unit: "ratio",
      quality: if trace.end_time > 0L {
        100
      } else {
        0
      },
    })
    let frequency = frequency_estimate(trace, signal.id)
    metrics.push({
      signal_id: signal.id,
      kind: Frequency,
      value: frequency.frequency,
      count: None,
      unit: "1/time",
      quality: if frequency.period_count > 0 {
        100
      } else {
        0
      },
    })
    metrics.push({
      signal_id: signal.id,
      kind: Entropy,
      value: Some(value_entropy(trace, signal.id)),
      count: None,
      unit: "bits",
      quality: 100,
    })
    metrics.push({
      signal_id: signal.id,
      kind: MeanChangeInterval,
      value: mean_change_interval(trace, signal.id),
      count: None,
      unit: "time",
      quality: if summary.changes > 1 {
        100
      } else {
        0
      },
    })
  }
  let quality = if metrics.length() == 0 {
    0
  } else {
    metrics.fold(init=0, fn(sum, item) { sum + item.quality }) /
    metrics.length()
  }
  {
    trace_end: trace.end_time,
    signal_count: trace.signals.length(),
    metrics,
    quality,
  }
}

///|
/// Return metrics for one signal.
pub fn TraceProfile::for_signal(
  self : TraceProfile,
  signal_id : String,
) -> Array[ProfileMetric] {
  self.metrics.filter(fn(item) { item.signal_id == signal_id })
}

///|
/// Return a metric by signal and kind.
pub fn TraceProfile::metric(
  self : TraceProfile,
  signal_id : String,
  kind : ProfileMetricKind,
) -> ProfileMetric? {
  for item in self.metrics {
    if item.signal_id == signal_id && item.kind == kind {
      return Some(item)
    }
  }
  None
}

///|
/// Export profile metrics as CSV.
pub fn profile_metrics_csv(metrics : Array[ProfileMetric]) -> String {
  let out = StringBuilder()
  out.write_string("signal,metric,value,count,unit,quality\n")
  for item in metrics {
    out.write_string(
      escape_text(item.signal_id) +
      "," +
      profile_metric_name(item.kind) +
      "," +
      profile_optional_double(item.value) +
      "," +
      profile_optional_int(item.count) +
      "," +
      item.unit +
      "," +
      item.quality.to_string() +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Render profile summary.
pub fn TraceProfile::to_text(self : TraceProfile) -> String {
  "signals=" +
  self.signal_count.to_string() +
  " metrics=" +
  self.metrics.length().to_string() +
  " quality=" +
  self.quality.to_string()
}

///|
/// Render profile metrics as JSON.
pub fn TraceProfile::to_json(self : TraceProfile) -> String {
  "{\"trace_end\":" +
  self.trace_end.to_string() +
  ",\"signals\":" +
  self.signal_count.to_string() +
  ",\"quality\":" +
  self.quality.to_string() +
  ",\"metrics\":[" +
  self.metrics.map(profile_metric_json).join(",") +
  "]}"
}

///|
/// Assign a data-quality score to a change count.
fn profile_quality(changes : Int) -> Int {
  if changes == 0 {
    0
  } else {
    100
  }
}

///|
/// Name a profile metric.
pub fn profile_metric_name(kind : ProfileMetricKind) -> String {
  match kind {
    TransitionCount => "transition-count"
    RisingCount => "rising-count"
    FallingCount => "falling-count"
    UnknownCount => "unknown-count"
    DutyCycle => "duty-cycle"
    Frequency => "frequency"
    Entropy => "entropy"
    MeanChangeInterval => "mean-change-interval"
  }
}

///|
/// Encode an optional profile number.
fn profile_optional_double(value : Double?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Encode an optional profile count.
fn profile_optional_int(value : Int?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Encode a profile metric.
fn profile_metric_json(item : ProfileMetric) -> String {
  "{\"signal\":\"" +
  escape_text(item.signal_id) +
  "\",\"metric\":\"" +
  profile_metric_name(item.kind) +
  "\",\"value\":" +
  profile_optional_json_double(item.value) +
  ",\"count\":" +
  profile_optional_json_int(item.count) +
  ",\"quality\":" +
  item.quality.to_string() +
  "}"
}

///|
/// Encode an optional profile number as JSON.
fn profile_optional_json_double(value : Double?) -> String {
  match value {
    Some(item) => item.to_string()
    None => "null"
  }
}

///|
/// Encode an optional profile count as JSON.
fn profile_optional_json_int(value : Int?) -> String {
  match value {
    Some(item) => item.to_string()
    None => "null"
  }
}