///|
/// Policies used when a trace is divided into analysis windows.
pub enum WindowPolicy {
  FixedWidth
  FixedCount
  Overlapping
  ChangeDriven
  SignalActivity
} derive(Debug, Eq)

///|
/// Return every supported policy. Keeping the catalog public also makes it
/// possible for CLI frontends to present the same choices as the library.
pub fn window_policy_catalog() -> Array[WindowPolicy] {
  [FixedWidth, FixedCount, Overlapping, ChangeDriven, SignalActivity]
}

///|
/// A request for deterministic trace segmentation.
pub struct WindowSpec {
  policy : WindowPolicy
  width : Int64
  count : Int
  overlap : Int64
  minimum_changes : Int
  include_empty : Bool
} derive(Debug, Eq)

///|
/// A bounded view of a trace. The source trace remains unchanged.
pub struct TraceWindow {
  index : Int
  start : Int64
  end : Int64
  changes : Array[Change]
  signal_count : Int
  active_signals : Int
  transition_count : Int
} derive(Debug)

///|
/// Counters used to compare windows without serializing every change.
pub struct WindowMetric {
  index : Int
  start : Int64
  end : Int64
  duration : Int64
  changes : Int
  active_signals : Int
  density : Double
  first_change : Int64?
  last_change : Int64?
} derive(Debug, Eq)

///|
/// A complete segmentation plan with stable source bounds.
pub struct WindowPlan {
  source_start : Int64
  source_end : Int64
  windows : Array[TraceWindow]
  policy : WindowPolicy
  dropped_changes : Int
} derive(Debug)

///|
/// Return a conservative default for a trace with a useful duration.
pub fn default_window_spec(trace : Trace) -> WindowSpec {
  let duration = Int64::max(1L, trace.duration())
  {
    policy: FixedWidth,
    width: Int64::max(1L, duration / 16L),
    count: 16,
    overlap: 0L,
    minimum_changes: 1,
    include_empty: false,
  }
}

///|
/// Build a window specification with a fixed width.
pub fn fixed_window_spec(width : Int64) -> WindowSpec {
  {
    policy: FixedWidth,
    width: Int64::max(1L, width),
    count: 0,
    overlap: 0L,
    minimum_changes: 0,
    include_empty: true,
  }
}

///|
/// Build a window specification that creates exactly the requested count.
pub fn counted_window_spec(count : Int) -> WindowSpec {
  {
    policy: FixedCount,
    width: 0L,
    count: Int::max(1, count),
    overlap: 0L,
    minimum_changes: 0,
    include_empty: true,
  }
}

///|
/// Build an overlapping fixed-width specification.
pub fn overlapping_window_spec(width : Int64, overlap : Int64) -> WindowSpec {
  let bounded_width = Int64::max(1L, width)
  {
    policy: Overlapping,
    width: bounded_width,
    count: 0,
    overlap: Int64::min(Int64::max(0L, overlap), bounded_width - 1L),
    minimum_changes: 0,
    include_empty: true,
  }
}

///|
/// Return the inclusive duration represented by a window.
pub fn TraceWindow::duration(self : TraceWindow) -> Int64 {
  Int64::max(0L, self.end - self.start)
}

///|
/// Return whether a timestamp belongs to the inclusive window range.
pub fn TraceWindow::contains(self : TraceWindow, time : Int64) -> Bool {
  time >= self.start && time <= self.end
}

///|
/// Return a compact window label suitable for logs and CSV files.
pub fn TraceWindow::label(self : TraceWindow) -> String {
  "w" +
  self.index.to_string() +
  "[" +
  self.start.to_string() +
  "," +
  self.end.to_string() +
  "]"
}

///|
/// Return the window's changes as a defensive copy.
pub fn TraceWindow::copy_changes(self : TraceWindow) -> Array[Change] {
  self.changes.copy()
}

///|
/// Return the number of transitions in a window.
pub fn TraceWindow::change_count(self : TraceWindow) -> Int {
  self.changes.length()
}

///|
/// Return a stable list of identifiers active in a window.
pub fn TraceWindow::signal_ids(self : TraceWindow) -> Array[String] {
  let ids : Array[String] = []
  for change in self.changes {
    if !ids.contains(change.id) {
      ids.push(change.id)
    }
  }
  ids
}

