///|
pub fn FilenamePolicy::with_replacement(
  self : FilenamePolicy,
  replacement : String,
) -> FilenamePolicy {
  {
    replacement,
    max_length: self.max_length,
    default_name: self.default_name,
    allow_hidden: self.allow_hidden,
  }
}

///|
pub fn FilenamePolicy::with_max_length(
  self : FilenamePolicy,
  max_length : Int,
) -> FilenamePolicy {
  {
    replacement: self.replacement,
    max_length,
    default_name: self.default_name,
    allow_hidden: self.allow_hidden,
  }
}

///|
pub fn FilenamePolicy::with_default_name(
  self : FilenamePolicy,
  default_name : String,
) -> FilenamePolicy {
  {
    replacement: self.replacement,
    max_length: self.max_length,
    default_name,
    allow_hidden: self.allow_hidden,
  }
}

///|
pub fn FilenamePolicy::with_hidden_files(
  self : FilenamePolicy,
  allow_hidden : Bool,
) -> FilenamePolicy {
  {
    replacement: self.replacement,
    max_length: self.max_length,
    default_name: self.default_name,
    allow_hidden,
  }
}

///|
pub fn FilenamePolicy::validate(self : FilenamePolicy) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  if self.replacement.length() > 8 {
    push_diag(
      diagnostics,
      UnsafeFilename,
      Warning,
      "Replacement should be short to avoid surprising filenames",
      None,
    )
  }
  if self.default_name.trim(chars=" \t\r\n.").to_owned().is_empty() {
    push_diag(
      diagnostics,
      EmptyFilename,
      Error,
      "Default filename must not be empty after trimming",
      None,
    )
  }
  if self.max_length > 0 && self.max_length < 8 {
    push_diag(
      diagnostics,
      LengthReduced,
      Warning,
      "Very small filename length limits may remove useful extensions",
      None,
    )
  }
  diagnostics
}

///|
pub fn sanitize_filename_strict(filename : String) -> FilenameReport {
  sanitize_filename_with_policy(filename, FilenamePolicy::strict_download())
}

///|
pub fn sanitize_upload_filename(filename : String) -> FilenameReport {
  sanitize_filename_with_policy(filename, FilenamePolicy::multipart_upload())
}

///|
pub fn sanitize_object_key(filename : String) -> FilenameReport {
  sanitize_filename_with_policy(filename, FilenamePolicy::object_storage_key())
}

///|
pub fn sanitize_filename_preserve_extension(
  filename : String,
  policy : FilenamePolicy,
) -> FilenameReport {
  let base_policy : FilenamePolicy = {
    replacement: policy.replacement,
    max_length: 0,
    default_name: policy.default_name,
    allow_hidden: policy.allow_hidden,
  }
  let report = sanitize_filename_with_policy(filename, base_policy)
  if policy.max_length <= 0 || report.value.char_length() <= policy.max_length {
    return report
  }
  let shortened = shorten_preserving_extension(report.value, policy.max_length)
  push_diag(
    report.diagnostics,
    LengthReduced,
    Warning,
    "Filename was shortened while preserving the extension when possible",
    None,
  )
  {
    value: shortened,
    original: report.original,
    changed: true,
    diagnostics: report.diagnostics,
  }
}

///|
pub fn is_probably_safe_filename(filename : String) -> Bool {
  let report = sanitize_filename(filename)
  !report.changed && !report.diagnostics.any(d => d.severity == Error)
}

///|
pub fn filename_extension(filename : String) -> String? {
  match last_dot_index(filename) {
    Some(index) =>
      if index + 1 >= filename.length() {
        None
      } else {
        Some(filename[index + 1:].to_owned())
      }
    None => None
  }
}

///|
pub fn filename_stem(filename : String) -> String {
  match last_dot_index(filename) {
    Some(index) =>
      if index == 0 {
        filename
      } else {
        filename[0:index].to_owned()
      }
    None => filename
  }
}

///|
pub fn filename_has_extension(filename : String, extension : String) -> Bool {
  match filename_extension(filename) {
    Some(value) => value.to_lower() == normalize_extension(extension)
    None => false
  }
}

///|
pub fn normalize_extension(extension : String) -> String {
  let trimmed = extension.trim(chars=" \t\r\n").to_owned().to_lower()
  if trimmed.has_prefix(".") && trimmed.length() > 1 {
    trimmed[1:].to_owned()
  } else {
    trimmed
  }
}

///|
fn last_dot_index(filename : String) -> Int? {
  let mut result : Int? = None
  let mut index = 0
  while index < filename.length() {
    let c = char_at(filename, index)
    if c == '.' {
      result = Some(index)
    }
    index = index + c.utf16_len()
  }
  result
}

///|
fn shorten_preserving_extension(filename : String, max_chars : Int) -> String {
  if max_chars <= 0 || filename.char_length() <= max_chars {
    return filename
  }
  match last_dot_index(filename) {
    Some(dot) => {
      let ext = filename[dot:].to_owned()
      let ext_chars = ext.char_length()
      if dot == 0 || ext_chars + 2 >= max_chars {
        take_chars(filename, max_chars)
      } else {
        let stem_limit = max_chars - ext_chars
        take_chars(filename[0:dot].to_owned(), stem_limit) + ext
      }
    }
    None => take_chars(filename, max_chars)
  }
}