/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWebEngine module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

// This is based on chrome/browser/printing/print_view_manager_base.cc:
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE.Chromium file.

#include "print_view_manager_qt.h"

#include "type_conversion.h"
#include "web_engine_context.h"

#include "base/memory/ref_counted_memory.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/task/current_thread.h"
#include "base/task/post_task.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/printing/print_job_manager.h"
#include "chrome/browser/printing/printer_query.h"
#include "components/printing/browser/print_manager_utils.h"
#include "components/printing/common/print.mojom.h"
#include "components/printing/common/print_messages.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "printing/metafile_skia.h"
#include "printing/print_job_constants.h"
#include "printing/printed_document.h"

namespace QtWebEngineCore {

namespace {

// Runs |callback| with |params| to reply to
// mojom::PrintManagerHost::GetDefaultPrintSettings.
void GetDefaultPrintSettingsReply(printing::mojom::PrintManagerHost::GetDefaultPrintSettingsCallback callback,
                                  printing::mojom::PrintParamsPtr params)
{
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    std::move(callback).Run(std::move(params));
}

void GetDefaultPrintSettingsReplyOnIO(scoped_refptr<printing::PrintQueriesQueue> queue,
                                      std::unique_ptr<printing::PrinterQuery> printer_query,
                                      printing::mojom::PrintManagerHost::GetDefaultPrintSettingsCallback callback)
{
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
    printing::mojom::PrintParamsPtr params = printing::mojom::PrintParams::New();
    if (printer_query && printer_query->last_status() == printing::PrintingContext::OK) {
        RenderParamsFromPrintSettings(printer_query->settings(), params.get());
        params->document_cookie = printer_query->cookie();
    }

    content::GetUIThreadTaskRunner({})->PostTask(
                FROM_HERE,
                base::BindOnce(&GetDefaultPrintSettingsReply,
                               std::move(callback), std::move(params)));

    // If printing was enabled.
    if (printer_query) {
        // If user hasn't cancelled.
        if (printer_query->cookie() && printer_query->settings().dpi()) {
            queue->QueuePrinterQuery(std::move(printer_query));
        } else {
            printer_query->StopWorker();
        }
    }
}

void GetDefaultPrintSettingsOnIO(printing::mojom::PrintManagerHost::GetDefaultPrintSettingsCallback callback,
                                 scoped_refptr<printing::PrintQueriesQueue> queue,
                                 int process_id, int routing_id)
{
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

    std::unique_ptr<printing::PrinterQuery> printer_query = queue->PopPrinterQuery(0);
    if (!printer_query)
        printer_query = queue->CreatePrinterQuery(process_id, routing_id);

    // Loads default settings. This is asynchronous, only the mojo message sender
    // will hang until the settings are retrieved.
    auto *printer_query_ptr = printer_query.get();
    printer_query_ptr->GetSettings(
                printing::PrinterQuery::GetSettingsAskParam::DEFAULTS, 0, false,
                printing::mojom::MarginType::kDefaultMargins, false, false,
                base::BindOnce(&GetDefaultPrintSettingsReplyOnIO, queue,
                               std::move(printer_query), std::move(callback)));
}

}  // namespace

PrintViewManagerBaseQt::PrintViewManagerBaseQt(content::WebContents *contents)
    : printing::PrintManager(contents)
    , m_printingRFH(nullptr)
    , m_didPrintingSucceed(false)
    , m_printerQueriesQueue(WebEngineContext::current()->getPrintJobManager()->queue())
{
    // FIXME: Check if this needs to be executed async:
    // TODO: Add isEnabled to profile
    PrintViewManagerBaseQt::UpdatePrintingEnabled();
}

PrintViewManagerBaseQt::~PrintViewManagerBaseQt()
{
    ReleasePrinterQuery();
    DisconnectFromCurrentPrintJob();
}

void PrintViewManagerBaseQt::SetPrintingRFH(content::RenderFrameHost *rfh)
{
    DCHECK(!m_printingRFH);
    m_printingRFH = rfh;
}

void PrintViewManagerBaseQt::UpdatePrintingEnabled()
{
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    bool enabled = false;
#if QT_CONFIG(webengine_printing_and_pdf)
    enabled = true;
#endif
    web_contents()->ForEachFrame(
            base::Bind(&PrintViewManagerBaseQt::SendPrintingEnabled,
                    base::Unretained(this), enabled));
}

void PrintViewManagerBaseQt::NavigationStopped()
{
    // Cancel the current job, wait for the worker to finish.
    TerminatePrintJob(true);
}

base::string16 PrintViewManagerBaseQt::RenderSourceName()
{
     return toString16(QLatin1String(""));
}

void PrintViewManagerBaseQt::PrintDocument(scoped_refptr<base::RefCountedMemory> print_data,
                                           const gfx::Size &page_size,
                                           const gfx::Rect &content_area,
                                           const gfx::Point &offsets)
{
    std::unique_ptr<printing::MetafileSkia> metafile =
            std::make_unique<printing::MetafileSkia>();
    CHECK(metafile->InitFromData(*print_data));

    // Update the rendered document. It will send notifications to the listener.
    printing::PrintedDocument* document = m_printJob->document();
    document->SetDocument(std::move(metafile));
    ShouldQuitFromInnerMessageLoop();
}

void PrintViewManagerBaseQt::DidGetPrintedPagesCount(int32_t cookie, uint32_t number_pages)
{
    PrintManager::DidGetPrintedPagesCount(cookie, number_pages);
    OpportunisticallyCreatePrintJob(cookie);
}

bool PrintViewManagerBaseQt::PrintJobHasDocument(int cookie)
{
    if (!OpportunisticallyCreatePrintJob(cookie))
        return false;

    // These checks may fail since we are completely asynchronous. Old spurious
    // messages can be received if one of the processes is overloaded.
    printing::PrintedDocument* document = m_printJob->document();
    return document && document->cookie() == cookie;
}

// IPC handlers
void PrintViewManagerBaseQt::OnDidPrintDocument(content::RenderFrameHost* /*render_frame_host*/,
                                                const printing::mojom::DidPrintDocumentParams &params,
                                                std::unique_ptr<DelayedFrameDispatchHelper> helper)
{
    if (!PrintJobHasDocument(params.document_cookie))
        return;

    const printing::mojom::DidPrintContentParams &content = *params.content;
    if (!content.metafile_data_region.IsValid()) {
        NOTREACHED() << "invalid memory handle";
        web_contents()->Stop();
        return;
    }

    auto data = base::RefCountedSharedMemoryMapping::CreateFromWholeRegion(content.metafile_data_region);
    if (!data) {
        NOTREACHED() << "couldn't map";
        web_contents()->Stop();
        return;
    }

    PrintDocument(data, params.page_size, params.content_area,
                  params.physical_offsets);
    if (helper)
        helper->SendCompleted();
}

void PrintViewManagerBaseQt::GetDefaultPrintSettings(GetDefaultPrintSettingsCallback callback)
{
    content::RenderFrameHost* render_frame_host =
        print_manager_host_receivers_.GetCurrentTargetFrame();

    content::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&GetDefaultPrintSettingsOnIO, std::move(callback), m_printerQueriesQueue,
                       render_frame_host->GetProcess()->GetID(),
                       render_frame_host->GetRoutingID()));
}

