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

#include <memory>
#include <string>
#include <utility>

#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/grit/generated_resources.h"
#include "components/device_event_log/device_event_log.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/global_routing_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "printing/mojom/print.mojom.h"
#include "printing/print_job_constants.h"
#include "printing/printed_document.h"
#include "printing/printing_context.h"
#include "printing/printing_utils.h"
#include "ui/base/l10n/l10n_util.h"

#if BUILDFLAG(IS_WIN)
#include "base/threading/thread_restrictions.h"
#include "printing/printed_page_win.h"
#endif

using content::BrowserThread;

namespace printing {

namespace {

void DocDoneNotificationCallback(PrintJob* print_job,
                                 int job_id,
                                 PrintedDocument* document) {
  print_job->OnDocDone(job_id, document);
}

void FailedNotificationCallback(PrintJob* print_job) {
  print_job->OnFailed();
}

}  // namespace

bool PrintJobWorkerThread::IsRunning() const {
  return Thread::IsRunning() && !is_cleaned_up_.IsSet();
}

void PrintJobWorkerThread::CleanUp() {
  is_cleaned_up_.Set();
}

PrintJobWorker::PrintJobWorker(
    std::unique_ptr<PrintingContext::Delegate> printing_context_delegate,
    std::unique_ptr<PrintingContext> printing_context,
    PrintJob* print_job)
    : printing_context_delegate_(std::move(printing_context_delegate)),
      printing_context_(std::move(printing_context)),
      print_job_(print_job) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}

PrintJobWorker::~PrintJobWorker() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  Stop();
}

bool PrintJobWorker::StartPrintingSanityCheck(
    const PrintedDocument* new_document) const {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  if (page_number_ != PageNumber::npos()) {
    NOTREACHED();
  }

  if (!document_) {
    NOTREACHED();
  }

  if (document_.get() != new_document) {
    NOTREACHED();
  }

  return true;
}

std::u16string PrintJobWorker::GetDocumentName(
    const PrintedDocument* new_document) const {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  std::u16string document_name = SimplifyDocumentTitle(document_->name());
  if (document_name.empty()) {
    document_name = SimplifyDocumentTitle(
        l10n_util::GetStringUTF16(IDS_DEFAULT_PRINT_DOCUMENT_TITLE));
  }
  return document_name;
}

bool PrintJobWorker::SetupDocument(const std::u16string& document_name) {
  mojom::ResultCode result = printing_context_->NewDocument(document_name);
  switch (result) {
    case mojom::ResultCode::kSuccess:
      return true;
    case mojom::ResultCode::kCanceled:
      OnCancel();
      return false;
    default:
      OnFailure();
      return false;
  }
}

void PrintJobWorker::StartPrinting(PrintedDocument* new_document) {
  if (!StartPrintingSanityCheck(new_document))
    return;

  if (!SetupDocument(GetDocumentName(new_document))) {
    return;
  }

  // This will start a loop to wait for the page data.
  OnNewPage();
  // Don't touch this anymore since the instance could be destroyed. It happens
  // if all the pages are printed a one sweep and the client doesn't have a
  // handle to us anymore. There's a timing issue involved between the worker
  // thread and the UI thread. Take no chance.
}

void PrintJobWorker::OnDocumentChanged(PrintedDocument* new_document) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  if (page_number_ != PageNumber::npos()) {
    NOTREACHED();
  }

  document_ = new_document;
}

void PrintJobWorker::PostWaitForPage() {
  // We need to wait for the page to be available.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&PrintJobWorker::OnNewPage, weak_factory_.GetWeakPtr()),
      base::Milliseconds(500));
}

void PrintJobWorker::OnNewPage() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());

  if (!document_) {
    return;
  }

#if BUILDFLAG(IS_WIN)
  // Using the Windows GDI print API.
  if (!OnNewPageHelperGdi()) {
    return;
  }
#else
  if (!document_->HasDocument()) {
    PostWaitForPage();
    return;
  }
  if (!SpoolDocument()) {
    return;
  }
#endif  // BUILDFLAG(IS_WIN)

  OnDocumentDone();
  // Don't touch `this` anymore since the instance could be destroyed.
}

