// 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 "chrome/browser/printing/printer_query.h"

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/printing/print_job_worker.h"
#include "components/crash/core/common/crash_keys.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_task_traits.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/backend/print_backend.h"
#include "printing/buildflags/buildflags.h"
#include "printing/print_settings.h"
#include "ui/gfx/native_ui_types.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/android/tab_android.h"
#include "printing/printing_context_android.h"
#endif

#if BUILDFLAG(ENABLE_OOP_PRINTING)
#include "chrome/browser/printing/oop_features.h"
#include "chrome/browser/printing/printer_query_oop.h"
#endif

#if BUILDFLAG(IS_WIN)
#include "base/strings/utf_string_conversions.h"
#endif

#if BUILDFLAG(IS_ANDROID)
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/TabPrinter_jni.h"  // nogncheck
#endif

namespace printing {

namespace {

PrintingContext::OutOfProcessBehavior GetPrintingContextOutOfProcessBehavior() {
#if BUILDFLAG(ENABLE_OOP_PRINTING)
  if (ShouldPrintJobOop()) {
    return PrintingContext::OutOfProcessBehavior::kEnabledSkipSystemCalls;
  }
#endif
  return PrintingContext::OutOfProcessBehavior::kDisabled;
}

class PrintingContextDelegate : public PrintingContext::Delegate {
 public:
  explicit PrintingContextDelegate(content::GlobalRenderFrameHostId rfh_id);

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

  ~PrintingContextDelegate() override;

  gfx::NativeView GetParentView() override;
  std::string GetAppLocale() override;

  // Not exposed to PrintingContext::Delegate because of dependency issues.
  content::WebContents* GetWebContents();

  content::GlobalRenderFrameHostId rfh_id() const { return rfh_id_; }

 private:
  const content::GlobalRenderFrameHostId rfh_id_;
};

PrintingContextDelegate::PrintingContextDelegate(
    content::GlobalRenderFrameHostId rfh_id)
    : rfh_id_(rfh_id) {}

PrintingContextDelegate::~PrintingContextDelegate() = default;

gfx::NativeView PrintingContextDelegate::GetParentView() {
  content::WebContents* wc = GetWebContents();
  return wc ? wc->GetNativeView() : gfx::NativeView();
}

content::WebContents* PrintingContextDelegate::GetWebContents() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  auto* rfh = content::RenderFrameHost::FromID(rfh_id_);
  return rfh ? content::WebContents::FromRenderFrameHost(rfh) : nullptr;
}

std::string PrintingContextDelegate::GetAppLocale() {
  return g_browser_process->GetApplicationLocale();
}

CreatePrinterQueryCallback* g_create_printer_query_for_testing = nullptr;

}  // namespace

// static
std::unique_ptr<PrinterQuery> PrinterQuery::Create(
    content::GlobalRenderFrameHostId rfh_id) {
  if (g_create_printer_query_for_testing) {
    return g_create_printer_query_for_testing->Run(rfh_id);
  }

#if BUILDFLAG(ENABLE_OOP_PRINTING)
  if (ShouldPrintJobOop()) {
    return base::WrapUnique(new PrinterQueryOop(rfh_id));
  }
#endif
  return base::WrapUnique(new PrinterQuery(rfh_id));
}

PrinterQuery::PrinterQuery(content::GlobalRenderFrameHostId rfh_id)
    : printing_context_delegate_(
          std::make_unique<PrintingContextDelegate>(rfh_id)),
      printing_context_(
          PrintingContext::Create(printing_context_delegate_.get(),
                                  GetPrintingContextOutOfProcessBehavior())),
      rfh_id_(rfh_id),
      cookie_(PrintSettings::NewCookie()) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}

PrinterQuery::~PrinterQuery() {
  // The job should be finished (or at least canceled) when it is destroyed.
  DCHECK(!is_print_dialog_box_shown_);
}