void PrintViewManagerBaseQt::PrintingFailed(int32_t cookie)
{
    // Note: Not redundant with cookie checks in the same method in other parts of
    // the class hierarchy.
    if (!IsValidCookie(cookie))
        return;

    PrintManager::PrintingFailed(cookie);

    ReleasePrinterQuery();

    content::NotificationService::current()->Notify(
                chrome::NOTIFICATION_PRINT_JOB_RELEASED,
                content::Source<content::WebContents>(web_contents()),
                content::NotificationService::NoDetails());
}

void PrintViewManagerBaseQt::OnScriptedPrint(content::RenderFrameHost *render_frame_host,
                                             const printing::mojom::ScriptedPrintParams &params,
                                             IPC::Message *reply_msg)
{
    NOTREACHED() << "should be handled by printing::PrintingMessageFilter";
}

void PrintViewManagerBaseQt::ShowInvalidPrinterSettingsError()
{
}

void PrintViewManagerBaseQt::DidStartLoading()
{
    UpdatePrintingEnabled();
}

// Note: In PrintViewManagerQt we always initiate printing with PrintMsg_InitiatePrintPreview
// so m_printingRFH is never set and used at the moment.
void PrintViewManagerBaseQt::RenderFrameDeleted(content::RenderFrameHost *render_frame_host)
{
    PrintManager::RenderFrameDeleted(render_frame_host);

    // Terminates or cancels the print job if one was pending.
    if (render_frame_host != m_printingRFH)
        return;

    m_printingRFH = nullptr;

    PrintManager::PrintingRenderFrameDeleted();
    ReleasePrinterQuery();

    if (!m_printJob.get())
        return;

    scoped_refptr<printing::PrintedDocument> document(m_printJob->document());
    if (document) {
        // If IsComplete() returns false, the document isn't completely rendered.
        // Since our renderer is gone, there's nothing to do, cancel it. Otherwise,
        // the print job may finish without problem.
        TerminatePrintJob(!document->IsComplete());
    }
}

