// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
#include "chrome/browser/extensions/api/document_scan/scanner_discovery_runner.h"

#include "ash/constants/ash_pref_names.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/scanning/lorgnette_scanner_manager.h"
#include "chrome/browser/ash/scanning/lorgnette_scanner_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/views/extensions/dialogs/document_scanner_discovery_confirmation_dialog.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/image_loader.h"
#include "extensions/common/extension.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/native_window_tracker/native_window_tracker.h"

namespace extensions {

namespace {

// Icon size for confirmation dialogs.
constexpr int kIconSize = 64;

// There is no easy way to interact with UI dialogs that are generated by Chrome
// itself, so we need to have a way to bypass this for testing.
std::optional<bool> g_discovery_confirmation_result;

bool CanSkipConfirmation(content::BrowserContext* browser_context,
                         const ExtensionId& extension_id) {
  const base::ListValue& list =
      Profile::FromBrowserContext(browser_context)
          ->GetPrefs()
          ->GetList(ash::prefs::kDocumentScanAPITrustedExtensions);
  return list.contains(extension_id);
}

api::document_scan::OperationResult ToApiOperationResult(
    lorgnette::OperationResult result) {
  switch (result) {
    default:
      NOTREACHED();
    case lorgnette::OPERATION_RESULT_UNKNOWN:
      return api::document_scan::OperationResult::kUnknown;
    case lorgnette::OPERATION_RESULT_SUCCESS:
      return api::document_scan::OperationResult::kSuccess;
    case lorgnette::OPERATION_RESULT_UNSUPPORTED:
      return api::document_scan::OperationResult::kUnsupported;
    case lorgnette::OPERATION_RESULT_CANCELLED:
      return api::document_scan::OperationResult::kCancelled;
    case lorgnette::OPERATION_RESULT_DEVICE_BUSY:
      return api::document_scan::OperationResult::kDeviceBusy;
    case lorgnette::OPERATION_RESULT_INVALID:
      return api::document_scan::OperationResult::kInvalid;
    case lorgnette::OPERATION_RESULT_WRONG_TYPE:
      return api::document_scan::OperationResult::kWrongType;
    case lorgnette::OPERATION_RESULT_EOF:
      return api::document_scan::OperationResult::kEof;
    case lorgnette::OPERATION_RESULT_ADF_JAMMED:
      return api::document_scan::OperationResult::kAdfJammed;
    case lorgnette::OPERATION_RESULT_ADF_EMPTY:
      return api::document_scan::OperationResult::kAdfEmpty;
    case lorgnette::OPERATION_RESULT_COVER_OPEN:
      return api::document_scan::OperationResult::kCoverOpen;
    case lorgnette::OPERATION_RESULT_IO_ERROR:
      return api::document_scan::OperationResult::kIoError;
    case lorgnette::OPERATION_RESULT_ACCESS_DENIED:
      return api::document_scan::OperationResult::kAccessDenied;
    case lorgnette::OPERATION_RESULT_NO_MEMORY:
      return api::document_scan::OperationResult::kNoMemory;
    case lorgnette::OPERATION_RESULT_UNREACHABLE:
      return api::document_scan::OperationResult::kUnreachable;
    case lorgnette::OPERATION_RESULT_MISSING:
      return api::document_scan::OperationResult::kMissing;
    case lorgnette::OPERATION_RESULT_INTERNAL_ERROR:
      return api::document_scan::OperationResult::kInternalError;
  }
}

api::document_scan::ConnectionType ToApiConnectionType(
    lorgnette::ConnectionType type) {
  switch (type) {
    default:
      NOTREACHED();
    case lorgnette::ConnectionType::CONNECTION_UNSPECIFIED:
      return api::document_scan::ConnectionType::kUnspecified;
    case lorgnette::ConnectionType::CONNECTION_USB:
      return api::document_scan::ConnectionType::kUsb;
    case lorgnette::ConnectionType::CONNECTION_NETWORK:
      return api::document_scan::ConnectionType::kNetwork;
  }
}

}  // namespace

ScannerDiscoveryRunner::ScannerDiscoveryRunner(
    gfx::NativeWindow native_window,
    content::BrowserContext* browser_context,
    scoped_refptr<const Extension> extension)
    : native_window_(native_window),
      browser_context_(browser_context),
      extension_(std::move(extension)) {
  CHECK(extension_);
  if (native_window_) {
    native_window_tracker_ = ui::NativeWindowTracker::Create(native_window_);
  }
}

ScannerDiscoveryRunner::~ScannerDiscoveryRunner() = default;

// static
void ScannerDiscoveryRunner::SetDiscoveryConfirmationResultForTesting(
    bool result) {
  g_discovery_confirmation_result = result;
}

void ScannerDiscoveryRunner::Start(bool approved,
                                   api::document_scan::DeviceFilter filter,
                                   GetScannerListCallback callback) {
  CHECK(!callback_) << "discovery call already in progress";
  CHECK(callback);
  callback_ = std::move(callback);
  filter_.emplace(std::move(filter));

  if (approved || CanSkipConfirmation(browser_context_, extension_->id())) {
    SendGetScannerListRequest();
    return;
  }

  // If a test has set the confirmation result, go directly to the end handler
  // instead of displaying the dialog.
  if (g_discovery_confirmation_result) {
    OnConfirmationDialogClosed(g_discovery_confirmation_result.value());
    return;
  }

  ImageLoader::Get(browser_context_)
      ->LoadImageAtEveryScaleFactorAsync(
          extension_.get(), gfx::Size(kIconSize, kIconSize),
          base::BindOnce(&ScannerDiscoveryRunner::ShowScanDiscoveryDialog,
                         weak_ptr_factory_.GetWeakPtr()));
}

const ExtensionId& ScannerDiscoveryRunner::extension_id() const {
  return extension_->id();
}

void ScannerDiscoveryRunner::ShowScanDiscoveryDialog(const gfx::Image& icon) {
  // If the browser window was closed during API request handling, treat it the
  // same as if the user denied the request.
  if (native_window_tracker_ &&
      native_window_tracker_->WasNativeWindowDestroyed()) {
    OnConfirmationDialogClosed(false);
    return;
  }

  ShowDocumentScannerDiscoveryConfirmationDialog(
      native_window_, extension_->id(), base::UTF8ToUTF16(extension_->name()),
      icon.AsImageSkia(),
      base::BindOnce(&ScannerDiscoveryRunner::OnConfirmationDialogClosed,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ScannerDiscoveryRunner::OnConfirmationDialogClosed(bool approved) {
  if (approved) {
    SendGetScannerListRequest();
    return;
  }

  api::document_scan::GetScannerListResponse api_response;
  api_response.result = api::document_scan::OperationResult::kAccessDenied;
  std::move(callback_).Run(std::move(api_response));
}

void ScannerDiscoveryRunner::SendGetScannerListRequest() {
  using LocalScannerFilter = ash::LorgnetteScannerManager::LocalScannerFilter;
  using SecureScannerFilter = ash::LorgnetteScannerManager::SecureScannerFilter;

  ash::LorgnetteScannerManagerFactory::GetForBrowserContext(browser_context_)
      ->GetScannerInfoList(
          extension_->id(),
          filter_->local.value_or(false)
              ? LocalScannerFilter::kLocalScannersOnly
              : LocalScannerFilter::kIncludeNetworkScanners,
          filter_->secure.value_or(false)
              ? SecureScannerFilter::kSecureScannersOnly
              : SecureScannerFilter::kIncludeUnsecureScanners,
          base::BindOnce(&ScannerDiscoveryRunner::OnScannerListReceived,
                         weak_ptr_factory_.GetWeakPtr()));
}

void ScannerDiscoveryRunner::OnScannerListReceived(
    const std::optional<lorgnette::ListScannersResponse>& response) {
  if (!response.has_value()) {
    api::document_scan::GetScannerListResponse api_response;
    api_response.result = api::document_scan::OperationResult::kInternalError;
    std::move(callback_).Run(std::move(api_response));
    return;
  }

  api::document_scan::GetScannerListResponse api_response;
  api_response.result = ToApiOperationResult(response->result());
  for (const lorgnette::ScannerInfo& scanner : response->scanners()) {
    api::document_scan::ScannerInfo& scanner_out =
        api_response.scanners.emplace_back();
    scanner_out.scanner_id = scanner.name();
    scanner_out.name = scanner.display_name();
    scanner_out.manufacturer = scanner.manufacturer();
    scanner_out.model = scanner.model();
    scanner_out.device_uuid = scanner.device_uuid();
    scanner_out.connection_type =
        ToApiConnectionType(scanner.connection_type());
    scanner_out.secure = scanner.secure();
    for (const std::string& format : scanner.image_format()) {
      scanner_out.image_formats.emplace_back(format);
    }
    scanner_out.protocol_type = scanner.protocol_type();
  }

  std::move(callback_).Run(std::move(api_response));
}

}  // namespace extensions
