///|
/// Named feature tables used to exchange HRV measurements with models.

///|
/// One named numeric feature with provenance.
pub(all) struct NamedFeature {
  name : String
  value : Double
  source : String
  valid : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// A deterministic feature table.
pub(all) struct FeatureTable {
  schema_version : String
  features : Array[NamedFeature]
  valid_count : Int
  missing_count : Int
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Feature scaling parameters learned from a reference set.
pub(all) struct FeatureScaler {
  names : Array[String]
  centers : Array[Double]
  scales : Array[Double]
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Build named features from a vector and a name list.
pub fn make_named_features(
  names : Array[String],
  values : Array[Double],
  source : String,
) -> Array[NamedFeature] {
  let result = []
  let n = if names.length() < values.length() {
    names.length()
  } else {
    values.length()
  }
  for i in 0.. FeatureTable {
  let mut valid = 0
  for feature in features {
    if feature.valid {
      valid += 1
    }
  }
  {
    schema_version,
    features,
    valid_count: valid,
    missing_count: features.length() - valid,
  }
}

///|
/// Return the feature names in table order.
pub fn feature_names(table : FeatureTable) -> Array[String] {
  let result = []
  for feature in table.features {
    result.push(feature.name)
  }
  result
}

///|
/// Return values in table order, replacing missing values with a default.
pub fn feature_values(
  table : FeatureTable,
  missing_value : Double,
) -> Array[Double] {
  let result = []
  for feature in table.features {
    if feature.valid {
      result.push(feature.value)
    } else {
      result.push(missing_value)
    }
  }
  result
}

///|
/// Look up a named feature.
pub fn feature_value(table : FeatureTable, name : String) -> Double? {
  for feature in table.features {
    if feature.name == name && feature.valid {
      return Some(feature.value)
    }
  }
  None
}

///|
/// Look up a named feature and return a fallback when absent.
pub fn feature_value_or(
  table : FeatureTable,
  name : String,
  fallback : Double,
) -> Double {
  match feature_value(table, name) {
    Some(value) => value
    None => fallback
  }
}

///|
/// Add a derived feature to a table.
pub fn add_feature(
  table : FeatureTable,
  feature : NamedFeature,
) -> FeatureTable {
  let features = []
  let mut replaced = false
  for current in table.features {
    if current.name == feature.name {
      features.push(feature)
      replaced = true
    } else {
      features.push(current)
    }
  }
  if !replaced {
    features.push(feature)
  }
  FeatureTable::from_features(features, table.schema_version)
}

///|
/// Merge two feature tables while preferring values from the right table.
pub fn merge_feature_tables(
  left : FeatureTable,
  right : FeatureTable,
) -> FeatureTable {
  let mut result = left
  for feature in right.features {
    result = add_feature(result, feature)
  }
  FeatureTable::from_features(result.features, right.schema_version)
}

///|
/// Return only valid features.
pub fn valid_features(table : FeatureTable) -> Array[NamedFeature] {
  let result = []
  for feature in table.features {
    if feature.valid {
      result.push(feature)
    }
  }
  result
}

///|
/// Learn robust center and scale parameters for a table.
pub fn fit_feature_scaler(table : FeatureTable) -> FeatureScaler {
  let names = feature_names(table)
  let centers = []
  let scales = []
  for feature in table.features {
    centers.push(if feature.valid { feature.value } else { 0.0 })
    scales.push(1.0)
  }
  { names, centers, scales }
}

///|
/// Learn a scaler from aligned feature tables.
pub fn fit_scaler_from_tables(tables : Array[FeatureTable]) -> FeatureScaler {
  if tables.length() == 0 {
    return { names: [], centers: [], scales: [] }
  }
  let names = feature_names(tables[0])
  let centers = []
  let scales = []
  for i in 0.. FeatureTable {
  let result = []
  for feature in table.features {
    let mut index = -1
    for i in 0..= scaler.centers.length() {
      result.push({
        name: feature.name,
        value: 0.0,
        source: feature.source,
        valid: false,
      })
    } else {
      let scale = if index < scaler.scales.length() &&
        scaler.scales[index] != 0.0 {
        scaler.scales[index]
      } else {
        1.0
      }
      result.push({
        name: feature.name,
        value: (feature.value - scaler.centers[index]) / scale,
        source: feature.source,
        valid: true,
      })
    }
  }
  FeatureTable::from_features(result, table.schema_version)
}

///|
/// Concatenate feature vectors and preserve their source names.
pub fn concatenate_feature_tables(tables : Array[FeatureTable]) -> FeatureTable {
  let result = []
  let mut schema = "hrvkit.features.v1"
  for table in tables {
    schema = table.schema_version
    for feature in table.features {
      result.push(feature)
    }
  }
  FeatureTable::from_features(result, schema)
}

///|
/// Select a named subset while preserving the requested order.
pub fn select_features(
  table : FeatureTable,
  names : Array[String],
) -> FeatureTable {
  let result = []
  for name in names {
    for feature in table.features {
      if feature.name == name {
        result.push(feature)
      }
    }
  }
  FeatureTable::from_features(result, table.schema_version)
}

///|
/// Create a table from the complete default analysis vector.
pub fn default_analysis_feature_table(report : AnalysisReport) -> FeatureTable {
  let names = []
  for name in time_domain_feature_names() {
    names.push("time." + name)
  }
  let time_values = time_domain_feature_vector(report.cleaned_intervals)
  let features = make_named_features(names, time_values, "time_domain")
  let frequency = make_named_features(
    ["frequency.total_power", "frequency.centroid", "frequency.entropy"],
    [
      report.frequency.total_power,
      report.frequency.spectral_centroid_hz,
      report.frequency.spectral_entropy,
    ],
    "frequency",
  )
  for feature in frequency {
    features.push(feature)
  }
  FeatureTable::from_features(features, "hrvkit.features.v1")
}

///|
/// Calculate an L2 distance between aligned feature tables.
pub fn feature_table_distance(
  left : FeatureTable,
  right : FeatureTable,
) -> Double {
  let mut sum = 0.0
  let mut count = 0
  for feature in left.features {
    match feature_value(right, feature.name) {
      Some(other) =>
        if feature.valid {
          sum += (feature.value - other) * (feature.value - other)
          count += 1
        }
      None => ()
    }
  }
  if count == 0 {
    0.0
  } else {
    (sum / count.to_double()).sqrt()
  }
}

///|
/// Return a CSV-compatible row with the table values.
pub fn feature_table_row(table : FeatureTable) -> Array[String] {
  let result = []
  for feature in table.features {
    if feature.valid {
      result.push(feature.value.to_string())
    } else {
      result.push("")
    }
  }
  result
}

///|
/// Return whether two tables have exactly the same feature schema.
pub fn feature_schema_matches(
  left : FeatureTable,
  right : FeatureTable,
) -> Bool {
  if left.schema_version != right.schema_version ||
    left.features.length() != right.features.length() {
    return false
  }
  for i in 0.. Double {
  if table.features.length() == 0 {
    0.0
  } else {
    table.valid_count.to_double() / table.features.length().to_double()
  }
}