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

#include "components/services/font_data/font_data_service_impl.h"

#if BUILDFLAG(IS_WIN)
#include <windows.h>
#endif  // BUILDFLAG(IS_WIN)

#include <algorithm>
#include <utility>

#include "base/check.h"
#include "base/containers/heap_array.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "skia/ext/font_utils.h"
#include "third_party/skia/include/core/SkFontStyle.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/core/SkString.h"
#include "third_party/skia/include/core/SkTypeface.h"

namespace font_data_service {

namespace {

// Recorded in Chrome.FontDataService.CreateResult, don't modify/reorder without
// also changing FontDataServiceCreateResult in
// tools/metrics/histograms/metadata/chrome/enums.xml
enum class CreateResult {
  kNoTypeface = 0,
  kSuccessExistingSharedMemory = 1,
  kFailureExistingSharedMemory = 2,
  kSuccessSharingFileHandle = 3,
  kSuccessSharingNewMemoryRegion = 4,
  kFailureSharingNewMemoryRegion = 5,
  kMaxValue = kFailureSharingNewMemoryRegion,
};

// Recorded in Chrome.FontDataService.InvokedIPC, don't modify or re-order
// without also changing FontDataServiceIPC.
enum class FontDataServiceIPC {
  kMatchFamilyName = 0,
  kMatchFamilyNameCharacter = 1,
  kGetAllFamilyNames = 2,
  kLegacyMakeTypeface = 3,
  kMatchLocalFont = 4,
  kMaxValue = kMatchLocalFont,
};

// Value is arbitrary. The number should be small to conserve memory but large
// enough to fit a meaningful amount of fonts.
constexpr int kMemoryMapCacheSize = 128;

BASE_FEATURE(kDumpOnOOBFontDataServiceCache, base::FEATURE_DISABLED_BY_DEFAULT);

base::SequencedTaskRunner* GetFontDataServiceTaskRunner() {
  static base::NoDestructor<scoped_refptr<base::SequencedTaskRunner>>
      task_runner{base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_BLOCKING})};
  return task_runner->get();
}

void BindToFontService(
    mojo::PendingReceiver<font_data_service::mojom::FontDataService> receiver) {
  static base::NoDestructor<font_data_service::FontDataServiceImpl> service;
  service->BindReceiver(std::move(receiver));
}

constexpr SkFontStyle::Slant ConvertToFontStyle(mojom::TypefaceSlant slant) {
  switch (slant) {
    case mojom::TypefaceSlant::kRoman:
      return SkFontStyle::Slant::kUpright_Slant;
    case mojom::TypefaceSlant::kItalic:
      return SkFontStyle::Slant::kItalic_Slant;
    case mojom::TypefaceSlant::kOblique:
      return SkFontStyle::Slant::kOblique_Slant;
  }
  NOTREACHED();
}

}  // namespace

FontDataServiceImpl::MappedAsset::MappedAsset(
    std::unique_ptr<SkStreamAsset> asset,
    base::MappedReadOnlyRegion shared_memory)
    : asset(std::move(asset)), shared_memory(std::move(shared_memory)) {}

FontDataServiceImpl::MappedAsset::~MappedAsset() = default;

FontDataServiceImpl::FontDataServiceImpl()
    : font_manager_(skia::DefaultFontMgr()),
      local_font_matcher_(LocalFontMatcher::Create()) {
  CHECK(font_manager_);
}

FontDataServiceImpl::~FontDataServiceImpl() = default;

void FontDataServiceImpl::ConnectToFontService(
    mojo::PendingReceiver<font_data_service::mojom::FontDataService> receiver) {
  GetFontDataServiceTaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&BindToFontService, std::move(receiver)));
}

void FontDataServiceImpl::BindReceiver(
    mojo::PendingReceiver<mojom::FontDataService> receiver) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  receivers_.Add(this, std::move(receiver));
}

