///|
/// Multibase encodings supported by MoonLoom.
pub(all) enum Base {
  Base16
  Base16Upper
  Base32
  Base32Upper
  Base32Pad
  Base32PadUpper
  Base58Btc
  Base64
  Base64Pad
  Base64Url
  Base64UrlPad
} derive(Eq, @debug.Debug)

///|
pub(all) struct DecodedMultibase {
  base : Base
  data : Bytes
} derive(Eq, @debug.Debug)

///|
pub fn Base::prefix(self : Base) -> Char {
  match self {
    Base16 => 'f'
    Base16Upper => 'F'
    Base32 => 'b'
    Base32Upper => 'B'
    Base32Pad => 'c'
    Base32PadUpper => 'C'
    Base58Btc => 'z'
    Base64 => 'm'
    Base64Pad => 'M'
    Base64Url => 'u'
    Base64UrlPad => 'U'
  }
}

///|
pub fn Base::from_prefix(prefix : Char) -> Base? {
  match prefix {
    'f' => Some(Base16)
    'F' => Some(Base16Upper)
    'b' => Some(Base32)
    'B' => Some(Base32Upper)
    'c' => Some(Base32Pad)
    'C' => Some(Base32PadUpper)
    'z' => Some(Base58Btc)
    'm' => Some(Base64)
    'M' => Some(Base64Pad)
    'u' => Some(Base64Url)
    'U' => Some(Base64UrlPad)
    _ => None
  }
}

///|
pub fn Base::name(self : Base) -> String {
  match self {
    Base16 => "base16"
    Base16Upper => "base16upper"
    Base32 => "base32"
    Base32Upper => "base32upper"
    Base32Pad => "base32pad"
    Base32PadUpper => "base32padupper"
    Base58Btc => "base58btc"
    Base64 => "base64"
    Base64Pad => "base64pad"
    Base64Url => "base64url"
    Base64UrlPad => "base64urlpad"
  }
}

///|
pub fn Base::from_name(name : StringView) -> Base? {
  match name.to_owned() {
    "base16" => Some(Base16)
    "base16upper" => Some(Base16Upper)
    "base32" => Some(Base32)
    "base32upper" => Some(Base32Upper)
    "base32pad" => Some(Base32Pad)
    "base32padupper" => Some(Base32PadUpper)
    "base58btc" => Some(Base58Btc)
    "base64" => Some(Base64)
    "base64pad" => Some(Base64Pad)
    "base64url" => Some(Base64Url)
    "base64urlpad" => Some(Base64UrlPad)
    _ => None
  }
}

///|
pub fn supported_bases() -> Array[Base] {
  [
    Base16,
    Base16Upper,
    Base32,
    Base32Upper,
    Base32Pad,
    Base32PadUpper,
    Base58Btc,
    Base64,
    Base64Pad,
    Base64Url,
    Base64UrlPad,
  ]
}

///|
fn case_is_canonical(text : StringView, upper_required : Bool) -> Bool {
  for code_unit in text.code_units() {
    let code = code_unit.to_int()
    if code >= 65 && code <= 90 && !upper_required {
      return false
    }
    if code >= 97 && code <= 122 && upper_required {
      return false
    }
  }
  true
}

///|
fn invalid_base32_offset(text : StringView, upper : Bool) -> Int? {
  for index, code_unit in text.code_units() {
    let code = code_unit.to_int()
    let letter = if upper {
      code >= 65 && code <= 90
    } else {
      code >= 97 && code <= 122
    }
    let digit = code >= 50 && code <= 55
    if !letter && !digit && code != 61 {
      return Some(index)
    }
  }
  None
}

///|
fn has_ascii(text : StringView, expected : Int) -> Bool {
  for code_unit in text.code_units() {
    if code_unit.to_int() == expected {
      return true
    }
  }
  false
}

///|
fn trailing_padding(text : StringView) -> Int {
  let mut count = 0
  for index in text.length()>..0 {
    if text.unsafe_get(index).to_int() == 61 {
      count += 1
    } else {
      break
    }
  }
  count
}

///|
fn encode_hex(data : BytesView, upper : Bool) -> String {
  @base16.encode(
    data,
    kind=if upper { @base16.Kind::Upper } else { @base16.Kind::Lower },
  )
}

///|
fn decode_hex(text : StringView, upper : Bool) -> Result[Bytes, MoonLoomError] {
  let prefix = if upper { 'F' } else { 'f' }
  if text.length() % 2 != 0 {
    return Err(InvalidPadding("base16", text.length()))
  }
  if !case_is_canonical(text, upper) {
    return Err(InvalidBaseCharacter(prefix, 0))
  }
  let bytes = @base16.decode(text) catch {
    _ => return Err(InvalidBaseCharacter(prefix, 0))
  }
  Ok(bytes)
}

///|
fn lower_ascii(value : String) -> String {
  let out = StringBuilder(size_hint=value.length())
  for code_unit in value.code_units() {
    let code = code_unit.to_int()
    if code >= 65 && code <= 90 {
      out.write_char((code + 32).unsafe_to_char())
    } else {
      out.write_char(code.unsafe_to_char())
    }
  }
  out.to_string()
}

