///|
fn fit_standard(data : Dataset) -> FeatureScaler {
  let columns = data.feature_count()
  let rows = data.features()
  let offsets = Array::make(columns, 0.0)
  let scales = Array::make(columns, 0.0)
  let constants = Array::make(columns, false)
  for column = 0; column < columns; column = column + 1 {
    let mut total = 0.0
    for row in rows {
      total = total + row[column]
    }
    let mean = total / rows.length().to_double()
    offsets[column] = mean
    let mut squared_total = 0.0
    for row in rows {
      let difference = row[column] - mean
      squared_total = squared_total + difference * difference
    }
    let deviation = (squared_total / rows.length().to_double()).sqrt()
    if deviation == 0.0 {
      scales[column] = 1.0
      constants[column] = true
    } else {
      scales[column] = deviation
    }
  }
  {
    scaler_kind: StandardScale,
    offset_values: offsets,
    scale_values: scales,
    constant_columns: constants,
    input_columns: columns,
  }
}

///|
fn fit_minmax(data : Dataset, lower : Double, upper : Double) -> FeatureScaler {
  let columns = data.feature_count()
  let rows = data.features()
  let offsets = Array::make(columns, 0.0)
  let scales = Array::make(columns, 1.0)
  let constants = Array::make(columns, false)
  for column = 0; column < columns; column = column + 1 {
    let mut minimum = rows[0][column]
    let mut maximum = rows[0][column]
    for row = 1; row < rows.length(); row = row + 1 {
      minimum = minimum.min(rows[row][column])
      maximum = maximum.max(rows[row][column])
    }
    offsets[column] = minimum
    let width = maximum - minimum
    if width == 0.0 {
      constants[column] = true
    } else {
      scales[column] = width
    }
  }
  {
    scaler_kind: MinMaxScale(lower, upper),
    offset_values: offsets,
    scale_values: scales,
    constant_columns: constants,
    input_columns: columns,
  }
}

///|
/// Fits preprocessing parameters from a validated training dataset.
pub fn fit_scaler(
  data : Dataset,
  kind : ScalerKind,
) -> Result[FeatureScaler, SvmError] {
  match kind {
    StandardScale => Ok(fit_standard(data))
    MinMaxScale(lower, upper) => {
      if !finite_double(lower) {
        return Err(InvalidConfiguration("minmax_lower", lower))
      }
      if !finite_double(upper) || upper <= lower {
        return Err(InvalidConfiguration("minmax_upper", upper))
      }
      Ok(fit_minmax(data, lower, upper))
    }
  }
}

///|
pub fn FeatureScaler::feature_count(self : FeatureScaler) -> Int {
  self.input_columns
}

///|
pub fn FeatureScaler::kind(self : FeatureScaler) -> ScalerKind {
  self.scaler_kind
}

///|
pub fn FeatureScaler::offsets(self : FeatureScaler) -> Array[Double] {
  self.offset_values.copy()
}

///|
pub fn FeatureScaler::scales(self : FeatureScaler) -> Array[Double] {
  self.scale_values.copy()
}

///|
fn FeatureScaler::validate_row(
  self : FeatureScaler,
  row : Array[Double],
) -> Result[Unit, SvmError] {
  if row.length() != self.input_columns {
    return Err(PredictionDimensionMismatch(self.input_columns, row.length()))
  }
  for column, value in row {
    if !finite_double(value) {
      return Err(NonFiniteFeature(0, column))
    }
  }
  Ok(())
}

///|
pub fn FeatureScaler::transform_row(
  self : FeatureScaler,
  row : Array[Double],
) -> Result[Array[Double], SvmError] {
  match self.validate_row(row) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let transformed = Array::make(self.input_columns, 0.0)
  for column = 0; column < self.input_columns; column = column + 1 {
    if self.constant_columns[column] {
      transformed[column] = 0.0
    } else {
      let normalized = (row[column] - self.offset_values[column]) /
        self.scale_values[column]
      transformed[column] = match self.scaler_kind {
        StandardScale => normalized
        MinMaxScale(lower, upper) => lower + normalized * (upper - lower)
      }
    }
  }
  Ok(transformed)
}

///|
pub fn FeatureScaler::inverse_row(
  self : FeatureScaler,
  row : Array[Double],
) -> Result[Array[Double], SvmError] {
  match self.validate_row(row) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let restored = Array::make(self.input_columns, 0.0)
  for column = 0; column < self.input_columns; column = column + 1 {
    if self.constant_columns[column] {
      restored[column] = self.offset_values[column]
    } else {
      let normalized = match self.scaler_kind {
        StandardScale => row[column]
        MinMaxScale(lower, upper) => (row[column] - lower) / (upper - lower)
      }
      restored[column] = self.offset_values[column] +
        normalized * self.scale_values[column]
    }
  }
  Ok(restored)
}

///|
/// Applies fitted parameters to every row while preserving class labels.
pub fn FeatureScaler::transform_dataset(
  self : FeatureScaler,
  data : Dataset,
  name : String,
) -> Result[Dataset, SvmError] {
  if data.feature_count() != self.input_columns {
    return Err(
      PredictionDimensionMismatch(self.input_columns, data.feature_count()),
    )
  }
  let transformed : Array[Array[Double]] = []
  for row in data.features() {
    match self.transform_row(row) {
      Err(error) => return Err(error)
      Ok(value) => transformed.push(value)
    }
  }
  dataset(transformed, data.labels(), name)
}