///|
/// Exchange artifact support for stable trace export/import.

///|
pub(all) suberror TraceArtifactError {
  InvalidArtifact(String)
  MissingField(String)
  InvalidField(String, String)
} derive(Debug)

///|
pub(all) enum TraceArtifactFormat {
  IsoTraceTextV1
} derive(Eq, Debug)

///|
pub fn TraceArtifactFormat::label(self : TraceArtifactFormat) -> String {
  match self {
    IsoTraceTextV1 => "isotrace-text-v1"
  }
}

///|
pub(all) struct TraceArtifactSidecar {
  schema : String
  pcapng_route : String
  pcapng_blocker : String?
  seed : Int
  event_count : Int
  rng_steps : Int
  digest_state : Int64
  portable_digest_state : Int64
  clock_domains : Array[String]
  backend_labels : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct TraceArtifact {
  format : TraceArtifactFormat
  sidecar : TraceArtifactSidecar
  trace_text : String
} derive(Eq, Debug)

///|
pub fn TraceArtifact::from_log(log : TraceLog, seed~ : Int) -> TraceArtifact {
  let digest = log.digest(seed~)
  let portable_digest = log.portable_digest(seed~)
  {
    format: IsoTraceTextV1,
    sidecar: {
      schema: "lockwire.trace.artifact.sidecar.v1",
      pcapng_route: "pcapng-epb+custom-block-sidecar-v1",
      pcapng_blocker: None,
      seed,
      event_count: log.len(),
      rng_steps: digest.rng_steps,
      digest_state: digest.state_digest,
      portable_digest_state: portable_digest.state_digest,
      clock_domains: collect_clock_domains(log),
      backend_labels: collect_backend_labels(log),
    },
    trace_text: log.to_golden_text(),
  }
}

///|
pub fn TraceLog::to_artifact(self : TraceLog, seed~ : Int) -> TraceArtifact {
  TraceArtifact::from_log(self, seed~)
}

///|
pub fn TraceArtifact::import_log(
  self : TraceArtifact,
) -> TraceLog raise TraceArtifactError {
  if self.format != IsoTraceTextV1 {
    raise TraceArtifactError::InvalidArtifact("unsupported artifact format")
  }
  let log = parse_trace_log_text(self.trace_text)
  validate_artifact_sidecar(log, self.sidecar)
  log
}

///|
pub fn TraceArtifact::replay_digest(
  self : TraceArtifact,
) -> @core.SimDigest raise TraceArtifactError {
  let log = self.import_log()
  Replay::from_log(log).digest(seed=self.sidecar.seed)
}

///|
pub fn TraceArtifact::to_text(self : TraceArtifact) -> String {
  let buf = StringBuilder::new()
  buf.write_string("# lockwire-trace-artifact-v1\n")
  buf.write_string("format=" + self.format.label() + "\n")
  buf.write_string("schema=" + self.sidecar.schema + "\n")
  buf.write_string("pcapng_route=" + self.sidecar.pcapng_route + "\n")
  buf.write_string(
    "pcapng_blocker=" +
    string_option_to_artifact_text(self.sidecar.pcapng_blocker) +
    "\n",
  )
  buf.write_string("seed=" + self.sidecar.seed.to_string() + "\n")
  buf.write_string("event_count=" + self.sidecar.event_count.to_string() + "\n")
  buf.write_string("rng_steps=" + self.sidecar.rng_steps.to_string() + "\n")
  buf.write_string(
    "digest_state=" + self.sidecar.digest_state.to_string() + "\n",
  )
  buf.write_string(
    "portable_digest_state=" +
    self.sidecar.portable_digest_state.to_string() +
    "\n",
  )
  buf.write_string(
    "clock_domains=" + self.sidecar.clock_domains.join(",") + "\n",
  )
  buf.write_string(
    "backend_labels=" + self.sidecar.backend_labels.join(",") + "\n",
  )
  buf.write_string("-- trace --\n")
  buf.write_string(self.trace_text)
  buf.to_string()
}

///|
pub fn parse_trace_artifact_text(
  text : String,
) -> TraceArtifact raise TraceArtifactError {
  let lines = split_artifact_lines(text)
  guard lines.get(0) is Some("# lockwire-trace-artifact-v1") else {
    raise TraceArtifactError::InvalidArtifact("missing artifact header")
  }
  let fields : Array[(String, String)] = []
  let trace_lines : Array[String] = []
  let mut in_trace = false
  for i in 1.. TraceLog raise TraceArtifactError {
  let log = TraceLog::new()
  let lines = split_artifact_lines(text)
  for i, line in lines {
    if !line[:].trim().is_empty() {
      log.append(parse_trace_event_line(line, line_number=i + 1))
    }
  }
  log
}

///|
fn validate_artifact_sidecar(
  log : TraceLog,
  sidecar : TraceArtifactSidecar,
) -> Unit raise TraceArtifactError {
  let digest = log.digest(seed=sidecar.seed)
  let portable_digest = log.portable_digest(seed=sidecar.seed)
  if digest.event_count != sidecar.event_count {
    raise TraceArtifactError::InvalidArtifact("event_count mismatch")
  }
  if digest.rng_steps != sidecar.rng_steps {
    raise TraceArtifactError::InvalidArtifact("rng_steps mismatch")
  }
  if digest.state_digest != sidecar.digest_state {
    raise TraceArtifactError::InvalidArtifact("digest_state mismatch")
  }
  if portable_digest.state_digest != sidecar.portable_digest_state {
    raise TraceArtifactError::InvalidArtifact("portable_digest_state mismatch")
  }
  if collect_clock_domains(log) != sidecar.clock_domains {
    raise TraceArtifactError::InvalidArtifact("clock_domains mismatch")
  }
  if collect_backend_labels(log) != sidecar.backend_labels {
    raise TraceArtifactError::InvalidArtifact("backend_labels mismatch")
  }
}

///|
fn parse_trace_event_line(
  line : String,
  line_number~ : Int,
) -> TraceEvent raise TraceArtifactError {
  let fields = parse_trace_line_pairs(line, line_number~)
  let event_id = parse_required_int(fields, "event_id")
  let raw_ns = parse_required_int64(fields, "raw_ns")
  let vtime_ns = parse_required_int64(fields, "vtime_ns")
  TraceEvent::make(
    event_id~,
    parent_id=parse_int_option(required_trace_field(fields, "parent_id")),
    vtime=@core.VTime::from_ns(vtime_ns),
    clock_domain=unescape_artifact_text(
      required_trace_field(fields, "clock_domain"),
    ),
    raw_ns~,
    node_id=unescape_artifact_text(required_trace_field(fields, "node_id")),
    medium_id=unescape_artifact_text(required_trace_field(fields, "medium_id")),
    channel_id=parse_channel_option(required_trace_field(fields, "channel_id")),
    direction=parse_trace_direction(required_trace_field(fields, "direction")),
    payload_digest=parse_int_option(
      required_trace_field(fields, "payload_digest"),
    ),
    rng_step=parse_required_int(fields, "rng_step"),
    seed=parse_required_int(fields, "seed"),
    backend=parse_backend_profile(required_trace_field(fields, "backend")),
    label=unescape_artifact_text(required_trace_field(fields, "label")),
  )
}

///|
fn parse_trace_line_pairs(
  line : String,
  line_number~ : Int,
) -> Array[(String, String)] raise TraceArtifactError {
  let fields : Array[(String, String)] = []
  for part in split_escaped(line, '|') {
    match part.split_once("=") {
      Some((key, value)) => fields.push((key.to_owned(), value.to_owned()))
      None =>
        raise TraceArtifactError::InvalidArtifact(
          "invalid trace line " + line_number.to_string(),
        )
    }
  }
  fields
}

///|
fn parse_artifact_pair(
  line : String,
) -> (String, String) raise TraceArtifactError {
  match line.split_once("=") {
    Some((key, value)) => (key.to_owned(), value.to_owned())
    None => raise TraceArtifactError::InvalidArtifact("invalid sidecar line")
  }
}

///|
fn required_artifact_field(
  fields : Array[(String, String)],
  name : String,
) -> String raise TraceArtifactError {
  for field in fields {
    let (key, value) = field
    if key == name {
      return value
    }
  }
  raise TraceArtifactError::MissingField(name)
}

///|
fn required_trace_field(
  fields : Array[(String, String)],
  name : String,
) -> String raise TraceArtifactError {
  required_artifact_field(fields, name)
}

///|
fn parse_artifact_int(
  fields : Array[(String, String)],
  name : String,
) -> Int raise TraceArtifactError {
  parse_int_field(required_artifact_field(fields, name), name)
}

///|
fn parse_artifact_int64(
  fields : Array[(String, String)],
  name : String,
) -> Int64 raise TraceArtifactError {
  parse_int64_field(required_artifact_field(fields, name), name)
}

///|
fn parse_required_int(
  fields : Array[(String, String)],
  name : String,
) -> Int raise TraceArtifactError {
  parse_int_field(required_trace_field(fields, name), name)
}

///|
fn parse_required_int64(
  fields : Array[(String, String)],
  name : String,
) -> Int64 raise TraceArtifactError {
  parse_int64_field(required_trace_field(fields, name), name)
}

///|
fn parse_int_field(
  value : String,
  name : String,
) -> Int raise TraceArtifactError {
  @string.parse_int(value[:]) catch {
    _ => raise TraceArtifactError::InvalidField(name, value)
  }
}

///|
fn parse_int64_field(
  value : String,
  name : String,
) -> Int64 raise TraceArtifactError {
  @string.parse_int64(value[:]) catch {
    _ => raise TraceArtifactError::InvalidField(name, value)
  }
}

///|
fn parse_int_option(value : String) -> Int? raise TraceArtifactError {
  if value == "none" {
    None
  } else {
    Some(parse_int_field(value, "int_option"))
  }
}

///|
fn parse_channel_option(
  value : String,
) -> @core.ChannelId? raise TraceArtifactError {
  if value == "none" {
    None
  } else {
    Some(@core.ChannelId(parse_int_field(value, "channel_id")))
  }
}

///|
fn parse_trace_direction(
  value : String,
) -> TraceDirection raise TraceArtifactError {
  match value {
    "tx" => Tx
    "rx" => Rx
    "fault" => Fault
    "tick" => Tick
    "charge" => Charge
    "process" => Process
    "resource" => Resource
    "probe" => Probe
    "meta" => Meta
    other => raise TraceArtifactError::InvalidField("direction", other)
  }
}

///|
fn parse_backend_profile(
  value : String,
) -> @core.BackendProfile raise TraceArtifactError {
  match value {
    "sim-native" => @core.SimNative
    "sim-wasm" => @core.SimWasm
    "replay" => @core.Replay
    "real-linux" => @core.RealLinux
    "real-embedded" => @core.RealEmbedded
    other => raise TraceArtifactError::InvalidField("backend", other)
  }
}

///|
fn parse_artifact_format(
  value : String,
) -> TraceArtifactFormat raise TraceArtifactError {
  match value {
    "isotrace-text-v1" => IsoTraceTextV1
    other => raise TraceArtifactError::InvalidField("format", other)
  }
}

///|
fn string_option_to_artifact_text(value : String?) -> String {
  match value {
    Some(text) => escape_text(text)
    None => "none"
  }
}

///|
fn parse_string_option_artifact_text(value : String) -> String? {
  if value == "none" {
    None
  } else {
    Some(unescape_artifact_text(value))
  }
}

///|
fn collect_clock_domains(log : TraceLog) -> Array[String] {
  let domains : Array[String] = []
  for event in log.events {
    push_unique(domains, event.clock_domain)
  }
  domains
}

///|
fn collect_backend_labels(log : TraceLog) -> Array[String] {
  let labels : Array[String] = []
  for event in log.events {
    push_unique(labels, @core.backend_profile_label(event.backend))
  }
  labels
}

///|
fn push_unique(items : Array[String], value : String) -> Unit {
  let mut found = false
  for item in items {
    if item == value {
      found = true
    }
  }
  if !found {
    items.push(value)
  }
}

///|
fn split_artifact_lines(text : String) -> Array[String] {
  let lines : Array[String] = []
  let buf = StringBuilder::new()
  for c in text {
    if c == '\n' {
      lines.push(strip_trailing_cr(buf.to_string()))
      buf.reset()
    } else {
      buf.write_char(c)
    }
  }
  let remaining = strip_trailing_cr(buf.to_string())
  if remaining.length() > 0 {
    lines.push(remaining)
  }
  lines
}

///|
fn strip_trailing_cr(value : String) -> String {
  if value.has_suffix("\r") {
    value[:value.length() - 1].to_owned()
  } else {
    value
  }
}

///|
fn join_lines(lines : Array[String]) -> String {
  let buf = StringBuilder::new()
  for i, line in lines {
    if i > 0 {
      buf.write_char('\n')
    }
    buf.write_string(line)
  }
  buf.to_string()
}

///|
fn split_csv(value : String) -> Array[String] {
  if value == "" {
    []
  } else {
    [
      for part in value.split(",") => part.to_owned()
    ]
  }
}

///|
fn split_escaped(value : String, separator : Char) -> Array[String] {
  let out : Array[String] = []
  let buf = StringBuilder::new()
  let mut escaped = false
  for c in value {
    if c == separator && !escaped {
      out.push(buf.to_string())
      buf.reset()
    } else {
      if c == '\\' && !escaped {
        escaped = true
      } else {
        escaped = false
      }
      buf.write_char(c)
    }
  }
  out.push(buf.to_string())
  out
}

///|
fn unescape_artifact_text(value : String) -> String {
  let buf = StringBuilder::new()
  let mut escaped = false
  for c in value {
    if escaped {
      match c {
        'n' => buf.write_char('\n')
        'r' => buf.write_char('\r')
        '\\' => buf.write_char('\\')
        '|' => buf.write_char('|')
        other => buf.write_char(other)
      }
      escaped = false
    } else if c == '\\' {
      escaped = true
    } else {
      buf.write_char(c)
    }
  }
  if escaped {
    buf.write_char('\\')
  }
  buf.to_string()
}