///|
/// A signal/time selection used to derive a smaller trace.
pub struct TraceSelection {
  pattern : String?
  start : Int64
  end : Int64
  include_empty_signals : Bool
} derive(Debug, Eq)

///|
/// Create a selection covering the complete trace.
pub fn full_selection(trace : Trace) -> TraceSelection {
  { pattern: None, start: 0L, end: trace.end_time, include_empty_signals: true }
}

///|
/// Select signals by glob and changes by inclusive time range.
pub fn select_trace(trace : Trace, selection : TraceSelection) -> Trace {
  let selected_signals = match selection.pattern {
    Some(pattern) => find_signals_glob(trace, pattern)
    None => trace.signals.copy()
  }
  let ids = selected_signals.map(fn(signal) { signal.id })
  let changes = trace.changes.filter(fn(change) {
    ids.contains(change.id) &&
    change.time >= selection.start &&
    change.time <= selection.end
  })
  let final_signals = if selection.include_empty_signals {
    selected_signals
  } else {
    selected_signals.filter(fn(signal) {
      ids.contains(signal.id) &&
      changes.any(fn(change) { change.id == signal.id })
    })
  }
  {
    header: trace.header,
    signals: final_signals,
    changes,
    end_time: Int64::min(
      trace.end_time,
      Int64::max(selection.start, selection.end),
    ),
  }
}

///|
/// Clip a trace to an inclusive time window.
pub fn clip_trace(trace : Trace, start~ : Int64, end~ : Int64) -> Trace {
  select_trace(trace, { pattern: None, start, end, include_empty_signals: true })
}

///|
/// Keep only declarations matching a path glob.
pub fn filter_trace_signals(trace : Trace, pattern : String) -> Trace {
  select_trace(trace, {
    pattern: Some(pattern),
    start: 0L,
    end: trace.end_time,
    include_empty_signals: true,
  })
}

///|
/// Remove consecutive duplicate values for each signal.
pub fn compact_trace(trace : Trace) -> Trace {
  let result : Array[Change] = []
  let previous : Map[String, Value] = Map([])
  for change in trace.changes {
    match previous.get(change.id) {
      Some(value) if value == change.value => ()
      _ => {
        result.push(change)
        previous[change.id] = change.value
      }
    }
  }
  {
    header: trace.header,
    signals: trace.signals,
    changes: result,
    end_time: trace.end_time,
  }
}

///|
/// Remove duplicate writes at the same signal and timestamp, keeping the last.
pub fn deduplicate_trace(trace : Trace) -> Trace {
  let result : Array[Change] = []
  for change in trace.changes {
    let previous = result.last()
    match previous {
      Some(item) if item.id == change.id && item.time == change.time =>
        result[result.length() - 1] = change
      _ => result.push(change)
    }
  }
  {
    header: trace.header,
    signals: trace.signals,
    changes: result,
    end_time: trace.end_time,
  }
}

///|
/// Scale all timestamps and the trace end time by an integer multiplier.
pub fn scale_trace_time(trace : Trace, multiplier : Int64) -> Trace {
  let factor = Int64::max(0L, multiplier)
  let changes = trace.changes.map(fn(change) {
    { time: change.time * factor, id: change.id, value: change.value }
  })
  {
    header: trace.header,
    signals: trace.signals,
    changes,
    end_time: trace.end_time * factor,
  }
}

///|
/// Rename signal identifiers while preserving all values.
pub fn rename_signal(trace : Trace, old_id : String, new_id : String) -> Trace {
  let signals = trace.signals.map(fn(signal) {
    if signal.id == old_id {
      {
        id: new_id,
        name: signal.name,
        width: signal.width,
        kind: signal.kind,
        scope: signal.scope,
      }
    } else {
      signal
    }
  })
  let changes = trace.changes.map(fn(change) {
    if change.id == old_id {
      { time: change.time, id: new_id, value: change.value }
    } else {
      change
    }
  })
  { header: trace.header, signals, changes, end_time: trace.end_time }
}

///|
/// Return a trace with all changes ordered by timestamp and stable input order.
pub fn sort_trace_changes(trace : Trace) -> Trace {
  let changes = trace.changes.copy()
  changes.sort_by(fn(left, right) {
    if left.time < right.time {
      -1
    } else if left.time > right.time {
      1
    } else {
      0
    }
  })
  {
    header: trace.header,
    signals: trace.signals,
    changes,
    end_time: trace.end_time,
  }
}

///|
/// Return a trace with a new explicit end timestamp.
pub fn with_end_time(trace : Trace, end_time : Int64) -> Trace {
  {
    header: trace.header,
    signals: trace.signals,
    changes: trace.changes,
    end_time: Int64::max(0L, end_time),
  }
}

///|
/// Merge changes from two traces with the same declaration shape.
pub fn merge_traces(left : Trace, right : Trace) -> Trace {
  let changes = left.changes + right.changes
  let merged = sort_trace_changes({
    header: left.header,
    signals: left.signals,
    changes,
    end_time: Int64::max(left.end_time, right.end_time),
  })
  deduplicate_trace(merged)
}

///|
/// Keep one change at every `step`-sized time bucket.
pub fn decimate_trace(trace : Trace, step : Int64) -> Trace {
  let safe_step = Int64::max(1L, step)
  let seen : Map[(String, Int64), Bool] = Map([])
  let result : Array[Change] = []
  for change in trace.changes {
    let bucket = change.time / safe_step
    let key = (change.id, bucket)
    if !seen.contains(key) {
      seen[key] = true
      result.push(change)
    }
  }
  {
    header: trace.header,
    signals: trace.signals,
    changes: result,
    end_time: trace.end_time,
  }
}

///|
/// Return whether a transformed trace still references declared identifiers.
pub fn transform_is_valid(trace : Trace) -> Bool {
  validate(trace).length() == 0
}