///|
pub(all) struct QualityPolicy {
  max_missing_fraction : Double
  require_monotone : Bool
  require_uniform : Bool
  min_points : Int32
} derive(@debug.Debug, ToJson)

///|
pub fn QualityPolicy::default() -> QualityPolicy {
  {
    max_missing_fraction: 0.1,
    require_monotone: true,
    require_uniform: false,
    min_points: 2,
  }
}

///|
pub(all) struct Summary {
  title : String
  points : Int32
  missing : Int32
  x_min : Double
  x_max : Double
  y_min : Double?
  y_max : Double?
  y_mean : Double?
  direction : Int32
  uniform : Bool
  longest_missing_run : Int32
  diagnostics : Array[Diagnostic]
  accepted : Bool
} derive(@debug.Debug, ToJson)

///|
pub(all) struct BatchInput {
  name : String
  text : String
} derive(@debug.Debug, ToJson)

///|
pub(all) struct BatchItem {
  name : String
  accepted : Bool
  spectra : Array[Summary]
  diagnostics : Array[Diagnostic]
} derive(@debug.Debug, ToJson)

///|
pub(all) struct BatchReport {
  accepted : Int32
  rejected : Int32
  items : Array[BatchItem]
} derive(@debug.Debug, ToJson)

///|
fn check_policy(policy : QualityPolicy) -> Unit raise JcampError {
  guard finite(policy.max_missing_fraction) &&
    policy.max_missing_fraction >= 0.0 &&
    policy.max_missing_fraction <= 1.0 else {
    reject("QUALITY_POLICY", "missing fraction must be between zero and one")
  }
  guard policy.min_points >= 1 &&
    policy.min_points <= Limits::default().max_points else {
    reject("QUALITY_POLICY", "min_points must be inside the point budget")
  }
}

///|
fn quality_diagnostic(
  code : String,
  message : String,
  line : Int32,
) -> Diagnostic {
  { severity: Warning, code, message, line, column: 1, }
}

///|
pub fn summarize(
  spectrum : Spectrum,
  policy~ : QualityPolicy = QualityPolicy::default(),
) -> Summary raise JcampError {
  checked_spectrum(spectrum)
  check_policy(policy)
  let mut missing = 0
  let mut run = 0
  let mut longest = 0
  let mut count = 0
  let mut mean = 0.0
  let mut minimum : Double? = None
  let mut maximum : Double? = None
  let mut x_min = spectrum.points[0].x
  let mut x_max = x_min
  for p in spectrum.points {
    if p.x < x_min {
      x_min = p.x
    }
    if p.x > x_max {
      x_max = p.x
    }
    match p.y {
      None => {
        missing += 1
        run += 1
        if run > longest {
          longest = run
        }
      }
      Some(y) => {
        run = 0
        count += 1
        // Weighted incremental mean avoids summing all ordinates first.
        mean = mean * ((count - 1).to_double() / count.to_double()) +
          y / count.to_double()
        guard finite(mean) else {
          reject("QUALITY_RANGE", "mean calculation overflow")
        }
        minimum = match minimum {
          None => Some(y)
          Some(v) => Some(if y < v { y } else { v })
        }
        maximum = match maximum {
          None => Some(y)
          Some(v) => Some(if y > v { y } else { v })
        }
      }
    }
  }
  let direction = axis_direction(spectrum.points)
  let uniform = uniform_step(spectrum.points) is Some(_)
  let diagnostics : Array[Diagnostic] = []
  if spectrum.points.length() < policy.min_points {
    diagnostics.push(
      quality_diagnostic(
        "QUALITY_POINTS",
        "fewer than the required number of samples",
        spectrum.source_line,
      ),
    )
  }
  if missing.to_double() / spectrum.points.length().to_double() >
    policy.max_missing_fraction {
    diagnostics.push(
      quality_diagnostic(
        "QUALITY_MISSING",
        "missing-value fraction exceeds policy",
        spectrum.source_line,
      ),
    )
  }
  if policy.require_monotone && direction == 0 && spectrum.points.length() > 1 {
    diagnostics.push(
      quality_diagnostic(
        "QUALITY_AXIS",
        "axis is not strictly monotone",
        spectrum.source_line,
      ),
    )
  }
  if policy.require_uniform && !uniform {
    diagnostics.push(
      quality_diagnostic(
        "QUALITY_GRID",
        "axis is not uniformly sampled",
        spectrum.source_line,
      ),
    )
  }
  {
    title: spectrum.title,
    points: spectrum.points.length(),
    missing,
    x_min,
    x_max,
    y_min: minimum,
    y_max: maximum,
    y_mean: if count == 0 {
      None
    } else {
      Some(mean)
    },
    direction,
    uniform,
    longest_missing_run: longest,
    accepted: diagnostics.is_empty(),
    diagnostics,
  }
}

///|
pub fn inspect_batch(
  inputs : Array[BatchInput],
  policy~ : QualityPolicy = QualityPolicy::default(),
  options~ : ParseOptions = ParseOptions::default(),
) -> BatchReport raise JcampError {
  check_policy(policy)
  check_options(options)
  guard !inputs.is_empty() && inputs.length() <= options.limits.max_blocks else {
    reject("BATCH_SIZE", "batch must be nonempty and bounded")
  }
  let mut chars = 0
  let mut points = 0
  let names : Map[String, Bool] = Map([])
  for input in inputs {
    guard trim(input.name) != "" && !names.contains(input.name) else {
      reject("BATCH_NAME", "batch names must be nonempty and unique")
    }
    names[input.name] = true
    guard input.text.length() <= options.limits.max_chars - chars else {
      reject("LIMIT_TEXT", "batch text budget exceeded")
    }
    chars += input.text.length()
  }
  let items : Array[BatchItem] = []
  let mut accepted = 0
  for input in inputs {
    let item : BatchItem = try {
      let source = read_spectra(input.text, options~)
      let summaries : Array[Summary] = []
      for spectrum in source {
        guard spectrum.points.length() <= options.limits.max_points - points else {
          reject("LIMIT_POINTS", "aggregate batch point budget exceeded")
        }
        points += spectrum.points.length()
        summaries.push(summarize(spectrum, policy~))
      }
      {
        name: input.name,
        accepted: summaries.iter().all(s => s.accepted),
        spectra: summaries,
        diagnostics: [],
      }
    } catch {
      Invalid(d) =>
        { name: input.name, accepted: false, spectra: [], diagnostics: [d], }
    }
    if item.accepted {
      accepted += 1
    }
    items.push(item)
  }
  { accepted, rejected: inputs.length() - accepted, items, }
}

///|
fn parse_quality_policy(value : Json) -> QualityPolicy raise JcampError {
  let fields = json_object(
    value,
    [
      "max_missing_fraction", "require_monotone", "require_uniform", "min_points",
    ],
    "quality policy",
  )
  fn boolean(key : String, default : Bool) -> Bool raise JcampError {
    match fields.get(key) {
      None => default
      Some(True) => true
      Some(False) => false
      _ => reject("JSON_TYPE", key + " must be boolean")
    }
  }
  let policy = {
    max_missing_fraction: json_double(fields, "max_missing_fraction", 0.1),
    require_monotone: boolean("require_monotone", true),
    require_uniform: boolean("require_uniform", false),
    min_points: json_int(fields, "min_points", 2),
  }
  check_policy(policy)
  policy
}