// Copyright 2014 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 file.

#include "chrome/browser/printing/pdf_to_emf_converter.h"

#include <stdint.h>
#include <windows.h>

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

#include "base/callback.h"
#include "base/containers/queue.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner.h"
#include "base/task_scheduler/post_task.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/services/printing/public/mojom/constants.mojom.h"
#include "chrome/services/printing/public/mojom/pdf_to_emf_converter.mojom.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/common/service_manager_connection.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "printing/emf_win.h"
#include "printing/pdf_render_settings.h"
#include "services/service_manager/public/cpp/connector.h"

using content::BrowserThread;

namespace printing {

namespace {

class PdfToEmfConverterClientImpl : public mojom::PdfToEmfConverterClient {
 public:
  explicit PdfToEmfConverterClientImpl(
      mojom::PdfToEmfConverterClientRequest request)
      : binding_(this, std::move(request)) {}

 private:
  // mojom::PdfToEmfConverterClient implementation.
  void PreCacheFontCharacters(
      const std::vector<uint8_t>& logfont_data,
      const base::string16& characters,
      PreCacheFontCharactersCallback callback) override {
    // TODO(scottmg): pdf/ppapi still require the renderer to be able to
    // precache GDI fonts (http://crbug.com/383227), even when using
    // DirectWrite. Eventually this shouldn't be added and should be moved to
    // FontCacheDispatcher too. http://crbug.com/356346.

    // First, comments from FontCacheDispatcher::OnPreCacheFont do apply here
    // too. Except that for True Type fonts, GetTextMetrics will not load the
    // font in memory. The only way windows seem to load properly, it is to
    // create a similar device (like the one in which we print), then do an
    // ExtTextOut, as we do in the printing thread, which is sandboxed.
    const LOGFONT* logfont =
        reinterpret_cast<const LOGFONT*>(&logfont_data.at(0));

    HDC hdc = CreateEnhMetaFile(nullptr, nullptr, nullptr, nullptr);
    HFONT font_handle = CreateFontIndirect(logfont);
    DCHECK(font_handle != nullptr);

    HGDIOBJ old_font = SelectObject(hdc, font_handle);
    DCHECK(old_font != nullptr);

    ExtTextOut(hdc, 0, 0, ETO_GLYPH_INDEX, 0, characters.c_str(),
               characters.length(), nullptr);

    SelectObject(hdc, old_font);
    DeleteObject(font_handle);

    HENHMETAFILE metafile = CloseEnhMetaFile(hdc);

    if (metafile)
      DeleteEnhMetaFile(metafile);

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

  mojo::Binding<mojom::PdfToEmfConverterClient> binding_;
};

// Emf subclass that knows how to play back PostScript data embedded as EMF
// comment records.
class PostScriptMetaFile : public Emf {
 public:
  PostScriptMetaFile() {}
  ~PostScriptMetaFile() override {}

 private:
  // Emf:
  bool SafePlayback(HDC hdc) const override;

  DISALLOW_COPY_AND_ASSIGN(PostScriptMetaFile);
};

// Class for converting PDF to another format for printing (Emf, Postscript).
// Class lives on the UI thread.
// Internal workflow is following:
// 1. Create instance on the UI thread.
// 2. Initialize() binds to printing service and start conversion on the UI
//    thread (Mojo actually makes that happen transparently on the IO thread).
// 3. Printing service returns page count.
// 4. For each page:
//   1. Clients requests page.
//   2. Utility converts the page, and sends back the data in a memory region.
class PdfConverterImpl : public PdfConverter {
 public:
  PdfConverterImpl(const scoped_refptr<base::RefCountedMemory>& data,
                   const PdfRenderSettings& conversion_settings,
                   StartCallback start_callback);
  ~PdfConverterImpl() override;

  static void set_fail_when_initializing_conversion_for_tests(bool fail) {
    simulate_failure_initializing_conversion_ = fail;
  }

