// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gtk/select_file_dialog_linux_gtk.h"

#include <glib/gi18n.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <algorithm>
#include <cmath>
#include <cstddef>
#include <memory>
#include <set>
#include <utility>
#include <vector>

#include "base/byte_size.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/aura/window_observer.h"
#include "ui/base/glib/scoped_gobject.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gtk/gtk_compat.h"
#include "ui/gtk/gtk_ui.h"
#include "ui/gtk/gtk_ui_platform.h"
#include "ui/gtk/gtk_util.h"
#include "ui/shell_dialogs/select_file_dialog.h"
#include "ui/shell_dialogs/select_file_dialog_linux.h"
#include "ui/shell_dialogs/selected_file_info.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h"
#include "url/gurl.h"

namespace gtk {

namespace {

// GTK's internal response IDs use negative integers (eg. GTK_RESPONSE_CANCEL),
// leaving zero and positive integers for application-defined response IDs. Use
// zero for the accept response type since GTK will preselect
// GTK_RESPONSE_ACCEPT as the default button, which should be avoided to prevent
// an exploit where the user is instructed to hold Enter before the dialog
// appears.
constexpr GtkResponseType kResponseTypeAccept = static_cast<GtkResponseType>(0);

// TODO(crbug.com/41469294): These getters will be unnecessary after
// migrating to GtkFileChooserNative.
const char* GettextPackage() {
  static base::NoDestructor<std::string> gettext_package(
      "gtk" + base::NumberToString(GtkVersion().components()[0]) + "0");
  return gettext_package->c_str();
}

const char* GtkGettext(const char* str) {
  return g_dgettext(GettextPackage(), str);
}

const char* GetCancelLabel() {
  if (!GtkCheckVersion(4))
    return "gtk-cancel";  // In GTK3, this is GTK_STOCK_CANCEL.
  static const char* cancel = GtkGettext("_Cancel");
  return cancel;
}

const char* GetOpenLabel() {
  if (!GtkCheckVersion(4))
    return "gtk-open";  // In GTK3, this is GTK_STOCK_OPEN.
  static const char* open = GtkGettext("_Open");
  return open;
}

const char* GetSaveLabel() {
  if (!GtkCheckVersion(4))
    return "gtk-save";  // In GTK3, this is GTK_STOCK_SAVE.
  static const char* save = GtkGettext("_Save");
  return save;
}

void GtkFileChooserSetFilename(GtkFileChooser* dialog,
                               const base::FilePath& path) {
  if (GtkCheckVersion(4)) {
    auto file = TakeGObject(g_file_new_for_path(path.value().c_str()));
    gtk_file_chooser_set_file(dialog, file, nullptr);
  } else {
    gtk_file_chooser_set_filename(dialog, path.value().c_str());
  }
}

int GtkDialogSelectedFilterIndex(GtkWidget* dialog) {
  GtkFileFilter* selected_filter =
      gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
  int idx = -1;
  if (GtkCheckVersion(4)) {
    auto filters =
        TakeGObject(gtk_file_chooser_get_filters(GTK_FILE_CHOOSER(dialog)));
    int size = g_list_model_get_n_items(filters);
    for (; idx < size; ++idx) {
      if (g_list_model_get_item(filters, idx) == selected_filter)
        break;
    }
  } else {
    GSList* filters = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog));
    idx = g_slist_index(filters, selected_filter);
    g_slist_free(filters);
  }
  return idx;
}

std::string GtkFileChooserGetFilename(GtkWidget* dialog) {
  struct GFreeDeleter {
    void operator()(gchar* ptr) const { g_free(ptr); }
  };
  if (GtkCheckVersion(4)) {
    if (auto file =
            TakeGObject(gtk_file_chooser_get_file(GTK_FILE_CHOOSER(dialog)))) {
      if (const char* filename = g_file_peek_path(file)) {
        return std::string(filename);
      }
    }
  } else {
    auto gchar_filename = std::unique_ptr<gchar, GFreeDeleter>(
        gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)));
    if (const char* filename = gchar_filename.get()) {
      return std::string(filename);
    }
  }
  return std::string();
}

