///|
pub let min_cost = 4

///|
pub let max_cost = 31

///|
pub let salt_length = 16

///|
pub let hash_length = 60

///|
pub suberror BcryptError {
  InvalidCost(Int)
  InvalidSaltLength(Int)
  InvalidHashFormat
  UnsupportedVersion(String)
  InvalidBase64
} derive(Eq, Debug)

///|
pub impl Show for BcryptError with fn output(self, logger) {
  match self {
    InvalidCost(n) => {
      logger.write_string("InvalidCost(")
      logger.write_object(n)
      logger.write_string(")")
    }
    InvalidSaltLength(n) => {
      logger.write_string("InvalidSaltLength(")
      logger.write_object(n)
      logger.write_string(")")
    }
    InvalidHashFormat => logger.write_string("InvalidHashFormat")
    UnsupportedVersion(v) => {
      logger.write_string("UnsupportedVersion(")
      logger.write_string(v)
      logger.write_string(")")
    }
    InvalidBase64 => logger.write_string("InvalidBase64")
  }
}

///|
priv struct ParsedHash {
  minor : Char
  cost : Int
  salt_text : String
  salt : Bytes
}

///|
pub fn hash_password(
  password : Bytes,
  salt : Bytes,
  cost : Int,
) -> Result[String, BcryptError] {
  if cost < min_cost || cost > max_cost {
    Err(InvalidCost(cost))
  } else if salt.length() != salt_length {
    Err(InvalidSaltLength(salt.length()))
  } else {
    let salt_text = bcrypt_base64_encode(salt.view(), salt_length)
    let digest = bcrypt_hash_raw(password.view(), salt.view(), cost)
    let hash_text = bcrypt_base64_encode(digest.view(), 23)
    Ok("$2b$" + two_digit(cost) + "$" + salt_text + hash_text)
  }
}

///|
pub fn verify(password : Bytes, hash : String) -> Result[Bool, BcryptError] {
  match parse_hash(hash) {
    Err(err) => Err(err)
    Ok(parsed) => {
      let digest = bcrypt_hash_raw(
        password.view(),
        parsed.salt.view(),
        parsed.cost,
      )
      let recomputed = "$2" +
        parsed.minor.to_string() +
        "$" +
        two_digit(parsed.cost) +
        "$" +
        parsed.salt_text +
        bcrypt_base64_encode(digest.view(), 23)
      Ok(timing_safe_equal(hash, recomputed))
    }
  }
}

///|
pub fn cost(hash : String) -> Result[Int, BcryptError] {
  match parse_hash(hash) {
    Err(err) => Err(err)
    Ok(parsed) => Ok(parsed.cost)
  }
}

///|
fn parse_hash(hash : String) -> Result[ParsedHash, BcryptError] {
  if hash.length() != hash_length {
    Err(InvalidHashFormat)
  } else if hash[0].to_int() != '$'.to_int() ||
    hash[1].to_int() != '2'.to_int() ||
    hash[3].to_int() != '$'.to_int() ||
    hash[6].to_int() != '$'.to_int() {
    Err(InvalidHashFormat)
  } else {
    let minor = hash.get_char(2).unwrap()
    if minor != 'b' && minor != 'y' {
      Err(UnsupportedVersion("2" + minor.to_string()))
    } else if !is_ascii_digit_code(hash[4].to_int()) ||
      !is_ascii_digit_code(hash[5].to_int()) {
      Err(InvalidHashFormat)
    } else {
      let parsed_cost = (hash[4].to_int() - '0'.to_int()) * 10 +
        (hash[5].to_int() - '0'.to_int())
      if parsed_cost < min_cost || parsed_cost > max_cost {
        Err(InvalidCost(parsed_cost))
      } else {
        let salt_text = hash.unsafe_substring(start=7, end=29)
        match bcrypt_base64_decode(salt_text, salt_length) {
          Err(_) => Err(InvalidBase64)
          Ok(salt) =>
            if !bcrypt_base64_all_valid(hash.unsafe_substring(start=29, end=60)) {
              Err(InvalidBase64)
            } else {
              Ok({ minor, cost: parsed_cost, salt_text, salt })
            }
        }
      }
    }
  }
}

///|
fn two_digit(n : Int) -> String {
  let tens = n / 10 + '0'.to_int()
  let ones = n % 10 + '0'.to_int()
  String::from_array([tens.unsafe_to_char(), ones.unsafe_to_char()])
}

///|
fn is_ascii_digit_code(code : Int) -> Bool {
  code >= '0'.to_int() && code <= '9'.to_int()
}

///|
fn timing_safe_equal(a : String, b : String) -> Bool {
  let mut diff = a.length() ^ b.length()
  let n = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0..