///|
pub suberror FcsError {
  Invalid(String)
  Unsupported(String)
  Limit(String)
} derive(Debug)

///|
pub(all) enum NumberType {
  Integer
  Float32
  Float64
} derive(Eq, Debug, ToJson)

///|
pub(all) enum ByteOrder {
  LittleEndian
  BigEndian
} derive(Eq, Debug, ToJson)

///|
pub struct Channel {
  name : String
  stain : String
  bits : Int
  range : Double
  decades : Double
  zero : Double
  gain : Double
} derive(Debug, ToJson)

///|
pub struct Dataset {
  version : String
  priv channels : Array[Channel]
  events : Int
  kind : NumberType
  order : ByteOrder
  priv metadata : Array[(String, String)]
  priv analysis : Array[(String, String)]
  priv diagnostics : Array[String]
  file_offset : Int
  next_offset : Int
  priv raw : Bytes
  priv width : Int
  priv offsets : Array[Int]
} derive(Debug)

///|
pub struct ChannelStats {
  count : Int
  nonfinite : Int
  minimum : Double?
  maximum : Double?
  mean : Double?
  variance : Double?
} derive(Debug, ToJson)

///|
pub(all) struct Bounds {
  channel : Int
  lower : Double
  upper : Double
} derive(Debug)

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

///|
fn need(b : Bytes, p : Int, n : Int) -> Unit raise FcsError {
  if p < 0 || n < 0 || p > b.length() || n > b.length() - p {
    raise Invalid("truncated or invalid byte range at \{p}, length \{n}")
  }
}

///|
fn part(b : Bytes, p : Int, n : Int) -> Bytes {
  Bytes::makei(n, i => b[p + i])
}

///|
fn natural(s : String) -> Int raise FcsError {
  if s.is_empty() {
    raise Invalid("empty unsigned integer")
  }
  let mut n = 0
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i].to_int() - 48
    if c < 0 || c > 9 || n > (2147483647 - c) / 10 {
      raise Invalid("invalid or oversized unsigned integer: \{s}")
    }
    n = n * 10 + c
  }
  n
}

///|
fn number(s : String) -> Double raise FcsError {
  if s.is_empty() || s.trim().to_owned() != s {
    raise Invalid("invalid numeric metadata")
  }
  let x = @string.parse_double(s) catch {
    _ => raise Invalid("invalid number: \{s}")
  }
  if !finite(x) {
    raise Invalid("non-finite numeric metadata")
  }
  x
}

///|
fn lookup(pairs : Array[(String, String)], key : String) -> String? {
  let key = key.to_upper()
  for pair in pairs {
    if pair.0.to_upper() == key {
      return Some(pair.1)
    }
  }
  None
}

///|
fn required(
  pairs : Array[(String, String)],
  key : String,
) -> String raise FcsError {
  match lookup(pairs, key) {
    Some(v) => v
    None => raise Invalid("missing required keyword \{key}")
  }
}

///|
fn put(pairs : Array[(String, String)], key : String, value : String) -> Unit {
  for i = 0; i < pairs.length(); i = i + 1 {
    if pairs[i].0.to_upper() == key.to_upper() {
      pairs[i] = (key, value)
      return
    }
  }
  pairs.push((key, value))
}

///|
fn ascii(b : Bytes, p : Int, n : Int) -> String raise FcsError {
  need(b, p, n)
  let chars = []
  for i = 0; i < n; i = i + 1 {
    let c = b[p + i].to_int()
    if c < 32 || c > 126 {
      raise Invalid("non-ASCII header at \{p+i}")
    }
    chars.push(c.to_char().unwrap())
  }
  String::from_iter(chars.iter())
}

///|
fn header_offset(b : Bytes, base : Int, p : Int) -> Int raise FcsError {
  let s = ascii(b, base + p, 8).trim().to_owned()
  if s.is_empty() {
    0
  } else {
    natural(s)
  }
}

///|
fn uword(b : Bytes, p : Int, n : Int, order : ByteOrder) -> UInt64 {
  let mut v = 0UL
  for i = 0; i < n; i = i + 1 {
    let j = if order == LittleEndian { n - 1 - i } else { i }
    v = (v << 8) | b[p + j].to_uint64()
  }
  v
}

///|
fn emit_word(out : Array[Byte], v : UInt64, n : Int, order : ByteOrder) -> Unit {
  for i = 0; i < n; i = i + 1 {
    let shift = if order == LittleEndian { i * 8 } else { (n - 1 - i) * 8 }
    out.push(((v >> shift) & 255UL).to_byte())
  }
}