///|
/// Multibase encodings supported by MoonLoom.
pub(all) enum Base {
  Base16
  Base16Upper
  Base32
  Base32Upper
  Base32Pad
  Base32PadUpper
  Base36
  Base36Upper
  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'
    Base36 => 'k'
    Base36Upper => 'K'
    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)
    'k' => Some(Base36)
    'K' => Some(Base36Upper)
    '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"
    Base36 => "base36"
    Base36Upper => "base36upper"
    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)
    "base36" => Some(Base36)
    "base36upper" => Some(Base36Upper)
    "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,
    Base36,
    Base36Upper,
    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) -> Int32? {
  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 : Int32) -> Bool {
  for code_unit in text.code_units() {
    if code_unit.to_int() == expected {
      return true
    }
  }
  false
}

///|
fn base32_value(code : Int32, upper : Bool) -> Int32? {
  if upper {
    if code >= 65 && code <= 90 {
      Some(code - 65)
    } else if code >= 50 && code <= 55 {
      Some(code - 50 + 26)
    } else {
      None
    }
  } else if code >= 97 && code <= 122 {
    Some(code - 97)
  } else if code >= 50 && code <= 55 {
    Some(code - 50 + 26)
  } else {
    None
  }
}

///|
fn validate_base32_tail(
  text : StringView,
  data_length : Int32,
  upper : Bool,
) -> Result[Unit, MoonLoomError] {
  let remainder = data_length % 8
  let allowed = remainder == 0 ||
    remainder == 2 ||
    remainder == 4 ||
    remainder == 5 ||
    remainder == 7
  if !allowed {
    return Err(InvalidPadding("base32", data_length))
  }
  if remainder == 0 {
    return Ok(())
  }
  let value = match
    base32_value(text.unsafe_get(data_length - 1).to_int(), upper) {
    Some(value) => value
    None => return Err(InvalidBaseCharacter('b', data_length - 1))
  }
  let mask = match remainder {
    2 => 3
    4 => 15
    5 => 1
    _ => 7
  }
  if value % (mask + 1) != 0 {
    Err(InvalidPadding("base32", data_length - 1))
  } else {
    Ok(())
  }
}

///|
fn base64_value(code : Int32, url : Bool) -> Int32? {
  if code >= 65 && code <= 90 {
    Some(code - 65)
  } else if code >= 97 && code <= 122 {
    Some(code - 97 + 26)
  } else if code >= 48 && code <= 57 {
    Some(code - 48 + 52)
  } else if url && code == 45 {
    Some(62)
  } else if url && code == 95 {
    Some(63)
  } else if !url && code == 43 {
    Some(62)
  } else if !url && code == 47 {
    Some(63)
  } else {
    None
  }
}

///|
fn validate_base64_tail(
  text : StringView,
  data_length : Int32,
  url : Bool,
) -> Result[Unit, MoonLoomError] {
  let remainder = data_length % 4
  if remainder == 0 {
    return Ok(())
  }
  if remainder == 1 {
    return Err(InvalidPadding("base64", data_length))
  }
  let value = match
    base64_value(text.unsafe_get(data_length - 1).to_int(), url) {
    Some(value) => value
    None => return Err(InvalidBaseCharacter('m', data_length - 1))
  }
  let mask = if remainder == 2 { 15 } else { 3 }
  if value % (mask + 1) != 0 {
    Err(InvalidPadding("base64", data_length - 1))
  } else {
    Ok(())
  }
}

///|
fn trailing_padding(text : StringView) -> Int32 {
  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))
  }
  match validate_base32_tail(text, data_length, upper) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  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 encode_base36(data : BytesView, upper : Bool) -> String {
  @base36.encode(
    data,
    kind=if upper { @base36.Kind::Upper } else { @base36.Kind::Lower },
  )
}

///|
fn decode_base36(
  text : StringView,
  prefix : Char,
  upper : Bool,
) -> Result[Bytes, MoonLoomError] {
  let bytes = @base36.decode(
    text,
    kind=if upper { @base36.Kind::Upper } else { @base36.Kind::Lower },
  ) catch {
    _ => return Err(InvalidBaseCharacter(prefix, 0))
  }
  Ok(bytes)
}

///|
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] {
  let mut data_length = text.length()
  if padding {
    if text.length() % 4 != 0 {
      return Err(InvalidPadding("base64", text.length()))
    }
    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()))
  }
  match validate_base64_tail(text, data_length, url) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  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)
    Base36 => encode_base36(data, false)
    Base36Upper => encode_base36(data, 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)
    Base36 => decode_base36(payload, 'k', false)
    Base36Upper => decode_base36(payload, 'K', 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}