std::tuple<base::File, uint64_t> FontDataServiceImpl::GetFileHandle(
    SkTypeface& typeface) {
  SkString font_path;
  typeface.getResourceName(&font_path);
  base::UmaHistogramBoolean("Chrome.FontDataService.EmptyPathOnGetFileHandle",
                            font_path.isEmpty());
  if (font_path.isEmpty()) {
    return {};
  }

  auto font_file_path = base::FilePath::FromUTF8Unsafe(font_path.c_str());
  base::UmaHistogramBoolean(
      "Chrome.FontDataService.FileHandlePathReferencesParent",
      font_file_path.ReferencesParent());

  auto font_file =
      base::File(font_file_path, base::File::FLAG_OPEN | base::File::FLAG_READ |
                                     base::File::FLAG_WIN_EXCLUSIVE_WRITE);
#if BUILDFLAG(IS_WIN)
  if (!font_file.IsValid()) {
    base::UmaHistogramSparse("Chrome.FontDataService.WinLastError",
                             ::GetLastError());
  }
#endif  // BUILDFLAG(IS_WIN)

  return std::make_tuple(std::move(font_file), GetUniqueFileId(font_file_path));
}

// This is a port of a DWriteFontProxy workaround, reimplemented for
// compatibility. This workaround makes Chrome non-compliant with the CSS font
// matching algorithm standard (see
// https://www.w3.org/TR/css-fonts-3/#font-style-matching) and should be removed
// (see crbug.com/475810976).

// As a workaround for crbug.com/635932, refuse to
// load some common fonts that do not contain certain styles. We found that
// sometimes these fonts are installed only in specialized styles ('Open Sans'
// might only be available in the condensed light variant, or Helvetica might
// only be available in bold). That results in a poor user experience because
// websites that use those fonts usually expect them to be rendered in the
// regular variant.
//
// The specific logic is:
// If the matched typeface's style matches the requested style, return it
//  (the exact requested font is installed)
//
//   - Request is regular and match is regular -> return regular
//   - Request is non-regular and match is non-regular -> return non-regular
//
// If the matched typeface's style doesn't match the requested style, but the
//  matched typeface's style is regular, return it. Otherwise return null (we
//  assume DirectWrite returns the most appropriate font it can find, so we have
//  to evaluate if it's good enough)
//
//   - Request is regular and match is non-regular -> return null (addresses the
//     original bug)
//   - Request is non-regular and match is regular -> return regular (complies
//     with the standard + using regular for a non-regular request is less
//     problematic UX-wise than using a styled font for a regular request)
//
// For each of these criteria, the exact value for each style component isn't
// used directly. For instance, the weight doesn't need to be an exact match but
// does need to be in the same "direction" (light/normal/bold) as the request.
// Likewise for width (condensed/normal/expanded).
bool FontDataServiceImpl::CheckMatchesRequiredStyle(
    const SkFontStyle& actual_style,
    const std::string& requested_family_name,
    const SkFontStyle& requested_style) {
#if BUILDFLAG(IS_WIN)
  static const std::string kFamiliesWithRequiredStyles[] = {
      // The regular version of Gill Sans is actually in the Gill Sans MT
      // family,
      // and the Gill Sans family typically contains just the ultra-bold styles.
      "gill sans",
      "helvetica",
      "open sans",
  };

  // Returns the "direction" of a given style component. For example, using this
  // lambda for "weight" will return -1 if the typeface is light, 1 if it's
  // bold, and 0 if it's normal. Used to compare styles directionally between 2
  // typefaces.
  auto find_style_component_direction = [](auto value, auto lower_bound,
                                           auto upper_bound) {
    if (value <= lower_bound) {
      return -1;
    }
    if (value >= upper_bound) {
      return 1;
    }
    return 0;
  };

  // SkTypeface defines "is bold" as >= Semibold. The enum has "light" as the
  // first weight under "normal". That only leaves "medium" to classify, which
  // is between "normal" and "semi-bold", so let's consider medium as "normal".
  int requested_weight_direction = find_style_component_direction(
      requested_style.weight(), SkFontStyle::kLight_Weight,
      SkFontStyle::kSemiBold_Weight);
  // For width, there's no precedent anywhere else in the codebase but the enum
  // is pretty explicit. Anything under semi-condensed is "condensed", anything
  // above semi-expanded is "expanded", and "normal" is right in the middle.
  int requested_width_direction = find_style_component_direction(
      requested_style.width(), SkFontStyle::kSemiCondensed_Width,
      SkFontStyle::kSemiExpanded_Width);

  for (const auto& family_name : kFamiliesWithRequiredStyles) {
    if (base::EqualsCaseInsensitiveASCII(requested_family_name, family_name)) {
      // As an additional check, verify if the family actually contains a
      // regular face. If it doesn't, we should refuse to use any face from this
      // family to avoid poor UX (e.g. using ultra-bold for a regular request).
      // This matches the legacy logic in DWriteFontProxyImpl::FindFamily.
      sk_sp<SkTypeface> regular_face = font_manager_->matchFamilyStyle(
          requested_family_name.c_str(), SkFontStyle::Normal());
      if (!regular_face || regular_face->fontStyle() != SkFontStyle::Normal()) {
        return false;
      }

      int found_weight_direction = find_style_component_direction(
          actual_style.weight(), SkFontStyle::kLight_Weight,
          SkFontStyle::kSemiBold_Weight);
      int found_width_direction = find_style_component_direction(
          actual_style.width(), SkFontStyle::kSemiCondensed_Width,
          SkFontStyle::kSemiExpanded_Width);

      // The regular variant is always usable if it's found.
      if (found_weight_direction == 0 && found_width_direction == 0 &&
          actual_style.slant() == SkFontStyle::kUpright_Slant) {
        return true;
      }

      // If the requested style and the found typeface's styles match, consider
      // the found typeface to be usable.
      return requested_weight_direction == found_weight_direction &&
             requested_width_direction == found_width_direction &&
             requested_style.slant() == actual_style.slant();
    }
  }
#endif
  // The requested family doesn't have style requirements, consider it usable.
  return true;
}

