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

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/observer_list.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/printing/print_job_manager.h"
#include "chrome/browser/printing/print_job_worker.h"
#include "chrome/browser/printing/printer_query.h"
#include "components/enterprise/buildflags/buildflags.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "printing/mojom/print.mojom.h"
#include "printing/printed_document.h"

#if BUILDFLAG(IS_WIN)
#include <optional>

#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/pdf/pdf_pref_names.h"
#include "chrome/browser/printing/pdf_to_emf_converter.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "printing/backend/win_helper.h"
#include "printing/page_number.h"
#include "printing/pdf_render_settings.h"
#include "printing/printed_page_win.h"
#include "printing/printing_features.h"
#include "url/gurl.h"
#endif


namespace printing {

namespace {

// Helper function to ensure `job` is valid until at least `callback` returns.
void HoldRefCallback(scoped_refptr<PrintJob> job, base::OnceClosure callback) {
  std::move(callback).Run();
}

#if BUILDFLAG(IS_WIN)
// Those must be kept in sync with the values defined in policy_templates.json.
enum class PrintPostScriptMode {
  // Do normal PostScript generation. Text is always rendered with Type 3 fonts.
  // Default value when policy not set.
  kDefault = 0,
  // Text is rendered with Type 42 fonts if possible.
  kType42 = 1,
  kMaxValue = kType42,
};

// Those must be kept in sync with the values defined in policy_templates.json.
enum class PrintRasterizationMode {
  // Do full page rasterization if necessary. Default value when policy not set.
  kFull = 0,
  // Avoid rasterization if possible.
  kFast = 1,
  kMaxValue = kFast,
};

bool PrintWithPostScriptType42Fonts(PrefService* prefs) {
  // Managed preference takes precedence over user preference and field trials.
  if (prefs && prefs->IsManagedPreference(prefs::kPrintPostScriptMode)) {
    int value = prefs->GetInteger(prefs::kPrintPostScriptMode);
    return value == static_cast<int>(PrintPostScriptMode::kType42);
  }

  return base::FeatureList::IsEnabled(
      features::kPrintWithPostScriptType42Fonts);
}

bool PrintWithReducedRasterization(PrefService* prefs) {
  // Managed preference takes precedence over user preference and field trials.
  if (prefs && prefs->IsManagedPreference(prefs::kPrintRasterizationMode)) {
    int value = prefs->GetInteger(prefs::kPrintRasterizationMode);
    return value == static_cast<int>(PrintRasterizationMode::kFast);
  }

  return base::FeatureList::IsEnabled(features::kPrintWithReducedRasterization);
}

PrefService* GetPrefsForWebContents(content::WebContents* web_contents) {
  // TODO(thestig): Figure out why crbug.com/40692772 occurred, which is likely
  // because `web_contents` was null. As a result, this section has many more
  // pointer checks to avoid crashing.
  content::BrowserContext* context =
      web_contents ? web_contents->GetBrowserContext() : nullptr;
  return context ? Profile::FromBrowserContext(context)->GetPrefs() : nullptr;
}

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

#endif  // BUILDFLAG(IS_WIN)

}  // namespace

#if BUILDFLAG(IS_WIN)
class PrintJob::PdfConversionState {
 public:
  PdfConversionState(const gfx::Size& page_size,
                     const gfx::Rect& content_area,
                     const std::optional<bool>& use_skia,
                     const GURL& url)
      : page_size_(page_size),
        content_area_(content_area),
        use_skia_(use_skia),
        url_(url) {}

  void Start(scoped_refptr<base::RefCountedMemory> data,
             const PdfRenderSettings& conversion_settings,
             PdfConverter::StartCallback start_callback) {
    converter_ = PdfConverter::StartPdfConverter(
        data, conversion_settings, use_skia_, url_, std::move(start_callback));
  }

  void GetMorePages(PdfConverter::GetPageCallback get_page_callback) {
    const int kMaxNumberOfTempFilesPerDocument = 3;
    while (pages_in_progress_ < kMaxNumberOfTempFilesPerDocument &&
           current_page_index_ < page_count_) {
      ++pages_in_progress_;
      converter_->GetPage(current_page_index_++, get_page_callback);
    }
  }

