///|
pub fn FeatureProfile::feature_index(self : FeatureProfile) -> Int {
  self.source_column
}

///|
pub fn FeatureProfile::minimum(self : FeatureProfile) -> Double {
  self.minimum_value
}

///|
pub fn FeatureProfile::maximum(self : FeatureProfile) -> Double {
  self.maximum_value
}

///|
pub fn FeatureProfile::mean(self : FeatureProfile) -> Double {
  self.arithmetic_mean
}

///|
pub fn FeatureProfile::variance(self : FeatureProfile) -> Double {
  self.population_variance
}

///|
pub fn FeatureProfile::standard_deviation(self : FeatureProfile) -> Double {
  self.population_standard_deviation
}

///|
pub fn FeatureProfile::is_constant(self : FeatureProfile) -> Bool {
  self.constant_column
}

///|
pub fn ClassProfile::label(self : ClassProfile) -> Int {
  self.class_label
}

///|
pub fn ClassProfile::count(self : ClassProfile) -> Int {
  self.observation_count
}

///|
pub fn ClassProfile::proportion(self : ClassProfile) -> Double {
  self.observation_proportion
}

///|
pub fn DatasetProfile::name(self : DatasetProfile) -> String {
  self.source_name
}

///|
pub fn DatasetProfile::row_count(self : DatasetProfile) -> Int {
  self.source_row_count
}

///|
pub fn DatasetProfile::feature_count(self : DatasetProfile) -> Int {
  self.source_feature_count
}

///|
pub fn DatasetProfile::class_count(self : DatasetProfile) -> Int {
  self.source_class_count
}

///|
pub fn DatasetProfile::features(self : DatasetProfile) -> Array[FeatureProfile] {
  self.feature_profiles.copy()
}

///|
pub fn DatasetProfile::classes(self : DatasetProfile) -> Array[ClassProfile] {
  self.class_profiles.copy()
}

///|
pub fn DatasetProfile::centroid(self : DatasetProfile) -> Array[Double] {
  self.feature_centroid.copy()
}

///|
pub fn DatasetProfile::minimum_squared_norm(self : DatasetProfile) -> Double {
  self.smallest_squared_row_norm
}

///|
pub fn DatasetProfile::maximum_squared_norm(self : DatasetProfile) -> Double {
  self.largest_squared_row_norm
}

///|
pub fn DatasetProfile::mean_squared_norm(self : DatasetProfile) -> Double {
  self.average_squared_row_norm
}

///|
fn summarize_feature(
  rows : Array[Array[Double]],
  column : Int,
) -> FeatureProfile {
  let mut minimum = rows[0][column]
  let mut maximum = rows[0][column]
  let mut total = 0.0
  for row in rows {
    let value = row[column]
    if value < minimum {
      minimum = value
    }
    if value > maximum {
      maximum = value
    }
    total = total + value
  }
  let mean = total / rows.length().to_double()
  let mut squared_total = 0.0
  for row in rows {
    let difference = row[column] - mean
    squared_total = squared_total + difference * difference
  }
  let variance = squared_total / rows.length().to_double()
  {
    source_column: column,
    minimum_value: minimum,
    maximum_value: maximum,
    arithmetic_mean: mean,
    population_variance: variance,
    population_standard_deviation: variance.sqrt(),
    constant_column: minimum == maximum,
  }
}

///|
fn summarize_classes(data : Dataset) -> Array[ClassProfile] {
  let profiles : Array[ClassProfile] = []
  for label in data.classes() {
    let count = data.class_count(label)
    profiles.push({
      class_label: label,
      observation_count: count,
      observation_proportion: count.to_double() / data.row_count().to_double(),
    })
  }
  profiles
}

///|
/// Computes reproducible descriptive statistics from validated dense data.
pub fn dataset_profile(data : Dataset) -> DatasetProfile {
  let rows = data.features()
  let features : Array[FeatureProfile] = []
  let centroid = Array::make(data.feature_count(), 0.0)
  for column = 0; column < data.feature_count(); column = column + 1 {
    let feature = summarize_feature(rows, column)
    features.push(feature)
    centroid[column] = feature.mean()
  }
  let mut minimum_norm = 0.0
  let mut maximum_norm = 0.0
  let mut norm_total = 0.0
  for row_index, row in rows {
    let mut squared_norm = 0.0
    for value in row {
      squared_norm = squared_norm + value * value
    }
    if row_index == 0 || squared_norm < minimum_norm {
      minimum_norm = squared_norm
    }
    if row_index == 0 || squared_norm > maximum_norm {
      maximum_norm = squared_norm
    }
    norm_total = norm_total + squared_norm
  }
  {
    source_name: data.name(),
    source_row_count: data.row_count(),
    source_feature_count: data.feature_count(),
    source_class_count: data.classes().length(),
    feature_profiles: features,
    class_profiles: summarize_classes(data),
    feature_centroid: centroid,
    smallest_squared_row_norm: minimum_norm,
    largest_squared_row_norm: maximum_norm,
    average_squared_row_norm: norm_total / data.row_count().to_double(),
  }
}

///|
pub fn WeightProfile::count(self : WeightProfile) -> Int {
  self.weight_count
}

///|
pub fn WeightProfile::minimum(self : WeightProfile) -> Double {
  self.minimum_weight
}

///|
pub fn WeightProfile::maximum(self : WeightProfile) -> Double {
  self.maximum_weight
}

///|
pub fn WeightProfile::total(self : WeightProfile) -> Double {
  self.total_weight
}

///|
pub fn WeightProfile::mean(self : WeightProfile) -> Double {
  self.average_weight
}

///|
pub fn WeightProfile::effective_sample_size(self : WeightProfile) -> Double {
  self.effective_observation_count
}

///|
/// Computes Kish's effective sample size for finite positive weights.
pub fn weight_profile(
  weights : Array[Double],
) -> Result[WeightProfile, SvmError] {
  if weights.is_empty() {
    return Err(EmptyMetricInput)
  }
  let mut minimum = weights[0]
  let mut maximum = weights[0]
  let mut total = 0.0
  let mut squared_total = 0.0
  for index, value in weights {
    if !finite_double(value) || value <= 0.0 {
      return Err(InvalidWeight(index, value))
    }
    if value < minimum {
      minimum = value
    }
    if value > maximum {
      maximum = value
    }
    total = total + value
    squared_total = squared_total + value * value
  }
  Ok({
    weight_count: weights.length(),
    minimum_weight: minimum,
    maximum_weight: maximum,
    total_weight: total,
    average_weight: total / weights.length().to_double(),
    effective_observation_count: total * total / squared_total,
  })
}