  static bool fail_when_initializing_conversion_for_tests() {
    return simulate_failure_initializing_conversion_;
  }

 private:
  class GetPageCallbackData {
   public:
    GetPageCallbackData(int page_number, PdfConverter::GetPageCallback callback)
        : page_number_(page_number), callback_(callback) {}

    GetPageCallbackData(GetPageCallbackData&& other) {
      *this = std::move(other);
    }

    GetPageCallbackData& operator=(GetPageCallbackData&& rhs) {
      page_number_ = rhs.page_number_;
      callback_ = rhs.callback_;
      return *this;
    }

    int page_number() const { return page_number_; }

    const PdfConverter::GetPageCallback& callback() const { return callback_; }

   private:
    int page_number_;

    PdfConverter::GetPageCallback callback_;

    DISALLOW_COPY_AND_ASSIGN(GetPageCallbackData);
  };

  void Initialize(const scoped_refptr<base::RefCountedMemory>& data);

  void GetPage(int page_number,
               const PdfConverter::GetPageCallback& get_page_callback) override;

  void Stop();

  std::unique_ptr<MetafilePlayer> GetMetaFileFromMapping(
      base::ReadOnlySharedMemoryMapping mapping);

  void OnPageCount(mojom::PdfToEmfConverterPtr converter, uint32_t page_count);
  void OnPageDone(base::ReadOnlySharedMemoryRegion emf_region,
                  float scale_factor);

  void OnFailed(const std::string& error_message);

  void RecordConversionMetrics();

  PdfRenderSettings settings_;

  // Document loaded callback.
  PdfConverter::StartCallback start_callback_;

  // Queue of callbacks for GetPage() requests. Utility process should reply
  // with PageDone in the same order as requests were received.
  // Use containers that keeps element pointers valid after push() and pop().
  using GetPageCallbacks = base::queue<GetPageCallbackData>;
  GetPageCallbacks get_page_callbacks_;

  // Keep track of document size and page counts for metrics.
  size_t bytes_generated_ = 0;
  uint32_t pages_generated_ = 0;
  uint32_t page_count_ = 0;

  std::unique_ptr<PdfToEmfConverterClientImpl>
      pdf_to_emf_converter_client_impl_;

  mojom::PdfToEmfConverterPtr pdf_to_emf_converter_;

  mojom::PdfToEmfConverterFactoryPtr pdf_to_emf_converter_factory_;

  base::WeakPtrFactory<PdfConverterImpl> weak_ptr_factory_;

  static bool simulate_failure_initializing_conversion_;

  DISALLOW_COPY_AND_ASSIGN(PdfConverterImpl);
};

// static
bool PdfConverterImpl::simulate_failure_initializing_conversion_ = false;

std::unique_ptr<MetafilePlayer> PdfConverterImpl::GetMetaFileFromMapping(
    base::ReadOnlySharedMemoryMapping mapping) {
  std::unique_ptr<Emf> metafile;
  if (settings_.mode == PdfRenderSettings::Mode::POSTSCRIPT_LEVEL2 ||
      settings_.mode == PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3 ||
      settings_.mode == PdfRenderSettings::Mode::TEXTONLY) {
    metafile = std::make_unique<PostScriptMetaFile>();
  } else {
    metafile = std::make_unique<Emf>();
  }
  if (!metafile->InitFromData(mapping.memory(), mapping.size()))
    metafile.reset();
  return metafile;
}

bool PostScriptMetaFile::SafePlayback(HDC hdc) const {
  Emf::Enumerator emf_enum(*this, nullptr, nullptr);
  for (const Emf::Record& record : emf_enum) {
    auto* emf_record = record.record();
    if (emf_record->iType != EMR_GDICOMMENT)
      continue;

    const EMRGDICOMMENT* comment =
        reinterpret_cast<const EMRGDICOMMENT*>(emf_record);
    const char* data = reinterpret_cast<const char*>(comment->Data);
    const uint16_t* ptr = reinterpret_cast<const uint16_t*>(data);
    int ret = ExtEscape(hdc, PASSTHROUGH, 2 + *ptr, data, 0, nullptr);
    DCHECK_EQ(*ptr, ret);
  }
  return true;
}

PdfConverterImpl::PdfConverterImpl(
    const scoped_refptr<base::RefCountedMemory>& data,
    const PdfRenderSettings& settings,
    StartCallback start_callback)
    : settings_(settings),
      start_callback_(std::move(start_callback)),
      weak_ptr_factory_(this) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(start_callback_);

