///|
pub suberror NtpError {
  Invalid(String)
} derive(Debug)

///|
pub(all) struct Timestamp {
  seconds : UInt
  fraction : UInt
} derive(Debug, Eq)

///|
pub(all) struct Packet {
  leap : Int
  version : Int
  mode : Int
  stratum : Int
  poll : Int
  precision : Int
  root_delay : Int
  root_dispersion : UInt
  reference_id : UInt
  reference : Timestamp
  origin : Timestamp
  receive : Timestamp
  transmit : Timestamp
} derive(Debug, Eq)

///|
pub fn Timestamp::zero() -> Timestamp {
  { seconds: 0U, fraction: 0U, }
}

///|
/// NTP era is supplied by the caller: 0 before 2036 rollover, 1 afterwards.
pub fn Timestamp::unix_seconds(
  self : Timestamp,
  era : Int,
) -> Double raise NtpError {
  if era < 0 || era > 1 {
    raise Invalid("only eras 0 and 1 supported")
  }
  era.to_double() * 4294967296.0 +
  self.seconds.to_double() -
  2208988800.0 +
  self.fraction.to_double() / 4294967296.0
}

///|
pub fn from_unix(seconds : Double) -> (Timestamp, Int) raise NtpError {
  if !finite(seconds) || seconds < -2208988800.0 || seconds >= 6380945792.0 {
    raise Invalid("time outside supported eras")
  }
  let integer = seconds.floor()
  let whole = (integer + 2208988800.0).to_uint64()
  let era = (whole / 4294967296UL).to_int()
  let stamp : Timestamp = {
    seconds: whole.to_uint(),
    fraction: ((seconds - integer) * 4294967296.0).to_uint(),
  }
  (stamp, era)
}

///|
/// Modular signed seconds handle era rollover; intervals must be less than 2^31 seconds.
pub fn Timestamp::difference(self : Timestamp, earlier : Timestamp) -> Double {
  (self.seconds - earlier.seconds).reinterpret_as_int().to_double() +
  (self.fraction.to_double() - earlier.fraction.to_double()) / 4294967296.0
}

///|
fn read32(bytes : Bytes, pos : Int) -> UInt {
  (bytes[pos].to_uint() << 24) |
  (bytes[pos + 1].to_uint() << 16) |
  (bytes[pos + 2].to_uint() << 8) |
  bytes[pos + 3].to_uint()
}

///|
fn write32(out : Array[Byte], n : UInt) -> Unit {
  out.push((n >> 24).to_byte())
  out.push((n >> 16).to_byte())
  out.push((n >> 8).to_byte())
  out.push(n.to_byte())
}

///|
fn stamp(bytes : Bytes, pos : Int) -> Timestamp {
  { seconds: read32(bytes, pos), fraction: read32(bytes, pos + 4), }
}

///|
pub fn decode(bytes : Bytes) -> Packet raise NtpError {
  if bytes.length() != 48 {
    raise Invalid("expected base 48-byte NTP packet; extensions unsupported")
  }
  let signed = fn(b : Byte) {
    let n = b.to_int()
    if n >= 128 {
      n - 256
    } else {
      n
    }
  }
  {
    leap: bytes[0].to_int() >> 6,
    version: (bytes[0].to_int() >> 3) & 7,
    mode: bytes[0].to_int() & 7,
    stratum: bytes[1].to_int(),
    poll: signed(bytes[2]),
    precision: signed(bytes[3]),
    root_delay: read32(bytes, 4).reinterpret_as_int(),
    root_dispersion: read32(bytes, 8),
    reference_id: read32(bytes, 12),
    reference: stamp(bytes, 16),
    origin: stamp(bytes, 24),
    receive: stamp(bytes, 32),
    transmit: stamp(bytes, 40),
  }
}

///|
pub fn Packet::encode(self : Packet) -> Bytes raise NtpError {
  if self.leap < 0 ||
    self.leap > 3 ||
    self.version < 1 ||
    self.version > 4 ||
    self.mode < 0 ||
    self.mode > 7 ||
    self.stratum < 0 ||
    self.stratum > 255 ||
    self.poll < -128 ||
    self.poll > 127 ||
    self.precision < -128 ||
    self.precision > 127 {
    raise Invalid("invalid packet field")
  }
  let out : Array[Byte] = [
    ((self.leap << 6) | (self.version << 3) | self.mode).to_byte(),
    self.stratum.to_byte(),
    self.poll.to_byte(),
    self.precision.to_byte(),
  ]
  write32(out, self.root_delay.reinterpret_as_uint())
  write32(out, self.root_dispersion)
  write32(out, self.reference_id)
  for t in [self.reference, self.origin, self.receive, self.transmit] {
    write32(out, t.seconds)
    write32(out, t.fraction)
  }
  Bytes::from_array(out)
}

///|
pub fn request(transmit : Timestamp) -> Packet {
  {
    leap: 0,
    version: 4,
    mode: 3,
    stratum: 0,
    poll: 6,
    precision: -20,
    root_delay: 0,
    root_dispersion: 0U,
    reference_id: 0U,
    reference: Timestamp::zero(),
    origin: Timestamp::zero(),
    receive: Timestamp::zero(),
    transmit,
  }
}