std::vector<base::FilePath> GtkFileChooserGetFilenames(GtkWidget* dialog) {
  std::vector<base::FilePath> filenames_fp;
  if (GtkCheckVersion(4)) {
    auto files = Gtk4FileChooserGetFiles(GTK_FILE_CHOOSER(dialog));
    auto size = g_list_model_get_n_items(files);
    for (unsigned int i = 0; i < size; ++i) {
      auto file = TakeGObject(G_FILE(g_list_model_get_object(files, i)));
      filenames_fp.emplace_back(g_file_peek_path(file));
    }
  } else {
    GSList* filenames =
        gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
    if (!filenames)
      return {};
    for (GSList* iter = filenames; iter != nullptr; iter = g_slist_next(iter)) {
      base::FilePath path(static_cast<char*>(iter->data));
      g_free(iter->data);
      filenames_fp.push_back(path);
    }
    g_slist_free(filenames);
  }
  return filenames_fp;
}

ScopedGObject<GdkPixbuf> ConvertSkBitmapToGdkPixbuf(const SkBitmap& bitmap) {
  if (bitmap.isNull() || bitmap.empty()) {
    return {};
  }

  int width = bitmap.width();
  int height = bitmap.height();

  // Create a new GdkPixbuf. GDK_COLORSPACE_RGB is standard and only colorspace.
  // Use has_alpha = TRUE and bits_per_sample = 8.
  auto pixbuf =
      TakeGObject(gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, width, height));
  if (!pixbuf) {
    return {};
  }

  guchar* gdk_pixels = gdk_pixbuf_get_pixels(pixbuf.get());
  int gdk_rowstride = gdk_pixbuf_get_rowstride(pixbuf.get());

  SkImageInfo dst_info = SkImageInfo::Make(
      width, height, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType);
  if (!bitmap.readPixels(dst_info, gdk_pixels, gdk_rowstride, 0, 0)) {
    return {};
  }

  return pixbuf;
}

}  // namespace

// The size of the preview we display for selected image files. We set height
// larger than width because generally there is more free space vertically
// than horizontally (setting the preview image will always expand the width of
// the dialog, but usually not the height). The image's aspect ratio will always
// be preserved.  Only used on GTK3.
static const int kPreviewWidth = 256;
static const int kPreviewHeight = 512;

SelectFileDialogLinuxGtk::DialogState::DialogState() = default;

SelectFileDialogLinuxGtk::DialogState::DialogState(
    std::vector<ScopedGSignal> signals,
    aura::Window* parent,
    base::OnceClosure reenable_parent_events)
    : signals(std::move(signals)),
      parent(parent),
      reenable_parent_events(std::move(reenable_parent_events)) {}

SelectFileDialogLinuxGtk::DialogState::DialogState(DialogState&& other) =
    default;

SelectFileDialogLinuxGtk::DialogState&
SelectFileDialogLinuxGtk::DialogState::operator=(DialogState&& other) = default;

SelectFileDialogLinuxGtk::DialogState::~DialogState() = default;

SelectFileDialogLinuxGtk::SelectFileDialogLinuxGtk(
    Listener* listener,
    std::unique_ptr<ui::SelectFilePolicy> policy,
    GtkUiPlatform* platform)
    : SelectFileDialogLinux(listener, std::move(policy)), platform_(platform) {}

SelectFileDialogLinuxGtk::~SelectFileDialogLinuxGtk() {
  // `OnFileChooserDestroy()` mutates `dialogs_`, so make a copy to avoid
  // modifying it during iteration.
  std::vector<GtkWidget*> dialogs;
  dialogs.reserve(dialogs_.size());
  for (auto& pair : dialogs_) {
    // Disconnect the signal handler now so `GtkWindowDestroy()` doesn't
    // trigger `OnFileChooserDestroy()`.
    pair.second.signals.clear();
    dialogs.push_back(pair.first);
  }
  for (GtkWidget* dialog : dialogs) {
    CHECK(dialog);
    // `GtkWindowDestroy()` synchronously drops the only reference and frees
    // the dialog, so run `OnFileChooserDestroy()` (which calls
    // `g_object_set_data()` on `dialog`) first to avoid a use-after-free.
    OnFileChooserDestroy(dialog);
    GtkWindowDestroy(dialog);
  }
  CHECK(dialogs_.empty());
}

