// Copyright 2022 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/printing/printer_query_oop.h"

#include <memory>
#include <utility>

#include "base/check_op.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/expected.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/printing/oop_features.h"
#include "chrome/browser/printing/print_backend_service_manager.h"
#include "chrome/browser/printing/print_job_worker_oop.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/web_contents.h"
#include "printing/buildflags/buildflags.h"

namespace printing {

PrinterQueryOop::PrinterQueryOop(content::GlobalRenderFrameHostId rfh_id)
    : PrinterQuery(rfh_id) {}

PrinterQueryOop::~PrinterQueryOop() = default;

std::unique_ptr<PrintJobWorker> PrinterQueryOop::TransferContextToNewWorker(
    PrintJob* print_job) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  return CreatePrintJobWorkerOop(print_job);
}

#if BUILDFLAG(IS_WIN)
void PrinterQueryOop::UpdatePrintableArea(
    PrintSettings* print_settings,
    OnDidUpdatePrintableAreaCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::string printer_name = base::UTF16ToUTF8(print_settings->device_name());
  PRINTER_LOG(EVENT) << "Updating paper printable area via service for "
                     << printer_name;

  PrintBackendServiceManager& service_mgr =
      PrintBackendServiceManager::GetInstance();

  // Caller is required to ensure `print_settings` stays alive until `callback`
  // runs.
  service_mgr.GetPaperPrintableArea(
      printer_name, print_settings->requested_media(),
      base::BindOnce(&PrinterQueryOop::OnDidGetPaperPrintableArea,
                     weak_factory_.GetWeakPtr(), print_settings,
                     std::move(callback)));
}
#endif

void PrinterQueryOop::SetClientId(
    PrintBackendServiceManager::ClientId client_id) {
  query_with_ui_client_id_ = client_id;
}

void PrinterQueryOop::OnDidUseDefaultSettings(
    SettingsCallback callback,
    base::expected<PrintSettings, mojom::ResultCode> print_settings) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  mojom::ResultCode result;
  if (!print_settings.has_value()) {
    result = print_settings.error();
    DCHECK_NE(result, mojom::ResultCode::kSuccess);
    PRINTER_LOG(ERROR) << "Error trying to use default settings via service: "
                       << result;

    // TODO(crbug.com/40561724)  Fill in support for handling of access-denied
    // result code.  Blocked on crbug.com/40787526 for Windows.
  } else {
    VLOG(1) << "Use default settings from service complete";
    result = mojom::ResultCode::kSuccess;
    printing_context()->SetPrintSettings(print_settings.value());
  }

  InvokeSettingsCallback(std::move(callback), result);
}

#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
void PrinterQueryOop::OnDidAskUserForSettings(
    SettingsCallback callback,
    base::expected<PrintSettings, mojom::ResultCode> print_settings) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  mojom::ResultCode result;
  if (print_settings.has_value()) {
    VLOG(1) << "Ask user for settings from service complete";
    result = mojom::ResultCode::kSuccess;
    printing_context()->SetPrintSettings(print_settings.value());

    // Use the same PrintBackendService for querying and printing, so that the
    // same device context can be used with both.
    print_document_client_id_ =
        PrintBackendServiceManager::GetInstance()
            .RegisterPrintDocumentClientReusingClientRemote(
                *query_with_ui_client_id_);
    if (!print_document_client_id_.has_value()) {
      // A failure after getting settings, override result to failure.
      result = mojom::ResultCode::kFailed;
      PRINTER_LOG(ERROR)
          << "Error after getting settings due to client registration failure; "
             "service or renderer likely has terminated";
    }
  } else {
    result = print_settings.error();
    DCHECK_NE(result, mojom::ResultCode::kSuccess);
    if (result != mojom::ResultCode::kCanceled) {
      PRINTER_LOG(ERROR) << "Error getting settings from user via service: "
                         << result;
    }

    // TODO(crbug.com/40561724)  Fill in support for handling of access-denied
    // result code.  Blocked on crbug.com/40787526 for Windows.
  }

  InvokeSettingsCallback(std::move(callback), result);
}
#else   // BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
void PrinterQueryOop::OnDidAskUserForSettings(
    SettingsCallback callback,
    std::unique_ptr<PrintSettings> new_settings,
    mojom::ResultCode result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (result == mojom::ResultCode::kSuccess) {
    // Want the same PrintBackend service as the query so that we use the same
    // device context.
    print_document_client_id_ =
        PrintBackendServiceManager::GetInstance()
            .RegisterPrintDocumentClientReusingClientRemote(
                *query_with_ui_client_id_);
    if (!print_document_client_id_.has_value()) {
      // A failure after getting settings, override result to failure.
      result = mojom::ResultCode::kFailed;
      PRINTER_LOG(ERROR)
          << "Error after getting settings due to client registration failure; "
             "service or renderer likely has terminated";
    }
  }
  std::move(callback).Run(std::move(new_settings), result);
}
#endif  // BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)

