///|
/// Lightweight local explanations for linear and sparse predictions.
pub struct FeatureAttribution {
  index : Int
  contribution : Double
}

///|
pub fn FeatureAttribution::new(
  index : Int,
  contribution : Double,
) -> FeatureAttribution {
  { index, contribution }
}

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

///|
pub fn FeatureAttribution::contribution(self : FeatureAttribution) -> Double {
  self.contribution
}

///|
pub fn explain_linear(
  weights : Array[Double],
  features : Array[Double],
  top_k? : Int = 10,
) -> Array[FeatureAttribution] {
  let size = if weights.length() < features.length() {
    weights.length()
  } else {
    features.length()
  }
  let attributions = Array::makei(size, i => {
    FeatureAttribution::new(i, weights[i] * features[i])
  })
  attributions.sort_by((left, right) => {
    let left_magnitude = left.contribution.abs()
    let right_magnitude = right.contribution.abs()
    if left_magnitude > right_magnitude {
      -1
    } else if left_magnitude < right_magnitude {
      1
    } else {
      left.index - right.index
    }
  })
  let limit = if top_k < 0 {
    0
  } else if top_k > attributions.length() {
    attributions.length()
  } else {
    top_k
  }
  attributions[:limit].to_owned()
}

///|
pub fn explain_sparse(
  weights : SparseVector,
  features : SparseVector,
  top_k? : Int = 10,
) -> Array[FeatureAttribution] {
  let attributions = Array::make(0, FeatureAttribution::new(0, 0.0))
  for entry in features.entries() {
    attributions.push(
      FeatureAttribution::new(
        entry.index(),
        entry.value() * weights.get(entry.index()),
      ),
    )
  }
  attributions.sort_by((left, right) => {
    if left.contribution.abs() > right.contribution.abs() {
      -1
    } else if left.contribution.abs() < right.contribution.abs() {
      1
    } else {
      left.index - right.index
    }
  })
  let limit = if top_k < 0 {
    0
  } else if top_k > attributions.length() {
    attributions.length()
  } else {
    top_k
  }
  attributions[:limit].to_owned()
}

///|
pub fn attribution_sum(attributions : Array[FeatureAttribution]) -> Double {
  attributions.fold(init=0.0, (total, item) => total + item.contribution)
}

///|
pub struct StabilityTracker {
  previous : Array[Double]
  mut observations : Int
  mut drift : Double
}

///|
pub fn StabilityTracker::new(dimension : Int) -> StabilityTracker {
  {
    previous: Array::make(if dimension < 0 { 0 } else { dimension }, 0.0),
    observations: 0,
    drift: 0.0,
  }
}

///|
pub fn StabilityTracker::observe(
  self : StabilityTracker,
  values : Array[Double],
) -> Double {
  let size = if values.length() < self.previous.length() {
    values.length()
  } else {
    self.previous.length()
  }
  let mut distance = 0.0
  for i in 0.. Double {
  if self.observations == 0 {
    0.0
  } else {
    self.drift / self.observations.to_double()
  }
}

///|
pub fn StabilityTracker::observations(self : StabilityTracker) -> Int {
  self.observations
}

///|
pub fn StabilityTracker::reset(self : StabilityTracker) -> Unit {
  self.previous.fill(0.0)
  self.observations = 0
  self.drift = 0.0
}