///|
pub struct ScenarioPoint {
  name : String
  input : Float
  output : Float
  score : Float
  status : ResultStatus
  note : String
} derive(Debug, Eq)

///|
pub fn scenario_point(
  name : String,
  input : Float,
  output : Float,
  score : Float,
  status : ResultStatus,
  note : String,
) -> ScenarioPoint {
  { name, input, output, score, status, note }
}

///|
pub fn ScenarioPoint::efficiency(self : ScenarioPoint) -> Float {
  if self.input == 0.0 {
    0.0
  } else {
    self.output / self.input
  }
}

///|
pub fn ScenarioPoint::is_acceptable(
  self : ScenarioPoint,
  minimum_score : Float,
) -> Bool {
  self.score >= minimum_score && self.status is Normal
}

///|
pub struct ScenarioSeries {
  name : String
  points : Array[ScenarioPoint]
  input_unit : String
  output_unit : String
} derive(Debug, Eq)

///|
pub fn scenario_series(
  name : String,
  points : Array[ScenarioPoint],
  input_unit : String,
  output_unit : String,
) -> ScenarioSeries {
  { name, points, input_unit, output_unit }
}

///|
pub fn ScenarioSeries::count(self : ScenarioSeries) -> Int {
  self.points.length()
}

///|
pub fn ScenarioSeries::inputs(self : ScenarioSeries) -> Array[Float] {
  self.points.map(fn(item) { item.input })
}

///|
pub fn ScenarioSeries::outputs(self : ScenarioSeries) -> Array[Float] {
  self.points.map(fn(item) { item.output })
}

///|
pub fn ScenarioSeries::scores(self : ScenarioSeries) -> Array[Float] {
  self.points.map(fn(item) { item.score })
}

///|
pub fn ScenarioSeries::summary(self : ScenarioSeries) -> Statistics {
  summarize(self.scores())
}

///|
pub fn ScenarioSeries::best(self : ScenarioSeries) -> ScenarioPoint? {
  if self.points.length() == 0 {
    None
  } else {
    let mut result = self.points[0]
    for point in self.points[1:] {
      if point.score > result.score {
        result = point
      }
    }
    Some(result)
  }
}

///|
pub fn ScenarioSeries::worst(self : ScenarioSeries) -> ScenarioPoint? {
  if self.points.length() == 0 {
    None
  } else {
    let mut result = self.points[0]
    for point in self.points[1:] {
      if point.score < result.score {
        result = point
      }
    }
    Some(result)
  }
}

///|
pub fn ScenarioSeries::acceptable(
  self : ScenarioSeries,
  minimum_score : Float,
) -> Array[ScenarioPoint] {
  let result : Array[ScenarioPoint] = []
  for point in self.points {
    if point.is_acceptable(minimum_score) {
      result.push(point)
    }
  }
  result
}

///|
pub fn ScenarioSeries::flagged(self : ScenarioSeries) -> Array[ScenarioPoint] {
  let result : Array[ScenarioPoint] = []
  for point in self.points {
    if point.status != Normal {
      result.push(point)
    }
  }
  result
}

///|
pub fn ScenarioSeries::at(self : ScenarioSeries, index : Int) -> ScenarioPoint? {
  self.points.get(index)
}

///|
pub fn ScenarioSeries::with_point(
  self : ScenarioSeries,
  point : ScenarioPoint,
) -> ScenarioSeries {
  let points = self.points.copy()
  points.push(point)
  { ..self, points, }
}

///|
pub fn ScenarioSeries::to_table(self : ScenarioSeries) -> ReportTable {
  let rows : Array[Array[String]] = []
  for point in self.points {
    rows.push([
      point.name,
      "{point.input}",
      "{point.output}",
      "{point.score}",
      point.note,
    ])
  }
  table(["scenario", self.input_unit, self.output_unit, "score", "note"], rows)
}

///|
pub struct SweepConfig {
  start : Float
  stop : Float
  steps : Int
  label : String
} derive(Debug, Eq)

///|
pub fn sweep_config(
  start : Float,
  stop : Float,
  steps : Int,
  label : String,
) -> SweepConfig {
  { start, stop, steps, label }
}