bool SelectFileDialogLinuxGtk::IsRunning(
    gfx::NativeWindow parent_window) const {
  return std::any_of(dialogs_.begin(), dialogs_.end(),
                     [=](const std::pair<GtkWidget*, DialogState>& pair) {
                       return pair.second.parent == parent_window;
                     });
}

bool SelectFileDialogLinuxGtk::HasMultipleFileTypeChoicesImpl() {
  return file_types().extensions.size() > 1;
}

void SelectFileDialogLinuxGtk::OnWindowDestroying(aura::Window* window) {
  // Remove the |parent| property associated with the |dialog|.
  for (auto& pair : dialogs_) {
    GtkWidget* dialog = pair.first;
    auto& state = pair.second;
    if (state.parent == window) {
      ClearAuraTransientParent(dialog, window, platform_);
      window->RemoveObserver(this);
      state.parent = nullptr;
      return;
    }
  }
}

// We ignore |default_extension|.
void SelectFileDialogLinuxGtk::SelectFileImpl(
    Type type,
    const std::u16string& title,
    const base::FilePath& default_path,
    const FileTypeInfo* file_types,
    int file_type_index,
    const base::FilePath::StringType& default_extension,
    gfx::NativeWindow owning_window,
    const GURL* caller) {
  set_type(type);

  std::string title_string = base::UTF16ToUTF8(title);

  set_file_type_index(file_type_index);
  if (file_types)
    set_file_types(*file_types);

  GtkWidget* dialog = nullptr;
  std::vector<ScopedGSignal> signals;
  auto connect = [&](const char* detailed_signal, auto receiver) {
    // Unretained() is safe since SelectFileDialogLinuxGtk will own the
    // ScopedGSignal.
    signals.emplace_back(dialog, detailed_signal,
                         base::BindRepeating(receiver, base::Unretained(this)),
                         G_CONNECT_AFTER);
  };
  switch (type) {
    case SELECT_FOLDER:
    case SELECT_UPLOAD_FOLDER:
    case SELECT_EXISTING_FOLDER:
      dialog = CreateSelectFolderDialog(type, title_string, default_path,
                                        owning_window);
      connect("response",
              &SelectFileDialogLinuxGtk::OnSelectSingleFolderDialogResponse);
      break;
    case SELECT_OPEN_FILE:
      dialog = CreateFileOpenDialog(title_string, default_path, owning_window);
      connect("response",
              &SelectFileDialogLinuxGtk::OnSelectSingleFileDialogResponse);
      break;
    case SELECT_OPEN_MULTI_FILE:
      dialog =
          CreateMultiFileOpenDialog(title_string, default_path, owning_window);
      connect("response",
              &SelectFileDialogLinuxGtk::OnSelectMultiFileDialogResponse);
      break;
    case SELECT_SAVEAS_FILE:
      dialog = CreateSaveAsDialog(title_string, default_path, owning_window);
      connect("response",
              &SelectFileDialogLinuxGtk::OnSelectSingleFileDialogResponse);
      break;
    case SELECT_NONE:
      NOTREACHED();
  }
  if (GtkCheckVersion(4)) {
    gtk_window_set_hide_on_close(GTK_WINDOW(dialog), true);
  } else {
    signals.emplace_back(dialog, "delete-event",
                         base::BindRepeating(gtk_widget_hide_on_delete));
  }

  if (owning_window) {
    owning_window->AddObserver(this);
  }

  connect("destroy", &SelectFileDialogLinuxGtk::OnFileChooserDestroy);

  GtkWidget* preview = nullptr;
  if (!GtkCheckVersion(4)) {
    preview = gtk_image_new();
    connect("update-preview", &SelectFileDialogLinuxGtk::OnUpdatePreview);
    gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog), preview);
  }

  base::OnceClosure reenable_input_events =
      DisableHostInputHandling(dialog, owning_window);

  DialogState state(std::move(signals), owning_window,
                    std::move(reenable_input_events));
  state.preview_widget = preview;
  dialogs_[dialog] = std::move(state);

  if (!GtkCheckVersion(4))
    gtk_widget_show_all(dialog);
  platform_->ShowGtkWindow(GTK_WINDOW(dialog));
}

