///|
pub struct Neighbor {
  sample : String
  index : Int
  separation : Double
}

///|
pub fn Neighbor::sample_name(self : Neighbor) -> String {
  self.sample
}

///|
pub fn Neighbor::sample_index(self : Neighbor) -> Int {
  self.index
}

///|
pub fn Neighbor::distance(self : Neighbor) -> Double {
  self.separation
}

///|
/// Full symmetric distance matrix, bounded to avoid accidental quadratic memory.
pub fn pairwise_distances(
  data : Dataset,
) -> Array[Array[Double]] raise CodaError {
  if data.row_count() > 512 {
    raise Budget("pairwise matrix limited to 512 observations")
  }
  let n = data.row_count()
  let out = zero_matrix(n, n)
  for i in 0.. Array[Neighbor] raise CodaError {
  if count < 1 || count > data.row_count() {
    raise Invalid("neighbor count outside 1..N")
  }
  let out : Array[Neighbor] = []
  for i, row in data.cells {
    let item = Neighbor::{
      sample: data.samples[i],
      index: i,
      separation: distance(query, row),
    }
    let mut pos = out.length()
    out.push(item)
    while pos > 0 && out[pos - 1].separation > item.separation {
      out[pos] = out[pos - 1]
      pos -= 1
    }
    out[pos] = item
    if out.length() > count {
      ignore(out.pop())
    }
  }
  out
}

///|
/// Observed medoid minimizes total squared Aitchison separation.
pub fn medoid(data : Dataset) -> Medoid raise CodaError {
  let distances = pairwise_distances(data)
  let mut best = 0
  let mut objective = 1.0e300
  for i, row in distances {
    let mut s = 0.0
    for v in row {
      s += v * v
    }
    if s < objective {
      objective = s
      best = i
    }
  }
  { sample: data.samples[best], index: best, objective, }
}

///|
pub struct Medoid {
  sample : String
  index : Int
  objective : Double
}

///|
pub fn Medoid::sample_name(self : Medoid) -> String {
  self.sample
}

///|
pub fn Medoid::sample_index(self : Medoid) -> Int {
  self.index
}

///|
/// Sum of squared distances, deliberately not called a distance.
pub fn Medoid::sum_squared_distance(self : Medoid) -> Double {
  self.objective
}