///|
let sheet_protection_spin_count = 100000

///|
let hex_digits_upper : Array[Char] = "0123456789ABCDEF".to_array()

///|
fn gen_sheet_password(plaintext : StringView) -> String {
  let bytes = @encoding/utf16.encode(plaintext, bom=false, endianness=Little)
  let mut password : Int64 = 0
  let mut char_pos = 1
  let mut i = 0
  while i + 1 < bytes.length() {
    let lo = bytes[i].to_int()
    let hi = bytes[i + 1].to_int()
    let value = (hi << 8) | lo
    let shifted = value.to_int64() << char_pos
    char_pos = char_pos + 1
    let rotated = shifted >> 15
    let masked = shifted & 0x7fff
    password = password ^ (masked | rotated)
    i = i + 2
  }
  let len_units = (bytes.length() / 2).to_int64()
  password = password ^ len_units
  let xor_const : Int64 = 0xCE4B
  password = password ^ xor_const
  format_hex_upper(password)
}

///|
fn format_hex_upper(value : Int64) -> String {
  let mask : Int64 = 0xffff
  let mut n = value & mask
  if n == 0 {
    return "0"
  }
  let mut len = 0
  let mut tmp = n
  while tmp > 0 {
    len = len + 1
    tmp = tmp >> 4
  }
  let out : FixedArray[Char] = FixedArray::make(len, '0')
  let mut idx = len - 1
  while true {
    let digit = (n & 0xf).to_int()
    out[idx] = hex_digits_upper[digit]
    n = n >> 4
    if n == 0 {
      break
    }
    idx = idx - 1
  }
  let sb = StringBuilder::new()
  for c in out {
    sb.write_char(c)
  }
  sb.to_string()
}

///|
fn normalize_sheet_hash_algorithm(name : StringView) -> String? {
  match name {
    "MD4" => Some("md4")
    "MD5" => Some("md5")
    "SHA-1" => Some("sha1")
    "SHA-256" => Some("sha256")
    "SHA-384" => Some("sha384")
    "SHA-512" => Some("sha512")
    _ => None
  }
}

///|
fn gen_iso_password_hash(
  password : StringView,
  algorithm_name : StringView,
  salt : StringView,
  spin_count : Int,
) -> (String, String) raise XlsxError {
  let pass_len = count_utf16_units(password)
  if pass_len <= 0 || pass_len > max_password_length {
    raise InvalidPasswordLength(len=pass_len)
  }
  let algorithm = match normalize_sheet_hash_algorithm(algorithm_name) {
    Some(value) => value
    None => raise InvalidSheetProtection(msg="unsupported hash algorithm")
  }
  let salt_value = if salt == "" {
    random_bytes(16)
  } else {
    base64_decode(salt) catch {
      _ => raise InvalidBase64(msg="salt value invalid")
    }
  }
  let password_bytes = @encoding/utf16.encode(
    password,
    bom=false,
    endianness=Little,
  )
  let initial = @crypto.hash_concat(algorithm, [salt_value, password_bytes])
  let mut key = initial
  for i in 0.. SheetProtection raise XlsxError {
  let protection : SheetProtection = {
    algorithm_name: "",
    password: "",
    hash_value: "",
    salt_value: "",
    spin_count: 0,
    sheet: true,
    objects: !options.edit_objects,
    scenarios: !options.edit_scenarios,
    format_cells: !options.format_cells,
    format_columns: !options.format_columns,
    format_rows: !options.format_rows,
    insert_columns: !options.insert_columns,
    insert_rows: !options.insert_rows,
    insert_hyperlinks: !options.insert_hyperlinks,
    delete_columns: !options.delete_columns,
    delete_rows: !options.delete_rows,
    select_locked_cells: !options.select_locked_cells,
    sort: !options.sort,
    auto_filter: !options.auto_filter,
    pivot_tables: !options.pivot_tables,
    select_unlocked_cells: !options.select_unlocked_cells,
  }
  if options.password != "" {
    if options.algorithm_name == "" {
      protection.password = gen_sheet_password(options.password)
    } else {
      let (hash_value, salt_value) = gen_iso_password_hash(
        options.password,
        options.algorithm_name,
        "",
        sheet_protection_spin_count,
      )
      protection.algorithm_name = options.algorithm_name
      protection.hash_value = hash_value
      protection.salt_value = salt_value
      protection.spin_count = sheet_protection_spin_count
    }
  }
  protection
}

///|
fn verify_sheet_protection_password(
  protection : SheetProtection,
  password : StringView,
) -> Unit raise XlsxError {
  if protection.algorithm_name == "" {
    if protection.password == "" {
      raise InvalidSheetProtection(msg="sheet not protected")
    }
    if protection.password != gen_sheet_password(password) {
      raise InvalidSheetProtection(msg="sheet protection password mismatch")
    }
    return
  }
  let (hash_value, _salt) = gen_iso_password_hash(
    password,
    protection.algorithm_name,
    protection.salt_value,
    protection.spin_count,
  )
  if protection.hash_value != hash_value {
    raise InvalidSheetProtection(msg="sheet protection password mismatch")
  }
}