///|
/// Return a one-line metric record for a window.
pub fn window_metric(window : TraceWindow) -> WindowMetric {
  let first : Int64? = if window.changes.length() == 0 {
    None
  } else {
    Some(window.changes[0].time)
  }
  let last : Int64? = if window.changes.length() == 0 {
    None
  } else {
    Some(window.changes[window.changes.length() - 1].time)
  }
  let duration = window.duration()
  let density = if duration == 0L {
    window.changes.length().to_double()
  } else {
    window.changes.length().to_double() / duration.to_double()
  }
  {
    index: window.index,
    start: window.start,
    end: window.end,
    duration,
    changes: window.changes.length(),
    active_signals: window.active_signals,
    density,
    first_change: first,
    last_change: last,
  }
}

///|
/// Render metrics as deterministic CSV.
pub fn window_metrics_csv(metrics : Array[WindowMetric]) -> String {
  let rows : Array[String] = [
    "index,start,end,duration,changes,active_signals,density,first_change,last_change",
  ]
  for metric in metrics {
    rows.push(
      metric.index.to_string() +
      "," +
      metric.start.to_string() +
      "," +
      metric.end.to_string() +
      "," +
      metric.duration.to_string() +
      "," +
      metric.changes.to_string() +
      "," +
      metric.active_signals.to_string() +
      "," +
      metric.density.to_string() +
      "," +
      optional_int64_text(metric.first_change) +
      "," +
      optional_int64_text(metric.last_change),
    )
  }
  rows.join("\n")
}

///|
/// Render metrics as a small JSON array without external dependencies.
pub fn window_metrics_json(metrics : Array[WindowMetric]) -> String {
  metrics
  .map(fn(metric) {
    "{\"index\":" +
    metric.index.to_string() +
    ",\"start\":" +
    metric.start.to_string() +
    ",\"end\":" +
    metric.end.to_string() +
    ",\"duration\":" +
    metric.duration.to_string() +
    ",\"changes\":" +
    metric.changes.to_string() +
    ",\"active_signals\":" +
    metric.active_signals.to_string() +
    ",\"density\":" +
    metric.density.to_string() +
    ",\"first_change\":" +
    optional_int64_json(metric.first_change) +
    ",\"last_change\":" +
    optional_int64_json(metric.last_change) +
    "}"
  })
  .join(",")
  |> fn(body) { "[" + body + "]" }
}

///|
/// Divide a trace into fixed-width inclusive windows.
pub fn fixed_windows(trace : Trace, width : Int64) -> Array[TraceWindow] {
  let bounded = Int64::max(1L, width)
  let end_time = Int64::max(
    trace.end_time,
    trace.changes.fold(init=0L, fn(maximum, change) {
      Int64::max(maximum, change.time)
    }),
  )
  let windows : Array[TraceWindow] = []
  let mut start = 0L
  let mut index = 0
  while start <= end_time {
    let end = Int64::min(end_time, start + bounded)
    windows.push(make_window(trace, index, start, end))
    if end == end_time {
      break
    }
    start = end + 1L
    index += 1
  }
  if windows.length() == 0 {
    windows.push(make_window(trace, 0, 0L, bounded))
  }
  windows
}