void PrinterQuery::GetSettingsDone(base::OnceClosure callback,
                                   std::optional<bool> maybe_is_modifiable,
                                   std::unique_ptr<PrintSettings> new_settings,
                                   mojom::ResultCode result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  is_print_dialog_box_shown_ = false;
  last_status_ = result;
  if (result == mojom::ResultCode::kSuccess) {
    settings_ = std::move(new_settings);
    if (maybe_is_modifiable.has_value())
      settings_->set_is_modifiable(maybe_is_modifiable.value());
    cookie_ = PrintSettings::NewCookie();
  } else {
    // Failure.
    cookie_ = PrintSettings::NewInvalidCookie();
  }

  std::move(callback).Run();
}

void PrinterQuery::PostSettingsDone(base::OnceClosure callback,
                                    std::optional<bool> maybe_is_modifiable,
                                    std::unique_ptr<PrintSettings> new_settings,
                                    mojom::ResultCode result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // `this` is owned by `callback`, so `base::Unretained()` is safe.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&PrinterQuery::GetSettingsDone, base::Unretained(this),
                     std::move(callback), maybe_is_modifiable,
                     std::move(new_settings), result));
}

std::unique_ptr<PrintJobWorker> PrinterQuery::TransferContextToNewWorker(
    PrintJob* print_job) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  return CreatePrintJobWorker(print_job);
}

const PrintSettings& PrinterQuery::settings() const {
  return *settings_;
}

std::unique_ptr<PrintSettings> PrinterQuery::ExtractSettings() {
  return std::move(settings_);
}

void PrinterQuery::SetSettingsForTest(std::unique_ptr<PrintSettings> settings) {
  settings_ = std::move(settings);
}

int PrinterQuery::cookie() const {
  return cookie_;
}

void PrinterQuery::GetDefaultSettings(base::OnceClosure callback,
                                      bool is_modifiable,
                                      bool want_pdf_settings) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Real work is done in PrinterQuery::UseDefaultSettings().
  is_print_dialog_box_shown_ = false;
  if (want_pdf_settings) {
    // `GetPdfSettings()` is always guaranteed to succeed.
    std::unique_ptr<PrintSettings> pdf_settings = GetPdfSettings();
    DCHECK(pdf_settings);
    PostSettingsDone(std::move(callback), is_modifiable,
                     std::move(pdf_settings), mojom::ResultCode::kSuccess);
    return;
  }

  printing_context_->set_margin_type(
      printing::mojom::MarginType::kDefaultMargins);
  // `this` is owned by `callback`, so `base::Unretained()` is safe.
  UseDefaultSettings(base::BindOnce(&PrinterQuery::PostSettingsDone,
                                    base::Unretained(this), std::move(callback),
                                    is_modifiable));
}

void PrinterQuery::GetSettingsFromUser(uint32_t document_page_count,
                                       bool has_selection,
                                       mojom::MarginType margin_type,
                                       bool is_scripted,
                                       bool is_modifiable,
                                       base::OnceClosure callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!is_print_dialog_box_shown_ || !is_scripted);

  // Real work is done in GetSettingsWithUI().
  is_print_dialog_box_shown_ = true;
  printing_context_->set_margin_type(margin_type);
  // `this` is owned by `callback`, so `base::Unretained()` is safe.
  GetSettingsWithUI(
      document_page_count, has_selection, is_scripted,
      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
                     std::move(callback), is_modifiable));
}

void PrinterQuery::SetSettings(base::DictValue new_settings,
                               base::OnceClosure callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // `this` is owned by `callback`, so `base::Unretained()` is safe.
  UpdatePrintSettings(
      std::move(new_settings),
      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
                     std::move(callback),
                     /*maybe_is_modifiable=*/std::nullopt));
}

#if BUILDFLAG(IS_CHROMEOS)
void PrinterQuery::SetSettingsFromPOD(
    std::unique_ptr<PrintSettings> new_settings,
    base::OnceClosure callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // `this` is owned by `callback`, so `base::Unretained()` is safe.
  UpdatePrintSettingsFromPOD(
      std::move(new_settings),
      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
                     std::move(callback),
                     /*maybe_is_modifiable=*/std::nullopt));
}
#endif

