// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// How an instant is written.
///
/// The library holds no clock, so every instant here arrives as nanoseconds
/// since the epoch and every local time is the instant shifted by an offset
/// that was handed in. `Custom` is a strftime spec, `Go` a reference-time
/// layout and `Ldml` a Unicode date pattern, each with everything moondate reads
/// in that style.
pub(all) enum Clock {
  Local
  Utc
  Rfc3339
  Iso
  Unix
  UnixMs
  UnixNs
  Custom(String)
  Go(String)
  Ldml(String)
} derive(Eq, Debug)

///|
pub extend Clock with Debug::{to_repr}

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

///|
/// A clock spec, as a `{time:…}` hole spells it. A spec that is none of the names
/// is strftime, unless `go:` or `ldml:` says otherwise.
pub fn Clock::parse(spec : StringView) -> Clock {
  match spec {
    "" | "local" => Local
    "utc" => Utc
    "rfc3339" => Rfc3339
    "iso" => Iso
    "unix" => Unix
    "unix_ms" => UnixMs
    "unix_ns" => UnixNs
    [.. "go:", .. layout] => Go(layout.to_owned())
    [.. "ldml:", .. pattern] => Ldml(pattern.to_owned())
    _ => Custom(spec.to_owned())
  }
}

///|
/// Writes `time` in this clock's form; `offset` is seconds east of UTC.
pub fn Clock::write(
  self : Clock,
  out : StringBuilder,
  time : Int64,
  offset : Int32,
) -> Unit {
  match self {
    Local =>
      out.write_string(
        moment(time, offset).isoformat(sep=' ', fraction=3, offset=false),
      )
    Utc => out.write_string(instant(time).isoformat(fraction=0))
    Rfc3339 => out.write_string(moment(time, offset).isoformat(fraction=6))
    Iso => write_iso(out, time)
    Unix => out.write_string(floor_div(time, 1_000_000_000L).to_string())
    UnixMs => out.write_string(floor_div(time, 1_000_000L).to_string())
    UnixNs => out.write_string(time.to_string())
    Custom(spec) => write_strftime(out, time, offset, spec)
    Go(layout) => out.write_string(moment(time, offset).layout(layout))
    Ldml(pattern) => out.write_string(moment(time, offset).pattern(pattern))
  }
}

///|
/// The instant at `offset` seconds east of UTC. moondate's offsets are whole minutes, as every
/// zone in use today is.
fn moment(time : Int64, offset : Int32) -> @moondate.Moment {
  let seconds = floor_div(time, 1_000_000_000L)
  @moondate.Moment::of_epoch(
    seconds,
    nano=(time - seconds * 1_000_000_000L).to_int(),
    zone=Offset(offset / 60),
  )
}

///|
/// The instant in UTC, which writes as `Z`.
fn instant(time : Int64) -> @moondate.Moment {
  let seconds = floor_div(time, 1_000_000_000L)
  @moondate.Moment::of_epoch(
    seconds,
    nano=(time - seconds * 1_000_000_000L).to_int(),
  )
}

///|
/// `2023-11-14T22:13:20.000Z`: milliseconds, which is what JSON schemas want.
fn write_iso(out : StringBuilder, time : Int64) -> Unit {
  out.write_string(instant(time).isoformat(fraction=3))
}

///|
/// `2023-11-14T22:13:20.000000000Z`: nanoseconds, as the OpenTelemetry model
/// writes a timestamp.
fn write_iso_nanos(out : StringBuilder, time : Int64) -> Unit {
  out.write_string(instant(time).isoformat(fraction=9))
}

///|
/// A strftime spec, at `offset` seconds east of UTC.
fn write_strftime(
  out : StringBuilder,
  time : Int64,
  offset : Int32,
  spec : String,
) -> Unit {
  out.write_string(moment(time, offset).strftime(spec))
}

///|
/// Writes `n` with at least `width` digits, zero-padded.
fn write_digits(out : StringBuilder, n : Int32, width : Int32) -> Unit {
  let text = n.to_string()
  let mut pad = width - text.length()
  while pad > 0 {
    out.write_char('0')
    pad = pad - 1
  }
  out.write_string(text)
}

///|
/// Division that rounds towards negative infinity, so instants before 1970
/// land on the day they belong to.
fn floor_div(a : Int64, b : Int64) -> Int64 {
  let q = a / b
  if a % b != 0L && (a < 0L) != (b < 0L) {
    q - 1L
  } else {
    q
  }
}