///|
/// Divide a trace into a requested number of equally sized windows.
pub fn counted_windows(trace : Trace, count : Int) -> Array[TraceWindow] {
  let total = Int::max(1, count)
  let end_time = Int64::max(0L, trace.end_time)
  let span = Int64::max(
    1L,
    (end_time + 1L + total.to_int64() - 1L) / total.to_int64(),
  )
  let windows : Array[TraceWindow] = []
  for index in 0.. Array[TraceWindow] {
  let bounded_width = Int64::max(1L, width)
  let bounded_overlap = Int64::min(Int64::max(0L, overlap), bounded_width - 1L)
  let step = Int64::max(1L, bounded_width - bounded_overlap)
  let end_time = Int64::max(0L, trace.end_time)
  let windows : Array[TraceWindow] = []
  let mut start = 0L
  let mut index = 0
  while start <= end_time {
    let end = Int64::min(end_time, start + bounded_width - 1L)
    windows.push(make_window(trace, index, start, end))
    if end == end_time {
      break
    }
    start += step
    index += 1
  }
  windows
}

///|
/// Build windows from change density. A new window starts after enough changes.
pub fn change_driven_windows(
  trace : Trace,
  minimum_changes : Int,
) -> Array[TraceWindow] {
  let threshold = Int::max(1, minimum_changes)
  if trace.changes.length() == 0 {
    return [make_window(trace, 0, 0L, trace.end_time)]
  }
  let windows : Array[TraceWindow] = []
  let mut start = trace.changes[0].time
  let mut current : Array[Change] = []
  let mut index = 0
  let mut last_time = start
  for change in trace.changes {
    if current.length() >= threshold && change.time != last_time {
      windows.push(
        make_window_from_changes(trace, index, start, last_time, current),
      )
      index += 1
      current = []
      start = change.time
    }
    current.push(change)
    last_time = change.time
  }
  if current.length() > 0 {
    windows.push(
      make_window_from_changes(trace, index, start, last_time, current),
    )
  }
  windows
}

///|
/// Select the segmentation algorithm requested by a specification.
pub fn segment_trace(trace : Trace, spec : WindowSpec) -> Array[TraceWindow] {
  let windows = match spec.policy {
    FixedWidth => fixed_windows(trace, spec.width)
    FixedCount => counted_windows(trace, spec.count)
    Overlapping => overlapping_windows(trace, spec.width, spec.overlap)
    ChangeDriven => change_driven_windows(trace, spec.minimum_changes)
    SignalActivity => activity_windows(trace, spec.width, spec.minimum_changes)
  }
  if spec.include_empty {
    windows
  } else {
    windows.filter(fn(window) { window.changes.length() > 0 })
  }
}

///|
/// Build and validate a full segmentation plan.
pub fn make_window_plan(trace : Trace, spec : WindowSpec) -> WindowPlan {
  let windows = segment_trace(trace, spec)
  let retained = windows.fold(init=0, fn(total, window) {
    total + window.changes.length()
  })
  {
    source_start: 0L,
    source_end: trace.end_time,
    windows,
    policy: spec.policy,
    dropped_changes: Int::max(0, trace.changes.length() - retained),
  }
}

///|
/// Return whether the windows cover every source change at least once.
pub fn WindowPlan::covers_changes(self : WindowPlan, trace : Trace) -> Bool {
  for change in trace.changes {
    if !self.windows.any(fn(window) { window.contains(change.time) }) {
      return false
    }
  }
  true
}

///|
/// Return the total number of retained change records.
pub fn WindowPlan::retained_changes(self : WindowPlan) -> Int {
  self.windows.fold(init=0, fn(total, window) {
    total + window.changes.length()
  })
}

///|
/// Return the number of windows that contain no changes.
pub fn WindowPlan::empty_count(self : WindowPlan) -> Int {
  self.windows.count_if(fn(window) { window.changes.length() == 0 })
}

///|
/// Return whether window starts are monotonically increasing.
pub fn WindowPlan::is_monotonic(self : WindowPlan) -> Bool {
  let mut previous : Int64? = None
  for window in self.windows {
    match previous {
      Some(value) if window.start < value => return false
      _ => ()
    }
    previous = Some(window.start)
  }
  true
}

///|
/// Find the first window containing a timestamp.
pub fn window_at(plan : WindowPlan, time : Int64) -> TraceWindow? {
  for window in plan.windows {
    if window.contains(time) {
      return Some(window)
    }
  }
  None
}

///|
/// Return all windows that intersect an inclusive range.
pub fn windows_between(
  plan : WindowPlan,
  start : Int64,
  end : Int64,
) -> Array[TraceWindow] {
  let low = Int64::min(start, end)
  let high = Int64::max(start, end)
  plan.windows.filter(fn(window) { window.end >= low && window.start <= high })
}

///|
/// Merge adjacent windows when their gap is below a threshold.
pub fn merge_adjacent_windows(
  windows : Array[TraceWindow],
  maximum_gap : Int64,
) -> Array[TraceWindow] {
  if windows.length() == 0 {
    return []
  }
  let merged : Array[TraceWindow] = []
  let mut current = windows[0]
  for next in windows[1:] {
    let gap = next.start - current.end - 1L
    if gap <= Int64::max(0L, maximum_gap) {
      let changes = current.changes.copy()
      for change in next.changes {
        changes.push(change)
      }
      current = make_window_from_changes(
        {
          header: { date: None, version: None, timescale: None, comment: None },
          signals: [],
          changes: [],
          end_time: 0L,
        },
        current.index,
        current.start,
        Int64::max(current.end, next.end),
        changes,
      )
      current = {
        ..current,
        signal_count: Int::max(current.signal_count, next.signal_count),
      }
    } else {
      merged.push(current)
      current = next
    }
  }
  merged.push(current)
  merged
}

///|
/// Resample a signal at evenly spaced timestamps inside a window.
pub fn resample_window(
  trace : Trace,
  signal_id : String,
  start : Int64,
  end : Int64,
  step : Int64,
) -> Array[Sample] {
  let bounded_step = Int64::max(1L, step)
  let low = Int64::min(start, end)
  let high = Int64::max(start, end)
  let samples : Array[Sample] = []
  let mut time = low
  while time <= high {
    samples.push({ signal_id, time, value: value_at(trace, signal_id, time) })
    if time > high - bounded_step {
      break
    }
    time += bounded_step
  }
  samples
}

///|
/// Return the last sample at or before the requested point for each signal.
pub fn snapshot_window(trace : Trace, time : Int64) -> Array[Sample] {
  trace.signals.map(fn(signal) {
    { signal_id: signal.id, time, value: value_at(trace, signal.id, time) }
  })
}

///|
/// Return a dense activity score based on distinct change timestamps.
pub fn window_activity_score(window : TraceWindow) -> Int {
  let times : Array[Int64] = []
  for change in window.changes {
    if !times.contains(change.time) {
      times.push(change.time)
    }
  }
  if window.duration() == 0L {
    times.length()
  } else {
    (times.length().to_double() * 1000.0 / window.duration().to_double()).to_int()
  }
}

///|
/// Produce one line per window for operational dashboards.
pub fn window_plan_report(plan : WindowPlan) -> String {
  let rows : Array[String] = [
    "windows=" + plan.windows.length().to_string(),
    "retained_changes=" + plan.retained_changes().to_string(),
    "dropped_changes=" + plan.dropped_changes.to_string(),
    "empty_windows=" + plan.empty_count().to_string(),
    "monotonic=" + plan.is_monotonic().to_string(),
  ]
  for window in plan.windows {
    let metric = window_metric(window)
    rows.push(
      window.label() +
      " changes=" +
      metric.changes.to_string() +
      " active_signals=" +
      metric.active_signals.to_string() +
      " density=" +
      metric.density.to_string(),
    )
  }
  rows.join("\n")
}

///|
/// Build a window from a source trace and inclusive time bounds.
fn make_window(
  trace : Trace,
  index : Int,
  start : Int64,
  end : Int64,
) -> TraceWindow {
  let changes = trace.changes.filter(fn(change) {
    change.time >= start && change.time <= end
  })
  make_window_from_changes(trace, index, start, end, changes)
}

///|
/// Build a window from already selected changes.
fn make_window_from_changes(
  trace : Trace,
  index : Int,
  start : Int64,
  end : Int64,
  changes : Array[Change],
) -> TraceWindow {
  let ids : Array[String] = []
  let transition_count = changes.fold(init=0, fn(total, change) {
    if ids.contains(change.id) {
      total + 1
    } else {
      ids.push(change.id)
      total
    }
  })
  {
    index,
    start,
    end: Int64::max(start, end),
    changes: changes.copy(),
    signal_count: trace.signals.length(),
    active_signals: ids.length(),
    transition_count,
  }
}

///|
/// Divide a trace using both time width and a minimum activity threshold.
fn activity_windows(
  trace : Trace,
  width : Int64,
  minimum_changes : Int,
) -> Array[TraceWindow] {
  let base = fixed_windows(trace, Int64::max(1L, width))
  let threshold = Int::max(1, minimum_changes)
  base.filter(fn(window) {
    window.changes.length() >= threshold || window.start == 0L
  })
}

///|
/// Return an optional integer as text without allocating an ad-hoc sentinel.
fn optional_int64_text(value : Int64?) -> String {
  match value {
    Some(number) => number.to_string()
    None => ""
  }
}

///|
/// Return an optional integer as JSON.
fn optional_int64_json(value : Int64?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "null"
  }
}