/// TOTP: time-based one-time password, RFC 6238.

///|
/// Compute a TOTP code from a Unix timestamp in seconds.
///
/// - `unix_time`: number of seconds since 1970-01-01.
/// - `digits`: output digits (RFC 6238 uses 6 or 8).
/// - `period`: time step in seconds (default 30).
pub fn totp(
  algorithm : HashAlgorithm,
  key : Bytes,
  unix_time : UInt64,
  digits? : Int = 6,
  period? : Int = 30,
) -> String raise OtpError {
  if period <= 0 {
    raise InvalidPeriod(period)
  }
  let counter = unix_time / period.to_uint64()
  hotp(algorithm, key, counter, digits~)
}

///|
/// Compute the TOTP code for the current system time.
pub fn totp_now(
  algorithm : HashAlgorithm,
  key : Bytes,
  digits? : Int = 6,
  period? : Int = 30,
) -> String raise OtpError {
  // @env.now() returns milliseconds.
  let unix_time = @env.now() / 1000UL
  totp(algorithm, key, unix_time, digits~, period~)
}

///|
/// Verify a submitted TOTP code, accepting `window` time steps of clock
/// drift in each direction (default 1 step).
pub fn totp_verify(
  algorithm : HashAlgorithm,
  key : Bytes,
  code : String,
  window? : Int = 1,
  digits? : Int = 6,
  period? : Int = 30,
) -> Bool raise OtpError {
  if window < 0 {
    raise InvalidUri("window must be non-negative")
  }
  // @env.now() returns milliseconds.
  let counter = @env.now() / 1000UL / period.to_uint64()
  let mut offset = 0
  while offset <= window {
    if hotp(algorithm, key, counter + offset.to_uint64(), digits~) == code {
      return true
    }
    if offset != 0 &&
      hotp(algorithm, key, counter - offset.to_uint64(), digits~) == code {
      return true
    }
    offset += 1
  }
  false
}