// 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 <cstddef>
#include <memory>
#include <set>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/nix/mime_util_xdg.h"
#include "base/nix/xdg_util.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/version.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/shell_dialogs/select_file_dialog_linux.h"
#include "ui/strings/grit/ui_strings.h"
#include "url/gurl.h"

namespace {

std::string GetTitle(const std::string& title, int message_id) {
  return title.empty() ? l10n_util::GetStringUTF8(message_id) : title;
}

const char kKdialogBinary[] = "kdialog";

}  // namespace

namespace ui {

// Implementation of SelectFileDialog that shows a KDE common dialog for
// choosing a file or folder. This acts as a modal dialog.
class SelectFileDialogLinuxKde : public SelectFileDialogLinux {
 public:
  SelectFileDialogLinuxKde(Listener* listener,
                           std::unique_ptr<ui::SelectFilePolicy> policy,
                           base::nix::DesktopEnvironment desktop,
                           const std::string& kdialog_version);

  SelectFileDialogLinuxKde(const SelectFileDialogLinuxKde&) = delete;
  SelectFileDialogLinuxKde& operator=(const SelectFileDialogLinuxKde&) = delete;

 protected:
  ~SelectFileDialogLinuxKde() override;

  // BaseShellDialog implementation:
  bool IsRunning(gfx::NativeWindow parent_window) const override;

  // SelectFileDialog implementation.
  // |params| is user data we pass back via the Listener interface.
  void 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,
                      void* params,
                      const GURL* caller) override;

 private:
  bool HasMultipleFileTypeChoicesImpl() override;

  struct KDialogParams {
    KDialogParams(const std::string& type,
                  const std::string& title,
                  const base::FilePath& default_path,
                  gfx::AcceleratedWidget parent,
                  bool file_operation,
                  bool multiple_selection)
        : type(type),
          title(title),
          default_path(default_path),
          parent(parent),
          file_operation(file_operation),
          multiple_selection(multiple_selection) {}

    std::string type;
    std::string title;
    base::FilePath default_path;
    gfx::AcceleratedWidget parent;
    bool file_operation;
    bool multiple_selection;
  };

  struct KDialogOutputParams {
    std::string output;
    int exit_code;
  };

  // Get the filters from |file_types_| and concatenate them into
  // |filter_string|.
  std::string GetMimeTypeFilterString();

  // Get KDialog command line representing the Argv array for KDialog.
  void GetKDialogCommandLine(const std::string& type,
                             const std::string& title,
                             const base::FilePath& default_path,
                             gfx::AcceleratedWidget parent,
                             bool file_operation,
                             bool multiple_selection,
                             base::CommandLine* command_line);

  // Call KDialog on the FILE thread and return the results.
  std::unique_ptr<KDialogOutputParams> CallKDialogOutput(
      const KDialogParams& params);

  // Notifies the listener that a single file was chosen.
  void FileSelected(const base::FilePath& path, void* params);

  // Notifies the listener that multiple files were chosen.
  void MultiFilesSelected(const std::vector<base::FilePath>& files,
                          void* params);

  // Notifies the listener that no file was chosen (the action was canceled).
  // Dialog is passed so we can find that |params| pointer that was passed to
  // us when we were told to show the dialog.
  void FileNotSelected(void* params);

  void CreateSelectFolderDialog(Type type,
                                const std::string& title,
                                const base::FilePath& default_path,
                                gfx::AcceleratedWidget parent,
                                void* params);

  void CreateFileOpenDialog(const std::string& title,
                            const base::FilePath& default_path,
                            gfx::AcceleratedWidget parent,
                            void* params);

  void CreateMultiFileOpenDialog(const std::string& title,
                                 const base::FilePath& default_path,
                                 gfx::AcceleratedWidget parent,
                                 void* params);

  void CreateSaveAsDialog(const std::string& title,
                          const base::FilePath& default_path,
                          gfx::AcceleratedWidget parent,
                          void* params);

  // Common function for OnSelectSingleFileDialogResponse and
  // OnSelectSingleFolderDialogResponse.
  void SelectSingleFileHelper(void* params,
                              bool allow_folder,
                              std::unique_ptr<KDialogOutputParams> results);

  void OnSelectSingleFileDialogResponse(
      gfx::AcceleratedWidget parent,
      void* params,
      std::unique_ptr<KDialogOutputParams> results);
  void OnSelectMultiFileDialogResponse(
      gfx::AcceleratedWidget parent,
      void* params,
      std::unique_ptr<KDialogOutputParams> results);
  void OnSelectSingleFolderDialogResponse(
      gfx::AcceleratedWidget parent,
      void* params,
      std::unique_ptr<KDialogOutputParams> results);

  // Should be either DESKTOP_ENVIRONMENT_KDE3, KDE4, KDE5, or KDE6.
  base::nix::DesktopEnvironment desktop_;

