/* Pango2
 *
 * Copyright (C) 2021 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "config.h"
#include <math.h>

#include <gio/gio.h>

#include "pangocoretext-fontmap.h"
#include "pango-hbfamily-private.h"
#include "pango-fontmap-private.h"
#include "pango-hbface-private.h"
#include "pango-hbfont-private.h"
#include "pango-context.h"
#include "pango-impl-utils.h"
#include "pango-trace-private.h"

#include <Carbon/Carbon.h>
#include <hb-ot.h>
#include <hb-coretext.h>


/**
 * Pango2CoreTextFontMap:
 *
 * `Pango2CoreTextFontMap` is a subclass of `Pango2FontMap` that uses
 * CoreText to populate the fontmap with the available fonts.
 */

/**
 * PANGO2_HAS_CORE_TEXT_FONTMAP:
 *
 * Defined to 1 at compile time if Pango was built with CoreText support.
 */

struct _Pango2CoreTextFontMap
{
  Pango2FontMap parent_instance;
};

struct _Pango2CoreTextFontMapClass
{
  Pango2FontMapClass parent_class;
};

/* {{{ CoreText utilities */

#define ct_weight_min -0.7f
#define ct_weight_max  0.8f

/* This map is based on empirical data from analyzing a large collection of
 * fonts and comparing the opentype value with the value that OSX returns.
 * see: https://bugzilla.gnome.org/show_bug.cgi?id=766148
 * FIXME: This need recalibrating, values outside these bounds do occur!
 */

static struct {
  float ct_weight;
  Pango2Weight pango2_weight;
} ct_weight_map[] = {
  { ct_weight_min,  PANGO2_WEIGHT_THIN },
  { -0.5,           PANGO2_WEIGHT_ULTRALIGHT },
  { -0.23,          PANGO2_WEIGHT_LIGHT },
  { -0.115,         PANGO2_WEIGHT_SEMILIGHT },
  {  0.00,          PANGO2_WEIGHT_NORMAL },
  {  0.2,           PANGO2_WEIGHT_MEDIUM },
  {  0.3,           PANGO2_WEIGHT_SEMIBOLD },
  {  0.4,           PANGO2_WEIGHT_BOLD },
  {  0.6,           PANGO2_WEIGHT_ULTRABOLD },
  {  ct_weight_max, PANGO2_WEIGHT_HEAVY }
};

static int
lerp (float x, float x1, float x2, int y1, int y2)
{
  float dx = x2 - x1;
  int dy = y2 - y1;
  return y1 + (dy * (x - x1) + dx / 2) / dx;
}

static Pango2Weight
ct_font_descriptor_get_weight (CTFontDescriptorRef desc)
{
  CFDictionaryRef dict;
  CFNumberRef cf_number;
  CGFloat value;
  Pango2Weight weight = PANGO2_WEIGHT_NORMAL;
  guint i;

  dict = CTFontDescriptorCopyAttribute (desc, kCTFontTraitsAttribute);
  cf_number = (CFNumberRef)CFDictionaryGetValue (dict, kCTFontWeightTrait);

  if (cf_number != NULL && CFNumberGetValue (cf_number, kCFNumberCGFloatType, &value))
    {
    if (!(value >= ct_weight_min && value <= ct_weight_max))
      {
        i = value > ct_weight_max ? G_N_ELEMENTS (ct_weight_map) - 1 : 0;
        weight = ct_weight_map[i].ct_weight;
      }
    else
      {
        for (i = 1; value > ct_weight_map[i].ct_weight; ++i)
          ;

        weight = lerp (value, ct_weight_map[i-1].ct_weight, ct_weight_map[i].ct_weight,
                       ct_weight_map[i-1].pango2_weight, ct_weight_map[i].pango2_weight);

      }
    }
  else
    weight = PANGO2_WEIGHT_NORMAL;

  CFRelease (dict);

  return weight;
}

static char *
gchar_from_cf_string (CFStringRef str)
{
  CFIndex len;
  char *buffer;

  /* GetLength returns the number of UTF-16 pairs, so this number
   * times 2 should definitely gives us enough space for UTF8.
   * We add one for the terminating zero.
   */
  len = CFStringGetLength (str) * 2 + 1;
  buffer = g_new0 (char, len);
  CFStringGetCString (str, buffer, len, kCFStringEncodingUTF8);

  return buffer;
}

