///|
/// Context policy when slicing a trace around an interval.
pub enum SliceContext {
  NoContext
  IncludeInitialValues
  IncludeFinalValues
  IncludeBothValues
} derive(Debug, Eq)

///|
/// A named slice request suitable for batch analysis.
pub struct TraceSliceRequest {
  name : String
  start : Int64
  end : Int64
  signal_ids : Array[String]
  context : SliceContext
  keep_empty : Bool
} derive(Debug)

///|
/// A slice and its provenance metadata.
pub struct TraceSlice {
  name : String
  trace : Trace
  source_start : Int64
  source_end : Int64
  before_count : Int
  retained_count : Int
  after_count : Int
} derive(Debug)

///|
/// List slice context policies.
pub fn slice_context_catalog() -> Array[SliceContext] {
  [NoContext, IncludeInitialValues, IncludeFinalValues, IncludeBothValues]
}

///|
/// Create a bounded slice request.
pub fn slice_request(
  name : String,
  start : Int64,
  end : Int64,
) -> TraceSliceRequest {
  {
    name,
    start: Int64::min(start, end),
    end: Int64::max(start, end),
    signal_ids: [],
    context: NoContext,
    keep_empty: true,
  }
}

///|
/// Select signal identifiers for a slice.
pub fn TraceSliceRequest::with_signals(
  self : TraceSliceRequest,
  signal_ids : Array[String],
) -> TraceSliceRequest {
  { ..self, signal_ids: signal_ids.copy() }
}

///|
/// Set context policy.
pub fn TraceSliceRequest::with_context(
  self : TraceSliceRequest,
  context : SliceContext,
) -> TraceSliceRequest {
  { ..self, context, }
}

///|
/// Include or omit empty signal declarations.
pub fn TraceSliceRequest::with_empty(
  self : TraceSliceRequest,
  keep_empty : Bool,
) -> TraceSliceRequest {
  { ..self, keep_empty, }
}

///|
/// Materialize one slice with optional boundary context.
pub fn slice_trace(trace : Trace, request : TraceSliceRequest) -> TraceSlice {
  let ids = if request.signal_ids.length() == 0 {
    trace.signals.map(fn(signal) { signal.id })
  } else {
    request.signal_ids
  }
  let before = trace.changes.filter(fn(change) {
    ids.contains(change.id) && change.time < request.start
  })
  let inside = trace.changes.filter(fn(change) {
    ids.contains(change.id) &&
    change.time >= request.start &&
    change.time <= request.end
  })
  let after = trace.changes.filter(fn(change) {
    ids.contains(change.id) && change.time > request.end
  })
  let retained : Array[Change] = []
  match request.context {
    IncludeInitialValues | IncludeBothValues =>
      for id in ids {
        match slice_last_for(before, id) {
          Some(change) => retained.push({ ..change, time: request.start })
          None => ()
        }
      }
    _ => ()
  }
  for change in inside {
    retained.push(change)
  }
  match request.context {
    IncludeFinalValues | IncludeBothValues =>
      for id in ids {
        match slice_first_for(after, id) {
          Some(change) => retained.push({ ..change, time: request.end })
          None => ()
        }
      }
    _ => ()
  }
  let selected_signals = trace.signals.filter(fn(signal) {
    ids.contains(signal.id) &&
    (request.keep_empty || retained.any(fn(change) { change.id == signal.id }))
  })
  let result_trace = sort_trace_changes({
    header: trace.header,
    signals: selected_signals,
    changes: retained,
    end_time: Int64::max(0L, request.end - request.start),
  })
  {
    name: request.name,
    trace: result_trace,
    source_start: request.start,
    source_end: request.end,
    before_count: before.length(),
    retained_count: retained.length(),
    after_count: after.length(),
  }
}

///|
/// Materialize a batch of slices.
pub fn slice_trace_batch(
  trace : Trace,
  requests : Array[TraceSliceRequest],
) -> Array[TraceSlice] {
  requests.map(fn(request) { slice_trace(trace, request) })
}

///|
/// Return whether a slice retained at least one source change.
pub fn TraceSlice::is_empty(self : TraceSlice) -> Bool {
  self.retained_count == 0
}

///|
/// Export slice metadata as CSV.
pub fn trace_slices_csv(slices : Array[TraceSlice]) -> String {
  let out = StringBuilder()
  out.write_string("name,start,end,before,retained,after,signals,empty\n")
  for item in slices {
    out.write_string(
      escape_text(item.name) +
      "," +
      item.source_start.to_string() +
      "," +
      item.source_end.to_string() +
      "," +
      item.before_count.to_string() +
      "," +
      item.retained_count.to_string() +
      "," +
      item.after_count.to_string() +
      "," +
      item.trace.signals.length().to_string() +
      "," +
      item.is_empty().to_string() +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Render slice metadata as JSON.
pub fn TraceSlice::to_json(self : TraceSlice) -> String {
  "{\"name\":\"" +
  escape_text(self.name) +
  "\",\"start\":" +
  self.source_start.to_string() +
  ",\"end\":" +
  self.source_end.to_string() +
  ",\"before\":" +
  self.before_count.to_string() +
  ",\"retained\":" +
  self.retained_count.to_string() +
  ",\"after\":" +
  self.after_count.to_string() +
  ",\"trace\":" +
  trace_json(self.trace) +
  "}"
}

///|
/// Return the latest change for one signal.
fn slice_last_for(changes : Array[Change], signal_id : String) -> Change? {
  let mut result : Change? = None
  for change in changes {
    if change.id == signal_id {
      result = Some(change)
    }
  }
  result
}

///|
/// Return the earliest change for one signal.
fn slice_first_for(changes : Array[Change], signal_id : String) -> Change? {
  for change in changes {
    if change.id == signal_id {
      return Some(change)
    }
  }
  None
}