  // The set of all parent windows for which we are currently running
  // dialogs. This should only be accessed on the UI thread.
  std::set<gfx::AcceleratedWidget> parents_;

  // Set to true if the kdialog version is new enough to support passing
  // multiple extensions with descriptions, eliminating the need for the lossy
  // conversion of extensions to mime-types.
  bool kdialog_supports_multiple_extensions_ = false;

  // A task runner for blocking pipe reads.
  scoped_refptr<base::SequencedTaskRunner> pipe_task_runner_;

  SEQUENCE_CHECKER(sequence_checker_);
};

// static
bool SelectFileDialogLinux::CheckKDEDialogWorksOnUIThread(
    std::string& kdialog_version) {
  // No choice. UI thread can't continue without an answer here. Fortunately we
  // only do this once, the first time a file dialog is displayed.
  base::ScopedAllowBlocking scoped_allow_blocking;

  base::CommandLine::StringVector cmd_vector;
  cmd_vector.push_back(kKdialogBinary);
  cmd_vector.push_back("--version");
  base::CommandLine command_line(cmd_vector);
  return base::GetAppOutput(command_line, &kdialog_version);
}

SelectFileDialog* NewSelectFileDialogLinuxKde(
    SelectFileDialog::Listener* listener,
    std::unique_ptr<ui::SelectFilePolicy> policy,
    base::nix::DesktopEnvironment desktop,
    const std::string& kdialog_version) {
  return new SelectFileDialogLinuxKde(listener, std::move(policy), desktop,
                                      kdialog_version);
}

SelectFileDialogLinuxKde::SelectFileDialogLinuxKde(
    Listener* listener,
    std::unique_ptr<ui::SelectFilePolicy> policy,
    base::nix::DesktopEnvironment desktop,
    const std::string& kdialog_version)
    : SelectFileDialogLinux(listener, std::move(policy)),
      desktop_(desktop),
      pipe_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
  DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
         desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
         desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE5 ||
         desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE6);
  // |kdialog_version| should be of the form "kdialog 1.2.3", so split on
  // whitespace and then try to parse a version from the second piece. If
  // parsing fails for whatever reason, we fall back to the behavior that works
  // with all currently known versions of kdialog.
  std::vector<base::StringPiece> version_pieces = base::SplitStringPiece(
      kdialog_version, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  if (version_pieces.size() >= 2) {
    base::Version parsed_version(version_pieces[1]);
    if (parsed_version.IsValid()) {
      kdialog_supports_multiple_extensions_ =
          parsed_version >= base::Version("19.12");
    }
  }
}

SelectFileDialogLinuxKde::~SelectFileDialogLinuxKde() = default;

bool SelectFileDialogLinuxKde::IsRunning(
    gfx::NativeWindow parent_window) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (parent_window && parent_window->GetHost()) {
    auto window = parent_window->GetHost()->GetAcceleratedWidget();
    return parents_.find(window) != parents_.end();
  }

  return false;
}

// We ignore |default_extension|.
void SelectFileDialogLinuxKde::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,
    void* params,
    const GURL* caller) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  set_type(type);

  gfx::AcceleratedWidget window = gfx::kNullAcceleratedWidget;
  if (owning_window && owning_window->GetHost()) {
    // |owning_window| can be null when user right-clicks on a downloadable item
    // and chooses 'Open Link in New Tab' when 'Ask where to save each file
    // before downloading.' preference is turned on. (http://crbug.com/29213)
    window = owning_window->GetHost()->GetAcceleratedWidget();
    parents_.insert(window);
  }

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

  set_file_type_index(file_type_index);
  if (file_types) {
    set_file_types(*file_types);
  } else {
    auto file_types_copy = SelectFileDialogLinux::file_types();
    file_types_copy.include_all_files = true;
    set_file_types(file_types_copy);
  }

  switch (type) {
    case SELECT_FOLDER:
    case SELECT_UPLOAD_FOLDER:
    case SELECT_EXISTING_FOLDER:
      CreateSelectFolderDialog(type, title_string, default_path, window,
                               params);
      return;
    case SELECT_OPEN_FILE:
      CreateFileOpenDialog(title_string, default_path, window, params);
      return;
    case SELECT_OPEN_MULTI_FILE:
      CreateMultiFileOpenDialog(title_string, default_path, window, params);
      return;
    case SELECT_SAVEAS_FILE:
      CreateSaveAsDialog(title_string, default_path, window, params);
      return;
    case SELECT_NONE:
      NOTREACHED();
      return;
  }
}

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