void PrinterQueryOop::UseDefaultSettings(SettingsCallback callback) {
#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
  CHECK(query_with_ui_client_id_.has_value());

  PRINTER_LOG(EVENT) << "Using printer default settings via service";

  // Any settings selected from the system dialog could need to be retained
  // for printing, so establish a printing context.
  CHECK(!context_id_.has_value());
  SendEstablishPrintingContext(*query_with_ui_client_id_,
                               /*printer_name=*/std::string());
  SendUseDefaultSettings(std::move(callback));
#else
  // `PrintingContextLinux::UseDefaultSettings()` is to be called prior to
  // `AskUserForSettings()` to establish a base device context.  If the system
  // print dialog will be invoked from within the browser process, then that
  // default setup needs to happen in browser as well.
  PrinterQuery::UseDefaultSettings(std::move(callback));
#endif
}

void PrinterQueryOop::GetSettingsWithUI(uint32_t document_page_count,
                                        bool has_selection,
                                        bool is_scripted,
                                        SettingsCallback callback) {
  // Save the print target type from the settings, since this will be needed
  // later when printing is started.
  print_from_system_dialog_ = true;

#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
  PRINTER_LOG(EVENT) << "Getting printer settings from user via service";
  SendAskUserForSettings(document_page_count, has_selection, is_scripted,
                         std::move(callback));
#else
  // Invoke the browser version of getting settings with the system UI:
  //   - macOS:  It is impossible to invoke a system dialog UI from a service
  //       utility and have that dialog be application modal for a window that
  //       was launched by the browser process.
  //   - Linux:  TODO(crbug.com/40561724)  Determine if Linux Wayland can be
  //   made
  //       to have a system dialog be modal against an application window in the
  //       browser process.
  //   - Other platforms don't have a system print UI or do not use OOP
  //     printing, so this does not matter.
  PrinterQuery::GetSettingsWithUI(
      document_page_count, has_selection, is_scripted,
      base::BindOnce(&PrinterQueryOop::OnDidAskUserForSettings,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
#endif
}

void PrinterQueryOop::UpdatePrintSettings(base::DictValue new_settings,
                                          SettingsCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Do not take a const reference, as `new_settings` will be modified below.
  std::string device_name = *new_settings.FindString(kSettingDeviceName);

  // Remember if this is from system print dialog, since this will be needed
  // later when printing is started.
  print_from_system_dialog_ =
      new_settings.FindBool(kSettingShowSystemDialog).value_or(false);

  // A device name is required for printing documents. If the device name is
  // empty then this is for a system print dialog, for which a destination is
  // not yet known.
  PrintBackendServiceManager& service_mgr =
      PrintBackendServiceManager::GetInstance();
  PrintBackendServiceManager::ClientId client_id;
  std::string printer_name;
  if (print_from_system_dialog_) {
    CHECK(!print_document_client_id_.has_value());
    client_id = *query_with_ui_client_id_;
#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG) && BUILDFLAG(IS_WIN)
    // `PrintingContextWin::UpdatePrintSettings()` is special because it can
    // invoke `AskUserForSettings()` and cause a system dialog to be displayed.
    // Running a dialog causes an exit to webpage-initiated fullscreen.
    // http://crbug.com/41322524
    content::WebContents* web_contents = GetWebContents();
    if (web_contents && web_contents->IsFullscreen()) {
      web_contents->ExitFullscreen(true);
    }
#endif
  } else {
    // Print the document from Print Preview.
    CHECK(!query_with_ui_client_id_.has_value());
    CHECK(!print_document_client_id_.has_value());

    print_document_client_id_ =
        service_mgr.RegisterPrintDocumentClient(device_name);
    client_id = *print_document_client_id_;
    printer_name = device_name;
  }
  SendEstablishPrintingContext(client_id, printer_name);

  VLOG(1) << "Updating print settings via service for " << device_name;

  service_mgr.UpdatePrintSettings(
      client_id, printer_name, *context_id_, std::move(new_settings),
      base::BindOnce(&PrinterQueryOop::OnDidUpdatePrintSettings,
                     weak_factory_.GetWeakPtr(), device_name,
                     std::move(callback)));
}

void PrinterQueryOop::OnDidUpdatePrintSettings(
    const std::string& device_name,
    SettingsCallback callback,
    base::expected<PrintSettings, mojom::ResultCode> print_settings) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  mojom::ResultCode result;
  if (!print_settings.has_value()) {
    result = print_settings.error();
    DCHECK_NE(result, mojom::ResultCode::kSuccess);
    PRINTER_LOG(ERROR) << "Error updating print settings via service for `"
                       << device_name << "`: " << result;

    // `PrintViewManagerBase` owns the client ID, so `PrinterQueryOop` must not
    // unregister it.  Just drop any local reference to it.
    query_with_ui_client_id_.reset();

    // With the failure to update the setting, the registered client must be
    // released.  The context ID is also no longer relevant to use.
    if (print_document_client_id_.has_value()) {
      PrintBackendServiceManager::GetInstance().UnregisterClient(
          print_document_client_id_.value());
      print_document_client_id_.reset();
      CHECK(context_id_.has_value());
      context_id_.reset();
    }

    // TODO(crbug.com/40561724)  Fill in support for handling of access-denied
    // result code.
  } else {
    VLOG(1) << "Update print settings via service complete for " << device_name;
    result = mojom::ResultCode::kSuccess;
    printing_context()->SetPrintSettings(print_settings.value());

    if (query_with_ui_client_id_.has_value()) {
      // Use the same PrintBackendService for querying and printing, so that the
      // same device context can be used with both.
      CHECK(!print_document_client_id_.has_value());
      print_document_client_id_ =
          PrintBackendServiceManager::GetInstance()
              .RegisterPrintDocumentClientReusingClientRemote(
                  *query_with_ui_client_id_);
      if (!print_document_client_id_.has_value()) {
        // A failure after getting settings, override result to failure.
        result = mojom::ResultCode::kFailed;
        PRINTER_LOG(ERROR)
            << "Error after updating print settings due to client registration "
               "failure; service or renderer likely has terminated";
      }
    }
  }
  InvokeSettingsCallback(std::move(callback), result);
}

#if BUILDFLAG(IS_WIN)
void PrinterQueryOop::OnDidGetPaperPrintableArea(
    PrintSettings* print_settings,
    OnDidUpdatePrintableAreaCallback callback,
    const gfx::Rect& printable_area_um) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (printable_area_um.IsEmpty()) {
    std::move(callback).Run(/*success=*/false);
    return;
  }

  print_settings->UpdatePrinterPrintableArea(printable_area_um);
  std::move(callback).Run(/*success=*/true);
}
#endif

