///|
pub(all) struct ScenarioRow {
  index : Int
  name : String
  factors : Array[Double]
  response : Double
  weight : Double
  feasible : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ScenarioMatrix {
  factor_names : Array[String]
  rows : Array[ScenarioRow]
  response_mean : Double
  response_minimum : Double
  response_maximum : Double
  feasible_fraction : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ScenarioSummary {
  factor : String
  low_response : Double
  high_response : Double
  response_change : Double
  elasticity : Double
  limiting_level : Double
} derive(Debug, Eq, ToJson)

///|
pub fn scenario_row(
  index : Int,
  name : String,
  factors : ArrayView[Double],
  response : Double,
  weight : Double,
  feasible : Bool,
) -> ScenarioRow {
  { index, name, factors: factors.to_owned(), response, weight, feasible }
}

///|
pub fn make_scenario_matrix(
  factor_names : ArrayView[String],
  rows : ArrayView[ScenarioRow],
) -> ScenarioMatrix {
  let mut total = 0.0
  let mut weights = 0.0
  let mut minimum = if rows.length() == 0 { 0.0 } else { rows[0].response }
  let mut maximum = minimum
  let mut feasible = 0.0
  for row in rows {
    total += row.response * row.weight
    weights += row.weight
    minimum = minimum.min(row.response)
    maximum = maximum.max(row.response)
    if row.feasible {
      feasible += row.weight
    }
  }
  {
    factor_names: factor_names.to_owned(),
    rows: rows.to_owned(),
    response_mean: if weights == 0.0 {
      0.0
    } else {
      total / weights
    },
    response_minimum: minimum,
    response_maximum: maximum,
    feasible_fraction: if weights == 0.0 {
      0.0
    } else {
      feasible / weights
    },
  }
}

///|
pub fn scenario_matrix_factor_count(matrix : ScenarioMatrix) -> Int {
  matrix.factor_names.length()
}

///|
pub fn scenario_matrix_row_count(matrix : ScenarioMatrix) -> Int {
  matrix.rows.length()
}

///|
pub fn scenario_matrix_values(matrix : ScenarioMatrix) -> Array[Double] {
  [
    for row in matrix.rows => row.response
  ]
}

///|
pub fn scenario_matrix_feasible_rows(
  matrix : ScenarioMatrix,
) -> Array[ScenarioRow] {
  let result = []
  for row in matrix.rows {
    if row.feasible {
      result.push(row)
    }
  }
  result
}

///|
pub fn scenario_matrix_infeasible_rows(
  matrix : ScenarioMatrix,
) -> Array[ScenarioRow] {
  let result = []
  for row in matrix.rows {
    if !row.feasible {
      result.push(row)
    }
  }
  result
}

///|
pub fn scenario_matrix_response_range(matrix : ScenarioMatrix) -> Double {
  matrix.response_maximum - matrix.response_minimum
}

///|
pub fn scenario_matrix_is_robust(
  matrix : ScenarioMatrix,
  minimum_fraction : Double,
) -> Bool {
  matrix.feasible_fraction >= minimum_fraction
}

///|
pub fn scenario_matrix_worst_row(matrix : ScenarioMatrix) -> ScenarioRow? {
  if matrix.rows.length() == 0 {
    None
  } else {
    let mut selected = matrix.rows[0]
    for row in matrix.rows {
      if row.response < selected.response {
        selected = row
      }
    }
    Some(selected)
  }
}

///|
pub fn scenario_matrix_best_row(matrix : ScenarioMatrix) -> ScenarioRow? {
  if matrix.rows.length() == 0 {
    None
  } else {
    let mut selected = matrix.rows[0]
    for row in matrix.rows {
      if row.response > selected.response {
        selected = row
      }
    }
    Some(selected)
  }
}

///|
pub fn scenario_matrix_filter_factor(
  matrix : ScenarioMatrix,
  factor_index : Int,
  lower : Double,
  upper : Double,
) -> ScenarioMatrix raise @domain.ExtractionError {
  if factor_index < 0 || factor_index >= matrix.factor_names.length() {
    raise @domain.ExtractionError::OutOfRange(
      name="factor index",
      value=factor_index.to_double(),
      lower=0.0,
      upper=(matrix.factor_names.length() - 1).to_double(),
    )
  }
  let selected = []
  for row in matrix.rows {
    if row.factors[factor_index] >= lower && row.factors[factor_index] <= upper {
      selected.push(row)
    }
  }
  make_scenario_matrix(matrix.factor_names, selected)
}

///|
pub fn scenario_matrix_filter_feasible(
  matrix : ScenarioMatrix,
  feasible : Bool,
) -> ScenarioMatrix {
  let selected = []
  for row in matrix.rows {
    if row.feasible == feasible {
      selected.push(row)
    }
  }
  make_scenario_matrix(matrix.factor_names, selected)
}

///|
pub fn scenario_matrix_sort_by_response(
  matrix : ScenarioMatrix,
  ascending : Bool,
) -> ScenarioMatrix {
  let rows = matrix.rows.copy()
  for first in 0.. rows[selected].response
      }
      if should_select {
        selected = second
      }
    }
    let temporary = rows[first]
    rows[first] = rows[selected]
    rows[selected] = temporary
  }
  make_scenario_matrix(matrix.factor_names, rows)
}

