// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "pdf/pdfium/pdfium_font_linux.h"

#include <algorithm>
#include <memory>
#include <string>

#include "base/check_op.h"
#include "base/files/file.h"
#include "base/i18n/encoding_detection.h"
#include "base/i18n/icu_string_conversions.h"
#include "base/no_destructor.h"
#include "base/sequence_checker.h"
#include "base/strings/string_util.h"
#include "components/services/font/public/cpp/font_loader.h"
#include "pdf/font_table_linux.h"
#include "pdf/pdfium/pdfium_engine.h"
#include "third_party/blink/public/platform/web_font_description.h"
#include "third_party/pdfium/public/fpdf_sysfontinfo.h"

namespace chrome_pdf {

namespace {

// Maps font description and charset to `FontId` as requested by PDFium, with
// `FontId` as an opaque type that PDFium works with. Based on the `FontId`,
// PDFium can read from the font files using GetFontData(). Properly frees the
// underlying resource type when PDFium is done with the mapped font.
class BlinkFontMapper {
 public:
  // Defined as the type most convenient for use with PDFium's
  // `FPDF_SYSFONTINFO` functions.
  using FontId = void*;

  BlinkFontMapper() = default;
  ~BlinkFontMapper() = delete;

  // Returns a handle to the font mapped based on `desc` and `charset`, for use
  // as the `font_id` in GetFontData() and DeleteFont() below. Returns nullptr
  // on failure.
  // TODO(thestig): Document how this handles TTC files.
  FontId MapFont(const blink::WebFontDescription& desc, int charset) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // If there was never a SkFontConfigInterface::SetGlobal() call, then `fci`
    // defaults to the direct interface, which is not suitable, as it does not
    // provide MatchFontWithFallback(). This only happens in unit tests, so just
    // refuse to map fonts there.
    sk_sp<SkFontConfigInterface> fci = SkFontConfigInterface::RefGlobal();
    if (fci.get() == SkFontConfigInterface::GetSingletonDirectInterface())
      return nullptr;

    auto font_file = std::make_unique<base::File>();
    // In RendererBlinkPlatform, SkFontConfigInterface::SetGlobal() only ever
    // sets the global to a FontLoader. Thus it is safe to assume the returned
    // result is just that.
    auto* font_loader = reinterpret_cast<font_service::FontLoader*>(fci.get());
    font_loader->MatchFontWithFallback(
        desc.family.Utf8(),
        desc.weight >= blink::WebFontDescription::kWeightBold, desc.italic,
        charset, desc.generic_family, font_file.get());
    if (!font_file->IsValid())
      return nullptr;

    // Release to PDFium. PDFium will free `font_file` in DeleteFont() below.
    return font_file.release();
  }

  // Releases the font file that `font_id` points to.
  void DeleteFont(FontId font_id) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    delete FileFromFontId(font_id);
  }

  // Reads data from the `font_id` handle for `table` into a `buffer` of
  // `buf_size`. Returns the amount of data read on success, or 0 on failure.
  // If `buffer` is null, then just return the required size for the buffer.
  // See content::GetFontTable() for information on the `table_tag` parameter.
  unsigned long GetFontData(FontId font_id,
                            unsigned int table_tag,
                            unsigned char* buffer,
                            unsigned long buf_size) const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // TODO(thestig): cache?
    base::PlatformFile platform_file =
        FileFromFontId(font_id)->GetPlatformFile();
    size_t size = buf_size;
    if (!pdf::GetFontTable(platform_file, table_tag, /*offset=*/0, buffer,
                           &size)) {
      return 0;
    }
    return size;
  }

 private:
  static base::File* FileFromFontId(FontId font_id) {
    return reinterpret_cast<base::File*>(font_id);
  }

  SEQUENCE_CHECKER(sequence_checker_);
};

BlinkFontMapper& GetBlinkFontMapper() {
  static base::NoDestructor<BlinkFontMapper> mapper;
  return *mapper;
}

blink::WebFontDescription::Weight WeightToBlinkWeight(int weight) {
  static_assert(blink::WebFontDescription::kWeight100 == 0, "Blink Weight min");
  static_assert(blink::WebFontDescription::kWeight900 == 8, "Blink Weight max");
  constexpr int kMinimumWeight = 100;
  constexpr int kMaximumWeight = 900;
  int normalized_weight = std::clamp(weight, kMinimumWeight, kMaximumWeight);
  normalized_weight = (normalized_weight / 100) - 1;
  return static_cast<blink::WebFontDescription::Weight>(normalized_weight);
}

// This list is for CPWL_FontMap::GetDefaultFontByCharset().
// We pretend to have these font natively and let the browser (or underlying
// fontconfig) pick the proper font on the system.
void EnumFonts(FPDF_SYSFONTINFO* sysfontinfo, void* mapper) {
  FPDF_AddInstalledFont(mapper, "Arial", FXFONT_DEFAULT_CHARSET);

  const FPDF_CharsetFontMap* font_map = FPDF_GetDefaultTTFMap();
  for (; font_map->charset != -1; ++font_map) {
    FPDF_AddInstalledFont(mapper, font_map->fontname, font_map->charset);
  }
}