static char *
ct_font_descriptor_get_family_name (CTFontDescriptorRef desc,
                                    gboolean            may_fail)
{
  CFStringRef cf_str;
  char *buffer;

  cf_str = CTFontDescriptorCopyAttribute (desc, kCTFontFamilyNameAttribute);
  if (!cf_str)
    {
      int i;

      /* No font family name is set, try to retrieve font name and deduce
       * the family name from that instead.
       */
      cf_str = CTFontDescriptorCopyAttribute (desc, kCTFontNameAttribute);
      if (!cf_str)
        {
          if (may_fail)
            return NULL;

          /* This font is likely broken, return a default family name ... */
          return g_strdup ("Sans");
        }

      buffer = gchar_from_cf_string (cf_str);
      CFRelease (cf_str);

      for (i = 0; i < strlen (buffer); i++)
        if (buffer[i] == '-')
          break;

      if (i < strlen (buffer))
        {
          char *ret;

          ret = g_strndup (buffer, i);
          g_free (buffer);

          return ret;
        }
      else
        return buffer;
    }

  buffer = gchar_from_cf_string (cf_str);
  CFRelease (cf_str);

  return buffer;
}

static char *
ct_font_descriptor_get_style_name (CTFontDescriptorRef desc)
{
  CFStringRef cf_str;
  char *buffer;

  cf_str = CTFontDescriptorCopyAttribute (desc, kCTFontStyleNameAttribute);
  if (!cf_str)
    return NULL;

  buffer = gchar_from_cf_string (cf_str);
  CFRelease (cf_str);

  return buffer;
}

static CTFontSymbolicTraits
ct_font_descriptor_get_traits (CTFontDescriptorRef desc)
{
  CFDictionaryRef dict;
  CFNumberRef cf_number;
  SInt64 traits;

  /* This is interesting, the value stored is a CTFontSymbolicTraits which
   * is defined as uint32_t.  CFNumber does not have an obvious type which
   * deals with unsigned values.  Upon inspection with CFNumberGetType,
   * it turns out this value is stored as SInt64, so we use that to
   * obtain the value from the CFNumber.
   */
  dict = CTFontDescriptorCopyAttribute (desc, kCTFontTraitsAttribute);
  cf_number = (CFNumberRef)CFDictionaryGetValue (dict, kCTFontSymbolicTrait);
  if (!CFNumberGetValue (cf_number, kCFNumberSInt64Type, &traits))
    traits = 0;
  CFRelease (dict);

  return (CTFontSymbolicTraits)traits;
}

static gboolean
ct_font_descriptor_is_small_caps (CTFontDescriptorRef desc)
{
  CFIndex i, count;
  CFArrayRef array;
  CFStringRef str;
  gboolean retval = FALSE;

  /* See http://stackoverflow.com/a/4811371 for why this works and an
   * explanation of the magic number "3" used below.
   */
  array = CTFontDescriptorCopyAttribute (desc, kCTFontFeaturesAttribute);
  if (!array)
    return FALSE;

  str = CFStringCreateWithCString (NULL, "CTFeatureTypeIdentifier",
                                   kCFStringEncodingASCII);

  count = CFArrayGetCount (array);
  for (i = 0; i < count; i++)
    {
      CFDictionaryRef dict = CFArrayGetValueAtIndex (array, i);
      CFNumberRef num;

      num = (CFNumberRef)CFDictionaryGetValue (dict, str);
      if (num)
        {
          int value = 0;

          if (CFNumberGetValue (num, kCFNumberSInt32Type, &value) &&
              value == 3)
            {
              /* This font supports small caps. */
              retval = TRUE;
              break;
            }
        }
    }

  CFRelease (str);
  CFRelease (array);

  return retval;
}

static gboolean
pango2_core_text_style_name_is_oblique (const char *style_name)
{
  return style_name && strstr (style_name, "Oblique");
}

static Pango2FontDescription *
font_description_from_ct_font_descriptor (CTFontDescriptorRef desc)
{
  SInt64 font_traits;
  char *family_name;
  char *style_name;
  Pango2FontDescription *font_desc;

  font_desc = pango2_font_description_new ();

  family_name = ct_font_descriptor_get_family_name (desc, FALSE);
  pango2_font_description_set_family (font_desc, family_name);
  g_free (family_name);

  pango2_font_description_set_weight (font_desc, ct_font_descriptor_get_weight (desc));

  font_traits = ct_font_descriptor_get_traits (desc);
  style_name = ct_font_descriptor_get_style_name (desc);

  if ((font_traits & kCTFontItalicTrait) == kCTFontItalicTrait)
    pango2_font_description_set_style (font_desc, PANGO2_STYLE_ITALIC);
  else if (pango2_core_text_style_name_is_oblique (style_name))
    pango2_font_description_set_style (font_desc, PANGO2_STYLE_OBLIQUE);
  else
    pango2_font_description_set_style (font_desc, PANGO2_STYLE_NORMAL);

  if ((font_traits & kCTFontCondensedTrait) == kCTFontCondensedTrait)
    pango2_font_description_set_stretch (font_desc, PANGO2_STRETCH_CONDENSED);

  if (ct_font_descriptor_is_small_caps (desc))
    pango2_font_description_set_variant (font_desc, PANGO2_VARIANT_SMALL_CAPS);
  else
    pango2_font_description_set_variant (font_desc, PANGO2_VARIANT_NORMAL);

  g_free (style_name);

  return font_desc;
}