///|
pub fn scenario_matrix_top(
  matrix : ScenarioMatrix,
  count : Int,
) -> Array[ScenarioRow] {
  let sorted = scenario_matrix_sort_by_response(matrix, false)
  let result = []
  for index in 0.. Array[ScenarioRow] {
  let sorted = scenario_matrix_sort_by_response(matrix, true)
  let result = []
  for index in 0.. Array[Double] raise @domain.ExtractionError {
  if factor_index < 0 || factor_index >= matrix.factor_names.length() {
    raise @domain.ExtractionError::OutOfRange(
      name="factor index",
      value=factor_index.to_double(),
      lower=0.0,
      upper=(matrix.factor_names.length() - 1).to_double(),
    )
  }
  [
    for row in matrix.rows => row.factors[factor_index]
  ]
}

///|
pub fn scenario_matrix_factor_mean(
  matrix : ScenarioMatrix,
  factor_index : Int,
) -> Double raise @domain.ExtractionError {
  let values = scenario_matrix_factor_values(matrix, factor_index)
  if values.length() == 0 {
    0.0
  } else {
    values.fold(init=0.0, (sum, value) => sum + value) /
    values.length().to_double()
  }
}

///|
pub fn scenario_matrix_factor_range(
  matrix : ScenarioMatrix,
  factor_index : Int,
) -> (Double, Double) raise @domain.ExtractionError {
  let values = scenario_matrix_factor_values(matrix, factor_index)
  if values.length() == 0 {
    (0.0, 0.0)
  } else {
    let mut lower = values[0]
    let mut upper = lower
    for value in values {
      lower = lower.min(value)
      upper = upper.max(value)
    }
    (lower, upper)
  }
}

///|
pub fn scenario_matrix_response_correlation(
  matrix : ScenarioMatrix,
  factor_index : Int,
) -> Double raise @domain.ExtractionError {
  let factors = scenario_matrix_factor_values(matrix, factor_index)
  let responses = scenario_matrix_values(matrix)
  if factors.length() != responses.length() || factors.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(
      required=1,
      actual=factors.length(),
    )
  }
  let factor_mean = factors.fold(init=0.0, (sum, value) => sum + value) /
    factors.length().to_double()
  let response_mean = responses.fold(init=0.0, (sum, value) => sum + value) /
    responses.length().to_double()
  let mut covariance = 0.0
  let mut factor_variance = 0.0
  let mut response_variance = 0.0
  for index in 0.. Array[ScenarioSummary] {
  let summaries = []
  for factor_index in 0.. high_level {
        high_level = level
        high_response = row.response
      }
      first = false
    }
    let change = high_response - low_response
    summaries.push({
      factor: matrix.factor_names[factor_index],
      low_response,
      high_response,
      response_change: change,
      elasticity: if high_level == low_level {
        0.0
      } else {
        change / (high_level - low_level)
      },
      limiting_level: if low_response <= high_response {
        low_level
      } else {
        high_level
      },
    })
  }
  summaries
}