  void OnPageProcessed(PdfConverter::GetPageCallback get_page_callback) {
    --pages_in_progress_;
    GetMorePages(get_page_callback);
    // Release converter if we don't need this any more.
    if (!pages_in_progress_ && current_page_index_ >= page_count_) {
      converter_.reset();
    }
  }

  void set_page_count(uint32_t page_count) { page_count_ = page_count; }
  const gfx::Size& page_size() const { return page_size_; }
  const gfx::Rect& content_area() const { return content_area_; }

 private:
  uint32_t page_count_ = 0;
  uint32_t current_page_index_ = 0;
  int pages_in_progress_ = 0;
  const gfx::Size page_size_;
  const gfx::Rect content_area_;
  const std::optional<bool> use_skia_;
  const GURL url_;
  std::unique_ptr<PdfConverter> converter_;
};
#endif  // BUILDFLAG(IS_WIN)

PrintJob::PrintJob(PrintJobManager* print_job_manager)
    : print_job_manager_(print_job_manager) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(print_job_manager_);
}

PrintJob::PrintJob() = default;

PrintJob::~PrintJob() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // The job should be finished (or at least canceled) when it is destroyed.
  DCHECK(!is_job_pending_);
  DCHECK(!is_canceling_);
  DCHECK(!worker_ || !worker_->IsRunning());

  for (auto& observer : observers_) {
    observer.OnDestruction();
  }
}

void PrintJob::Initialize(std::unique_ptr<PrinterQuery> query,
                          const std::u16string& name,
                          uint32_t page_count) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!worker_);
  DCHECK(!is_job_pending_);
  DCHECK(!is_canceling_);
  DCHECK(!document_);
  worker_ = query->TransferContextToNewWorker(this);
  worker_->Start();
  rfh_id_ = query->rfh_id();
  std::unique_ptr<PrintSettings> settings = query->ExtractSettings();

#if BUILDFLAG(IS_WIN)
  pdf_page_mapping_ = PageNumber::GetPages(settings->ranges(), page_count);
  PrefService* prefs = GetPrefsForWebContents(GetWebContents(rfh_id_));
  if (prefs && prefs->IsManagedPreference(prefs::kPdfUseSkiaRendererEnabled)) {
    use_skia_ = prefs->GetBoolean(prefs::kPdfUseSkiaRendererEnabled);
  }
#endif

  auto new_doc = base::MakeRefCounted<PrintedDocument>(std::move(settings),
                                                       name, query->cookie());
  new_doc->set_page_count(page_count);
  UpdatePrintedDocument(new_doc);
}

#if BUILDFLAG(IS_WIN)
// static
std::vector<uint32_t> PrintJob::GetFullPageMapping(
    const std::vector<uint32_t>& pages,
    uint32_t total_page_count) {
  std::vector<uint32_t> mapping(total_page_count, kInvalidPageIndex);
  for (uint32_t page_index : pages) {
    // Make sure the page is in range.
    if (page_index < total_page_count) {
      mapping[page_index] = page_index;
    }
  }
  return mapping;
}

void PrintJob::StartConversionToNativeFormat(
    scoped_refptr<base::RefCountedMemory> print_data,
    const gfx::Size& page_size,
    const gfx::Rect& content_area,
    const gfx::Point& physical_offsets,
    const GURL& url) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (PrintedDocument::HasDebugDumpPath())
    document()->DebugDumpData(print_data.get(), FILE_PATH_LITERAL(".pdf"));

  const PrintSettings& settings = document()->settings();
  if (settings.printer_language_is_textonly()) {
    StartPdfToTextConversion(print_data, page_size, url);
  } else if (settings.printer_language_is_ps2() ||
             settings.printer_language_is_ps3()) {
    StartPdfToPostScriptConversion(print_data, content_area, physical_offsets,
                                   settings.printer_language_is_ps2(), url);
  } else {
    StartPdfToEmfConversion(print_data, page_size, content_area, url);
  }

  // Indicate that the PDF is fully rendered and we no longer need the renderer
  // and web contents, so the print job does not need to be cancelled if they
  // die. This is needed on Windows because the `PrintedDocument` will not be
  // considered complete until PDF conversion finishes.
  document()->SetConvertingPdf();
}

