///|
/// The rule and class count are necessary to reproduce prediction sets.
pub fn ClassCalibrator::to_snapshot(self : ClassCalibrator) -> String {
  let (rule, penalty, after) = match self.rule {
    Lac => ("lac", 0.0, 0)
    Aps => ("aps", 0.0, 0)
    Raps(penalty, after) => ("raps", penalty, after)
  }
  Json::object(
    Map([
      ("schema", Json::number(1.0)),
      ("kind", Json::string("classifier")),
      ("classes", Json::number(self.classes.to_double())),
      ("rule", Json::string(rule)),
      ("penalty", Json::number(penalty)),
      ("after", Json::number(after.to_double())),
      ("scores", Json::array(self.scores.sorted.map(x => Json::number(x)))),
    ]),
  ).stringify()
}

///|
fn snapshot_integer(
  value : Json,
  low : Int32,
  high : Int32,
) -> Int32 raise ConformalError {
  let number = snapshot_number(value)
  if number < low.to_double() ||
    number > high.to_double() ||
    number.floor() != number {
    raise Invalid("snapshot integer outside range")
  }
  number.to_int()
}

///|
/// Restores only validated states; it cannot certify how calibration data was obtained.
pub fn ClassCalibrator::from_snapshot(
  text : String,
) -> ClassCalibrator raise ConformalError {
  let fields = snapshot_object(snapshot_parse(text), [
    "schema", "kind", "classes", "rule", "penalty", "after", "scores",
  ])
  snapshot_version(fields, "classifier")
  let classes = snapshot_integer(snapshot_field(fields, "classes"), 2, 1024)
  let penalty = snapshot_number(snapshot_field(fields, "penalty"))
  let after = snapshot_integer(snapshot_field(fields, "after"), 0, classes)
  let rule = match snapshot_field(fields, "rule") {
    String("lac") => {
      if penalty != 0.0 || after != 0 {
        raise Invalid("LAC snapshot has RAPS parameters")
      }
      Lac
    }
    String("aps") => {
      if penalty != 0.0 || after != 0 {
        raise Invalid("APS snapshot has RAPS parameters")
      }
      Aps
    }
    String("raps") => {
      if penalty < 0.0 {
        raise Invalid("negative snapshot penalty")
      }
      Raps(penalty, after)
    }
    _ => raise Invalid("unknown snapshot classification rule")
  }
  let scores = snapshot_scores(snapshot_field(fields, "scores"))
  let maximum = match rule {
    Lac | Aps => 1.0 + 1.0e-10
    Raps(lambda, k) => 1.0 + 1.0e-10 + lambda * (classes - k).to_double()
  }
  if !finite(maximum) {
    raise Invalid("snapshot rule overflows at full rank")
  }
  for score in scores.sorted {
    if score < 0.0 || score > maximum {
      raise Invalid("snapshot score outside rule range")
    }
  }
  { scores, classes, rule, }
}