///|
/// SRI 1.0 algorithms supported by this library.
#derive(Eq, Hash)
pub(all) enum Algorithm {
  Sha256
  Sha384
  Sha512
}

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

///|
pub extend Algorithm with Hash::{hash, hash_combine}

///|
/// A token with a recognized SRI hash algorithm.
pub(all) struct Metadata {
  algorithm : Algorithm
  digest : String
}

///| NoSupportedMetadata is deliberately distinct from a digest mismatch.

///|
/// An offline checker should not silently accept an empty or unknown-only list.
pub(all) enum Verdict {
  Match
  Mismatch
  NoSupportedMetadata
}

///|
fn Algorithm::label(self : Algorithm) -> String {
  match self {
    Sha256 => "sha256"
    Sha384 => "sha384"
    Sha512 => "sha512"
  }
}

///|
fn Algorithm::strength(self : Algorithm) -> Int32 {
  match self {
    Sha256 => 1
    Sha384 => 2
    Sha512 => 3
  }
}

///|
/// Generate one SRI metadata token for exact resource bytes.
pub fn generate(data : BytesView, algorithm : Algorithm) -> String {
  algorithm.label() + "-" + encoded_digest(data, algorithm)
}

///|
fn encoded_digest(data : BytesView, algorithm : Algorithm) -> String {
  let digest = match algorithm {
    Sha256 => @crypto.sha256(data)
    Sha384 => @crypto.sha384(data)
    Sha512 => @crypto.sha512(data)
  }
  let digest = Bytes::from_array(digest[:])
  @base64.encode(digest[:])
}

///|
fn Algorithm::kind(self : Algorithm) -> @sha2.Kind {
  match self {
    Sha256 => @sha2.Sha256
    Sha384 => @sha2.Sha384
    Sha512 => @sha2.Sha512
  }
}

///|
fn is_metadata_space(ch : Char) -> Bool {
  ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\u{000c}'
}

///|
fn parse_token(token : StringView) -> Metadata? {
  // SRI reads the expression before '?' and the digest in its second field.
  let expression = match token.split_once("?") {
    Some((expression, _)) => expression
    None => token
  }
  let (label, tail) = match expression.split_once("-") {
    Some(parts) => parts
    None => (expression, ""[:])
  }
  let algorithm = if label.compare_ignore_ascii_case("sha256") == 0 {
    Sha256
  } else if label.compare_ignore_ascii_case("sha384") == 0 {
    Sha384
  } else if label.compare_ignore_ascii_case("sha512") == 0 {
    Sha512
  } else {
    return None
  }
  let digest = match tail.split_once("-") {
    Some((digest, _)) => digest.to_owned()
    None => tail.to_owned()
  }
  Some({ algorithm, digest, })
}

///|
/// Parse SRI tokens using the algorithm in SRI 1.0, ignoring unknown algorithms.
pub fn parse_metadata(input : String) -> Array[Metadata] {
  let result : Array[Metadata] = []
  let mut start = 0
  let mut offset = 0
  for ch in input {
    let end = offset
    offset += ch.utf16_len()
    if is_metadata_space(ch) {
      if start < end &&
        parse_token(input.exact_view(start~, end~)) is Some(item) {
        result.push(item)
      }
      start = offset
    }
  }
  if start < offset && parse_token(input.exact_view(start~)) is Some(item) {
    result.push(item)
  }
  result
}

///| Compare resource bytes with only the strongest supported hash family.

///|
/// Multiple digests of that strength are alternatives, as in SRI 1.0.
pub fn verify_bytes(data : BytesView, input : String) -> Verdict {
  let items = parse_metadata(input)
  guard strongest_algorithm(items) is Some(strongest) else {
    return NoSupportedMetadata
  }
  verify_strongest(data, items, strongest)
}

///|
/// Normalize the effective digest set to its strongest recognized algorithm.
/// Digest case is preserved; duplicate digests are removed and the rest sorted.
pub fn canonical_metadata(input : String) -> String? {
  let items = parse_metadata(input)
  if items.length() == 1 {
    let item = items[0]
    return Some(item.algorithm.label() + "-" + item.digest)
  }
  guard strongest_algorithm(items) is Some(strongest) else { return None }
  let digests : Set[String] = Set([])
  for item in items {
    if item.algorithm == strongest {
      digests.add(item.digest)
    }
  }
  let sorted = digests.iter().to_array()
  sorted.sort_by((left, right) => left.lexical_compare(right))
  let prefix = strongest.label() + "-"
  Some(sorted.map(digest => prefix + digest).join(" "))
}

///|
/// Verify with a minimum algorithm strength. Err contains the strongest
/// advertised algorithm when it falls below the minimum.
pub fn verify_bytes_at_least(
  data : BytesView,
  input : String,
  minimum : Algorithm,
) -> Result[Verdict, Algorithm] {
  verify_using(input, Some(minimum), (items, algorithm) => {
    verify_strongest(data, items, algorithm)
  })
}

///|
/// Verify against a token computed for the selected algorithm and resource,
/// such as the output of `generate`. The provider runs only when hashing is
/// required, so callers can reuse a digest across different declarations.
pub fn verify_with_token(
  input : String,
  token_for : (Algorithm) -> String raise?,
  minimum~ : Algorithm? = None,
) -> Result[Verdict, Algorithm] raise? {
  verify_using(input, minimum, (items, algorithm) => {
    let token = token_for(algorithm)
    guard token.strip_prefix(algorithm.label() + "-") is Some(digest) else {
      return Mismatch
    }
    verify_digest(items, algorithm, digest)
  })
}

///|
fn verify_using(
  input : String,
  minimum : Algorithm?,
  verify : (Array[Metadata], Algorithm) -> Verdict raise?,
) -> Result[Verdict, Algorithm] raise? {
  let items = parse_metadata(input)
  guard strongest_algorithm(items) is Some(strongest) else {
    return Ok(NoSupportedMetadata)
  }
  if minimum is Some(minimum) && strongest.strength() < minimum.strength() {
    return Err(strongest)
  }
  Ok(verify(items, strongest))
}

///|
fn strongest_algorithm(items : Array[Metadata]) -> Algorithm? {
  guard items.get(0) is Some(first) else { return None }
  let mut strongest = first.algorithm
  for item in items {
    if item.algorithm.strength() > strongest.strength() {
      strongest = item.algorithm
    }
  }
  Some(strongest)
}

///|
fn verify_strongest(
  data : BytesView,
  items : Array[Metadata],
  strongest : Algorithm,
) -> Verdict {
  // The expected digest depends on the algorithm, not the number of candidates.
  let expected = encoded_digest(data, strongest)
  verify_digest(items, strongest, expected[:])
}

///|
fn verify_digest(
  items : Array[Metadata],
  strongest : Algorithm,
  expected : StringView,
) -> Verdict {
  for item in items {
    if item.algorithm == strongest && expected == item.digest[:] {
      return Match
    }
  }
  Mismatch
}