static Pango2HbFace *
face_from_ct_font_descriptor (CTFontDescriptorRef desc)
{
  Pango2HbFace *face;
  CTFontRef ctfont;
  CGFontRef cgfont;
  hb_face_t *hb_face;
  char *name;
  Pango2FontDescription *description;

  ctfont = CTFontCreateWithFontDescriptor (desc, 0.0, NULL);
  cgfont = CTFontCopyGraphicsFont (ctfont, NULL);

  hb_face = hb_coretext_face_create (cgfont);

  CFRelease (cgfont);
  CFRelease (ctfont);

  name = ct_font_descriptor_get_style_name (desc);
  description = font_description_from_ct_font_descriptor (desc);
  pango2_font_description_unset_fields (description, PANGO2_FONT_MASK_VARIANT |
                                                     PANGO2_FONT_MASK_SIZE |
                                                     PANGO2_FONT_MASK_GRAVITY);

  hb_face_make_immutable (hb_face);

  face = pango2_hb_face_new_from_hb_face (hb_face, -1, name, description);
  // FIXME: what about languages? see CTFontCopySupportedLanguages

  pango2_font_description_free (description);
  g_free (name);
  hb_face_destroy (hb_face);

  return face;
}

/* }}} */
/* {{{ Pango2FontMap implementation */

static void
pango2_core_text_font_map_populate (Pango2FontMap *map)
{
  CTFontCollectionRef collection;
  CFArrayRef ctfaces;
  CFIndex count;

  /* Add all system fonts */
  collection = CTFontCollectionCreateFromAvailableFonts (0);
  ctfaces = CTFontCollectionCreateMatchingFontDescriptors (collection);
  count = CFArrayGetCount (ctfaces);

  for (int i = 0; i < count; i++)
    {
      CTFontDescriptorRef desc = CFArrayGetValueAtIndex (ctfaces, i);
      Pango2HbFace *face = face_from_ct_font_descriptor (desc);
      pango2_font_map_add_face (map, PANGO2_FONT_FACE (face));
    }

  /* Add generic aliases */
  struct {
    const char *alias_name;
    const char *family_name;
  } aliases[] = {
    { "Monospace",  "Courier" },
    { "Sans-serif", "Helvetica" },
    { "Sans", "Helvetica" },
    { "Serif",      "Times" },
    { "Cursive",    "Apple Chancery" },
    { "Fantasy",    "Papyrus", },
    { "System-ui",  ".AppleSystemUIFont" },
    { "Emoji",      "Apple Color Emoji" }
  };

  for (int i = 0; i < G_N_ELEMENTS (aliases); i++)
    {
      Pango2FontFamily *family = pango2_font_map_get_family (map, aliases[i].family_name);

      if (family)
        {
          Pango2GenericFamily *alias_family;

          alias_family = pango2_generic_family_new (aliases[i].alias_name);
          pango2_generic_family_add_family (alias_family, family);
          pango2_font_map_add_family (map, PANGO2_FONT_FAMILY (alias_family));
        }
    }
}

/* }}} */
/* {{{ Pango2CoreTextFontMap implementation */

G_DEFINE_FINAL_TYPE (Pango2CoreTextFontMap, pango2_core_text_font_map, PANGO2_TYPE_FONT_MAP)

static void
pango2_core_text_font_map_init (Pango2CoreTextFontMap *self)
{
  pango2_font_map_repopulate (PANGO2_FONT_MAP (self), TRUE);
}

static void
pango2_core_text_font_map_finalize (GObject *object)
{
  G_OBJECT_CLASS (pango2_core_text_font_map_parent_class)->finalize (object);
}

static void
pango2_core_text_font_map_class_init (Pango2CoreTextFontMapClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  Pango2FontMapClass *font_map_class = PANGO2_FONT_MAP_CLASS (class);

  object_class->finalize = pango2_core_text_font_map_finalize;

  font_map_class->populate = pango2_core_text_font_map_populate;
}

/* }}} */
 /* {{{ Public API */

/**
 * pango2_core_text_font_map_new:
 *
 * Creates a new `Pango2CoreTextFontMap` object.
 *
 * Returns: a new `Pango2CoreTextFontMap`
 */
Pango2CoreTextFontMap *
pango2_core_text_font_map_new (void)
{
  return g_object_new (PANGO2_TYPE_CORE_TEXT_FONT_MAP, NULL);
}

/* }}} */

/* vim:set foldmethod=marker expandtab: */
