// Copyright (c) 2012 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 "ui/gfx/platform_font_mac.h"

#include <cmath>

#include <Cocoa/Cocoa.h>

#import "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#import "base/mac/scoped_nsobject.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/font.h"
#include "ui/gfx/font_render_params.h"

namespace gfx {

namespace {

// How to get from NORMAL weight to a fine-grained font weight using calls to
// -[NSFontManager convertWeight:(BOOL)upFlag ofFont:(NSFont)].
struct WeightSolver {
  int steps_up;    // Times to call with upFlag:YES.
  int steps_down;  // Times to call with upFlag:NO.
  // Either NORMAL or BOLD: whether to set the NSBoldFontMask symbolic trait.
  Font::Weight nearest;
};

// Solve changes to the font weight according to the following table, from
// https://developer.apple.com/reference/appkit/nsfontmanager/1462321-convertweight
// 1. ultralight                     | none
// 2. thin                           | W1. ultralight
// 3. light, extralight              | W2. extralight
// 4. book                           | W3. light
// 5. regular, plain, display, roman | W4. semilight
// 6. medium                         | W5. medium
// 7. demi, demibold                 | none
// 8. semi, semibold                 | W6. semibold
// 9. bold                           | W7. bold
// 10. extra, extrabold              | W8. extrabold
// 11. heavy, heavyface              | none
// 12. black, super                  | W9. ultrabold
// 13. ultra, ultrablack, fat        | none
// 14. extrablack, obese, nord       | none
WeightSolver WeightChangeFromNormal(Font::Weight desired) {
  using Weight = Font::Weight;
  switch (desired) {
    case Weight::THIN:
      // It's weird, but to get LIGHT and THIN fonts, first go up a step.
      // Without this, the font stays stuck at NORMAL. See
      // PlatformFontMacTest, FontWeightAPIConsistency.
      return {1, 3, Weight::NORMAL};
    case Weight::EXTRA_LIGHT:
      return {1, 2, Weight::NORMAL};
    case Weight::LIGHT:
      return {1, 1, Weight::NORMAL};
    case Weight::NORMAL:
      return {0, 0, Weight::NORMAL};
    case Weight::MEDIUM:
      return {1, 0, Weight::NORMAL};
    case Weight::SEMIBOLD:
      return {0, 1, Weight::BOLD};
    case Weight::BOLD:
      return {0, 0, Weight::BOLD};
    case Weight::EXTRA_BOLD:
      return {1, 0, Weight::BOLD};
    case Weight::BLACK:
      return {3, 0, Weight::BOLD};  // Skip row 12.
    case Weight::INVALID:
      return {0, 0, Weight::NORMAL};
  }
}

// Returns the font style for |font|. Disregards Font::UNDERLINE, since NSFont
// does not support it as a trait.
int GetFontStyleFromNSFont(NSFont* font) {
  int font_style = Font::NORMAL;
  NSFontSymbolicTraits traits = [[font fontDescriptor] symbolicTraits];
  if (traits & NSFontItalicTrait)
    font_style |= Font::ITALIC;
  return font_style;
}

// Returns the Font weight for |font|.
Font::Weight GetFontWeightFromNSFont(NSFont* font) {
  DCHECK(font);

  // Map CoreText weights in a manner similar to ct_weight_to_fontstyle() from
  // SkFontHost_mac.cpp, but adjust for MEDIUM so that the San Francisco's
  // custom MEDIUM weight can be picked out. San Francisco has weights:
  // [0.23, 0.23, 0.3, 0.4, 0.56, 0.62, 0.62, ...] (no thin weights).
  // See PlatformFontMacTest.FontWeightAPIConsistency for details.
  // Note that the table Skia uses is also determined by experiment.
  constexpr struct {
    CGFloat ct_weight;
    Font::Weight gfx_weight;
  } weight_map[] = {
      // Missing: Apple "ultralight".
      {-0.70, Font::Weight::THIN},
      {-0.50, Font::Weight::EXTRA_LIGHT},
      {-0.23, Font::Weight::LIGHT},
      {0.00, Font::Weight::NORMAL},
      {0.23, Font::Weight::MEDIUM},  // Note: adjusted from 0.20 vs Skia.
      // Missing: Apple "demibold".
      {0.30, Font::Weight::SEMIBOLD},
      {0.40, Font::Weight::BOLD},
      {0.60, Font::Weight::EXTRA_BOLD},
      // Missing: Apple "heavyface".
      // Values will be capped to BLACK (this entry is here for consistency).
      {0.80, Font::Weight::BLACK},
      // Missing: Apple "ultrablack".
      // Missing: Apple "extrablack".
  };
  base::ScopedCFTypeRef<CFDictionaryRef> traits(
      CTFontCopyTraits(base::mac::NSToCFCast(font)));
  DCHECK(traits);
  CFNumberRef cf_weight = base::mac::GetValueFromDictionary<CFNumberRef>(
      traits, kCTFontWeightTrait);
  // A missing weight attribute just means 0 -> NORMAL.
  if (!cf_weight)
    return Font::Weight::NORMAL;

  // Documentation is vague about what sized floating point type should be used.
  // However, numeric_limits::epsilon() for 64-bit types is too small to match
  // the above table, so use 32-bit float.
  float weight_value;
  Boolean success =
      CFNumberGetValue(cf_weight, kCFNumberFloatType, &weight_value);
  DCHECK(success);
  for (const auto& item : weight_map) {
    if (weight_value - item.ct_weight <= std::numeric_limits<float>::epsilon())
      return item.gfx_weight;
  }
  return Font::Weight::BLACK;
}

// Returns an autoreleased NSFont created with the passed-in specifications.
NSFont* NSFontWithSpec(const std::string& font_name,
                       int font_size,
                       int font_style,
                       Font::Weight font_weight) {
  NSFontSymbolicTraits trait_bits = 0;
  // TODO(mboc): Add support for other weights as well.
  if (font_weight >= Font::Weight::BOLD)
    trait_bits |= NSFontBoldTrait;
  if (font_style & Font::ITALIC)
    trait_bits |= NSFontItalicTrait;
  // The Mac doesn't support underline as a font trait, so just drop it.
  // (Underlines must be added as an attribute on an NSAttributedString.)
  NSDictionary* traits = @{ NSFontSymbolicTrait : @(trait_bits) };

  NSDictionary* attrs = @{
    NSFontFamilyAttribute : base::SysUTF8ToNSString(font_name),
    NSFontTraitsAttribute : traits
  };
  NSFontDescriptor* descriptor =
      [NSFontDescriptor fontDescriptorWithFontAttributes:attrs];
  NSFont* font = [NSFont fontWithDescriptor:descriptor size:font_size];
  if (font)
    return font;

  // Make one fallback attempt by looking up via font name rather than font
  // family name.
  attrs = @{
    NSFontNameAttribute : base::SysUTF8ToNSString(font_name),
    NSFontTraitsAttribute : traits
  };
  descriptor = [NSFontDescriptor fontDescriptorWithFontAttributes:attrs];
  return [NSFont fontWithDescriptor:descriptor size:font_size];
}

// Returns |font| or a default font if |font| is nil.
NSFont* ValidateFont(NSFont* font) {
  return font ? font : [NSFont systemFontOfSize:[NSFont systemFontSize]];
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// PlatformFontMac, public:

PlatformFontMac::PlatformFontMac()
    : PlatformFontMac([NSFont systemFontOfSize:[NSFont systemFontSize]]) {
}

PlatformFontMac::PlatformFontMac(NativeFont native_font)
    : PlatformFontMac(native_font,
                      base::SysNSStringToUTF8([native_font familyName]),
                      [native_font pointSize],
                      GetFontStyleFromNSFont(native_font),
                      GetFontWeightFromNSFont(native_font)) {
  DCHECK(native_font);  // Null should not be passed to this constructor.
}

PlatformFontMac::PlatformFontMac(const std::string& font_name, int font_size)
    : PlatformFontMac(font_name,
                      font_size,
                      Font::NORMAL,
                      Font::Weight::NORMAL) {}

////////////////////////////////////////////////////////////////////////////////
// PlatformFontMac, PlatformFont implementation:

Font PlatformFontMac::DeriveFont(int size_delta,
                                 int style,
                                 Font::Weight weight) const {
  // For some reason, creating fonts using the NSFontDescriptor API's seem to be
  // unreliable. Hence use the NSFontManager.
  NSFont* derived = native_font_;
  NSFontManager* font_manager = [NSFontManager sharedFontManager];

  if (weight != font_weight_) {
    // Find a font without any bold traits. Ideally, all bold traits are
    // removed here, but non-symbolic traits are read-only properties of a
    // particular set of glyphs. And attempting to "reset" the attribute with a
    // new font descriptor will lose internal properties that Mac decorates its
    // UI fonts with. E.g., solving the plans below from NORMAL result in a
    // CTFontDescriptor attribute entry of NSCTFontUIUsageAttribute in
    // CTFont{Regular,Medium,Demi,Emphasized,Heavy,Black}Usage. Attempting to
    // "solve" weights starting at other than NORMAL has unpredictable results.
    if (font_weight_ != Font::Weight::NORMAL)
      derived = [font_manager convertFont:derived toHaveTrait:NSUnboldFontMask];
    DLOG_IF(WARNING, GetFontWeightFromNSFont(derived) != Font::Weight::NORMAL)
        << "Deriving from a font with an internal unmodifiable weight.";

    WeightSolver plan = WeightChangeFromNormal(weight);
    if (plan.nearest == Font::Weight::BOLD)
      derived = [font_manager convertFont:derived toHaveTrait:NSBoldFontMask];
    for (int i = 0; i < plan.steps_up; ++i)
      derived = [font_manager convertWeight:YES ofFont:derived];
    for (int i = 0; i < plan.steps_down; ++i)
      derived = [font_manager convertWeight:NO ofFont:derived];
  }

  // Always apply the italic trait, even if the italic trait is not changing.
  // it's possible for a change in the weight to trigger the font to go italic.
  // This is due to an AppKit bug. See http://crbug.com/742261.
  if (style != font_style_ || weight != font_weight_) {
    NSFontTraitMask italic_trait_mask =
        (style & Font::ITALIC) ? NSItalicFontMask : NSUnitalicFontMask;
    derived = [font_manager convertFont:derived toHaveTrait:italic_trait_mask];
  }

  if (size_delta != 0)
    derived = [font_manager convertFont:derived toSize:font_size_ + size_delta];

  return Font(new PlatformFontMac(derived, font_name_, font_size_ + size_delta,
                                  style, weight));
}

int PlatformFontMac::GetHeight() {
  return height_;
}

int PlatformFontMac::GetBaseline() {
  return ascent_;
}

int PlatformFontMac::GetCapHeight() {
  return cap_height_;
}

int PlatformFontMac::GetExpectedTextWidth(int length) {
  if (!average_width_) {
    // -[NSFont boundingRectForGlyph:] seems to always return the largest
    // bounding rect that could be needed, which produces very wide expected
    // widths for strings. Instead, compute the actual width of a string
    // containing all the lowercase characters to find a reasonable guess at the
    // average.
    base::scoped_nsobject<NSAttributedString> attr_string(
        [[NSAttributedString alloc]
            initWithString:@"abcdefghijklmnopqrstuvwxyz"
                attributes:@{NSFontAttributeName : native_font_.get()}]);
    average_width_ = [attr_string size].width / [attr_string length];
    DCHECK_NE(0, average_width_);
  }
  return ceil(length * average_width_);
}

int PlatformFontMac::GetStyle() const {
  return font_style_;
}

Font::Weight PlatformFontMac::GetWeight() const {
  return font_weight_;
}

const std::string& PlatformFontMac::GetFontName() const {
  return font_name_;
}

std::string PlatformFontMac::GetActualFontNameForTesting() const {
  return base::SysNSStringToUTF8([native_font_ familyName]);
}

int PlatformFontMac::GetFontSize() const {
  return font_size_;
}

const FontRenderParams& PlatformFontMac::GetFontRenderParams() {
  return render_params_;
}

NativeFont PlatformFontMac::GetNativeFont() const {
  return [[native_font_.get() retain] autorelease];
}

////////////////////////////////////////////////////////////////////////////////
// PlatformFontMac, private:

PlatformFontMac::PlatformFontMac(const std::string& font_name,
                                 int font_size,
                                 int font_style,
                                 Font::Weight font_weight)
    : PlatformFontMac(
          NSFontWithSpec(font_name, font_size, font_style, font_weight),
          font_name,
          font_size,
          font_style,
          font_weight) {}

PlatformFontMac::PlatformFontMac(NativeFont font,
                                 const std::string& font_name,
                                 int font_size,
                                 int font_style,
                                 Font::Weight font_weight)
    : native_font_([ValidateFont(font) retain]),
      font_name_(font_name),
      font_size_(font_size),
      font_style_(font_style),
      font_weight_(font_weight) {
  CalculateMetricsAndInitRenderParams();
}

PlatformFontMac::~PlatformFontMac() {
}

void PlatformFontMac::CalculateMetricsAndInitRenderParams() {
  NSFont* font = native_font_.get();
  DCHECK(font);
  ascent_ = ceil([font ascender]);
  cap_height_ = ceil([font capHeight]);

  // PlatformFontMac once used -[NSLayoutManager defaultLineHeightForFont:] to
  // initialize |height_|. However, it has a silly rounding bug. Essentially, it
  // gives round(ascent) + round(descent). E.g. Helvetica Neue at size 16 gives
  // ascent=15.4634, descent=3.38208 -> 15 + 3 = 18. When the height should be
  // at least 19. According to the OpenType specification, these values should
  // simply be added, so do that. Note this uses the already-rounded |ascent_|
  // to ensure GetBaseline() + descender fits within GetHeight() during layout.
  height_ = ceil(ascent_ + std::abs([font descender]) + [font leading]);

  FontRenderParamsQuery query;
  query.families.push_back(font_name_);
  query.pixel_size = font_size_;
  query.style = font_style_;
  query.weight = font_weight_;
  render_params_ = gfx::GetFontRenderParams(query, NULL);
}

////////////////////////////////////////////////////////////////////////////////
// PlatformFont, public:

// static
PlatformFont* PlatformFont::CreateDefault() {
  return new PlatformFontMac;
}

// static
PlatformFont* PlatformFont::CreateFromNativeFont(NativeFont native_font) {
  return new PlatformFontMac(ValidateFont(native_font));
}

// static
PlatformFont* PlatformFont::CreateFromNameAndSize(const std::string& font_name,
                                                  int font_size) {
  return new PlatformFontMac(font_name, font_size);
}

}  // namespace gfx