void FontDataServiceImpl::MatchFamilyName(const std::string& family_name,
                                          mojom::TypefaceStylePtr style,
                                          MatchFamilyNameCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT("fonts", "FontDataServiceImpl::MatchFamilyName", "family_name",
              family_name);
  base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
                                FontDataServiceIPC::kMatchFamilyName);

  // Call the font manager of the browser process to process the proxied match
  // family request.
  SkFontStyle sk_font_style(style->weight, style->width,
                            ConvertToFontStyle(style->slant));
  sk_sp<SkTypeface> typeface =
      font_manager_->matchFamilyStyle(family_name.c_str(), sk_font_style);

  std::move(callback).Run(
      CreateMatchFamilyNameResult(typeface, family_name, sk_font_style));
}

void FontDataServiceImpl::MatchFamilyNameCharacter(
    const std::string& family_name,
    mojom::TypefaceStylePtr style,
    const std::vector<std::string>& bcp47s,
    int32_t character,
    MatchFamilyNameCharacterCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT("fonts", "FontDataServiceImpl::MatchFamilyNameCharacter",
              "family_name", family_name);
  base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
                                FontDataServiceIPC::kMatchFamilyNameCharacter);

  // Call the font manager of the browser process to process the proxied match
  // family request.
  SkFontStyle sk_font_style(style->weight, style->width,
                            ConvertToFontStyle(style->slant));

  // Skia passes the language tags as an array of null-terminated c-strings with
  // a count. We transform that to an std::vector<std::string> to pass it over
  // mojo, but have to recreate the same structure before passing it to skia
  // functions again.
  std::vector<const char*> bcp47s_array;
  for (const auto& bcp47 : bcp47s) {
    bcp47s_array.push_back(bcp47.c_str());
  }

  sk_sp<SkTypeface> typeface = font_manager_->matchFamilyStyleCharacter(
      family_name.c_str(), sk_font_style, bcp47s_array.data(), bcp47s.size(),
      character);

  std::move(callback).Run(
      CreateMatchFamilyNameResult(typeface, family_name, sk_font_style));
}

void FontDataServiceImpl::GetAllFamilyNames(
    GetAllFamilyNamesCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT("fonts", "FontDataServiceImpl::GetAllFamilyNames");
  base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
                                FontDataServiceIPC::kGetAllFamilyNames);

  int family_count = font_manager_->countFamilies();
  std::vector<std::string> result;
  result.reserve(family_count);

  for (int i = 0; i < family_count; ++i) {
    SkString out;
    font_manager_->getFamilyName(i, &out);
    result.emplace_back(out.begin(), out.end());
  }

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