  Initialize(data);
}

PdfConverterImpl::~PdfConverterImpl() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  RecordConversionMetrics();
}

void PdfConverterImpl::Initialize(
    const scoped_refptr<base::RefCountedMemory>& data) {
  if (simulate_failure_initializing_conversion_) {
    OnFailed(std::string("Failed to create PDF data mapping."));
    return;
  }

  base::MappedReadOnlyRegion memory =
      base::ReadOnlySharedMemoryRegion::Create(data->size());
  if (!memory.region.IsValid() || !memory.mapping.IsValid()) {
    OnFailed(std::string("Failed to create PDF data mapping."));
    return;
  }

  memcpy(memory.mapping.memory(), data->front(), data->size());

  content::ServiceManagerConnection::GetForProcess()
      ->GetConnector()
      ->BindInterface(printing::mojom::kChromePrintingServiceName,
                      &pdf_to_emf_converter_factory_);
  pdf_to_emf_converter_factory_.set_connection_error_handler(base::BindOnce(
      &PdfConverterImpl::OnFailed, weak_ptr_factory_.GetWeakPtr(),
      std::string("Connection to PdfToEmfConverterFactory error.")));

  mojom::PdfToEmfConverterClientPtr pdf_to_emf_converter_client_ptr;
  pdf_to_emf_converter_client_impl_ =
      std::make_unique<PdfToEmfConverterClientImpl>(
          mojo::MakeRequest(&pdf_to_emf_converter_client_ptr));

  pdf_to_emf_converter_factory_->CreateConverter(
      std::move(memory.region), settings_,
      std::move(pdf_to_emf_converter_client_ptr),
      base::BindOnce(&PdfConverterImpl::OnPageCount,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PdfConverterImpl::OnPageCount(mojom::PdfToEmfConverterPtr converter,
                                   uint32_t page_count) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!pdf_to_emf_converter_.is_bound());
  pdf_to_emf_converter_ = std::move(converter);
  pdf_to_emf_converter_.set_connection_error_handler(base::BindOnce(
      &PdfConverterImpl::OnFailed, weak_ptr_factory_.GetWeakPtr(),
      std::string("Connection to PdfToEmfConverter error.")));
  std::move(start_callback_).Run(page_count);
  page_count_ = page_count;
}

void PdfConverterImpl::GetPage(
    int page_number,
    const PdfConverter::GetPageCallback& get_page_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(pdf_to_emf_converter_.is_bound());

  // Store callback before any OnFailed() call to make it called on failure.
  get_page_callbacks_.push(GetPageCallbackData(page_number, get_page_callback));

  if (!pdf_to_emf_converter_)
    return OnFailed(std::string("No PdfToEmfConverter."));

  pdf_to_emf_converter_->ConvertPage(
      page_number, base::BindOnce(&PdfConverterImpl::OnPageDone,
                                  weak_ptr_factory_.GetWeakPtr()));
}

void PdfConverterImpl::OnPageDone(base::ReadOnlySharedMemoryRegion emf_region,
                                  float scale_factor) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (get_page_callbacks_.empty())
    return OnFailed(std::string("No get_page callbacks."));

  GetPageCallbackData& data = get_page_callbacks_.front();
  std::unique_ptr<MetafilePlayer> metafile;
  if (emf_region.IsValid()) {
    base::ReadOnlySharedMemoryMapping mapping = emf_region.Map();
    if (mapping.IsValid()) {
      size_t mapping_size = mapping.size();
      metafile = GetMetaFileFromMapping(std::move(mapping));
      if (metafile) {
        ++pages_generated_;
        bytes_generated_ += mapping_size;
      }
    }
  }

  base::WeakPtr<PdfConverterImpl> weak_this = weak_ptr_factory_.GetWeakPtr();
  data.callback().Run(data.page_number(), scale_factor, std::move(metafile));
  // WARNING: the callback might have deleted |this|!
  if (!weak_this)
    return;
  get_page_callbacks_.pop();
}

void PdfConverterImpl::Stop() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Disconnect interface ptrs so that the printing service process stop.
  pdf_to_emf_converter_factory_.reset();
  pdf_to_emf_converter_.reset();
}