void PrintJob::ResetPageMapping() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  pdf_page_mapping_ =
      GetFullPageMapping(pdf_page_mapping_, document_->page_count());
}
#endif  // BUILDFLAG(IS_WIN)

void PrintJob::StartPrinting() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!worker_->IsRunning() || is_job_pending_) {
    NOTREACHED();
  }

#if BUILDFLAG(IS_WIN)
  // Do not collect duration metric if the print job will need to invoke a
  // "Save Print Output As" dialog that waits on a user to select a filename.
  const std::string printer_name =
      base::UTF16ToUTF8(document_->settings().device_name());
  const bool capture_printing_time =
      !DoesDriverDisplayFileDialogForPrinting(printer_name);
#else
  constexpr bool capture_printing_time = true;
#endif
  if (capture_printing_time) {
    printing_start_time_ = base::TimeTicks::Now();
  }

  // Real work is done in `PrintJobWorker::StartPrinting()`.
  worker_->PostTask(
      FROM_HERE, base::BindOnce(&HoldRefCallback, base::WrapRefCounted(this),
                                base::BindOnce(&PrintJobWorker::StartPrinting,
                                               base::Unretained(worker_.get()),
                                               base::RetainedRef(document_))));
  // Set the flag right now.
  is_job_pending_ = true;

  print_job_manager_->OnStarted(this);
}

void PrintJob::Stop() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (quit_closure_) {
    // In case we're running a nested run loop to wait for a job to finish,
    // and we finished before the timeout, quit the nested loop right away.
    std::move(quit_closure_).Run();
  }

  // Be sure to live long enough.
  scoped_refptr<PrintJob> handle(this);

  if (worker_->IsRunning()) {
    ControlledWorkerShutdown();
  } else {
    // Flush the cached document.
    is_job_pending_ = false;
    ClearPrintedDocument();
  }
}

void PrintJob::Cancel() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (is_canceling_)
    return;

  is_canceling_ = true;

  for (auto& observer : observers_) {
    observer.OnCanceling();
  }

  if (worker_ && worker_->IsRunning()) {
    // Call this right now so it renders the context invalid. Do not use
    // InvokeLater since it would take too much time.
    worker_->Cancel();
  }
  OnFailed();
  is_canceling_ = false;
}

#if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
void PrintJob::CleanupAfterContentAnalysisDenial() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  worker_->CleanupAfterContentAnalysisDenial();
}
#endif

bool PrintJob::FlushJob(base::TimeDelta timeout) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Make sure the object outlive this message loop.
  scoped_refptr<PrintJob> handle(this);

  base::RunLoop loop(base::RunLoop::Type::kNestableTasksAllowed);
  quit_closure_ = loop.QuitClosure();
  content::GetUIThreadTaskRunner({})->PostDelayedTask(
      FROM_HERE, loop.QuitClosure(), timeout);

  loop.Run();

  return true;
}

bool PrintJob::is_job_pending() const {
  return is_job_pending_;
}

PrintedDocument* PrintJob::document() const {
  return document_.get();
}

const PrintSettings& PrintJob::settings() const {
  return document()->settings();
}

#if BUILDFLAG(IS_CHROMEOS)
void PrintJob::SetSource(PrintJob::Source source,
                         const std::string& source_id) {
  source_ = source;
  source_id_ = source_id;
}

PrintJob::Source PrintJob::source() const {
  return source_;
}

const std::string& PrintJob::source_id() const {
  return source_id_;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_WIN)
void PrintJob::StartPdfToEmfConversion(
    scoped_refptr<base::RefCountedMemory> bytes,
    const gfx::Size& page_size,
    const gfx::Rect& content_area,
    const GURL& url) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!pdf_conversion_state_);
  pdf_conversion_state_ = std::make_unique<PdfConversionState>(
      page_size, content_area, use_skia_, url);

  const PrintSettings& settings = document()->settings();

  PrefService* prefs = GetPrefsForWebContents(GetWebContents(rfh_id_));
  bool print_with_reduced_rasterization = PrintWithReducedRasterization(prefs);

  using RenderMode = PdfRenderSettings::Mode;
  RenderMode mode = print_with_reduced_rasterization
                        ? RenderMode::EMF_WITH_REDUCED_RASTERIZATION
                        : RenderMode::NORMAL;

  PdfRenderSettings render_settings(
      content_area, gfx::Point(0, 0), settings.dpi_size(),
      /*autorotate=*/true, settings.color() == mojom::ColorModel::kColor, mode);
  pdf_conversion_state_->Start(
      bytes, render_settings,
      base::BindOnce(&PrintJob::OnPdfConversionStarted, this));
}