///|
pub fn SweepConfig::is_valid(self : SweepConfig) -> Bool {
  self.steps >= 2 && self.stop >= self.start
}

///|
pub fn SweepConfig::values(self : SweepConfig) -> Array[Float] {
  let result : Array[Float] = []
  if !self.is_valid() {
    return result
  }
  let delta = (self.stop - self.start) / Float::from_int(self.steps - 1)
  for index in 0.. Float,
) -> ScenarioSeries {
  let points : Array[ScenarioPoint] = []
  for _, value in config.values() {
    points.push(
      scenario_point(
        config.label,
        value,
        evaluate(value),
        evaluate(value),
        Normal,
        "deterministic sweep",
      ),
    )
  }
  scenario_series(config.label, points, "input", "output")
}

///|
pub fn ScenarioSeries::normalized(self : ScenarioSeries) -> ScenarioSeries {
  let scores = self.scores()
  let summary = summarize(scores)
  let points : Array[ScenarioPoint] = []
  for point in self.points {
    let normalized : Float = if summary.standard_deviation == 0.0 {
      0.0
    } else {
      (point.score - summary.mean) / summary.standard_deviation
    }
    points.push({ ..point, score: normalized })
  }
  { ..self, points, }
}

///|
pub fn ScenarioSeries::merge(
  self : ScenarioSeries,
  other : ScenarioSeries,
) -> ScenarioSeries {
  let points = self.points.copy()
  for point in other.points {
    points.push(point)
  }
  {
    name: self.name + "+" + other.name,
    points,
    input_unit: self.input_unit,
    output_unit: self.output_unit,
  }
}

///|
pub fn ScenarioSeries::quality_gate(
  self : ScenarioSeries,
  minimum_score : Float,
  maximum_flagged : Int,
) -> Bool {
  self.acceptable(minimum_score).length() > 0 &&
  self.flagged().length() <= maximum_flagged
}

///|
pub fn ScenarioSeries::to_markdown(self : ScenarioSeries) -> String {
  self.to_table().to_markdown()
}

///|
pub fn ScenarioSeries::to_csv(self : ScenarioSeries) -> String {
  self.to_table().to_csv()
}

///|
pub fn ScenarioSeries::report_section(self : ScenarioSeries) -> ReportSection {
  { title: self.name, kind: Method, body: self.to_markdown() }
}

///|
pub fn sensitivity(
  base : Float,
  delta : Float,
  response_base : Float,
  response_delta : Float,
) -> Float {
  if delta == 0.0 || base == 0.0 {
    0.0
  } else {
    (response_delta - response_base) / delta * base / response_base
  }
}

///|
pub fn elasticity(
  input_before : Float,
  input_after : Float,
  output_before : Float,
  output_after : Float,
) -> Float {
  if input_before == 0.0 || output_before == 0.0 {
    0.0
  } else {
    percent_change(output_before, output_after) /
    percent_change(input_before, input_after)
  }
}

///|
pub fn scenario_distance(left : ScenarioPoint, right : ScenarioPoint) -> Float {
  ((left.input - right.input) * (left.input - right.input) +
  (left.output - right.output) * (left.output - right.output)).sqrt()
}

///|
pub fn nearest_scenario(
  series : ScenarioSeries,
  input : Float,
) -> ScenarioPoint? {
  if series.points.length() == 0 {
    None
  } else {
    let mut result = series.points[0]
    for point in series.points[1:] {
      if (point.input - input).abs() < (result.input - input).abs() {
        result = point
      }
    }
    Some(result)
  }
}

///|
pub fn interpolate_scenario(series : ScenarioSeries, input : Float) -> Float? {
  match nearest_scenario(series, input) {
    Some(point) => Some(point.output)
    None => None
  }
}

///|
pub fn scenario_status(
  score : Float,
  lower : Float,
  upper : Float,
) -> ResultStatus {
  if score < lower {
    Flagged
  } else if score > upper {
    Estimated
  } else {
    Normal
  }
}

///|
pub fn classify_series(
  series : ScenarioSeries,
  lower : Float,
  upper : Float,
) -> ScenarioSeries {
  let points : Array[ScenarioPoint] = []
  for point in series.points {
    points.push({ ..point, status: scenario_status(point.score, lower, upper) })
  }
  { ..series, points, }
}