void FontDataServiceImpl::LegacyMakeTypeface(
    const std::optional<std::string>& family_name,
    mojom::TypefaceStylePtr style,
    LegacyMakeTypefaceCallback callback) {
  base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
                                FontDataServiceIPC::kLegacyMakeTypeface);
  SkFontStyle sk_font_style(style->weight, style->width,
                            ConvertToFontStyle(style->slant));

  sk_sp<SkTypeface> typeface = font_manager_->legacyMakeTypeface(
      family_name ? family_name->c_str() : nullptr, sk_font_style);

  // CreateMatchFamilyNameResult uses `family_name` to check against a list of
  // hard-coded family names so passing "" when `family_name` is `nullopt` is
  // OK.
  std::move(callback).Run(CreateMatchFamilyNameResult(
      typeface, family_name ? *family_name : "", sk_font_style));
}

size_t FontDataServiceImpl::GetOrCreateAssetIndex(
    std::unique_ptr<SkStreamAsset> asset) {
  TRACE_EVENT("fonts", "FontDataServiceImpl::GetOrCreateAssetIndex");

  // An asset can be used for multiple typefaces (a.k.a different ttc_index).

  // On Windows, with DWrite font manager.
  //     SkDWriteFontFileStream : public SkStreamMemory
  // getMemoryBase would not be a nullptr in this case.
  intptr_t memory_base = reinterpret_cast<intptr_t>(asset->getMemoryBase());
  // Check into the memory assets cache.
  if (auto iter = address_to_asset_index_.find(memory_base);
      iter != address_to_asset_index_.end()) {
    return iter->second;
  }

  size_t asset_length = asset->getLength();
  base::MappedReadOnlyRegion shared_memory_region =
      base::ReadOnlySharedMemoryRegion::Create(asset_length);
  PCHECK(shared_memory_region.IsValid());

  size_t asset_index = assets_.size();

  {
    TRACE_EVENT("fonts",
                "FontDataServiceImpl::GetOrCreateAssetIndex - memory copy",
                "size", asset_length);
    size_t bytes_read = asset->read(shared_memory_region.mapping.memory(),
                                    shared_memory_region.mapping.size());
    CHECK_EQ(bytes_read, asset_length);
  }

  assets_.push_back(std::make_unique<MappedAsset>(
      std::move(asset), std::move(shared_memory_region)));

  // Update the assets cache.
  address_to_asset_index_[memory_base] = asset_index;

  return asset_index;
}

uint64_t FontDataServiceImpl::GetUniqueFileId(base::FilePath path) {
  uint64_t new_id = unique_path_ids_.size() + 1;
  auto [it, inserted] = unique_path_ids_.try_emplace(path, new_id);
  return it->second;
}

