///|
/// Create a named file filter for open/save dialogs.
///
/// `patterns` is copied defensively so later mutations to the caller-owned
/// array do not affect the dialog request. Backends may ignore filters that do
/// not contain any patterns.
///
/// Typical patterns look like `"*.txt"` or `"*.png"`, and several patterns can
/// be grouped under the same display name.
pub fn FileFilter::new(
  name : StringView,
  patterns : Array[String],
) -> FileFilter {
  { name: name.to_owned(), patterns: patterns.copy() }
}

///|
/// Attach filename filters to an open-file dialog.
///
/// This replaces any existing filters, copies the provided array defensively,
/// and preserves the dialog title and starting directory.
///
/// Use this when you want the picker UI to emphasize a narrow set of file
/// types while keeping the original request immutable.
pub fn OpenFileDialog::with_filters(
  self : OpenFileDialog,
  filters : Array[FileFilter],
) -> OpenFileDialog {
  {
    title: self.title,
    directory: self.directory,
    filters: copy_file_filters(filters),
  }
}

///|
/// Attach filename filters to a save-file dialog.
///
/// This replaces any existing filters, copies the provided array defensively,
/// and preserves the dialog title, directory, file name, and default
/// extension.
///
/// The native backend may apply these filters differently, but the request
/// always keeps the full structured filter list.
pub fn SaveFileDialog::with_filters(
  self : SaveFileDialog,
  filters : Array[FileFilter],
) -> SaveFileDialog {
  {
    title: self.title,
    directory: self.directory,
    file_name: self.file_name,
    filters: copy_file_filters(filters),
    default_extension: self.default_extension,
  }
}

///|
/// Set a default file extension for a save-file dialog.
///
/// This updates only the stored default extension and preserves the current
/// title, directory, file name, and filters.
///
/// Callers may pass either `"txt"` or `".txt"`; the native layer normalizes
/// the value before appending it to extensionless paths.
pub fn SaveFileDialog::with_default_extension(
  self : SaveFileDialog,
  extension : StringView,
) -> SaveFileDialog {
  {
    title: self.title,
    directory: self.directory,
    file_name: self.file_name,
    filters: copy_file_filters(self.filters),
    default_extension: extension.to_owned(),
  }
}

///|
/// Copy a single file filter and duplicate its pattern array.
fn copy_file_filter(filter : FileFilter) -> FileFilter {
  { name: filter.name, patterns: filter.patterns.copy() }
}

///|
/// Copy a filter list so dialog builders do not retain caller-owned arrays.
fn copy_file_filters(filters : Array[FileFilter]) -> Array[FileFilter] {
  let copied : Array[FileFilter] = []
  for filter in filters {
    copied.push(copy_file_filter(filter))
  }
  copied
}

///|
/// Attach filename filters to a multi-file open dialog.
///
/// This replaces any existing filters, copies the provided array defensively,
/// and preserves the dialog title and starting directory.
///
/// It behaves like `OpenFileDialog::with_filters`, but targets the multi-select
/// request type instead.
pub fn OpenFilesDialog::with_filters(
  self : OpenFilesDialog,
  filters : Array[FileFilter],
) -> OpenFilesDialog {
  {
    title: self.title,
    directory: self.directory,
    filters: copy_file_filters(filters),
  }
}

///|
/// Escape separator characters before serializing a filter or path component.
fn escape_serialized_component(text : String) -> String {
  let builder = StringBuilder::new(size_hint=text.length())
  for ch in text {
    match ch {
      '\\' => builder.write_view("\\\\")
      '\n' => builder.write_view("\\n")
      '\r' => builder.write_view("\\r")
      '\t' => builder.write_view("\\t")
      _ => builder.write_char(ch)
    }
  }
  builder.to_string()
}

///|
/// Restore escaped separator characters from a serialized component.
fn unescape_serialized_component(text : StringView) -> String {
  let builder = StringBuilder::new()
  let mut is_escaped = false
  for ch in text.to_owned() {
    if is_escaped {
      match ch {
        'n' => builder.write_char('\n')
        'r' => builder.write_char('\r')
        't' => builder.write_char('\t')
        _ => builder.write_char(ch)
      }
      is_escaped = false
    } else if ch == '\\' {
      is_escaped = true
    } else {
      builder.write_char(ch)
    }
  }
  if is_escaped {
    builder.write_char('\\')
  }
  builder.to_string()
}

///|
/// Serialize file filters into the UTF-8 format expected by the native stubs.
fn encode_file_filters(filters : Array[FileFilter]) -> Bytes {
  if filters.length() == 0 {
    return encode_utf8_bytes("")
  }

  let encoded_filters : Array[String] = []
  for filter in filters {
    if filter.patterns.length() == 0 {
      continue
    }
    let parts : Array[String] = [escape_serialized_component(filter.name)]
    for pattern in filter.patterns {
      parts.push(escape_serialized_component(pattern))
    }
    encoded_filters.push(parts.join("\t"))
  }
  encode_utf8_bytes(encoded_filters.join("\n"))
}

///|
/// Decode a newline-delimited UTF-8 path list emitted by the native stubs.
fn decode_path_list(bytes : Bytes) -> Array[String] {
  let raw = utf8_bytes_to_mbt_string(bytes)
  if raw == "" {
    return []
  }

  let decoded : Array[String] = []
  for component in raw.split("\n") {
    decoded.push(unescape_serialized_component(component))
  }
  decoded
}