void SelectFileDialogLinuxGtk::AddFilters(GtkFileChooser* chooser) {
  for (size_t i = 0; i < file_types().extensions.size(); ++i) {
    GtkFileFilter* filter = nullptr;
    std::set<std::string> fallback_labels;

    for (const auto& current_extension : file_types().extensions[i]) {
      if (!current_extension.empty()) {
        if (!filter)
          filter = gtk_file_filter_new();
        for (std::string pattern :
             {"*." + base::ToLowerASCII(current_extension),
              "*." + base::ToUpperASCII(current_extension)}) {
          gtk_file_filter_add_pattern(filter, pattern.c_str());
          fallback_labels.insert(pattern);
        }
      }
    }
    // We didn't find any non-empty extensions to filter on.
    if (!filter)
      continue;

    // The description vector may be blank, in which case we are supposed to
    // use some sort of default description based on the filter.
    if (i < file_types().extension_description_overrides.size()) {
      gtk_file_filter_set_name(
          filter,
          base::UTF16ToUTF8(file_types().extension_description_overrides[i])
              .c_str());
    } else {
      // There is no system default filter description so we use
      // the extensions themselves if the description is blank.
      std::vector<std::string> fallback_labels_vector(fallback_labels.begin(),
                                                      fallback_labels.end());
      std::string fallback_label =
          base::JoinString(fallback_labels_vector, ",");
      gtk_file_filter_set_name(filter, fallback_label.c_str());
    }

    gtk_file_chooser_add_filter(chooser, filter);
    if (i == file_type_index() - 1)
      gtk_file_chooser_set_filter(chooser, filter);
  }

  // Add the *.* filter, but only if we have added other filters (otherwise it
  // is implied).
  if (file_types().include_all_files && !file_types().extensions.empty()) {
    GtkFileFilter* filter = gtk_file_filter_new();
    gtk_file_filter_add_pattern(filter, "*");
    gtk_file_filter_set_name(
        filter, l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES).c_str());
    gtk_file_chooser_add_filter(chooser, filter);
  }
}

void SelectFileDialogLinuxGtk::FileSelected(GtkWidget* dialog,
                                            const base::FilePath& path) {
  if (type() == SELECT_SAVEAS_FILE) {
    set_last_saved_path(path.DirName());
  } else if (type() == SELECT_OPEN_FILE || type() == SELECT_FOLDER ||
             type() == SELECT_UPLOAD_FOLDER ||
             type() == SELECT_EXISTING_FOLDER) {
    set_last_opened_path(path.DirName());
  } else {
    NOTREACHED();
  }

  if (listener_) {
    listener_->FileSelected(ui::SelectedFileInfo(path),
                            GtkDialogSelectedFilterIndex(dialog) + 1);
  }
  GtkWindowDestroy(dialog);
}

void SelectFileDialogLinuxGtk::MultiFilesSelected(
    GtkWidget* dialog,
    const std::vector<base::FilePath>& files) {
  set_last_opened_path(files[0].DirName());

  if (listener_) {
    listener_->MultiFilesSelected(
        ui::FilePathListToSelectedFileInfoList(files));
  }
  GtkWindowDestroy(dialog);
}

void SelectFileDialogLinuxGtk::FileNotSelected(GtkWidget* dialog) {
  if (listener_) {
    listener_->FileSelectionCanceled();
  }
  GtkWindowDestroy(dialog);
}

GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::NativeWindow parent) {
  GtkWidget* dialog = GtkFileChooserDialogNew(
      title.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_OPEN, GetCancelLabel(),
      GTK_RESPONSE_CANCEL, GetOpenLabel(), kResponseTypeAccept);
  gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
  SetGtkTransientForAura(dialog, parent, platform_);
  AddFilters(GTK_FILE_CHOOSER(dialog));

  if (!default_path.empty()) {
    if (CallDirectoryExistsOnUIThread(default_path)) {
      GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog), default_path);
    } else {
      // If the file doesn't exist, this will just switch to the correct
      // directory. That's good enough.
      GtkFileChooserSetFilename(GTK_FILE_CHOOSER(dialog), default_path);
    }
  } else if (!last_opened_path()->empty()) {
    GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog),
                                   *last_opened_path());
  }
  return dialog;
}

GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog(
    Type type,
    const std::string& title,
    const base::FilePath& default_path,
    gfx::NativeWindow parent) {
  std::string title_string = title;
  if (title_string.empty()) {
    title_string =
        (type == SELECT_UPLOAD_FOLDER)
            ? l10n_util::GetStringUTF8(IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE)
            : l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE);
  }
  std::string accept_button_label =
      (type == SELECT_UPLOAD_FOLDER)
          ? l10n_util::GetStringUTF8(
                IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON)
          : GetOpenLabel();

  GtkWidget* dialog = GtkFileChooserDialogNew(
      title_string.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
      GetCancelLabel(), GTK_RESPONSE_CANCEL, accept_button_label.c_str(),
      kResponseTypeAccept);
  gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
  SetGtkTransientForAura(dialog, parent, platform_);
  GtkFileChooser* chooser = GTK_FILE_CHOOSER(dialog);
  if (type == SELECT_UPLOAD_FOLDER || type == SELECT_EXISTING_FOLDER)
    gtk_file_chooser_set_create_folders(chooser, FALSE);
  if (!default_path.empty())
    GtkFileChooserSetCurrentFolder(chooser, default_path);
  else if (!last_opened_path()->empty())
    GtkFileChooserSetCurrentFolder(chooser, *last_opened_path());
  GtkFileFilter* only_folders = gtk_file_filter_new();
  gtk_file_filter_set_name(
      only_folders,
      l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE).c_str());
  gtk_file_filter_add_mime_type(only_folders, "application/x-directory");
  gtk_file_filter_add_mime_type(only_folders, "inode/directory");
  gtk_file_filter_add_mime_type(only_folders, "text/directory");
  gtk_file_chooser_add_filter(chooser, only_folders);
  gtk_file_chooser_set_select_multiple(chooser, FALSE);
  return dialog;
}

GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenDialog(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::NativeWindow parent) {
  std::string title_string =
      !title.empty() ? title
                     : l10n_util::GetStringUTF8(IDS_OPEN_FILE_DIALOG_TITLE);

  GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent);
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
  return dialog;
}

GtkWidget* SelectFileDialogLinuxGtk::CreateMultiFileOpenDialog(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::NativeWindow parent) {
  std::string title_string =
      !title.empty() ? title
                     : l10n_util::GetStringUTF8(IDS_OPEN_FILES_DIALOG_TITLE);

  GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent);
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
  return dialog;
}

GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::NativeWindow parent) {
  std::string title_string =
      !title.empty() ? title
                     : l10n_util::GetStringUTF8(IDS_SAVE_AS_DIALOG_TITLE);

  GtkWidget* dialog = GtkFileChooserDialogNew(
      title_string.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SAVE,
      GetCancelLabel(), GTK_RESPONSE_CANCEL, GetSaveLabel(),
      kResponseTypeAccept);
  SetGtkTransientForAura(dialog, parent, platform_);

  AddFilters(GTK_FILE_CHOOSER(dialog));
  if (!default_path.empty()) {
    if (CallDirectoryExistsOnUIThread(default_path)) {
      // If this is an existing directory, navigate to that directory, with no
      // filename.
      GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog), default_path);
    } else {
      // The default path does not exist, or is an existing file. We use
      // set_current_folder() followed by set_current_name(), as per the
      // recommendation of the GTK docs.
      GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog),
                                     default_path.DirName());
      gtk_file_chooser_set_current_name(
          GTK_FILE_CHOOSER(dialog), default_path.BaseName().value().c_str());
    }
  } else if (!last_saved_path()->empty()) {
    GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog),
                                   *last_saved_path());
  }
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
  // Overwrite confirmation is always enabled in GTK4.
  if (!GtkCheckVersion(4)) {
    gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),
                                                   TRUE);
  }
  return dialog;
}

