// 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 "url_launcher_plugin.h"

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <shlwapi.h>
#include <windows.h>

#include <memory>
#include <optional>
#include <sstream>
#include <string>

#include "messages.g.h"

namespace url_launcher_windows {

namespace {

using flutter::EncodableMap;
using flutter::EncodableValue;

// Converts the given UTF-8 string to UTF-16.
std::wstring Utf16FromUtf8(const std::string& utf8_string) {
  if (utf8_string.empty()) {
    return std::wstring();
  }
  int target_length =
      ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(),
                            static_cast<int>(utf8_string.length()), nullptr, 0);
  if (target_length == 0) {
    return std::wstring();
  }
  std::wstring utf16_string;
  utf16_string.resize(target_length);
  int converted_length =
      ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(),
                            static_cast<int>(utf8_string.length()),
                            utf16_string.data(), target_length);
  if (converted_length == 0) {
    return std::wstring();
  }
  return utf16_string;
}

// Returns the URL argument from |method_call| if it is present, otherwise
// returns an empty string.
std::string GetUrlArgument(const flutter::MethodCall<>& method_call) {
  std::string url;
  const auto* arguments = std::get_if<EncodableMap>(method_call.arguments());
  if (arguments) {
    auto url_it = arguments->find(EncodableValue("url"));
    if (url_it != arguments->end()) {
      url = std::get<std::string>(url_it->second);
    }
  }
  return url;
}

}  // namespace

// static
void UrlLauncherPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrar* registrar) {
  std::unique_ptr<UrlLauncherPlugin> plugin =
      std::make_unique<UrlLauncherPlugin>();
  UrlLauncherApi::SetUp(registrar->messenger(), plugin.get());
  registrar->AddPlugin(std::move(plugin));
}

UrlLauncherPlugin::UrlLauncherPlugin()
    : system_apis_(std::make_unique<SystemApisImpl>()) {}

UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr<SystemApis> system_apis)
    : system_apis_(std::move(system_apis)) {}

UrlLauncherPlugin::~UrlLauncherPlugin() = default;

ErrorOr<bool> UrlLauncherPlugin::CanLaunchUrl(const std::string& url) {
  size_t separator_location = url.find(":");
  if (separator_location == std::string::npos) {
    return false;
  }
  std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location));

  HKEY key = nullptr;
  if (system_apis_->RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0,
                                  KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) {
    return false;
  }
  bool has_handler =
      system_apis_->RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr,
                                     nullptr) == ERROR_SUCCESS;
  system_apis_->RegCloseKey(key);
  return has_handler;
}

ErrorOr<bool> UrlLauncherPlugin::LaunchUrl(const std::string& url) {
  std::string url_to_open;
  if (url.find("file:") == 0) {
    // ShellExecuteW does not process %-encoded UTF8 strings in file URLs.
    DWORD unescaped_len = 0;
    std::string unescaped_url = url;
    if (FAILED(::UrlUnescapeA(unescaped_url.data(), /*pszUnescaped=*/nullptr,
                              &unescaped_len, URL_UNESCAPE_INPLACE))) {
      return FlutterError("open_error", "Failed to unescape file URL");
    }
    url_to_open = unescaped_url;
  } else {
    url_to_open = url;
  }

  int status =
      static_cast<int>(reinterpret_cast<INT_PTR>(system_apis_->ShellExecuteW(
          nullptr, TEXT("open"), Utf16FromUtf8(url_to_open).c_str(), nullptr,
          nullptr, SW_SHOWNORMAL)));

  // Per ::ShellExecuteW documentation, anything >32 indicates success.
  if (status <= 32) {
    if (status == SE_ERR_NOASSOC) {
      // NOASSOC just means there's nothing registered to handle launching;
      // return false rather than an error for better consistency with other
      // platforms.
      return false;
    }
    std::ostringstream error_message;
    error_message << "Failed to open " << url_to_open
                  << ": ShellExecute error code " << status;
    return FlutterError("open_error", error_message.str());
  }
  return true;
}

}  // namespace url_launcher_windows
