// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "file_selector_plugin.h"

#include <comdef.h>
#include <comip.h>
#include <flutter/flutter_view.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <shobjidl.h>
#include <windows.h>

#include <cassert>
#include <memory>
#include <string>
#include <vector>

#include "file_dialog_controller.h"
#include "string_utils.h"

_COM_SMARTPTR_TYPEDEF(IEnumShellItems, IID_IEnumShellItems);
_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog);
_COM_SMARTPTR_TYPEDEF(IShellItem, IID_IShellItem);
_COM_SMARTPTR_TYPEDEF(IShellItemArray, IID_IShellItemArray);

namespace file_selector_windows {

namespace {

using flutter::CustomEncodableValue;
using flutter::EncodableList;
using flutter::EncodableValue;

// The kind of file dialog to show.
enum class DialogMode { open, save };

// Returns the path for |shell_item| as a UTF-8 string, or an
// empty string on failure.
std::string GetPathForShellItem(IShellItem* shell_item) {
  if (shell_item == nullptr) {
    return "";
  }
  wchar_t* wide_path = nullptr;
  if (!SUCCEEDED(shell_item->GetDisplayName(SIGDN_FILESYSPATH, &wide_path))) {
    return "";
  }
  std::string path = Utf8FromUtf16(wide_path);
  ::CoTaskMemFree(wide_path);
  return path;
}

// Implementation of FileDialogControllerFactory that makes standard
// FileDialogController instances.
class DefaultFileDialogControllerFactory : public FileDialogControllerFactory {
 public:
  DefaultFileDialogControllerFactory() {}
  virtual ~DefaultFileDialogControllerFactory() {}

  // Disallow copy and assign.
  DefaultFileDialogControllerFactory(
      const DefaultFileDialogControllerFactory&) = delete;
  DefaultFileDialogControllerFactory& operator=(
      const DefaultFileDialogControllerFactory&) = delete;

  std::unique_ptr<FileDialogController> CreateController(
      IFileDialog* dialog) const override {
    assert(dialog != nullptr);
    return std::make_unique<FileDialogController>(dialog);
  }
};

// Wraps an IFileDialog, managing object lifetime as a scoped object and
// providing a simplified API for interacting with it as needed for the plugin.
class DialogWrapper {
 public:
  explicit DialogWrapper(const FileDialogControllerFactory& dialog_factory,
                         IID type) {
    is_open_dialog_ = type == CLSID_FileOpenDialog;
    IFileDialogPtr dialog = nullptr;
    last_result_ = CoCreateInstance(type, nullptr, CLSCTX_INPROC_SERVER,
                                    IID_PPV_ARGS(&dialog));
    dialog_controller_ = dialog_factory.CreateController(dialog);
  }

  // Attempts to set the default folder for the dialog to |path|,
  // if it exists.
  void SetFolder(std::string_view path) {
    std::wstring wide_path = Utf16FromUtf8(path);
    IShellItemPtr item;
    last_result_ = SHCreateItemFromParsingName(wide_path.c_str(), nullptr,
                                               IID_PPV_ARGS(&item));
    if (!SUCCEEDED(last_result_)) {
      return;
    }
    dialog_controller_->SetFolder(item);
  }

  // Sets the file name that is initially shown in the dialog.
  void SetFileName(std::string_view name) {
    std::wstring wide_name = Utf16FromUtf8(name);
    last_result_ = dialog_controller_->SetFileName(wide_name.c_str());
  }

  // Sets the label of the confirmation button.
  void SetOkButtonLabel(std::string_view label) {
    std::wstring wide_label = Utf16FromUtf8(label);
    last_result_ = dialog_controller_->SetOkButtonLabel(wide_label.c_str());
  }

  // Adds the given options to the dialog's current option set.
  void AddOptions(FILEOPENDIALOGOPTIONS new_options) {
    FILEOPENDIALOGOPTIONS options;
    last_result_ = dialog_controller_->GetOptions(&options);
    if (!SUCCEEDED(last_result_)) {
      return;
    }
    options |= new_options;
    if (options & FOS_PICKFOLDERS) {
      opening_directory_ = true;
    }
    last_result_ = dialog_controller_->SetOptions(options);
  }

  // Sets the filters for allowed file types to select.
  void SetFileTypeFilters(const EncodableList& filters) {
    const std::wstring spec_delimiter = L";";
    const std::wstring file_wildcard = L"*.";
    std::vector<COMDLG_FILTERSPEC> filter_specs;
    // Temporary ownership of the constructed strings whose data is used in
    // filter_specs, so that they live until the call to SetFileTypes is done.
    std::vector<std::wstring> filter_names;
    std::vector<std::wstring> filter_extensions;
    filter_extensions.reserve(filters.size());
    filter_names.reserve(filters.size());

    for (const EncodableValue& filter_info_value : filters) {
      const auto& type_group = std::any_cast<TypeGroup>(
          std::get<CustomEncodableValue>(filter_info_value));
      filter_names.push_back(Utf16FromUtf8(type_group.label()));
      filter_extensions.push_back(L"");
      std::wstring& spec = filter_extensions.back();
      if (type_group.extensions().empty()) {
        spec += L"*.*";
      } else {
        for (const EncodableValue& extension : type_group.extensions()) {
          if (!spec.empty()) {
            spec += spec_delimiter;
          }
          spec +=
              file_wildcard + Utf16FromUtf8(std::get<std::string>(extension));
        }
      }
      filter_specs.push_back({filter_names.back().c_str(), spec.c_str()});
    }
    last_result_ = dialog_controller_->SetFileTypes(
        static_cast<UINT>(filter_specs.size()), filter_specs.data());
  }