#if BUILDFLAG(IS_WIN)
void PrinterQuery::UpdatePrintableArea(
    PrintSettings* print_settings,
    OnDidUpdatePrintableAreaCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  scoped_refptr<PrintBackend> print_backend =
      PrintBackend::CreateInstance(g_browser_process->GetApplicationLocale());

  // Blocking is needed here because Windows printer drivers are oftentimes
  // not thread-safe and have to be accessed on the UI thread.
  base::ScopedAllowBlocking allow_blocking;
  std::string printer_name = base::UTF16ToUTF8(print_settings->device_name());
  crash_keys::ScopedPrinterInfo crash_key(
      printer_name, print_backend->GetPrinterDriverInfo(printer_name));

  PRINTER_LOG(EVENT) << "Updating paper printable area in-process for "
                     << printer_name;

  const PrintSettings::RequestedMedia& media =
      print_settings->requested_media();
  std::optional<gfx::Rect> printable_area_um =
      print_backend->GetPaperPrintableArea(printer_name, media.vendor_id,
                                           media.size_microns);
  if (!printable_area_um.has_value()) {
    std::move(callback).Run(/*success=*/false);
    return;
  }

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

// static
void PrinterQuery::ApplyDefaultPrintableAreaToVirtualPrinterPrintSettings(
    PrintSettings& print_settings) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // The purpose of `print_context` is to set the default printable area. To do
  // so, it doesn't need a RFH, so just default initialize the RFH id.
  PrintingContextDelegate delegate((content::GlobalRenderFrameHostId()));
  std::unique_ptr<PrintingContext> print_context = PrintingContext::Create(
      &delegate, PrintingContext::OutOfProcessBehavior::kDisabled);
  print_context->SetPrintSettings(print_settings);
  print_context->SetDefaultPrintableAreaForVirtualPrinters();
  print_settings = print_context->settings();
}

#if BUILDFLAG(ENABLE_OOP_PRINTING)
void PrinterQuery::SetClientId(PrintBackendServiceManager::ClientId client_id) {
  // Only supposed to be called for `PrinterQueryOop` objects.
  NOTREACHED();
}
#endif

std::unique_ptr<PrintSettings> PrinterQuery::GetPdfSettings() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  printing_context_->UsePdfSettings();
  return printing_context_->TakeAndResetSettings();
}

void PrinterQuery::InvokeSettingsCallback(SettingsCallback callback,
                                          mojom::ResultCode result) {
  std::move(callback).Run(printing_context_->TakeAndResetSettings(), result);
}

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

  std::unique_ptr<crash_keys::ScopedPrinterInfo> crash_key;
  mojom::PrinterType type = static_cast<mojom::PrinterType>(
      new_settings.FindInt(kSettingPrinterType).value());
  if (type == mojom::PrinterType::kLocal) {
#if BUILDFLAG(IS_WIN)
    // Blocking is needed here because Windows printer drivers are oftentimes
    // not thread-safe and have to be accessed on the UI thread.
    base::ScopedAllowBlocking allow_blocking;
#endif
    scoped_refptr<PrintBackend> print_backend =
        PrintBackend::CreateInstance(g_browser_process->GetApplicationLocale());
    std::string printer_name = *new_settings.FindString(kSettingDeviceName);
    crash_key = std::make_unique<crash_keys::ScopedPrinterInfo>(
        printer_name, print_backend->GetPrinterDriverInfo(printer_name));

#if BUILDFLAG(IS_LINUX) && BUILDFLAG(USE_CUPS)
    PrinterBasicInfo basic_info;
    if (print_backend->GetPrinterBasicInfo(printer_name, &basic_info) ==
        mojom::ResultCode::kSuccess) {
      base::DictValue advanced_settings;
      for (const auto& pair : basic_info.options) {
        advanced_settings.Set(pair.first, pair.second);
      }

      new_settings.Set(kSettingAdvancedSettings, std::move(advanced_settings));
    }
#endif  // BUILDFLAG(IS_LINUX) && BUILDFLAG(USE_CUPS)
  }

  mojom::ResultCode result;
  {
#if BUILDFLAG(IS_WIN)
    // Blocking is needed here because Windows printer drivers are oftentimes
    // not thread-safe and have to be accessed on the UI thread.
    base::ScopedAllowBlocking allow_blocking;
#endif
    result = printing_context_->UpdatePrintSettings(std::move(new_settings));
  }

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

