///|
/// A composite filter for searching runs.
///
/// All specified conditions must match (AND semantics). An empty filter
/// matches all runs.
pub struct RunFilter {
  priv statuses : Array[RunStatus]
  priv tags_any : Array[String]
  priv params_match : Array[(String, String)]
  priv metric_min : Array[(String, Double)]
  priv metric_max : Array[(String, Double)]
} derive(Debug)

///|
/// Build an empty run filter that matches everything.
pub fn RunFilter::new() -> RunFilter {
  {
    statuses: [],
    tags_any: [],
    params_match: [],
    metric_min: [],
    metric_max: [],
  }
}

///|
/// Restrict to runs with one of the given statuses.
pub fn RunFilter::with_statuses(
  self : RunFilter,
  statuses : Array[RunStatus],
) -> RunFilter {
  { ..self, statuses: statuses.copy() }
}

///|
/// Restrict to runs that have at least one of the given tags.
pub fn RunFilter::with_tags_any(
  self : RunFilter,
  tags : Array[String],
) -> RunFilter {
  { ..self, tags_any: tags.copy() }
}

///|
/// Restrict to runs that have a parameter with the given key and value.
pub fn RunFilter::with_param(
  self : RunFilter,
  key : String,
  value : String,
) -> RunFilter {
  let mut updated = self.params_match.copy()
  updated.push((key, value))
  { ..self, params_match: updated }
}

///|
/// Restrict to runs whose latest metric for `key` is at least `min`.
pub fn RunFilter::with_metric_min(
  self : RunFilter,
  key : String,
  min : Double,
) -> RunFilter {
  let mut updated = self.metric_min.copy()
  updated.push((key, min))
  { ..self, metric_min: updated }
}

///|
/// Restrict to runs whose latest metric for `key` is at most `max`.
pub fn RunFilter::with_metric_max(
  self : RunFilter,
  key : String,
  max : Double,
) -> RunFilter {
  let mut updated = self.metric_max.copy()
  updated.push((key, max))
  { ..self, metric_max: updated }
}

///|
/// Test whether a single run matches this filter.
pub fn RunFilter::matches(self : RunFilter, run : Run) -> Bool {
  // Status check
  if self.statuses.length() > 0 {
    let mut found = false
    for s in self.statuses {
      if run.status() == s {
        found = true
        break
      }
    }
    if !found {
      return false
    }
  }
  // Tags check (at least one must match)
  if self.tags_any.length() > 0 {
    let run_tags = run.tags()
    let mut found = false
    for t in self.tags_any {
      for rt in run_tags {
        if t == rt {
          found = true
          break
        }
      }
      if found {
        break
      }
    }
    if !found {
      return false
    }
  }
  // Params check
  if self.params_match.length() > 0 {
    for pair in self.params_match {
      let (key, expected) = pair
      match run.find_param(key) {
        Some(p) => if p.value() != expected { return false }
        None => return false
      }
    }
  }
  // Metric min check
  if self.metric_min.length() > 0 {
    for pair in self.metric_min {
      let (key, min_val) = pair
      match run.latest_metric(key) {
        Some(m) => if m.value() < min_val { return false }
        None => return false
      }
    }
  }
  // Metric max check
  if self.metric_max.length() > 0 {
    for pair in self.metric_max {
      let (key, max_val) = pair
      match run.latest_metric(key) {
        Some(m) => if m.value() > max_val { return false }
        None => return false
      }
    }
  }
  true
}

///|
/// Sort key for ordering runs by a metric or parameter value.
pub struct SortKey {
  priv key : String
  priv ascending : Bool
  priv by_metric : Bool
} derive(Debug)

///|
/// Build a sort key that sorts runs by a metric value in ascending order.
pub fn SortKey::by_metric_ascending(key : String) -> SortKey {
  { key, ascending: true, by_metric: true }
}

///|
/// Build a sort key that sorts runs by a metric value in descending order.
pub fn SortKey::by_metric_descending(key : String) -> SortKey {
  { key, ascending: false, by_metric: true }
}

///|
/// Build a sort key that sorts runs by a parameter value in ascending order.
pub fn SortKey::by_param_ascending(key : String) -> SortKey {
  { key, ascending: true, by_metric: false }
}

///|
/// Build a sort key that sorts runs by a parameter value in descending order.
pub fn SortKey::by_param_descending(key : String) -> SortKey {
  { key, ascending: false, by_metric: false }
}

///|
/// Search runs in the store that match the given filter.
/// Returns detached copies sorted by the given sort key (if any).
pub fn TrackingStore::search_runs(
  self : TrackingStore,
  filter : RunFilter,
  sort : SortKey?,
) -> Array[Run] {
  let result : Array[Run] = []
  for run in self.runs {
    if filter.matches(run) {
      result.push(run)
    }
  }
  match sort {
    None => result
    Some(sk) => sort_runs(result, sk)
  }
}

///|
/// Sort an array of runs by the given sort key.
fn sort_runs(runs : Array[Run], sk : SortKey) -> Array[Run] {
  // Simple insertion sort to avoid closure complexity.
  let arr = runs.copy()
  for i = 1; i < arr.length(); i = i + 1 {
    let mut j = i
    while j > 0 && compare_runs(arr[j - 1], arr[j], sk) > 0 {
      let temp = arr[j]
      arr[j] = arr[j - 1]
      arr[j - 1] = temp
      j = j - 1
    }
  }
  arr
}

///|
/// Compare two runs according to a sort key. Returns -1, 0, or 1.
fn compare_runs(a : Run, b : Run, sk : SortKey) -> Int {
  if sk.by_metric {
    let va = match a.latest_metric(sk.key) {
      Some(m) => m.value()
      None => return 1 // runs without the metric go last
    }
    let vb = match b.latest_metric(sk.key) {
      Some(m) => m.value()
      None => return -1
    }
    if sk.ascending {
      if va < vb {
        return -1
      }
      if va > vb {
        return 1
      }
      return 0
    } else {
      if va > vb {
        return -1
      }
      if va < vb {
        return 1
      }
      return 0
    }
  } else {
    let va = match a.find_param(sk.key) {
      Some(p) => p.value()
      None => return 1
    }
    let vb = match b.find_param(sk.key) {
      Some(p) => p.value()
      None => return -1
    }
    if sk.ascending {
      if va < vb {
        return -1
      }
      if va > vb {
        return 1
      }
      return 0
    } else {
      if va > vb {
        return -1
      }
      if va < vb {
        return 1
      }
      return 0
    }
  }
}