void PrinterQueryOop::SendEstablishPrintingContext(
    PrintBackendServiceManager::ClientId client_id,
    const std::string& printer_name) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(ShouldPrintJobOop());

  DVLOG(1) << "Establishing printing context for system print";

#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
  content::WebContents* web_contents = GetWebContents();
  gfx::NativeView parent_view =
      web_contents ? web_contents->GetTopLevelNativeWindow() : nullptr;
#endif

  PrintBackendServiceManager& service_mgr =
      PrintBackendServiceManager::GetInstance();

  context_id_ = service_mgr.EstablishPrintingContext(client_id, printer_name
#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
                                                     ,
                                                     parent_view
#endif
  );
}

void PrinterQueryOop::SendUseDefaultSettings(SettingsCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(ShouldPrintJobOop());
  CHECK(query_with_ui_client_id_.has_value());

  PrintBackendServiceManager& service_mgr =
      PrintBackendServiceManager::GetInstance();

  service_mgr.UseDefaultSettings(
      *query_with_ui_client_id_, *context_id_,
      base::BindOnce(&PrinterQueryOop::OnDidUseDefaultSettings,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

#if BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)
void PrinterQueryOop::SendAskUserForSettings(uint32_t document_page_count,
                                             bool has_selection,
                                             bool is_scripted,
                                             SettingsCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(ShouldPrintJobOop());

  if (document_page_count > kMaxPageCount) {
    InvokeSettingsCallback(std::move(callback), mojom::ResultCode::kFailed);
    return;
  }

  content::WebContents* web_contents = GetWebContents();

  // Running a dialog causes an exit to webpage-initiated fullscreen.
  // http://crbug.com/41322524
  if (web_contents && web_contents->IsFullscreen()) {
    web_contents->ExitFullscreen(true);
  }

  PrintBackendServiceManager& service_mgr =
      PrintBackendServiceManager::GetInstance();
  service_mgr.AskUserForSettings(
      *query_with_ui_client_id_, *context_id_, document_page_count,
      has_selection, is_scripted,
      base::BindOnce(&PrinterQueryOop::OnDidAskUserForSettings,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}
#endif  // BUILDFLAG(ENABLE_OOP_BASIC_PRINT_DIALOG)

std::unique_ptr<PrintJobWorkerOop> PrinterQueryOop::CreatePrintJobWorkerOop(
    PrintJob* print_job) {
  return std::make_unique<PrintJobWorkerOop>(
      std::move(printing_context_delegate_), std::move(printing_context_),
      print_document_client_id_, context_id_, print_job,
      print_from_system_dialog_);
}

}  // namespace printing