void* MapFont(FPDF_SYSFONTINFO*,
              int weight,
              int italic,
              int charset,
              int pitch_family,
              const char* face,
              int* exact) {
  // The code below is Blink-specific, return early in non-Blink mode to avoid
  // crashing.
  if (PDFiumEngine::GetFontMappingMode() != FontMappingMode::kBlink) {
    DCHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kNoMapping);
    return nullptr;
  }

  // Pretend the system does not have the Symbol font to force a fallback to
  // the built in Symbol font in CFX_FontMapper::FindSubstFont().
  if (strcmp(face, "Symbol") == 0)
    return nullptr;

  blink::WebFontDescription desc;
  if (pitch_family & FXFONT_FF_FIXEDPITCH) {
    desc.generic_family = blink::WebFontDescription::kGenericFamilyMonospace;
  } else if (pitch_family & FXFONT_FF_ROMAN) {
    desc.generic_family = blink::WebFontDescription::kGenericFamilySerif;
  } else {
    desc.generic_family = blink::WebFontDescription::kGenericFamilyStandard;
  }

  static const struct {
    const char* pdf_name;
    const char* face;
    bool bold;
    bool italic;
  } kPdfFontSubstitutions[] = {
      {"Courier", "Courier New", false, false},
      {"Courier-Bold", "Courier New", true, false},
      {"Courier-BoldOblique", "Courier New", true, true},
      {"Courier-Oblique", "Courier New", false, true},
      {"Helvetica", "Arial", false, false},
      {"Helvetica-Bold", "Arial", true, false},
      {"Helvetica-BoldOblique", "Arial", true, true},
      {"Helvetica-Oblique", "Arial", false, true},
      {"Times-Roman", "Times New Roman", false, false},
      {"Times-Bold", "Times New Roman", true, false},
      {"Times-BoldItalic", "Times New Roman", true, true},
      {"Times-Italic", "Times New Roman", false, true},

      // MS P?(Mincho|Gothic) are the most notable fonts in Japanese PDF files
      // without embedding the glyphs. Sometimes the font names are encoded
      // in Japanese Windows's locale (CP932/Shift_JIS) without space.
      // Most Linux systems don't have the exact font, but for outsourcing
      // fontconfig to find substitutable font in the system, we pass ASCII
      // font names to it.
      {"MS-PGothic", "MS PGothic", false, false},
      {"MS-Gothic", "MS Gothic", false, false},
      {"MS-PMincho", "MS PMincho", false, false},
      {"MS-Mincho", "MS Mincho", false, false},
      // MS PGothic in Shift_JIS encoding.
      {"\x82\x6C\x82\x72\x82\x6F\x83\x53\x83\x56\x83\x62\x83\x4E", "MS PGothic",
       false, false},
      // MS Gothic in Shift_JIS encoding.
      {"\x82\x6C\x82\x72\x83\x53\x83\x56\x83\x62\x83\x4E", "MS Gothic", false,
       false},
      // MS PMincho in Shift_JIS encoding.
      {"\x82\x6C\x82\x72\x82\x6F\x96\xBE\x92\xA9", "MS PMincho", false, false},
      // MS Mincho in Shift_JIS encoding.
      {"\x82\x6C\x82\x72\x96\xBE\x92\xA9", "MS Mincho", false, false},
  };

  // Similar logic exists in PDFium's CFX_FolderFontInfo::FindFont().
  if (charset == FXFONT_ANSI_CHARSET && (pitch_family & FXFONT_FF_FIXEDPITCH))
    face = "Courier New";

  // Map from the standard PDF fonts to TrueType font names.
  size_t i;
  for (i = 0; i < std::size(kPdfFontSubstitutions); ++i) {
    if (strcmp(face, kPdfFontSubstitutions[i].pdf_name) == 0) {
      desc.family = blink::WebString::FromUTF8(kPdfFontSubstitutions[i].face);
      if (kPdfFontSubstitutions[i].bold)
        desc.weight = blink::WebFontDescription::kWeightBold;
      if (kPdfFontSubstitutions[i].italic)
        desc.italic = true;
      break;
    }
  }

  if (i == std::size(kPdfFontSubstitutions)) {
    // Convert to UTF-8 and make sure it is valid.
    std::string face_utf8;
    if (base::IsStringUTF8(face)) {
      face_utf8 = face;
    } else {
      std::string encoding;
      if (base::DetectEncoding(face, &encoding)) {
        // ConvertToUtf8AndNormalize() clears `face_utf8` on failure.
        base::ConvertToUtf8AndNormalize(face, encoding, &face_utf8);
      }
    }

    if (face_utf8.empty())
      return nullptr;

    desc.family = blink::WebString::FromUTF8(face_utf8);
    desc.weight = WeightToBlinkWeight(weight);
    desc.italic = italic > 0;
  }

  return GetBlinkFontMapper().MapFont(desc, charset);
}

unsigned long GetFontData(FPDF_SYSFONTINFO*,
                          void* font_id,
                          unsigned int table,
                          unsigned char* buffer,
                          unsigned long buf_size) {
  DCHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kBlink);
  return GetBlinkFontMapper().GetFontData(font_id, table, buffer, buf_size);
}

void DeleteFont(FPDF_SYSFONTINFO*, void* font_id) {
  DCHECK_EQ(PDFiumEngine::GetFontMappingMode(), FontMappingMode::kBlink);
  GetBlinkFontMapper().DeleteFont(font_id);
}

FPDF_SYSFONTINFO g_font_info = {1,           0, EnumFonts, MapFont,   0,
                                GetFontData, 0, 0,         DeleteFont};

}  // namespace

void InitializeLinuxFontMapper() {
  FPDF_SetSystemFontInfo(&g_font_info);
}

}  // namespace chrome_pdf