///|
fn encode_base32(data : BytesView, upper : Bool, padding : Bool) -> String {
  let encoded = @base32.encode(data, kind=@base32.Kind::Std, padding~)
  if upper {
    encoded
  } else {
    lower_ascii(encoded)
  }
}

///|
fn decode_base32(
  text : StringView,
  prefix : Char,
  upper : Bool,
  padding : Bool,
) -> Result[Bytes, MoonLoomError] {
  match invalid_base32_offset(text, upper) {
    Some(offset) => return Err(InvalidBaseCharacter(prefix, offset))
    None => ()
  }
  let mut data_length = text.length()
  if padding {
    if text.length() % 8 != 0 {
      return Err(InvalidPadding("base32", text.length()))
    }
    while data_length > 0 && text.unsafe_get(data_length - 1).to_int() == 61 {
      data_length -= 1
    }
    let expected = match data_length % 8 {
      0 => 0
      2 => 6
      4 => 4
      5 => 3
      7 => 1
      _ => return Err(InvalidPadding("base32", data_length))
    }
    if text.length() - data_length != expected {
      return Err(InvalidPadding("base32", data_length))
    }
  } else if has_ascii(text, 61) {
    return Err(InvalidPadding("base32", 0))
  }
  let bytes = @base32.decode(text, kind=@base32.Kind::Std) catch {
    _ => return Err(InvalidBaseCharacter(prefix, 0))
  }
  Ok(bytes)
}

///|
fn encode_base58(data : BytesView) -> String {
  @base58.encode(data)
}

///|
fn decode_base58(text : StringView) -> Result[Bytes, MoonLoomError] {
  let bytes = @base58.decode(text) catch {
    _ => return Err(InvalidBaseCharacter('z', 0))
  }
  Ok(bytes)
}

///|
fn encode_base64(data : BytesView, url : Bool, padding : Bool) -> String {
  @base64.encode(
    data,
    kind=if url { @base64.Kind::Url } else { @base64.Kind::Std },
    padding~,
  )
}

///|
fn decode_base64(
  text : StringView,
  prefix : Char,
  url : Bool,
  padding : Bool,
) -> Result[Bytes, MoonLoomError] {
  if padding {
    if text.length() % 4 != 0 {
      return Err(InvalidPadding("base64", text.length()))
    }
    let data_length = text.length() - trailing_padding(text)
    let expected = match data_length % 4 {
      0 => 0
      2 => 2
      3 => 1
      _ => return Err(InvalidPadding("base64", data_length))
    }
    if text.length() - data_length != expected {
      return Err(InvalidPadding("base64", data_length))
    }
  } else if has_ascii(text, 61) {
    return Err(InvalidPadding("base64", 0))
  } else if text.length() % 4 == 1 {
    return Err(InvalidPadding("base64", text.length()))
  }
  let bytes = @base64.decode(
    text,
    kind=if url { @base64.Kind::Url } else { @base64.Kind::Std },
  ) catch {
    _ => return Err(InvalidBaseCharacter(prefix, 0))
  }
  Ok(bytes)
}

///|
pub fn multibase_encode(
  base : Base,
  data : BytesView,
) -> Result[String, MoonLoomError] {
  let encoded = match base {
    Base16 => encode_hex(data, false)
    Base16Upper => encode_hex(data, true)
    Base32 => encode_base32(data, false, false)
    Base32Upper => encode_base32(data, true, false)
    Base32Pad => encode_base32(data, false, true)
    Base32PadUpper => encode_base32(data, true, true)
    Base58Btc => encode_base58(data)
    Base64 => encode_base64(data, false, false)
    Base64Pad => encode_base64(data, false, true)
    Base64Url => encode_base64(data, true, false)
    Base64UrlPad => encode_base64(data, true, true)
  }
  Ok(base.prefix().to_string() + encoded)
}

///|
pub fn multibase_decode(
  text : StringView,
  limits : Limits,
) -> Result[DecodedMultibase, MoonLoomError] {
  match limits.check_input("multibase", text.length()) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if text.length() == 0 {
    return Err(UnknownBase('\u{0}'))
  }
  let prefix = match text.code_units()[0].to_char() {
    Some(value) => value
    None => return Err(UnknownBase('\u{0}'))
  }
  let base = match Base::from_prefix(prefix) {
    Some(value) => value
    None => return Err(UnknownBase(prefix))
  }
  let payload = text.exact_view(start=1)
  let data = match base {
    Base16 => decode_hex(payload, false)
    Base16Upper => decode_hex(payload, true)
    Base32 => decode_base32(payload, 'b', false, false)
    Base32Upper => decode_base32(payload, 'B', true, false)
    Base32Pad => decode_base32(payload, 'c', false, true)
    Base32PadUpper => decode_base32(payload, 'C', true, true)
    Base58Btc => decode_base58(payload)
    Base64 => decode_base64(payload, 'm', false, false)
    Base64Pad => decode_base64(payload, 'M', false, true)
    Base64Url => decode_base64(payload, 'u', true, false)
    Base64UrlPad => decode_base64(payload, 'U', true, true)
  }
  match data {
    Ok(bytes) => Ok({ base, data: bytes, })
    Err(err) => Err(err)
  }
}

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

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

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

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