bool SelectFileDialogLinuxGtk::IsCancelResponse(gint response_id) {
  bool is_cancel = response_id == GTK_RESPONSE_CANCEL ||
                   response_id == GTK_RESPONSE_DELETE_EVENT;
  if (is_cancel)
    return true;

  DCHECK(response_id == GTK_RESPONSE_ACCEPT ||
         response_id == kResponseTypeAccept);
  return false;
}

void SelectFileDialogLinuxGtk::SelectSingleFileHelper(GtkWidget* dialog,
                                                      gint response_id,
                                                      bool allow_folder) {
  if (IsCancelResponse(response_id)) {
    FileNotSelected(dialog);
    return;
  }

  auto filename = GtkFileChooserGetFilename(dialog);
  if (filename.empty()) {
    FileNotSelected(dialog);
    return;
  }
  base::FilePath path(filename);

  if (allow_folder) {
    FileSelected(dialog, path);
    return;
  }

  if (CallDirectoryExistsOnUIThread(path))
    FileNotSelected(dialog);
  else
    FileSelected(dialog, path);
}

void SelectFileDialogLinuxGtk::OnSelectSingleFileDialogResponse(
    GtkWidget* dialog,
    int response_id) {
  SelectSingleFileHelper(dialog, response_id, false);
}

void SelectFileDialogLinuxGtk::OnSelectSingleFolderDialogResponse(
    GtkWidget* dialog,
    int response_id) {
  SelectSingleFileHelper(dialog, response_id, true);
}

void SelectFileDialogLinuxGtk::OnSelectMultiFileDialogResponse(
    GtkWidget* dialog,
    int response_id) {
  if (IsCancelResponse(response_id)) {
    FileNotSelected(dialog);
    return;
  }

  auto filenames = GtkFileChooserGetFilenames(dialog);
  std::erase_if(filenames, [this](const base::FilePath& path) {
    return CallDirectoryExistsOnUIThread(path);
  });
  if (filenames.empty()) {
    FileNotSelected(dialog);
    return;
  }
  MultiFilesSelected(dialog, filenames);
}

void SelectFileDialogLinuxGtk::OnFileChooserDestroy(GtkWidget* dialog) {
  auto it = dialogs_.find(dialog);
  if (it == dialogs_.end()) {
    return;
  }
  auto& state = it->second;

  // `state.parent` can be nullptr when closing the host window
  // while opening the file-picker.
  if (state.parent) {
    CHECK(dialog);
    ClearAuraTransientParent(dialog, state.parent, platform_);
    state.parent->RemoveObserver(this);
  }
  state.signals.clear();
  if (state.reenable_parent_events) {
    std::move(state.reenable_parent_events).Run();
  }
  dialogs_.erase(it);
}

void SelectFileDialogLinuxGtk::OnUpdatePreview(GtkWidget* chooser) {
  DCHECK(!GtkCheckVersion(4));
  auto it = dialogs_.find(chooser);
  if (it == dialogs_.end()) {
    return;
  }
  auto& state = it->second;

  gchar* filename =
      gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(chooser));
  if (!filename) {
    state.preview_file_path.clear();
    gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
                                               FALSE);
    return;
  }

  // Don't attempt to open anything which isn't a regular file. If a named pipe,
  // this may hang. See https://crbug.com/534754.
  // Don't attempt to preview files over 100MB to avoid excessive memory use
  // and crashes when decoding very large images.
  struct stat stat_buf;
  constexpr base::ByteSize kMaxPreviewFileSize = base::MiB(100);
  if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode) ||
      static_cast<uint64_t>(stat_buf.st_size) > kMaxPreviewFileSize.InBytes()) {
    g_free(filename);
    state.preview_file_path.clear();
    gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
                                               FALSE);
    return;
  }

  base::FilePath file_path(filename);
  g_free(filename);

  state.preview_file_path = file_path;

  ScopedGObject<GtkWidget> chooser_wrap = WrapGObject(chooser);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&base::ReadFileToBytes, file_path),
      base::BindOnce(&SelectFileDialogLinuxGtk::OnPreviewFileRead,
                     weak_factory_.GetWeakPtr(), file_path,
                     std::move(chooser_wrap)));
}

