///|
pub enum ObjectiveKind {
  Squared
  Logistic
  Huber(delta~ : Double)
  Quantile(probability~ : Double)
  Hinge
} derive(ToJson, FromJson, Debug, Eq)

///|
pub fn ObjectiveKind::loss(
  self : ObjectiveKind,
  prediction : Double,
  label : Double,
) -> Double {
  match self {
    Squared => 0.5 * squared_error(prediction, label)
    Logistic => binary_cross_entropy(sigmoid(prediction), label)
    Huber(delta~) => smooth_l1_loss(prediction - label, beta=delta)
    Quantile(probability~) => {
      let error = label - prediction
      if error >= 0.0 {
        probability * error
      } else {
        (probability - 1.0) * error
      }
    }
    Hinge => hinge_loss(prediction, if label >= 0.5 { 1.0 } else { -1.0 })
  }
}

///|
pub fn ObjectiveKind::gradient(
  self : ObjectiveKind,
  prediction : Double,
  label : Double,
) -> Double {
  match self {
    Squared => prediction - label
    Logistic => sigmoid(prediction) - clamp(label, 0.0, 1.0)
    Huber(delta~) => {
      let error = prediction - label
      clamp(error, -delta, delta)
    }
    Quantile(probability~) =>
      if label - prediction >= 0.0 {
        -probability
      } else {
        1.0 - probability
      }
    Hinge =>
      if 1.0 - prediction * (if label >= 0.5 { 1.0 } else { -1.0 }) > 0.0 {
        -(if label >= 0.5 { 1.0 } else { -1.0 })
      } else {
        0.0
      }
  }
}

///|
pub struct Regularizer {
  l1 : Double
  l2 : Double
}

///|
pub fn Regularizer::new(l1? : Double = 0.0, l2? : Double = 0.0) -> Regularizer {
  { l1: if l1 < 0.0 { 0.0 } else { l1 }, l2: if l2 < 0.0 { 0.0 } else { l2 } }
}

///|
pub fn Regularizer::penalty(
  self : Regularizer,
  weights : Array[Double],
) -> Double {
  self.l1 * l1_norm(weights) + 0.5 * self.l2 * squared_norm(weights)
}

///|
pub fn Regularizer::gradient(
  self : Regularizer,
  weights : Array[Double],
  index : Int,
) -> Double {
  let value = weights.get(index).unwrap_or(0.0)
  let sign = if value < 0.0 { -1.0 } else if value > 0.0 { 1.0 } else { 0.0 }
  self.l1 * sign + self.l2 * value
}

///|
pub fn Regularizer::proximal(
  self : Regularizer,
  value : Double,
  step : Double,
) -> Double {
  soft_threshold(value, self.l1 * step) / (1.0 + self.l2 * step)
}

///|
pub struct ObjectiveTracker {
  objective : ObjectiveKind
  mut count : Double
  mut total : Double
  mut last : Double
}

///|
pub fn ObjectiveTracker::new(objective : ObjectiveKind) -> ObjectiveTracker {
  { objective, count: 0.0, total: 0.0, last: 0.0 }
}

///|
pub fn ObjectiveTracker::observe(
  self : ObjectiveTracker,
  prediction : Double,
  label : Double,
  weight? : Double = 1.0,
) -> Unit {
  let loss = self.objective.loss(prediction, label)
  self.count += weight
  self.total += weight * loss
  self.last = loss
}

///|
pub fn ObjectiveTracker::mean(self : ObjectiveTracker) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    self.total / self.count
  }
}

///|
pub fn ObjectiveTracker::last(self : ObjectiveTracker) -> Double {
  self.last
}

///|
pub fn ObjectiveTracker::count(self : ObjectiveTracker) -> Double {
  self.count
}

///|
pub fn ObjectiveTracker::reset(self : ObjectiveTracker) -> Unit {
  self.count = 0.0
  self.total = 0.0
  self.last = 0.0
}