///|
const FILE_DIALOG_SELECTED = 0

///|
const FILE_DIALOG_CANCELLED = 1

///|
const FILE_DIALOG_UNSUPPORTED = -1

///|
fn file_dialog_kind_code(kind : FileDialogKind) -> Int {
  match kind {
    OpenFile => 0
    OpenFiles => 1
    SaveFile => 2
    PickDirectory => 3
  }
}

///|
fn invalid_file_dialog_options(message : String) -> Result[Bytes, WebViewError] {
  Err(WebViewError::NativeFailure(0, message))
}

///|
fn encode_file_dialog_filters(
  filters : Array[FileDialogFilter],
) -> Result[Bytes, WebViewError] {
  if filters.length() > 32 {
    return invalid_file_dialog_options(
      "file dialogs support at most 32 filters",
    )
  }
  let encoded : Array[String] = []
  for filter in filters {
    let name = filter.name.trim().to_owned()
    if name.is_empty() || name.contains("\t") || name.contains("\n") {
      return invalid_file_dialog_options(
        "file dialog filter names must be non-empty and contain no tab or newline",
      )
    }
    if filter.extensions.is_empty() || filter.extensions.length() > 32 {
      return invalid_file_dialog_options(
        "file dialog filters must contain between 1 and 32 extensions",
      )
    }
    let patterns : Array[String] = []
    for extension in filter.extensions {
      let extension = extension.trim().to_owned()
      if extension.is_empty() ||
        extension.contains("\t") ||
        extension.contains("\n") ||
        extension.contains(";") ||
        extension.contains("*") ||
        extension.has_prefix(".") {
        return invalid_file_dialog_options(
          "file dialog extensions must be bare extension names",
        )
      }
      patterns.push("*." + extension)
    }
    let pattern = patterns.join(";")
    encoded.push("\{name}\t\{pattern}")
  }
  Ok(encode_utf8(encoded.join("\n")))
}

///|
fn decode_file_dialog_selection(payload : Bytes) -> Array[String]? {
  let mut offset = 0
  let count = match read_u32be(payload, offset) {
    Some(count) => count
    None => return None
  }
  offset += 4
  if count < 0 || count > 1024 {
    return None
  }
  let paths : Array[String] = []
  for _ in 0.. length
      None => return None
    }
    offset += 4
    if length <= 0 || length > payload.length() - offset {
      return None
    }
    paths.push(decode_utf8(payload[offset:offset + length]))
    offset += length
  }
  if offset == payload.length() {
    Some(paths)
  } else {
    None
  }
}

///|
/// Shows a native file dialog owned by this WebView's native container.
///
/// The method must run on the WebView creation thread. A user cancellation is
/// returned as `FileDialogResult::Cancelled`; backend and native failures are
/// returned as `WebViewError` values.
pub fn WebView::show_file_dialog(
  self : WebView,
  options : FileDialogOptions,
) -> Result[FileDialogResult, WebViewError] {
  if !self.on_owner_thread() {
    return Err(WebViewError::WrongThread)
  }
  match self.lifecycle() {
    WebViewLifecycle::Destroyed => return Err(WebViewError::Destroyed)
    WebViewLifecycle::Failed(error) => return Err(error)
    _ => ()
  }
  let filters = match encode_file_dialog_filters(options.filters) {
    Ok(filters) => filters
    Err(error) => return Err(error)
  }
  let status = Ref(FILE_DIALOG_UNSUPPORTED)
  let response = native_show_file_dialog(
    self.handle,
    file_dialog_kind_code(options.kind),
    encode_utf8(options.title.unwrap_or("")),
    filters,
    encode_utf8(options.default_name.unwrap_or("")),
    encode_utf8(options.initial_directory.unwrap_or("")),
    status,
  )
  match status.val {
    FILE_DIALOG_SELECTED =>
      match decode_file_dialog_selection(response) {
        Some(paths) => Ok(FileDialogResult::Selected(paths))
        None =>
          Err(
            WebViewError::NativeFailure(
              0, "file dialog backend returned an invalid selection",
            ),
          )
      }
    FILE_DIALOG_CANCELLED => Ok(FileDialogResult::Cancelled)
    FILE_DIALOG_UNSUPPORTED =>
      Err(
        WebViewError::Unsupported(
          "native file dialogs are unavailable on this backend",
        ),
      )
    code => Err(WebViewError::NativeFailure(code, "native file dialog failed"))
  }
}

///|
test "file dialog filters have a deterministic native encoding" {
  let filters = [
    FileDialogFilter::new(name="Text", extensions=["txt", "md"]),
    FileDialogFilter::new(name="Images", extensions=["png"]),
  ]
  match encode_file_dialog_filters(filters) {
    Ok(encoded) =>
      inspect(
        decode_utf8(encoded[:]),
        content="Text\t*.txt;*.md\nImages\t*.png",
      )
    Err(_) => fail("expected valid filter encoding")
  }
}

///|
test "file dialog filters reject ambiguous native delimiters" {
  match
    encode_file_dialog_filters([
      FileDialogFilter::new(name="Bad\tName", extensions=["txt"]),
    ]) {
    Err(WebViewError::NativeFailure(_, _)) => ()
    _ => fail("expected invalid filter rejection")
  }
}

///|
test "file dialog selections require a complete length-prefixed frame" {
  let selected : Array[Byte] = []
  append_u32be(selected, 1)
  let path = encode_utf8("C:/example.txt")
  append_u32be(selected, path.length())
  append_bytes(selected, path)
  match decode_file_dialog_selection(Bytes::from_array(selected)) {
    Some(paths) => assert_eq(paths, ["C:/example.txt"])
    None => fail("expected selection")
  }
  assert_eq(decode_file_dialog_selection(b"\x00\x00\x00\x01"), None)
}