void SelectFileDialogLinuxGtk::OnPreviewFileRead(
    const base::FilePath& file_path,
    ScopedGObject<GtkWidget> chooser,
    std::optional<std::vector<uint8_t>> bytes) {
  auto it = dialogs_.find(chooser.get());
  if (it == dialogs_.end() || it->second.preview_file_path != file_path) {
    return;
  }

  if (!bytes || bytes->empty()) {
    gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser.get()),
                                               FALSE);
    return;
  }

  // Use the out-of-process sandboxed image decoder. This supports WebP, GIF,
  // PNG, JPEG, etc. and safely parses untrusted image data. By specifying a
  // desired frame size, the sandboxed process handles both decoding and
  // resizing, saving UI thread CPU and IPC bandwidth.
  data_decoder::DecodeImageIsolated(
      *bytes, data_decoder::mojom::ImageCodec::kDefault,
      /*shrink_to_fit=*/true, data_decoder::kDefaultMaxSizeInBytes,
      gfx::Size(kPreviewWidth, kPreviewHeight),
      base::BindOnce(&SelectFileDialogLinuxGtk::OnPreviewImageDecoded,
                     weak_factory_.GetWeakPtr(), file_path,
                     std::move(chooser)));
}

void SelectFileDialogLinuxGtk::OnPreviewImageDecoded(
    const base::FilePath& file_path,
    ScopedGObject<GtkWidget> chooser,
    const SkBitmap& bitmap) {
  auto it = dialogs_.find(chooser.get());
  if (it == dialogs_.end() || it->second.preview_file_path != file_path) {
    return;
  }

  if (bitmap.isNull()) {
    gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser.get()),
                                               FALSE);
    return;
  }

  int original_width = bitmap.width();
  int original_height = bitmap.height();
  float width_ratio = static_cast<float>(kPreviewWidth) / original_width;
  float height_ratio = static_cast<float>(kPreviewHeight) / original_height;
  float ratio = std::min(width_ratio, height_ratio);

  if (ratio < 1.0f) {
    int target_width =
        std::max(1, static_cast<int>(std::round(original_width * ratio)));
    int target_height =
        std::max(1, static_cast<int>(std::round(original_height * ratio)));
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::USER_VISIBLE},
        base::BindOnce(
            [](const SkBitmap& bmp, int target_w, int target_h) {
              return skia::ImageOperations::Resize(
                  bmp, skia::ImageOperations::RESIZE_LANCZOS3, target_w,
                  target_h);
            },
            bitmap, target_width, target_height),
        base::BindOnce(&SelectFileDialogLinuxGtk::OnPreviewImageResized,
                       weak_factory_.GetWeakPtr(), file_path,
                       std::move(chooser)));
  } else {
    OnPreviewImageResized(file_path, std::move(chooser), bitmap);
  }
}

void SelectFileDialogLinuxGtk::OnPreviewImageResized(
    const base::FilePath& file_path,
    ScopedGObject<GtkWidget> chooser,
    const SkBitmap& bitmap) {
  auto it = dialogs_.find(chooser.get());
  if (it == dialogs_.end() || it->second.preview_file_path != file_path) {
    return;
  }

  ScopedGObject<GdkPixbuf> pixbuf = ConvertSkBitmapToGdkPixbuf(bitmap);
  if (pixbuf) {
    gtk_image_set_from_pixbuf(GTK_IMAGE(it->second.preview_widget.get()),
                              pixbuf.get());
  }
  gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser.get()),
                                             pixbuf ? TRUE : FALSE);
}

}  // namespace gtk
