///|
/// A named factor for a laboratory or process experiment.
pub(all) struct FactorDefinition {
  name : String
  low : Double
  high : Double
  center : Double
} derive(Debug, Eq, ToJson)

///|
pub fn FactorDefinition::new(
  name~ : String,
  low~ : Double,
  high~ : Double,
) -> FactorDefinition raise @domain.ExtractionError {
  if name.trim().length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="factor name cannot be blank",
    )
  }
  @domain.require_finite("factor low", low)
  @domain.require_finite("factor high", high)
  if high <= low {
    raise @domain.ExtractionError::InvalidArgument(
      message="factor bounds must increase",
    )
  }
  { name, low, high, center: (low + high) / 2.0 }
}

///|
pub fn FactorDefinition::scale(
  self : FactorDefinition,
  coded : Double,
) -> Double {
  self.center + coded * (self.high - self.low) / 2.0
}

///|
pub fn FactorDefinition::code(
  self : FactorDefinition,
  actual : Double,
) -> Double {
  2.0 * (actual - self.center) / (self.high - self.low)
}

///|
pub(all) struct ExperimentRun {
  run_number : Int
  block : Int
  coded_values : Array[Double]
  actual_values : Array[Double]
  center_point : Bool
  response : Double?
} derive(Debug, Eq, ToJson)

///|
pub fn full_factorial(
  factors : ArrayView[FactorDefinition],
  center_points? : Int = 0,
) -> Array[ExperimentRun] raise @domain.ExtractionError {
  if factors.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  if center_points < 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="center point count cannot be negative",
    )
  }
  let coded : Array[Array[Double]] = [[]]
  for _ in factors {
    let next : Array[Array[Double]] = []
    for prefix in coded {
      for level in [-1.0, 1.0] {
        let value = prefix.copy()
        value.push(level)
        next.push(value)
      }
    }
    coded.clear()
    for value in next {
      coded.push(value)
    }
  }
  let runs : Array[ExperimentRun] = []
  let mut run_number = 1
  for values in coded {
    runs.push({
      run_number,
      block: 1,
      coded_values: values.copy(),
      actual_values: [
        for index, value in values => factors[index].scale(value)
      ],
      center_point: false,
      response: None,
    })
    run_number += 1
  }
  for _ in 0.. 0.0
      ],
      actual_values: [
        for factor in factors => factor.center
      ],
      center_point: true,
      response: None,
    })
    run_number += 1
  }
  runs
}

///|
pub fn assign_blocks(
  runs : ArrayView[ExperimentRun],
  block_count~ : Int,
) -> Array[ExperimentRun] raise @domain.ExtractionError {
  if block_count < 1 {
    raise @domain.ExtractionError::InvalidArgument(
      message="block count must be positive",
    )
  }
  [
    for index, run in runs => {
      {
        run_number: run.run_number,
        block: index % block_count + 1,
        coded_values: run.coded_values,
        actual_values: run.actual_values,
        center_point: run.center_point,
        response: run.response,
      }
    }
  ]
}

///|
pub fn randomize_runs(
  runs : ArrayView[ExperimentRun],
  seed~ : Int64,
) -> Array[ExperimentRun] raise @domain.ExtractionError {
  let remaining = runs.to_owned()
  let result : Array[ExperimentRun] = []
  let mut generator = DeterministicRng::new(seed)
  while remaining.length() > 0 {
    let (next, index) = generator.integer(lower=0, upper=remaining.length() - 1)
    generator = next
    let chosen = remaining[index]
    ignore(remaining.remove(index))
    result.push({
      run_number: result.length() + 1,
      block: chosen.block,
      coded_values: chosen.coded_values,
      actual_values: chosen.actual_values,
      center_point: chosen.center_point,
      response: chosen.response,
    })
  }
  result
}

///|
pub fn set_response(
  run : ExperimentRun,
  response : Double,
) -> ExperimentRun raise @domain.ExtractionError {
  @domain.require_finite("experiment response", response)
  {
    run_number: run.run_number,
    block: run.block,
    coded_values: run.coded_values,
    actual_values: run.actual_values,
    center_point: run.center_point,
    response: Some(response),
  }
}

///|
pub(all) struct DesignSummary {
  runs : Int
  factors : Int
  center_points : Int
  blocks : Int
  completed : Int
  response_mean : Double
  response_range : Double
} derive(Debug, Eq, ToJson)

///|
pub fn summarize_design(
  runs : ArrayView[ExperimentRun],
) -> DesignSummary raise @domain.ExtractionError {
  if runs.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mut center = 0
  let mut completed = 0
  let mut sum = 0.0
  let mut minimum = 1.0e300
  let mut maximum = -1.0e300
  let mut blocks = 0
  for run in runs {
    if run.center_point {
      center += 1
    }
    blocks = blocks.max(run.block)
    match run.response {
      Some(value) => {
        completed += 1
        sum += value
        minimum = minimum.min(value)
        maximum = maximum.max(value)
      }
      None => ()
    }
  }
  {
    runs: runs.length(),
    factors: runs[0].coded_values.length(),
    center_points: center,
    blocks,
    completed,
    response_mean: if completed == 0 {
      0.0
    } else {
      sum / completed.to_double()
    },
    response_range: if completed == 0 {
      0.0
    } else {
      maximum - minimum
    },
  }
}

///|
pub fn design_matrix(runs : ArrayView[ExperimentRun]) -> Array[Array[Double]] {
  [
    for run in runs => {
      let values = [1.0]
      for value in run.coded_values {
        values.push(value)
      }
      for index in 0.. Double raise @domain.ExtractionError {
  let values = [
    for run in runs if run.center_point => {
      match run.response {
        Some(value) => value
        None => 0.0
      }
    }
  ]
  if values.length() < 2 {
    raise @domain.ExtractionError::InsufficientData(
      required=2,
      actual=values.length(),
    )
  }
  let mean = values.fold(init=0.0, (sum, value) => sum + value) /
    values.length().to_double()
  values.fold(init=0.0, (sum, value) => sum + (value - mean) * (value - mean)) /
  (values.length() - 1).to_double()
}