///|
/// Aggregate evaluation metrics for a group of completed rollouts.
pub struct EvaluationReport {
  episodes : Int
  total_steps : Int
  total_reward : Double
  mean_reward : Double
  best_reward : Double
  worst_reward : Double
  terminated : Int
  truncated : Int
}

///|
pub fn EvaluationReport::episodes(self : EvaluationReport) -> Int {
  self.episodes
}

///|
pub fn EvaluationReport::total_steps(self : EvaluationReport) -> Int {
  self.total_steps
}

///|
pub fn EvaluationReport::total_reward(self : EvaluationReport) -> Double {
  self.total_reward
}

///|
pub fn EvaluationReport::mean_reward(self : EvaluationReport) -> Double {
  self.mean_reward
}

///|
pub fn EvaluationReport::best_reward(self : EvaluationReport) -> Double {
  self.best_reward
}

///|
pub fn EvaluationReport::worst_reward(self : EvaluationReport) -> Double {
  self.worst_reward
}

///|
pub fn EvaluationReport::terminated(self : EvaluationReport) -> Int {
  self.terminated
}

///|
pub fn EvaluationReport::truncated(self : EvaluationReport) -> Int {
  self.truncated
}

///|
pub fn[S, A] evaluate_episodes(
  episodes : Array[Episode[S, A]],
) -> EvaluationReport {
  if episodes.length() == 0 {
    return {
      episodes: 0,
      total_steps: 0,
      total_reward: 0.0,
      mean_reward: 0.0,
      best_reward: 0.0,
      worst_reward: 0.0,
      terminated: 0,
      truncated: 0,
    }
  }
  let mut total_steps = 0
  let mut total_reward = 0.0
  let mut best_reward = episodes[0].total_reward()
  let mut worst_reward = episodes[0].total_reward()
  let mut terminated = 0
  let mut truncated = 0
  for episode in episodes {
    let reward = episode.total_reward()
    total_steps = total_steps + episode.len()
    total_reward = total_reward + reward
    if reward > best_reward {
      best_reward = reward
    }
    if reward < worst_reward {
      worst_reward = reward
    }
    if episode.is_terminated() {
      terminated = terminated + 1
    }
    if episode.is_truncated() {
      truncated = truncated + 1
    }
  }
  {
    episodes: episodes.length(),
    total_steps,
    total_reward,
    mean_reward: total_reward / episodes.length().to_double(),
    best_reward,
    worst_reward,
    terminated,
    truncated,
  }
}

///|
pub fn[S, A] success_rate(
  episodes : Array[Episode[S, A]],
  reward_threshold : Double,
) -> Double {
  if episodes.length() == 0 {
    return 0.0
  }
  let mut successful = 0
  for episode in episodes {
    if episode.total_reward() >= reward_threshold {
      successful = successful + 1
    }
  }
  successful.to_double() / episodes.length().to_double()
}

///|
pub fn[S, A] episode_reward_histogram(
  episodes : Array[Episode[S, A]],
  lower : Double,
  upper : Double,
  buckets : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  if buckets <= 0 {
    return result
  }
  let mut i = 0
  while i < buckets {
    result.push(0)
    i = i + 1
  }
  if upper <= lower {
    return result
  }
  let width = (upper - lower) / buckets.to_double()
  for episode in episodes {
    let raw = ((episode.total_reward() - lower) / width).to_int()
    let index = if raw < 0 {
      0
    } else if raw >= buckets {
      buckets - 1
    } else {
      raw
    }
    result[index] = result[index] + 1
  }
  result
}

///|
pub fn[S, A] episode_lengths_histogram(
  episodes : Array[Episode[S, A]],
  buckets : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  if buckets <= 0 {
    return result
  }
  let mut i = 0
  while i < buckets {
    result.push(0)
    i = i + 1
  }
  for episode in episodes {
    let index = if episode.len() >= buckets {
      buckets - 1
    } else {
      episode.len()
    }
    result[index] = result[index] + 1
  }
  result
}