void PrintViewManagerBaseQt::Observe(int type,
        const content::NotificationSource& /*source*/,
        const content::NotificationDetails& details)
{
    DCHECK_EQ(chrome::NOTIFICATION_PRINT_JOB_EVENT, type);
    OnNotifyPrintJobEvent(*content::Details<printing::JobEventDetails>(details).ptr());
}

void PrintViewManagerBaseQt::OnNotifyPrintJobEvent(const printing::JobEventDetails& event_details)
{
    switch (event_details.type()) {
    case printing::JobEventDetails::FAILED: {
        TerminatePrintJob(true);

        content::NotificationService::current()->Notify(
                chrome::NOTIFICATION_PRINT_JOB_RELEASED,
                content::Source<content::WebContents>(web_contents()),
                content::NotificationService::NoDetails());
        break;
    }
    case printing::JobEventDetails::USER_INIT_DONE:
    case printing::JobEventDetails::DEFAULT_INIT_DONE:
    case printing::JobEventDetails::USER_INIT_CANCELED: {
        NOTREACHED();
        break;
    }
    case printing::JobEventDetails::ALL_PAGES_REQUESTED: {
        ShouldQuitFromInnerMessageLoop();
        break;
    }
    case printing::JobEventDetails::NEW_DOC:
#if defined(OS_WIN)
    case printing::JobEventDetails::PAGE_DONE:
#endif
    case printing::JobEventDetails::DOC_DONE: {
        // Don't care about the actual printing process.
        break;
    }
    case printing::JobEventDetails::JOB_DONE: {
        // Printing is done, we don't need it anymore.
        // print_job_->is_job_pending() may still be true, depending on the order
        // of object registration.
        m_didPrintingSucceed = true;
        ReleasePrintJob();

        content::NotificationService::current()->Notify(
                chrome::NOTIFICATION_PRINT_JOB_RELEASED,
                content::Source<content::WebContents>(web_contents()),
                content::NotificationService::NoDetails());
        break;
    }
    default:
        NOTREACHED();
        break;
    }
}