///|
pub fn scenario_summary_markdown(
  summaries : ArrayView[ScenarioSummary],
) -> String {
  let lines = [
    "| Factor | Low response | High response | Change | Elasticity | Limiting level |",
    "|---|---:|---:|---:|---:|---:|",
  ]
  for summary in summaries {
    lines.push(
      "| " +
      summary.factor +
      " | " +
      summary.low_response.to_string() +
      " | " +
      summary.high_response.to_string() +
      " | " +
      summary.response_change.to_string() +
      " | " +
      summary.elasticity.to_string() +
      " | " +
      summary.limiting_level.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn scenario_matrix_csv(matrix : ScenarioMatrix) -> String {
  let header = ["index", "name"]
  let columns = header.copy()
  columns.append(matrix.factor_names)
  columns.append(["response", "weight", "feasible"])
  let lines = [columns.join(",")]
  for row in matrix.rows {
    lines.push(
      row.index.to_string() +
      "," +
      row.name +
      "," +
      row.factors.map(value => value.to_string()).join(",") +
      "," +
      row.response.to_string() +
      "," +
      row.weight.to_string() +
      "," +
      row.feasible.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn scenario_matrix_markdown(matrix : ScenarioMatrix) -> String {
  let header = ["Index", "Name"]
  header.append(matrix.factor_names)
  header.push("Response")
  header.push("Weight")
  header.push("Feasible")
  let lines = [
    "| " + header.join(" | ") + " |",
    "|" + Array::make(header.length(), "---").join("|") + "|",
  ]
  for row in matrix.rows {
    let cells = [row.index.to_string(), row.name]
    cells.append(row.factors.map(value => value.to_string()))
    cells.append([
      row.response.to_string(),
      row.weight.to_string(),
      row.feasible.to_string(),
    ])
    lines.push("| " + cells.join(" | ") + " |")
  }
  lines.join("\n")
}

///|
pub fn scenario_matrix_weighted_response(matrix : ScenarioMatrix) -> Double {
  matrix.response_mean
}

///|
pub fn scenario_matrix_weighted_feasibility(matrix : ScenarioMatrix) -> Double {
  matrix.feasible_fraction
}

///|
pub fn scenario_matrix_distance(
  first : ScenarioMatrix,
  second : ScenarioMatrix,
) -> Double {
  let count = first.rows.length().min(second.rows.length())
  let mut total = 0.0
  for index in 0.. ScenarioMatrix {
  let rows = first.rows.copy()
  let offset = rows.length()
  for row in second.rows {
    rows.push({
      index: offset + row.index,
      name: row.name,
      factors: row.factors,
      response: row.response,
      weight: row.weight,
      feasible: row.feasible,
    })
  }
  make_scenario_matrix(first.factor_names, rows)
}

///|
pub fn scenario_matrix_reweight(
  matrix : ScenarioMatrix,
  weights : ArrayView[Double],
) -> ScenarioMatrix {
  let rows = []
  for index in 0.. Double {
  if matrix.rows.length() == 0 {
    0.0
  } else {
    scenario_matrix_feasible_rows(matrix).length().to_double() /
    matrix.rows.length().to_double()
  }
}

///|
pub fn scenario_matrix_response_stddev(matrix : ScenarioMatrix) -> Double {
  if matrix.rows.length() == 0 {
    return 0.0
  }
  let mean = matrix.response_mean
  let mut total = 0.0
  for row in matrix.rows {
    let difference = row.response - mean
    total += difference * difference
  }
  (total / matrix.rows.length().to_double()).sqrt()
}

///|
pub fn scenario_matrix_confidence_width(matrix : ScenarioMatrix) -> Double {
  2.0 * scenario_matrix_response_stddev(matrix)
}

///|
pub fn scenario_matrix_alerts(
  matrix : ScenarioMatrix,
  minimum_feasible_fraction : Double,
) -> Array[String] {
  let alerts = []
  if matrix.rows.length() == 0 {
    alerts.push("matrix has no scenarios")
  }
  if matrix.feasible_fraction < minimum_feasible_fraction {
    alerts.push("feasible fraction is below target")
  }
  if scenario_matrix_response_range(matrix) == 0.0 {
    alerts.push("response has no observed variation")
  }
  alerts
}

///|
pub fn scenario_matrix_name_vector(matrix : ScenarioMatrix) -> Array[String] {
  [
    for row in matrix.rows => row.name
  ]
}

///|
pub fn scenario_matrix_response_vector(
  matrix : ScenarioMatrix,
) -> Array[Double] {
  [
    for row in matrix.rows => row.response
  ]
}

///|
pub fn scenario_matrix_feasibility_vector(
  matrix : ScenarioMatrix,
) -> Array[Bool] {
  [
    for row in matrix.rows => row.feasible
  ]
}