///|
fn euclidean_distance(
  a : Array[Double],
  b : Array[Double],
  scales : Array[Double],
) -> Double {
  let n = if a.length() < b.length() { a.length() } else { b.length() }
  let mut distance = 0.0
  for j in 0.. 1.0e-12 {
      scales[j]
    } else {
      1.0
    }
    let difference = (a[j] - b[j]) / scale
    distance += difference * difference
  }
  distance.sqrt()
}

///|
fn column_scales(covariates : Array[Array[Double]]) -> Array[Double] {
  if covariates.length() == 0 {
    return []
  }
  let width = covariates[0].length()
  let result = Array::make(width, 0.0)
  for row in covariates {
    for j in 0.. Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
/// Performs one-to-one nearest-neighbor matching on standardized covariates.
pub fn nearest_neighbor_match(
  covariates : Array[Array[Double]],
  treatment : Array[Bool],
  caliper? : Double = 0.0,
  replace? : Bool = false,
) -> Array[MatchPair] {
  let scales = column_scales(covariates)
  let controls = Array::new()
  for i in 0..= 0 && (caliper <= 0.0 || best_distance <= caliper) {
        pairs.push({
          treated_index,
          control_index: best_index,
          distance: best_distance,
          weight: 1.0,
        })
        if !replace {
          used.push(best_index)
        }
      }
    }
  }
  pairs
}

///|
/// Computes the matched-sample effect and its pair-level standard error.
pub fn estimate_matched_ate(
  outcomes : Array[Double],
  pairs : Array[MatchPair],
) -> Estimate {
  let differences = Array::new(capacity=pairs.length())
  for pair in pairs {
    if pair.treated_index >= 0 &&
      pair.control_index >= 0 &&
      pair.treated_index < outcomes.length() &&
      pair.control_index < outcomes.length() {
      differences.push(
        outcomes[pair.treated_index] - outcomes[pair.control_index],
      )
    }
  }
  let n = differences.length()
  let se = if n <= 1 {
    0.0
  } else {
    std_dev(differences, sample=true) / n.to_double().sqrt()
  }
  Estimate::from_standard_error(
    mean(differences),
    se,
    n,
    n.to_double(),
    "ATE (nearest-neighbor matching)",
  )
}

///|
pub fn matching_effects(
  outcomes : Array[Double],
  pairs : Array[MatchPair],
) -> Array[Double] {
  let result = Array::new(capacity=pairs.length())
  for pair in pairs {
    if pair.treated_index < outcomes.length() &&
      pair.control_index < outcomes.length() {
      result.push(outcomes[pair.treated_index] - outcomes[pair.control_index])
    }
  }
  result
}

///|
pub fn matching_mean_distance(pairs : Array[MatchPair]) -> Double {
  let distances = Array::new(capacity=pairs.length())
  for pair in pairs {
    distances.push(pair.distance)
  }
  mean(distances)
}