void PrintJob::OnPdfConversionStarted(uint32_t page_count) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (page_count <= 0) {
    // Be sure to live long enough.
    scoped_refptr<PrintJob> handle(this);
    pdf_conversion_state_.reset();
    Cancel();
    return;
  }
  pdf_conversion_state_->set_page_count(page_count);
  pdf_conversion_state_->GetMorePages(
      base::BindRepeating(&PrintJob::OnPdfPageConverted, this));
}

void PrintJob::OnPdfPageConverted(uint32_t page_index,
                                  float scale_factor,
                                  std::unique_ptr<MetafilePlayer> metafile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(pdf_conversion_state_);
  if (!document_ || !metafile || page_index == kInvalidPageIndex ||
      page_index >= pdf_page_mapping_.size()) {
    // Be sure to live long enough.
    scoped_refptr<PrintJob> handle(this);
    pdf_conversion_state_.reset();
    Cancel();
    return;
  }

  // Add the page to the document if it is one of the pages requested by the
  // user. If it is not, ignore it.
  if (pdf_page_mapping_[page_index] != kInvalidPageIndex) {
    // Update the rendered document. It will send notifications to the listener.
    document_->SetPage(pdf_page_mapping_[page_index], std::move(metafile),
                       scale_factor, pdf_conversion_state_->page_size(),
                       pdf_conversion_state_->content_area());
  }

  pdf_conversion_state_->GetMorePages(
      base::BindRepeating(&PrintJob::OnPdfPageConverted, this));
}

void PrintJob::StartPdfToTextConversion(
    scoped_refptr<base::RefCountedMemory> bytes,
    const gfx::Size& page_size,
    const GURL& url) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!pdf_conversion_state_);
  pdf_conversion_state_ = std::make_unique<PdfConversionState>(
      gfx::Size(), gfx::Rect(), use_skia_, url);
  gfx::Rect page_area = gfx::Rect(0, 0, page_size.width(), page_size.height());
  const PrintSettings& settings = document()->settings();
  PdfRenderSettings render_settings(
      page_area, gfx::Point(0, 0), settings.dpi_size(),
      /*autorotate=*/true,
      /*use_color=*/true, PdfRenderSettings::Mode::TEXTONLY);
  pdf_conversion_state_->Start(
      bytes, render_settings,
      base::BindOnce(&PrintJob::OnPdfConversionStarted, this));
}

void PrintJob::StartPdfToPostScriptConversion(
    scoped_refptr<base::RefCountedMemory> bytes,
    const gfx::Rect& content_area,
    const gfx::Point& physical_offsets,
    bool ps_level2,
    const GURL& url) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!pdf_conversion_state_);
  pdf_conversion_state_ = std::make_unique<PdfConversionState>(
      gfx::Size(), gfx::Rect(), use_skia_, url);
  const PrintSettings& settings = document()->settings();

  PdfRenderSettings::Mode mode;
  if (ps_level2) {
    mode = PdfRenderSettings::Mode::POSTSCRIPT_LEVEL2;
  } else {
    PrefService* prefs = GetPrefsForWebContents(GetWebContents(rfh_id_));
    mode = PrintWithPostScriptType42Fonts(prefs)
               ? PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3_WITH_TYPE42_FONTS
               : PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3;
  }
  PdfRenderSettings render_settings(
      content_area, physical_offsets, settings.dpi_size(),
      /*autorotate=*/true, settings.color() == mojom::ColorModel::kColor, mode);
  pdf_conversion_state_->Start(
      bytes, render_settings,
      base::BindOnce(&PrintJob::OnPdfConversionStarted, this));
}
#endif  // BUILDFLAG(IS_WIN)

void PrintJob::UpdatePrintedDocument(
    scoped_refptr<PrintedDocument> new_document) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(new_document);

  document_ = new_document;
  if (worker_)
    SyncPrintedDocumentToWorker();
}

