///|
pub(all) enum Severity {
  Error
  Warning
} derive(Eq, @debug.Debug, ToJson)

///|
pub(all) struct Diagnostic {
  severity : Severity
  code : String
  message : String
  line : Int32
  column : Int32
} derive(Eq, @debug.Debug, ToJson)

///|
pub(all) suberror JcampError {
  Invalid(Diagnostic)
} derive(@debug.Debug)

///|
pub(all) enum NumericMode {
  Auto
  Affn
  Asdf
} derive(Eq, @debug.Debug, ToJson)

///|
pub(all) enum Encoding {
  Plain
  Packed
  Squeezed
  Difference
} derive(Eq, @debug.Debug, ToJson)

///|
pub(all) struct Limits {
  max_chars : Int32
  max_lines : Int32
  max_line_chars : Int32
  max_records : Int32
  max_blocks : Int32
  max_depth : Int32
  max_points : Int32
  max_tokens_per_line : Int32
} derive(Eq, @debug.Debug, ToJson)

///|
pub fn Limits::default() -> Limits {
  {
    max_chars: 16000000,
    max_lines: 250000,
    max_line_chars: 16384,
    max_records: 50000,
    max_blocks: 1000,
    max_depth: 16,
    max_points: 1000000,
    max_tokens_per_line: 100000,
  }
}

///|
pub(all) struct ParseOptions {
  limits : Limits
  numeric_mode : NumericMode
  relative_tolerance : Double
  absolute_tolerance : Double
} derive(@debug.Debug, ToJson)

///|
pub fn ParseOptions::default() -> ParseOptions {
  {
    limits: Limits::default(),
    numeric_mode: Auto,
    relative_tolerance: 0.000001,
    absolute_tolerance: 0.00000001,
  }
}

///|
pub(all) struct Record {
  label : String
  value : String
  line : Int32
} derive(Eq, @debug.Debug, ToJson, @json.FromJson)

///|
pub(all) struct DataLine {
  text : String
  line : Int32
} derive(Eq, @debug.Debug, ToJson)

///|
pub(all) struct DataTable {
  label : String
  descriptor : String
  rows : Array[DataLine]
  line : Int32
} derive(Eq, @debug.Debug, ToJson)

///|
pub(all) struct Block {
  records : Array[Record]
  tables : Array[DataTable]
  children : Array[Block]
  start_line : Int32
  end_line : Int32
} derive(@debug.Debug, ToJson)

///|
pub(all) struct Document {
  blocks : Array[Block]
} derive(@debug.Debug, ToJson)

///|
pub(all) struct Point {
  x : Double
  y : Double?
} derive(Eq, @debug.Debug, @json.FromJson)

///|
pub impl ToJson for Point with fn to_json(self) {
  let y : Json = match self.y {
    Some(y) => Json(y)
    None => Json::null()
  }
  { "x": Json(self.x), "y": y }
}

///|
pub(all) struct Spectrum {
  title : String
  data_type : String
  x_units : String
  y_units : String
  points : Array[Point]
  records : Array[Record]
  source_line : Int32
} derive(@debug.Debug, ToJson, @json.FromJson)

///|
pub(all) struct Report {
  valid : Bool
  spectra : Int32
  points : Int32
  missing : Int32
  diagnostics : Array[Diagnostic]
} derive(@debug.Debug, ToJson)

///|
fn[T] reject(
  code : String,
  message : String,
  line~ : Int32 = 0,
  column~ : Int32 = 1,
) -> T raise JcampError {
  raise Invalid({ severity: Error, code, message, line, column, })
}

///|
fn finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn close(a : Double, b : Double, options : ParseOptions) -> Bool {
  let scale = if a.abs() > b.abs() { a.abs() } else { b.abs() }
  (a - b).abs() <=
  options.absolute_tolerance + options.relative_tolerance * scale
}

///|
fn check_options(options : ParseOptions) -> Unit raise JcampError {
  let l = options.limits
  for
    limit in [
      l.max_chars,
      l.max_lines,
      l.max_line_chars,
      l.max_records,
      l.max_blocks,
      l.max_depth,
      l.max_points,
      l.max_tokens_per_line,
    ] {
    guard limit > 0 else {
      reject("OPTION_LIMIT", "resource budgets must be positive")
    }
  }
  guard l.max_depth <= 128 else {
    reject("OPTION_DEPTH", "maximum nesting may not exceed 128")
  }
  guard finite(options.relative_tolerance) &&
    finite(options.absolute_tolerance) &&
    options.relative_tolerance >= 0.0 &&
    options.absolute_tolerance >= 0.0 else {
    reject("OPTION_TOLERANCE", "tolerances must be finite and nonnegative")
  }
}