std::string SelectFileDialogLinuxKde::GetMimeTypeFilterString() {
  DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence());

  if (!kdialog_supports_multiple_extensions_) {
    // We need a filter set because the same mime type can appear multiple
    // times.
    std::set<std::string> filter_set;
    for (auto& extensions : file_types().extensions) {
      for (auto& extension : extensions) {
        if (!extension.empty()) {
          std::string mime_type = base::nix::GetFileMimeType(
              base::FilePath("name").ReplaceExtension(extension));
          filter_set.insert(mime_type);
        }
      }
    }
    std::vector<std::string> filter_vector(filter_set.cbegin(),
                                           filter_set.cend());
    // Add the *.* filter, but only if we have added other filters (otherwise it
    // is implied). It needs to be added last to avoid being picked as the
    // default filter.
    if (file_types().include_all_files && !file_types().extensions.empty()) {
      DCHECK(filter_set.find("application/octet-stream") == filter_set.end());
      filter_vector.push_back("application/octet-stream");
    }
    return base::JoinString(filter_vector, " ");
  }

  std::vector<std::string> filters;
  for (size_t i = 0; i < file_types().extensions.size(); ++i) {
    std::set<std::string> extension_filters;
    for (const auto& extension : file_types().extensions[i]) {
      if (extension.empty())
        continue;
      extension_filters.insert(std::string("*.") + extension);
    }

    // We didn't find any non-empty extensions to filter on.
    if (extension_filters.empty())
      continue;

    std::vector<std::string> extension_filters_vector(extension_filters.begin(),
                                                      extension_filters.end());

    std::string description;
    // 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()) {
      description =
          base::UTF16ToUTF8(file_types().extension_description_overrides[i]);
      // Filter out any characters that would mess up kdialog's parsing.
      base::ReplaceChars(description, "|()", "", &description);
    } else {
      // There is no system default filter description so we use
      // the extensions themselves if the description is blank.
      description = base::JoinString(extension_filters_vector, ",");
    }

    filters.push_back(description + " (" +
                      base::JoinString(extension_filters_vector, " ") + ")");
  }

  if (file_types().include_all_files && !file_types().extensions.empty())
    filters.push_back(l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES) + " (*)");

  return base::JoinString(filters, "|");
}

std::unique_ptr<SelectFileDialogLinuxKde::KDialogOutputParams>
SelectFileDialogLinuxKde::CallKDialogOutput(const KDialogParams& params) {
  DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence());
  base::CommandLine::StringVector cmd_vector;
  cmd_vector.push_back(kKdialogBinary);
  base::CommandLine command_line(cmd_vector);
  GetKDialogCommandLine(params.type, params.title, params.default_path,
                        params.parent, params.file_operation,
                        params.multiple_selection, &command_line);

  auto results = std::make_unique<KDialogOutputParams>();
  // Get output from KDialog
  base::GetAppOutputWithExitCode(command_line, &results->output,
                                 &results->exit_code);
  if (!results->output.empty())
    results->output.erase(results->output.size() - 1);
  return results;
}

void SelectFileDialogLinuxKde::GetKDialogCommandLine(
    const std::string& type,
    const std::string& title,
    const base::FilePath& path,
    gfx::AcceleratedWidget parent,
    bool file_operation,
    bool multiple_selection,
    base::CommandLine* command_line) {
  CHECK(command_line);

  // Attach to the current Chrome window.
  if (parent != gfx::kNullAcceleratedWidget) {
    command_line->AppendSwitchNative(
        desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ? "--embed"
                                                        : "--attach",
        base::NumberToString(static_cast<uint32_t>(parent)));
  }

  // Set the correct title for the dialog.
  if (!title.empty())
    command_line->AppendSwitchNative("--title", title);
  // Enable multiple file selection if we need to.
  if (multiple_selection) {
    command_line->AppendSwitch("--multiple");
    command_line->AppendSwitch("--separate-output");
  }
  command_line->AppendSwitch(type);
  // The path should never be empty. If it is, set it to PWD.
  if (path.empty())
    command_line->AppendArgPath(base::FilePath("."));
  else
    command_line->AppendArgPath(path);
  // Depending on the type of the operation we need, get the path to the
  // file/folder and set up mime type filters.
  if (file_operation)
    command_line->AppendArg(GetMimeTypeFilterString());
  VLOG(1) << "KDialog command line: " << command_line->GetCommandLineString();
}

void SelectFileDialogLinuxKde::FileSelected(const base::FilePath& path,
                                            void* params) {
  if (type() == SELECT_SAVEAS_FILE)
    set_last_saved_path(path.DirName());
  else if (type() == SELECT_OPEN_FILE)
    set_last_opened_path(path.DirName());
  else if (type() == SELECT_FOLDER || type() == SELECT_UPLOAD_FOLDER ||
           type() == SELECT_EXISTING_FOLDER)
    set_last_opened_path(path);
  else
    NOTREACHED();
  if (listener_) {  // What does the filter index actually do?
    // TODO(dfilimon): Get a reasonable index value from somewhere.
    listener_->FileSelected(path, 1, params);
  }
}