mojom::MatchFamilyNameResultPtr
FontDataServiceImpl::CreateMatchFamilyNameResult(
    sk_sp<SkTypeface> typeface,
    const std::string& family_name,
    const SkFontStyle& requested_style) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CreateResult result_status = CreateResult::kNoTypeface;

  auto result = mojom::MatchFamilyNameResult::New();

  if (typeface) {
    if (!CheckMatchesRequiredStyle(typeface->fontStyle(), family_name,
                                   requested_style)) {
      return nullptr;
    }

    auto iter = typeface_to_asset_index_.find(typeface->uniqueID());
    if (iter != typeface_to_asset_index_.end()) {
      const size_t asset_index = iter->second.asset_index;
      base::ReadOnlySharedMemoryRegion region =
          assets_[asset_index]->shared_memory.region.Duplicate();
      result->ttc_index = iter->second.ttc_index;
      if (region.IsValid()) {
        result->typeface_data =
            mojom::TypefaceData::NewRegion(std::move(region));
        result_status = CreateResult::kSuccessExistingSharedMemory;
      } else {
        result_status = CreateResult::kFailureExistingSharedMemory;
      }
    } else {
      // While the stream is not necessary for file handles, fetch the ttc_index
      // if available. It is possible that the index will be set even if
      // openStream fails.
      auto stream = typeface->openStream(&result->ttc_index);

      // Try to share the font with a base::File. This is avoiding copy of the
      // content of the file.
      base::File font_file;
      uint64_t font_file_unique_id;
      std::tie(font_file, font_file_unique_id) = GetFileHandle(*typeface);
      if (font_file.IsValid()) {
        TRACE_EVENT("fonts", "FontDataServiceImpl - sharing file handle");
        result->typeface_data =
            mojom::TypefaceData::NewFontFile(mojom::TypefaceFile::New(
                std::move(font_file), font_file_unique_id));

        result_status = CreateResult::kSuccessSharingFileHandle;
      } else {
        TRACE_EVENT("fonts", "FontDataServiceImpl - sharing memory region");
        // If it failed to share as an base::File, try sharing with shared
        // memory. Try to open the stream and prepare shared memory that will be
        // shared with renderers. The content of the stream is copied into the
        // shared memory. If the stream data is invalid or if the cache is full,
        // return an invalid memory map region.
        // TODO(crbug.com/335680565): Improve cache by transitioning to LRU.
        if (stream && stream->hasLength() && (stream->getLength() > 0u) &&
            stream->getMemoryBase()) {
          UMA_HISTOGRAM_COUNTS_10000(
              "Chrome.FontDataService.MemoryMapCacheSize", assets_.size());
          if (assets_.size() >= kMemoryMapCacheSize &&
              base::FeatureList::IsEnabled(kDumpOnOOBFontDataServiceCache)) {
            base::debug::DumpWithoutCrashing();
          }
          const size_t asset_index = GetOrCreateAssetIndex(std::move(stream));
          base::ReadOnlySharedMemoryRegion region =
              assets_[asset_index]->shared_memory.region.Duplicate();
          typeface_to_asset_index_[typeface->uniqueID()] =
              MappedTypeface{asset_index, result->ttc_index};
          if (region.IsValid()) {
            result->typeface_data =
                mojom::TypefaceData::NewRegion(std::move(region));
            result_status = CreateResult::kSuccessSharingNewMemoryRegion;
          } else {
            result_status = CreateResult::kFailureSharingNewMemoryRegion;
          }
        }
      }
    }
  }

  UMA_HISTOGRAM_ENUMERATION("Chrome.FontDataService.CreateResult",
                            result_status);

  if (!result->typeface_data) {
    return nullptr;
  }

  result->synthetic_bold = typeface->isSyntheticBold();
  result->synthetic_oblique = typeface->isSyntheticOblique();

  const int axis_count = typeface->getVariationDesignPosition({});
  if (axis_count > 0) {
    auto coordinate_list =
        base::HeapArray<SkFontArguments::VariationPosition::Coordinate>::Uninit(
            axis_count);
    if (typeface->getVariationDesignPosition(coordinate_list) > 0) {
      result->variation_position = mojom::VariationPosition::New();
      result->variation_position->coordinates.reserve(coordinate_list.size());
      result->variation_position->coordinateCount = axis_count;
      std::ranges::transform(
          coordinate_list,
          std::back_inserter(result->variation_position->coordinates),
          [](const SkFontArguments::VariationPosition::Coordinate& coordinate) {
            return mojom::Coordinate::New(coordinate.axis, coordinate.value);
          });
    }
  }

  return result;
}

void FontDataServiceImpl::MatchLocalFont(const std::string& font_unique_name,
                                         MatchLocalFontCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT("fonts", "FontDataServiceImpl::MatchLocalFont",
              "font_unique_name", font_unique_name);
  base::UmaHistogramEnumeration("Chrome.FontDataService.InvokedIPC",
                                FontDataServiceIPC::kMatchLocalFont);

  if (local_font_matcher_) {
    std::optional<LocalFontMatchResult> match =
        local_font_matcher_->MatchLocalFont(font_unique_name);
    base::UmaHistogramBoolean(
        "Chrome.FontDataService.MatchLocalFont.FilterSuccess",
        match.has_value());
    if (match) {
      base::File font_file(match->file_path,
                           base::File::FLAG_OPEN | base::File::FLAG_READ |
                               base::File::FLAG_WIN_EXCLUSIVE_WRITE);
#if BUILDFLAG(IS_WIN)
      if (!font_file.IsValid()) {
        base::UmaHistogramSparse("Chrome.FontDataService.WinLastError",
                                 ::GetLastError());
      }
#endif  // BUILDFLAG(IS_WIN)
      if (font_file.IsValid()) {
        auto result = mojom::MatchFamilyNameResult::New();
        result->ttc_index = match->ttc_index;
        result->typeface_data =
            mojom::TypefaceData::NewFontFile(mojom::TypefaceFile::New(
                std::move(font_file), GetUniqueFileId(match->file_path)));
        std::move(callback).Run(std::move(result));
        return;
      }
    }
  }
  std::move(callback).Run(nullptr);
}

}  // namespace font_data_service