// Requests the RenderView to render all the missing pages for the print job.
// No-op if no print job is pending. Returns true if at least one page has
// been requested to the renderer.
bool PrintViewManagerBaseQt::RenderAllMissingPagesNow()
{
    if (!m_printJob.get() || !m_printJob->is_job_pending())
      return false;

    // Is the document already complete?
    if (m_printJob->document() && m_printJob->document()->IsComplete()) {
        m_didPrintingSucceed = true;
        return true;
    }

    // We can't print if there is no renderer.
    if (!web_contents() ||
        !web_contents()->GetRenderViewHost() ||
        !web_contents()->GetRenderViewHost()->IsRenderViewLive()) {
      return false;
    }

    // WebContents is either dying or a second consecutive request to print
    // happened before the first had time to finish. We need to render all the
    // pages in an hurry if a print_job_ is still pending. No need to wait for it
    // to actually spool the pages, only to have the renderer generate them. Run
    // a message loop until we get our signal that the print job is satisfied.
    // PrintJob will send a ALL_PAGES_REQUESTED after having received all the
    // pages it needs. MessageLoop::current()->Quit() will be called as soon as
    // print_job_->document()->IsComplete() is true on either ALL_PAGES_REQUESTED
    // or in DidPrintPage(). The check is done in
    // ShouldQuitFromInnerMessageLoop().
    // BLOCKS until all the pages are received. (Need to enable recursive task)
    if (!RunInnerMessageLoop()) {
      // This function is always called from DisconnectFromCurrentPrintJob() so we
      // know that the job will be stopped/canceled in any case.
      return false;
    }
    return true;
}

void PrintViewManagerBaseQt::ShouldQuitFromInnerMessageLoop()
{
    // Look at the reason.
    DCHECK(m_printJob->document());
    if (m_printJob->document() && m_printJob->document()->IsComplete() && m_quitInnerLoop) {
        // We are in a message loop created by RenderAllMissingPagesNow. Quit from
        // it.
        std::move(m_quitInnerLoop).Run();
    }
}

bool PrintViewManagerBaseQt::CreateNewPrintJob(std::unique_ptr<printing::PrinterQuery> query)
{
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK(!m_quitInnerLoop);
    DCHECK(query);

    // Disconnect the current |m_printJob|.
    DisconnectFromCurrentPrintJob();

    // We can't print if there is no renderer.
    if (!web_contents()->GetRenderViewHost() ||
        !web_contents()->GetRenderViewHost()->IsRenderViewLive()) {
        return false;
    }

    // Ask the renderer to generate the print preview, create the print preview
    // view and switch to it, initialize the printer and show the print dialog.
    DCHECK(!m_printJob.get());

    m_printJob = base::MakeRefCounted<printing::PrintJob>();
    m_printJob->Initialize(std::move(query), RenderSourceName(), number_pages_);
    m_registrar.Add(this, chrome::NOTIFICATION_PRINT_JOB_EVENT,
                    content::Source<printing::PrintJob>(m_printJob.get()));
    m_didPrintingSucceed = false;
    return true;
}

void PrintViewManagerBaseQt::DisconnectFromCurrentPrintJob()
{
    // Make sure all the necessary rendered page are done. Don't bother with the
    // return value.
    bool result = RenderAllMissingPagesNow();

    // Verify that assertion.
    if (m_printJob.get() &&
        m_printJob->document() &&
        !m_printJob->document()->IsComplete()) {
        DCHECK(!result);
        // That failed.
        TerminatePrintJob(true);
    } else {
        // DO NOT wait for the job to finish.
        ReleasePrintJob();
    }
}

void PrintViewManagerBaseQt::TerminatePrintJob(bool cancel)
{
    if (!m_printJob.get())
        return;

    if (cancel) {
        // We don't need the metafile data anymore because the printing is canceled.
        m_printJob->Cancel();
        m_quitInnerLoop.Reset();
    } else {
        DCHECK(!m_quitInnerLoop);
        DCHECK(!m_printJob->document() || m_printJob->document()->IsComplete());

        // WebContents is either dying or navigating elsewhere. We need to render
        // all the pages in an hurry if a print job is still pending. This does the
        // trick since it runs a blocking message loop:
        m_printJob->Stop();
    }
    ReleasePrintJob();
}

