// Labelled digest values (ISO 28500:2017 clauses 5.8-5.9).
//
// WARC-Payload-Digest and WARC-Block-Digest carry an algorithm label
// (a token, conventionally `sha1`) followed by a colon and an encoded
// digest. This module parses that `algorithm:value` envelope and
// checks the value against the base-encoding alphabets. It does not
// compute digests: the MoonBit core library ships no hash primitive,
// so digest verification is deferred and documented in the
// limitations.

///|
/// A parsed labelled digest such as `sha1:B2QYELGDQWXWAWJ4OA2GSCBXDAOB7UHR`.
pub struct WarcDigest {
  algorithm : String
  value : String
}

///|
/// Construct a labelled digest from its parts.
pub fn WarcDigest::new(algorithm : String, value : String) -> WarcDigest {
  { algorithm, value }
}

///|
/// The algorithm label (e.g. `sha1`).
pub fn WarcDigest::algorithm(self : WarcDigest) -> String {
  self.algorithm
}

///|
/// The encoded digest value.
pub fn WarcDigest::value(self : WarcDigest) -> String {
  self.value
}

///|
/// The canonical `algorithm:value` rendering.
pub fn WarcDigest::to_string(self : WarcDigest) -> String {
  "\{self.algorithm}:\{self.value}"
}

///|
/// A structured invalid-digest error.
fn bad_digest(record_index : Int64, context : String) -> WarcError {
  WarcError::new(
    WarcErrorStage::Digest,
    WarcErrorKind::InvalidDigest,
    0L,
    record_index,
    context,
  )
}

///|
/// True when the byte may appear in an encoded digest value: ASCII
/// letters and digits plus `+`, `/`, `=`, `-` and `_` cover the
/// base16/base32/base64 alphabets.
fn is_digest_byte(b : Byte) -> Bool {
  if is_digit(b) {
    return true
  }
  if (b >= b'a' && b <= b'z') || (b >= b'A' && b <= b'Z') {
    return true
  }
  b == b'+' || b == b'/' || b == b'=' || b == b'-' || b == b'_'
}

///|
/// Parse a `algorithm:value` digest field value.
///
/// The algorithm must be a legal token, the separator a single colon,
/// and the value non-empty and made only of base-encoding alphabet
/// characters.
pub fn parse_digest(
  text : String,
  record_index : Int64,
) -> Result[WarcDigest, WarcError] {
  let data = @utf8.encode(text)
  let len = data.length()
  // Locate the first colon: it must be present and not the last byte.
  let mut colon = -1
  for i = 0; i < len; i = i + 1 {
    if data[i] == b':' {
      colon = i
      break
    }
  }
  if colon <= 0 {
    return Err(
      bad_digest(record_index, "digest must be written as algorithm:value"),
    )
  }
  if colon == len - 1 {
    return Err(bad_digest(record_index, "digest value must not be empty"))
  }
  if !valid_field_name(data, 0, colon) {
    return Err(
      bad_digest(record_index, "digest algorithm is not a legal token"),
    )
  }
  for i = colon + 1; i < len; i = i + 1 {
    if !is_digest_byte(data[i]) {
      return Err(
        bad_digest(
          record_index, "digest value contains characters outside the base-encoding alphabets",
        ),
      )
    }
  }
  let algorithm = @utf8.decode(data.view(start=0, end=colon)) catch {
    _ =>
      return Err(
        bad_digest(record_index, "digest algorithm is not valid UTF-8"),
      )
  }
  let value = @utf8.decode(data.view(start=colon + 1, end=len)) catch {
    _ => return Err(bad_digest(record_index, "digest value is not valid UTF-8"))
  }
  Ok(WarcDigest::new(algorithm, value))
}