void PrintJob::ClearPrintedDocument() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!document_)
    return;

  document_ = nullptr;
  if (worker_)
    SyncPrintedDocumentToWorker();
}

void PrintJob::SyncPrintedDocumentToWorker() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(worker_);
  DCHECK(!is_job_pending_);
  worker_->PostTask(
      FROM_HERE,
      base::BindOnce(&HoldRefCallback, base::WrapRefCounted(this),
                     base::BindOnce(&PrintJobWorker::OnDocumentChanged,
                                    base::Unretained(worker_.get()),
                                    base::RetainedRef(document_))));
}

#if BUILDFLAG(IS_WIN)
void PrintJob::OnPageDone(PrintedPage* page) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (pdf_conversion_state_) {
    pdf_conversion_state_->OnPageProcessed(
        base::BindRepeating(&PrintJob::OnPdfPageConverted, this));
  }
  document_->RemovePage(page);
}
#endif  // BUILDFLAG(IS_WIN)

void PrintJob::OnFailed() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  Stop();

  print_job_manager_->OnFailed(this);

  for (auto& observer : observers_) {
    observer.OnFailed();
  }
}

void PrintJob::OnDocDone(int job_id, PrintedDocument* document) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (printing_start_time_.has_value()) {
    base::UmaHistogramMediumTimes(
        "Printing.PrintDuration.LocalPrinter.Success",
        base::TimeTicks::Now() - printing_start_time_.value());
    printing_start_time_.reset();
  }

  print_job_manager_->OnDocDone(this, document, job_id);

  for (auto& observer : observers_) {
    observer.OnDocDone(job_id, document);
  }

  // This will call `Stop()` and broadcast a `JOB_DONE` message.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&PrintJob::OnDocumentDone, this));
}

void PrintJob::OnDocumentDone() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Be sure to live long enough. The instance could be destroyed by the
  // `JOB_DONE` broadcast.
  scoped_refptr<PrintJob> handle(this);

  // Stop the worker thread.
  Stop();

  print_job_manager_->OnJobDone(this);

  for (auto& observer : observers_) {
    observer.OnJobDone();
  }
}

void PrintJob::ControlledWorkerShutdown() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // The deadlock this code works around is specific to window messaging on
  // Windows, so we aren't likely to need it on any other platforms.
#if BUILDFLAG(IS_WIN)
  // We could easily get into a deadlock case if worker_->Stop() is used; the
  // printer driver created a window as a child of the browser window. By
  // canceling the job, the printer driver initiated dialog box is destroyed,
  // which sends a blocking message to its parent window. If the browser window
  // thread is not processing messages, a deadlock occurs.
  //
  // This function ensures that the dialog box will be destroyed in a timely
  // manner by the mere fact that the thread will terminate. So the potential
  // deadlock is eliminated.
  worker_->StopSoon();

  // Delay shutdown until the worker terminates.  We want this code path
  // to wait on the thread to quit before continuing.
  if (worker_->IsRunning()) {
    content::GetUIThreadTaskRunner({})->PostDelayedTask(
        FROM_HERE, base::BindOnce(&PrintJob::ControlledWorkerShutdown, this),
        base::Milliseconds(100));
    return;
  }
#endif

  // Now make sure the thread object is cleaned up. Do this on a worker
  // thread because it may block.
  base::ThreadPool::PostTaskAndReply(
      FROM_HERE,
      {base::MayBlock(), base::WithBaseSyncPrimitives(),
       base::TaskPriority::BEST_EFFORT,
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(&PrintJobWorker::Stop, base::Unretained(worker_.get())),
      base::BindOnce(&PrintJob::HoldUntilStopIsCalled, this));

  is_job_pending_ = false;
  ClearPrintedDocument();
}

bool PrintJob::PostTask(const base::Location& from_here,
                        base::OnceClosure task) {
  return content::GetUIThreadTaskRunner({})->PostTask(from_here,
                                                      std::move(task));
}

void PrintJob::HoldUntilStopIsCalled() {
}

void PrintJob::set_job_pending_for_testing(bool pending) {
  is_job_pending_ = pending;
}

void PrintJob::AddObserver(Observer& observer) {
  observers_.AddObserver(&observer);
}

void PrintJob::RemoveObserver(Observer& observer) {
  observers_.RemoveObserver(&observer);
}

}  // namespace printing