void PdfConverterImpl::OnFailed(const std::string& error_message) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  LOG(ERROR) << "Failed to convert PDF: " << error_message;
  base::WeakPtr<PdfConverterImpl> weak_this = weak_ptr_factory_.GetWeakPtr();
  if (!start_callback_.is_null()) {
    std::move(start_callback_).Run(/*page_count=*/0);
    if (!weak_this)
      return;  // Protect against the |start_callback_| deleting |this|.
  }

  while (!get_page_callbacks_.empty()) {
    OnPageDone(base::ReadOnlySharedMemoryRegion(), 0.0f);
    if (!weak_this) {
      // OnPageDone invokes the GetPageCallback which might end up deleting
      // this.
      return;
    }
  }

  Stop();
}

void PdfConverterImpl::RecordConversionMetrics() {
  if (!page_count_ || page_count_ != pages_generated_) {
    // TODO(thestig): Consider adding UMA to track failure rates.
    return;
  }

  DCHECK(bytes_generated_);
  size_t average_page_size_in_kb = bytes_generated_ / 1024;
  average_page_size_in_kb /= page_count_;
  switch (settings_.mode) {
    case PdfRenderSettings::Mode::NORMAL:
      UMA_HISTOGRAM_MEMORY_KB("Printing.ConversionSize.Emf",
                              average_page_size_in_kb);
      return;
    case PdfRenderSettings::Mode::TEXTONLY:
      // Intentionally not logged.
      return;
    case PdfRenderSettings::Mode::GDI_TEXT:
      UMA_HISTOGRAM_MEMORY_KB("Printing.ConversionSize.EmfWithGdiText",
                              average_page_size_in_kb);
      return;
    case PdfRenderSettings::Mode::POSTSCRIPT_LEVEL2:
      UMA_HISTOGRAM_MEMORY_KB("Printing.ConversionSize.PostScript2",
                              average_page_size_in_kb);
      return;
    case PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3:
      UMA_HISTOGRAM_MEMORY_KB("Printing.ConversionSize.PostScript3",
                              average_page_size_in_kb);
      return;
    default:
      NOTREACHED();
      return;
  }
}

}  // namespace

PdfConverter::~PdfConverter() = default;

// static
std::unique_ptr<PdfConverter> PdfConverter::StartPdfConverter(
    const scoped_refptr<base::RefCountedMemory>& data,
    const PdfRenderSettings& conversion_settings,
    StartCallback start_callback) {
  return std::make_unique<PdfConverterImpl>(data, conversion_settings,
                                            std::move(start_callback));
}

ScopedSimulateFailureCreatingTempFileForTests::
    ScopedSimulateFailureCreatingTempFileForTests() {
  PdfConverterImpl::set_fail_when_initializing_conversion_for_tests(true);
}

ScopedSimulateFailureCreatingTempFileForTests::
    ~ScopedSimulateFailureCreatingTempFileForTests() {
  PdfConverterImpl::set_fail_when_initializing_conversion_for_tests(false);
}

}  // namespace printing