#if BUILDFLAG(IS_WIN)
bool PrintJobWorker::OnNewPageHelperGdi() {
  if (page_number_ == PageNumber::npos()) {
    // Find first page to print.
    int page_count = document_->page_count();
    if (!page_count) {
      // We still don't know how many pages the document contains.
      return false;
    }
    // We have enough information to initialize `page_number_`.
    page_number_.Init(document_->settings().ranges(), page_count);
  }

  while (true) {
    scoped_refptr<PrintedPage> page = document_->GetPage(page_number_.ToUint());
    if (!page) {
      PostWaitForPage();
      return false;
    }
    // The page is there, print it.
    if (!SpoolPage(page.get()))
      return false;
    ++page_number_;
    if (page_number_ == PageNumber::npos())
      break;
  }
  return true;
}
#endif  // BUILDFLAG(IS_WIN)

void PrintJobWorker::Cancel() {
  // This is the only function that can be called from any thread.
  printing_context_->Cancel();
  // Cannot touch any member variable since we don't know in which thread
  // context we run.
}

#if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
void PrintJobWorker::CleanupAfterContentAnalysisDenial() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DVLOG(1) << "Canceling job due to content analysis";
}
#endif

bool PrintJobWorker::IsRunning() const {
  return thread_.IsRunning();
}

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

void PrintJobWorker::StopSoon() {
  thread_.StopSoon();
}

void PrintJobWorker::Stop() {
  thread_.Stop();
}

bool PrintJobWorker::Start() {
  bool result = thread_.Start();
  task_runner_ = thread_.task_runner();
  return result;
}

void PrintJobWorker::CheckDocumentSpoolingComplete() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK_EQ(page_number_, PageNumber::npos());
  // PrintJob must own this, because only PrintJob can send notifications.
  DCHECK(print_job_);
}

void PrintJobWorker::OnDocumentDone() {
  CheckDocumentSpoolingComplete();

  int job_id = printing_context_->job_id();
  if (printing_context_->DocumentDone() != mojom::ResultCode::kSuccess) {
    OnFailure();
    return;
  }

  FinishDocumentDone(job_id);
}

void PrintJobWorker::FinishDocumentDone(int job_id) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK(document_);
  print_job_->PostTask(
      FROM_HERE, base::BindOnce(&DocDoneNotificationCallback,
                                base::RetainedRef(print_job_.get()), job_id,
                                base::RetainedRef(document_)));

  // Makes sure the variables are reinitialized.
  document_ = nullptr;
}

#if BUILDFLAG(IS_WIN)
bool PrintJobWorker::SpoolPage(PrintedPage* page) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK_NE(page_number_, PageNumber::npos());

  // Actual printing.
  if (document_->RenderPrintedPage(*page, printing_context_.get()) !=
      mojom::ResultCode::kSuccess) {
    OnFailure();
    return false;
  }

  // Signal everyone that the page is printed.
  DCHECK(print_job_);
  print_job_->PostTask(FROM_HERE,
                       base::BindOnce(&PrintJob::OnPageDone, print_job_,
                                      base::RetainedRef(page)));
  return true;
}
#endif  // BUILDFLAG(IS_WIN)

bool PrintJobWorker::SpoolDocument() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  mojom::ResultCode result =
      document_->RenderPrintedDocument(printing_context_.get());
  if (result != mojom::ResultCode::kSuccess) {
    PRINTER_LOG(ERROR) << "Failure to render printed document - error "
                       << result;
    OnFailure();
    return false;
  }
  return true;
}

void PrintJobWorker::OnCancel() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK(print_job_);

  print_job_->PostTask(
      FROM_HERE,
      base::BindOnce(&PrintJob::Cancel, base::RetainedRef(print_job_.get())));
}

void PrintJobWorker::OnFailure() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  DCHECK(print_job_);

  // We may loose our last reference by broadcasting the FAILED event.
  scoped_refptr<PrintJob> handle(print_job_.get());

  print_job_->PostTask(FROM_HERE,
                       base::BindOnce(&FailedNotificationCallback,
                                      base::RetainedRef(print_job_.get())));
  Cancel();

  // Makes sure the variables are reinitialized.
  document_ = nullptr;
  page_number_ = PageNumber::npos();
}

}  // namespace printing