#if BUILDFLAG(IS_CHROMEOS)
void PrinterQuery::UpdatePrintSettingsFromPOD(
    std::unique_ptr<PrintSettings> new_settings,
    SettingsCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  mojom::ResultCode result =
      printing_context_->UpdatePrintSettingsFromPOD(std::move(new_settings));
  InvokeSettingsCallback(std::move(callback), result);
}
#endif

std::unique_ptr<PrintJobWorker> PrinterQuery::CreatePrintJobWorker(
    PrintJob* print_job) {
  return std::make_unique<PrintJobWorker>(std::move(printing_context_delegate_),
                                          std::move(printing_context_),
                                          print_job);
}

void PrinterQuery::GetSettingsWithUI(uint32_t document_page_count,
                                     bool has_selection,
                                     bool is_scripted,
                                     SettingsCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

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

  content::WebContents* web_contents = GetWebContents();

#if BUILDFLAG(IS_ANDROID)
  if (is_scripted) {
    TabAndroid* tab =
        web_contents ? TabAndroid::FromWebContents(web_contents) : nullptr;

    // Regardless of whether the following call fails or not, the javascript
    // call will return since startPendingPrint will make it return immediately
    // in case of error.
    if (tab) {
      JNIEnv* env = base::android::AttachCurrentThread();
      auto* printing_context_delegate = static_cast<PrintingContextDelegate*>(
          printing_context_delegate_.get());
      // TODO(crbug.com/379869738) Remove GetUnsafeValue.
      PrintingContextAndroid::SetPendingPrint(
          web_contents->GetTopLevelNativeWindow(),
          Java_TabPrinter_getPrintable(env, tab->GetJavaObject()),
          printing_context_delegate->rfh_id().child_id.GetUnsafeValue(),
          printing_context_delegate->rfh_id().frame_routing_id);
    }
  }
#endif

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

  PRINTER_LOG(EVENT) << "Getting printer settings from user in-process";
  printing_context_->AskUserForSettings(
      base::checked_cast<int>(document_page_count), has_selection, is_scripted,
      base::BindOnce(&PrinterQuery::InvokeSettingsCallback,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void PrinterQuery::UseDefaultSettings(SettingsCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  PRINTER_LOG(EVENT) << "Using printer default settings in-process";
  mojom::ResultCode result;
  {
#if BUILDFLAG(IS_WIN)
    // Blocking is needed here because Windows printer drivers are oftentimes
    // not thread-safe and have to be accessed on the UI thread.
    base::ScopedAllowBlocking allow_blocking;
#endif
    result = printing_context_->UseDefaultSettings();
  }
  InvokeSettingsCallback(std::move(callback), result);
}

// static
void PrinterQuery::SetCreatePrinterQueryCallbackForTest(
    CreatePrinterQueryCallback* callback) {
  g_create_printer_query_for_testing = callback;
}

bool PrinterQuery::is_valid() const {
  return !!printing_context_;
}

content::WebContents* PrinterQuery::GetWebContents() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  PrintingContextDelegate* printing_context_delegate =
      static_cast<PrintingContextDelegate*>(printing_context_delegate_.get());
  return printing_context_delegate->GetWebContents();
}

}  // namespace printing
