///|
/// Recoverable input errors carry a byte offset where available.
pub suberror LasError {
  Truncated(Int32)
  Limit(String)
  Invalid(String)
  Unsupported(String)
} derive(Debug)

///|
fn require(ok : Bool, message : String) -> Unit raise LasError {
  if !ok {
    raise Invalid(message)
  }
}

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

///|
/// Budgets apply before allocating record arrays or metadata payloads.
pub(all) struct Limits {
  max_bytes : Int32
  max_points : Int32
  max_vlrs : Int32
  max_cells : Int32
} derive(Debug, Eq)

///|
pub fn Limits::default() -> Limits {
  {
    max_bytes: 67108864,
    max_points: 1000000,
    max_vlrs: 4096,
    max_cells: 1000000,
  }
}

///|
pub fn Limits::validate(self : Limits) -> Unit raise LasError {
  require(
    self.max_bytes >= 375 && self.max_bytes <= 1073741824,
    "byte budget outside safe range",
  )
  require(
    self.max_points >= 0 && self.max_points <= 10000000,
    "invalid point budget",
  )
  require(self.max_vlrs >= 0 && self.max_vlrs <= 65535, "invalid VLR budget")
  require(
    self.max_cells > 0 && self.max_cells <= 10000000,
    "invalid cell budget",
  )
}

///|
/// XYZ scale factors and offsets. They never imply a coordinate reference system.
pub(all) struct Transform {
  sx : Double
  sy : Double
  sz : Double
  ox : Double
  oy : Double
  oz : Double
} derive(Debug, Eq)

///|
pub fn Transform::new(scale : Double) -> Transform raise LasError {
  let t = { sx: scale, sy: scale, sz: scale, ox: 0.0, oy: 0.0, oz: 0.0, }
  t.validate()
  t
}

///|
pub fn Transform::validate(self : Transform) -> Unit raise LasError {
  for s in [self.sx, self.sy, self.sz] {
    require(finite(s) && s > 0.0, "scale must be finite and positive")
  }
  for o in [self.ox, self.oy, self.oz] {
    require(finite(o), "offset must be finite")
  }
}

///|
/// Raw integer attributes retain the exact LAS record semantics.
pub(all) struct Point {
  x : Int32
  y : Int32
  z : Int32
  intensity : Int32
  return_number : Int32
  returns : Int32
  classification : Int32
  flags : Int32
  scanner_channel : Int32
  scan_direction : Bool
  edge : Bool
  scan_angle : Int32
  user_data : Int32
  source_id : Int32
  gps_time : Double?
  rgb : (Int32, Int32, Int32)?
  nir : Int32?
  extra : Bytes
} derive(Debug, Eq)

///|
pub fn Point::new(x : Int32, y : Int32, z : Int32) -> Point {
  {
    x,
    y,
    z,
    intensity: 0,
    return_number: 1,
    returns: 1,
    classification: 0,
    flags: 0,
    scanner_channel: 0,
    scan_direction: false,
    edge: false,
    scan_angle: 0,
    user_data: 0,
    source_id: 0,
    gps_time: None,
    rgb: None,
    nir: None,
    extra: b"",
  }
}

///|
pub fn record_size(format : Int32) -> Int32 raise LasError {
  match format {
    0 => 20
    1 => 28
    2 => 26
    3 => 34
    6 => 30
    7 => 36
    8 => 38
    _ => raise Unsupported("point format: \{format}")
  }
}

///|
pub fn Point::validate(self : Point, format : Int32) -> Unit raise LasError {
  ignore(record_size(format))
  let old = format <= 3
  let cap = if old { 5 } else { 15 }
  require(
    self.return_number >= 1 &&
    self.return_number <= self.returns &&
    self.returns <= cap,
    "invalid return numbering",
  )
  require(self.intensity >= 0 && self.intensity <= 65535, "invalid intensity")
  require(
    self.classification >= 0 &&
    self.classification <= (if old { 31 } else { 255 }),
    "invalid classification",
  )
  require(
    self.flags >= 0 && self.flags <= (if old { 7 } else { 15 }),
    "invalid flags",
  )
  require(
    self.scanner_channel >= 0 &&
    self.scanner_channel <= (if old { 0 } else { 3 }),
    "invalid scanner channel",
  )
  require(
    self.scan_angle >= (if old { -90 } else { -30000 }) &&
    self.scan_angle <= (if old { 90 } else { 30000 }),
    "invalid scan angle",
  )
  require(self.user_data >= 0 && self.user_data <= 255, "invalid user data")
  require(
    self.source_id >= 0 && self.source_id <= 65535,
    "invalid point source ID",
  )
  let gps = format == 1 || format == 3 || format >= 6
  require((self.gps_time is Some(_)) == gps, "GPS field disagrees with format")
  if self.gps_time is Some(t) {
    require(finite(t), "nonfinite GPS time")
  }
  let color = format == 2 || format == 3 || format == 7 || format == 8
  require((self.rgb is Some(_)) == color, "RGB field disagrees with format")
  if self.rgb is Some((r, g, b)) {
    for c in [r, g, b] {
      require(c >= 0 && c <= 65535, "invalid RGB component")
    }
  }
  require(
    (self.nir is Some(_)) == (format == 8),
    "NIR field disagrees with format",
  )
  if self.nir is Some(n) {
    require(n >= 0 && n <= 65535, "invalid NIR value")
  }
}

///|
pub extend LasError with @debug.Debug::{to_repr}

///|
pub extend Point with @debug.Debug::{to_repr}

///|
pub extend Point with Eq::{equal, not_equal}

///|
pub extend Transform with @debug.Debug::{to_repr}

///|
pub extend Transform with Eq::{equal, not_equal}

///|
pub extend Limits with @debug.Debug::{to_repr}

///|
pub extend Limits with Eq::{equal, not_equal}