void SelectFileDialogLinuxKde::MultiFilesSelected(
    const std::vector<base::FilePath>& files,
    void* params) {
  set_last_opened_path(files[0].DirName());
  if (listener_)
    listener_->MultiFilesSelected(files, params);
}

void SelectFileDialogLinuxKde::FileNotSelected(void* params) {
  if (listener_)
    listener_->FileSelectionCanceled(params);
}

void SelectFileDialogLinuxKde::CreateSelectFolderDialog(
    Type type,
    const std::string& title,
    const base::FilePath& default_path,
    gfx::AcceleratedWidget parent,
    void* params) {
  int title_message_id = (type == SELECT_UPLOAD_FOLDER)
                             ? IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE
                             : IDS_SELECT_FOLDER_DIALOG_TITLE;
  pipe_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          &SelectFileDialogLinuxKde::CallKDialogOutput, this,
          KDialogParams(
              "--getexistingdirectory", GetTitle(title, title_message_id),
              default_path.empty() ? *last_opened_path() : default_path, parent,
              false, false)),
      base::BindOnce(
          &SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse, this,
          parent, params));
}

void SelectFileDialogLinuxKde::CreateFileOpenDialog(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::AcceleratedWidget parent,
    void* params) {
  pipe_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          &SelectFileDialogLinuxKde::CallKDialogOutput, this,
          KDialogParams(
              "--getopenfilename", GetTitle(title, IDS_OPEN_FILE_DIALOG_TITLE),
              default_path.empty() ? *last_opened_path() : default_path, parent,
              true, false)),
      base::BindOnce(
          &SelectFileDialogLinuxKde::OnSelectSingleFileDialogResponse, this,
          parent, params));
}

void SelectFileDialogLinuxKde::CreateMultiFileOpenDialog(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::AcceleratedWidget parent,
    void* params) {
  pipe_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          &SelectFileDialogLinuxKde::CallKDialogOutput, this,
          KDialogParams(
              "--getopenfilename", GetTitle(title, IDS_OPEN_FILES_DIALOG_TITLE),
              default_path.empty() ? *last_opened_path() : default_path, parent,
              true, true)),
      base::BindOnce(&SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse,
                     this, parent, params));
}

void SelectFileDialogLinuxKde::CreateSaveAsDialog(
    const std::string& title,
    const base::FilePath& default_path,
    gfx::AcceleratedWidget parent,
    void* params) {
  pipe_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(
          &SelectFileDialogLinuxKde::CallKDialogOutput, this,
          KDialogParams(
              "--getsavefilename", GetTitle(title, IDS_SAVE_AS_DIALOG_TITLE),
              default_path.empty() ? *last_saved_path() : default_path, parent,
              true, false)),
      base::BindOnce(
          &SelectFileDialogLinuxKde::OnSelectSingleFileDialogResponse, this,
          parent, params));
}

void SelectFileDialogLinuxKde::SelectSingleFileHelper(
    void* params,
    bool allow_folder,
    std::unique_ptr<KDialogOutputParams> results) {
  VLOG(1) << "[kdialog] SingleFileResponse: " << results->output;
  if (results->exit_code || results->output.empty()) {
    FileNotSelected(params);
    return;
  }

  base::FilePath path(results->output);
  if (allow_folder) {
    FileSelected(path, params);
    return;
  }

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

void SelectFileDialogLinuxKde::OnSelectSingleFileDialogResponse(
    gfx::AcceleratedWidget parent,
    void* params,
    std::unique_ptr<KDialogOutputParams> results) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  parents_.erase(parent);
  SelectSingleFileHelper(params, false, std::move(results));
}

void SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse(
    gfx::AcceleratedWidget parent,
    void* params,
    std::unique_ptr<KDialogOutputParams> results) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  parents_.erase(parent);
  SelectSingleFileHelper(params, true, std::move(results));
}

void SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse(
    gfx::AcceleratedWidget parent,
    void* params,
    std::unique_ptr<KDialogOutputParams> results) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  VLOG(1) << "[kdialog] MultiFileResponse: " << results->output;

  parents_.erase(parent);

  if (results->exit_code || results->output.empty()) {
    FileNotSelected(params);
    return;
  }

  std::vector<base::FilePath> filenames_fp;
  for (const base::StringPiece& line :
       base::SplitStringPiece(results->output, "\n", base::KEEP_WHITESPACE,
                              base::SPLIT_WANT_NONEMPTY)) {
    base::FilePath path(line);
    if (CallDirectoryExistsOnUIThread(path))
      continue;
    filenames_fp.push_back(path);
  }

  if (filenames_fp.empty()) {
    FileNotSelected(params);
    return;
  }
  MultiFilesSelected(filenames_fp, params);
}

}  // namespace ui