  // Displays the dialog, and returns the result, or nullopt on error.
  std::optional<FileDialogResult> Show(HWND parent_window) {
    assert(dialog_controller_);
    last_result_ = dialog_controller_->Show(parent_window);
    if (!SUCCEEDED(last_result_)) {
      return std::nullopt;
    }

    EncodableList files;
    if (is_open_dialog_) {
      IShellItemArrayPtr shell_items;
      last_result_ = dialog_controller_->GetResults(&shell_items);
      if (!SUCCEEDED(last_result_)) {
        return std::nullopt;
      }
      IEnumShellItemsPtr item_enumerator;
      last_result_ = shell_items->EnumItems(&item_enumerator);
      if (!SUCCEEDED(last_result_)) {
        return std::nullopt;
      }
      IShellItemPtr shell_item;
      while (item_enumerator->Next(1, &shell_item, nullptr) == S_OK) {
        files.push_back(EncodableValue(GetPathForShellItem(shell_item)));
      }
    } else {
      IShellItemPtr shell_item;
      last_result_ = dialog_controller_->GetResult(&shell_item);
      if (!SUCCEEDED(last_result_)) {
        return std::nullopt;
      }
      files.push_back(EncodableValue(GetPathForShellItem(shell_item)));
    }
    FileDialogResult result(files, nullptr);
    UINT file_type_index;
    if (SUCCEEDED(dialog_controller_->GetFileTypeIndex(&file_type_index)) &&
        file_type_index > 0) {
      // Convert from the one-based index to a Dart index.
      result.set_type_group_index(file_type_index - 1);
    }
    return result;
  }

  // Returns the result of the last Win32 API call related to this object.
  HRESULT last_result() { return last_result_; }

 private:
  // The dialog controller that all interactions are mediated through, to allow
  // for unit testing.
  std::unique_ptr<FileDialogController> dialog_controller_;
  bool is_open_dialog_;
  bool opening_directory_ = false;
  HRESULT last_result_;
};

ErrorOr<FileDialogResult> ShowDialog(
    const FileDialogControllerFactory& dialog_factory, HWND parent_window,
    DialogMode mode, const SelectionOptions& options,
    const std::string* initial_directory, const std::string* suggested_name,
    const std::string* confirm_label) {
  IID dialog_type =
      mode == DialogMode::save ? CLSID_FileSaveDialog : CLSID_FileOpenDialog;
  DialogWrapper dialog(dialog_factory, dialog_type);
  if (!SUCCEEDED(dialog.last_result())) {
    return FlutterError(
        "System error", "Could not create dialog",
        EncodableValue(std::in_place_type<int32_t>, dialog.last_result()));
  }

  FILEOPENDIALOGOPTIONS dialog_options = 0;
  if (options.select_folders()) {
    dialog_options |= FOS_PICKFOLDERS;
  }
  if (options.allow_multiple()) {
    dialog_options |= FOS_ALLOWMULTISELECT;
  }
  if (dialog_options != 0) {
    dialog.AddOptions(dialog_options);
  }

  if (initial_directory) {
    dialog.SetFolder(*initial_directory);
  }
  if (suggested_name) {
    dialog.SetFileName(*suggested_name);
  }
  if (confirm_label) {
    dialog.SetOkButtonLabel(*confirm_label);
  }

  if (!options.allowed_types().empty()) {
    dialog.SetFileTypeFilters(options.allowed_types());
  }

  std::optional<FileDialogResult> result = dialog.Show(parent_window);
  if (!result) {
    if (dialog.last_result() != HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
      return FlutterError(
          "System error", "Could not show dialog",
          EncodableValue(std::in_place_type<int32_t>, dialog.last_result()));
    } else {
      return FileDialogResult(EncodableList(), nullptr);
    }
  }
  return std::move(result.value());
}

// Returns the top-level window that owns |view|.
HWND GetRootWindow(flutter::FlutterView* view) {
  return ::GetAncestor(view->GetNativeWindow(), GA_ROOT);
}

}  // namespace

// static
void FileSelectorPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrarWindows* registrar) {
  std::unique_ptr<FileSelectorPlugin> plugin =
      std::make_unique<FileSelectorPlugin>(
          [registrar] { return GetRootWindow(registrar->GetView()); },
          std::make_unique<DefaultFileDialogControllerFactory>());

  FileSelectorApi::SetUp(registrar->messenger(), plugin.get());
  registrar->AddPlugin(std::move(plugin));
}

FileSelectorPlugin::FileSelectorPlugin(
    FlutterRootWindowProvider window_provider,
    std::unique_ptr<FileDialogControllerFactory> dialog_controller_factory)
    : get_root_window_(std::move(window_provider)),
      controller_factory_(std::move(dialog_controller_factory)) {}

FileSelectorPlugin::~FileSelectorPlugin() = default;

ErrorOr<FileDialogResult> FileSelectorPlugin::ShowOpenDialog(
    const SelectionOptions& options, const std::string* initialDirectory,
    const std::string* confirmButtonText) {
  return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::open,
                    options, initialDirectory, nullptr, confirmButtonText);
}

ErrorOr<FileDialogResult> FileSelectorPlugin::ShowSaveDialog(
    const SelectionOptions& options, const std::string* initialDirectory,
    const std::string* suggestedName, const std::string* confirmButtonText) {
  return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::save,
                    options, initialDirectory, suggestedName,
                    confirmButtonText);
}

}  // namespace file_selector_windows
