///|
/// Hash algorithms currently understood by MoonLoom.
pub(all) enum HashAlgorithm {
  Identity
  Sha2_256
  Sha2_512
} derive(Eq, @debug.Debug)

///|
pub fn HashAlgorithm::name(self : HashAlgorithm) -> String {
  match self {
    Identity => "identity"
    Sha2_256 => "sha2-256"
    Sha2_512 => "sha2-512"
  }
}

///|
pub fn HashAlgorithm::code(self : HashAlgorithm) -> UInt64 {
  match self {
    Identity => 0UL
    Sha2_256 => 18UL
    Sha2_512 => 19UL
  }
}

///|
pub fn HashAlgorithm::from_code(code : UInt64) -> HashAlgorithm? {
  match code {
    0UL => Some(Identity)
    18UL => Some(Sha2_256)
    19UL => Some(Sha2_512)
    _ => None
  }
}

///|
pub fn HashAlgorithm::from_name(name : StringView) -> HashAlgorithm? {
  match name.to_owned() {
    "identity" => Some(Identity)
    "sha2-256" => Some(Sha2_256)
    "sha2-512" => Some(Sha2_512)
    _ => None
  }
}

///|
pub fn supported_hash_algorithms() -> Array[HashAlgorithm] {
  [Identity, Sha2_256, Sha2_512]
}

///|
pub fn HashAlgorithm::expected_digest_size(self : HashAlgorithm) -> Int? {
  match self {
    Identity => None
    Sha2_256 => Some(32)
    Sha2_512 => Some(64)
  }
}

///|
/// Provider seam for cryptographic hash implementations.
pub struct HashProvider {
  digest : (HashAlgorithm, BytesView) -> Result[Bytes, MoonLoomError]
}

///|
pub fn HashProvider::new(
  digest : (HashAlgorithm, BytesView) -> Result[Bytes, MoonLoomError],
) -> HashProvider {
  { digest, }
}

///|
pub fn HashProvider::hash(
  self : HashProvider,
  algorithm : HashAlgorithm,
  data : BytesView,
) -> Result[Bytes, MoonLoomError] {
  (self.digest)(algorithm, data)
}

///|
/// Built-in provider backed by the reviewed MoonCrypt SHA-2 implementation.
pub fn sha2_provider() -> HashProvider {
  HashProvider::new(fn(algorithm, data) {
    match algorithm {
      Sha2_256 => Ok(@sha2.hash(data, kind=@sha2.Kind::Sha256))
      Sha2_512 => Ok(@sha2.hash(data, kind=@sha2.Kind::Sha512))
      Identity =>
        Err(UnsupportedHashAlgorithm("identity is not a cryptographic hash"))
    }
  })
}

///|
pub(all) struct Multihash {
  code : UInt64
  digest : Bytes
} derive(Eq, @debug.Debug)

///|
pub fn Multihash::create(
  algorithm : HashAlgorithm,
  digest : BytesView,
  limits : Limits,
) -> Result[Multihash, MoonLoomError] {
  if digest.length() > limits.max_digest_bytes() {
    return Err(
      InputTooLong(
        "multihash digest",
        limits.max_digest_bytes(),
        digest.length(),
      ),
    )
  }
  match algorithm.expected_digest_size() {
    Some(expected) =>
      if digest.length() != expected {
        return Err(
          DigestLengthMismatch(algorithm.name(), expected, digest.length()),
        )
      }
    None => ()
  }
  Ok({ code: algorithm.code(), digest: digest.to_owned(), })
}

///|
pub fn Multihash::identity(
  data : BytesView,
  limits : Limits,
) -> Result[Multihash, MoonLoomError] {
  Multihash::create(Identity, data, limits)
}

///|
pub fn Multihash::is_identity(self : Multihash) -> Bool {
  self.code == 0UL
}

///|
pub fn Multihash::algorithm(self : Multihash) -> HashAlgorithm? {
  HashAlgorithm::from_code(self.code)
}

///|
pub fn Multihash::encode(self : Multihash) -> Bytes {
  let writer = WireWriter::new()
  writer.write_varint(self.code)
  writer.write_length_prefixed(self.digest)
  writer.to_bytes()
}

///|
pub fn Multihash::decode(
  input : BytesView,
  limits : Limits,
) -> Result[(Multihash, Int), MoonLoomError] {
  let reader = match WireReader::new(input, limits) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let code = match reader.read_varint("multihash code") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let digest_length = match reader.read_varint("multihash digest length") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if digest_length > limits.max_digest_bytes().to_uint64() {
    return Err(
      InputTooLong(
        "multihash digest",
        limits.max_digest_bytes(),
        digest_length.to_int(),
      ),
    )
  }
  let digest_size = digest_length.to_int()
  let digest = match reader.read_fixed("multihash digest", digest_size) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(({ code, digest: digest.to_owned(), }, reader.position()))
}

///|
pub fn Multihash::to_text(
  self : Multihash,
  base : Base,
) -> Result[String, MoonLoomError] {
  multibase_encode(base, self.encode())
}

///|
pub fn Multihash::to_text_canonical(
  self : Multihash,
) -> Result[String, MoonLoomError] {
  self.to_text(Base58Btc)
}

///|
pub fn Multihash::from_text(
  text : StringView,
  limits : Limits,
) -> Result[Multihash, MoonLoomError] {
  match multibase_decode(text, limits) {
    Ok(value) =>
      match Multihash::decode(value.data, limits) {
        Ok((decoded, consumed)) =>
          if consumed == value.data.length() {
            Ok(decoded)
          } else {
            Err(InvalidMultihash("trailing bytes", consumed))
          }
        Err(err) => Err(err)
      }
    Err(err) => Err(err)
  }
}

///|
pub fn Multihash::verify(
  self : Multihash,
  content : BytesView,
  provider : HashProvider,
  limits : Limits,
) -> Result[Unit, MoonLoomError] {
  match limits.check_input("multihash content", content.length()) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match self.algorithm() {
    Some(Identity) =>
      if self.digest[:] == content {
        Ok(())
      } else {
        Err(HashMismatch)
      }
    Some(algorithm) =>
      match provider.hash(algorithm, content) {
        Ok(digest) =>
          if digest == self.digest {
            Ok(())
          } else {
            Err(HashMismatch)
          }
        Err(err) => Err(err)
      }
    None => Err(UnknownCodec(self.code))
  }
}

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

///|
pub extend HashAlgorithm with @debug.Debug::{to_repr}

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

///|
pub extend Multihash with @debug.Debug::{to_repr}