void PrintViewManagerBaseQt::ReleasePrintJob()
{
    content::RenderFrameHost* rfh = m_printingRFH;
    m_printingRFH = nullptr;

    if (!m_printJob.get())
        return;

    if (rfh)
      GetPrintRenderFrame(rfh)->PrintingDone(m_didPrintingSucceed);

    m_registrar.Remove(this, chrome::NOTIFICATION_PRINT_JOB_EVENT,
                       content::Source<printing::PrintJob>(m_printJob.get()));
    // Don't close the worker thread.
    m_printJob = nullptr;
}

bool PrintViewManagerBaseQt::RunInnerMessageLoop()
{
  // This value may actually be too low:
  //
  // - If we're looping because of printer settings initialization, the premise
  // here is that some poor users have their print server away on a VPN over a
  // slow connection. In this situation, the simple fact of opening the printer
  // can be dead slow. On the other side, we don't want to die infinitely for a
  // real network error. Give the printer 60 seconds to comply.
  //
  // - If we're looping because of renderer page generation, the renderer could
  // be CPU bound, the page overly complex/large or the system just
  // memory-bound.
  static const int kPrinterSettingsTimeout = 60000;
  base::OneShotTimer quit_timer;
  base::RunLoop run_loop;
  quit_timer.Start(FROM_HERE,
                   base::TimeDelta::FromMilliseconds(kPrinterSettingsTimeout),
                   run_loop.QuitWhenIdleClosure());

  m_quitInnerLoop = run_loop.QuitClosure();

  // Need to enable recursive task.
  {
      base::CurrentThread::ScopedNestableTaskAllower allow;
      run_loop.Run();
  }

  bool success = !m_quitInnerLoop;
  m_quitInnerLoop.Reset();

  return success;
}

bool PrintViewManagerBaseQt::OpportunisticallyCreatePrintJob(int cookie)
{
    if (m_printJob.get())
      return true;

    if (!cookie) {
      // Out of sync. It may happens since we are completely asynchronous. Old
      // spurious message can happen if one of the processes is overloaded.
      return false;
    }

    // The job was initiated by a script. Time to get the corresponding worker
    // thread.
    std::unique_ptr<printing::PrinterQuery> queued_query = m_printerQueriesQueue->PopPrinterQuery(cookie);
    if (!queued_query) {
      NOTREACHED();
      return false;
    }

    if (!CreateNewPrintJob(std::move(queued_query))) {
      // Don't kill anything.
      return false;
    }

    // Settings are already loaded. Go ahead. This will set
    // print_job_->is_job_pending() to true.
    m_printJob->StartPrinting();
    return true;
}

void PrintViewManagerBaseQt::ReleasePrinterQuery()
{
    if (!cookie_)
        return;

    int cookie = cookie_;
    cookie_ = 0;

    printing::PrintJobManager* printJobManager = WebEngineContext::current()->getPrintJobManager();
    // May be NULL in tests.
    if (!printJobManager)
        return;

    std::unique_ptr<printing::PrinterQuery> printerQuery;
    printerQuery = m_printerQueriesQueue->PopPrinterQuery(cookie);
    if (!printerQuery)
        return;
    base::PostTask(FROM_HERE, {content::BrowserThread::IO},
                   base::BindOnce(&printing::PrinterQuery::StopWorker, std::move(printerQuery)));
}

// Originally from print_preview_message_handler.cc:
void PrintViewManagerBaseQt::StopWorker(int documentCookie)
{
    if (documentCookie <= 0)
        return;
    std::unique_ptr<printing::PrinterQuery> printer_query =
            m_printerQueriesQueue->PopPrinterQuery(documentCookie);
    if (printer_query.get()) {
        base::PostTask(FROM_HERE, {content::BrowserThread::IO},
                       base::BindOnce(&printing::PrinterQuery::StopWorker, std::move(printer_query)));
    }
}

void PrintViewManagerBaseQt::SendPrintingEnabled(bool enabled, content::RenderFrameHost* rfh)
{
    GetPrintRenderFrame(rfh)->SetPrintingEnabled(enabled);
}

} // namespace QtWebEngineCore
