text: # Copyright 2015 Google Inc. All Rights Reserved.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  
  from collections import defaultdict
  import re
  
  from glyphsLib.util import bin_to_int_list, int_list_to_bin
  from .filters import parse_glyphs_filter
  from .constants import (
      GLYPHS_PREFIX,
      UFO2FT_FILTERS_KEY,
      UFO2FT_USE_PROD_NAMES_KEY,
      CODEPAGE_RANGES,
      REVERSE_CODEPAGE_RANGES,
      PUBLIC_PREFIX,
      UFO_FILENAME_CUSTOM_PARAM,
  )
  from .features import replace_feature, replace_prefixes
  
  """Set Glyphs custom parameters in UFO info or lib, where appropriate.
  
  Custom parameter data will be extracted from a Glyphs object such as GSFont,
  GSFontMaster or GSInstance by wrapping it in the GlyphsObjectProxy.
  This proxy normalizes and speeds up the API used to access custom parameters,
  and also keeps track of which customParameters have been read from the object.
  
  Note:
      In the special case of GSInstance -> UFO, the source object is not
      actually the GSInstance but a designspace InstanceDescriptor wrapped in
      InstanceDescriptorAsGSInstance. This is because the generation of
      instance UFOs from a Glyphs font happens in two steps:
  
          1. the GSFont is turned into a designspace + master UFOS
          2. the designspace + master UFOs are interpolated into instance UFOs
  
      We want step 2. to rely only on information from the designspace, that's why
      we use the InstanceDescriptor as a source of customParameters to put into
      the instance UFO.
  
  In the other direction, put information from UFO info or lib into a GSFont or a
  GSFontMaster. The UFO source is wrapped in a UFOProxy that records which
  attributes are read/written.
  
  In order to go in both directions, each known parameter is managed by a
  ParamHandler object that can implement special rules to translate the value
  between Glyphs and UFO formats. This files aims at providing at least one
  handler per defined UFO info attribute, plus a bunch of handlers for known
  Custom Paramerters or known UFO lib elements.
  
  To go for example from UFO to Glyphs, each registered ParamHandler is called,
  and each tries to find its parameter in the UFO's info or lib data. Accesses to
  the UFO lib are recorded by the UFO proxy. After all registered ParamHandlers
  have worked, we know which UFO lib fields have been "consumed" in a smart way,
  and we can stupidly copy the other ones over to the Glyphs side. Same when
  going from Glyphs to UFOs.
  """
  
  CUSTOM_PARAM_PREFIX = GLYPHS_PREFIX + "customParameter."
  
  
  def identity(value):
      return value
  
  
  class GlyphsObjectProxy:
      """Accelerate and record access to the glyphs object's custom parameters"""
  
      def __init__(self, glyphs_object, glyphs_module):
          self._owner = glyphs_object
          # This is a key part to be used in UFO lib keys to be able to choose
          # between master and font attributes during roundtrip
          self.sub_key = glyphs_object.__class__.__name__ + "."
          self._glyphs_module = glyphs_module
          self._lookup = defaultdict(list)
          for param in glyphs_object.customParameters:
              self._lookup[param.name].append(param.value)
          self._handled = set()
  
      def get_attribute_value(self, key):
          if not hasattr(self._owner, key):
              return None
          return getattr(self._owner, key)
  
      def set_attribute_value(self, key, value):
          if not hasattr(self._owner, key):
              return
          setattr(self._owner, key, value)
  
      def get_custom_value(self, key):
          """Return the first and only custom parameter matching the given name."""
          self._handled.add(key)
          values = self._lookup[key]
          if len(values) > 1:
              raise RuntimeError(f"More than one value for this customParameter: {key}")
          if values:
              return values[0]
          return None
  
      def get_custom_values(self, key):
          """Return a set of values for the given customParameter name."""
          self._handled.add(key)
          return self._lookup[key]
  
      def set_custom_value(self, key, value):
          """Set one custom parameter with the given value.
          We assume that the list of custom parameters does not already contain
          the given parameter so we only append.
          """
          self._owner.customParameters.append(
              self._glyphs_module.GSCustomParameter(name=key, value=value)
          )
  
      def set_custom_values(self, key, values):
          """Set several values for the customParameter with the given key.
          We append one GSCustomParameter per value.
          """
          for value in values:
              self.set_custom_value(key, value)
  
      def unhandled_custom_parameters(self):
          for param in self._owner.customParameters:
              if param.name not in self._handled:
                  yield param
  
      def mark_handled(self, key):
          """Mark a key as handled so it is ignored by `unhandled_custom_parameters`.
  
          Use e.g. when you handle a custom parameter outside this module.
          """
          self._handled.add(key)
  
      def is_font(self):
          """Returns whether we are looking at a top-level GSFont object as
          opposed to a master or instance."""
          return hasattr(self._owner, "glyphs")
  
  
  class UFOProxy:
      """Record access to the UFO's lib custom parameters"""
  
      def __init__(self, ufo):
          self._owner = ufo
          self._handled = set()
  
      def has_info_attr(self, name):
          return hasattr(self._owner.info, name)
  
      def get_info_value(self, name):
          return getattr(self._owner.info, name)
  
      def set_info_value(self, name, value):
          setattr(self._owner.info, name, value)
  
      def has_lib_key(self, name):
          return name in self._owner.lib
  
      def get_lib_value(self, name):
          if name not in self._owner.lib:
              return None
          self._handled.add(name)
          return self._owner.lib[name]
  
      def set_lib_value(self, name, value):
          self._owner.lib[name] = value
  
      def unhandled_lib_items(self):
          for key, value in self._owner.lib.items():
              if key.startswith(CUSTOM_PARAM_PREFIX) and key not in self._handled:
                  yield (key, value)
  
  
  class AbstractParamHandler:
      # @abstractmethod
      def to_glyphs(self):
          pass
  
      # @abstractmethod
      def to_ufo(self):
          pass
  
  
  class ParamHandler(AbstractParamHandler):
      def __init__(
          self,
          glyphs_name,
          ufo_name=None,
          glyphs_long_name=None,
          glyphs_multivalued=False,
          ufo_prefix=CUSTOM_PARAM_PREFIX,
          ufo_info=True,
          ufo_default=None,
          value_to_ufo=identity,
          value_to_glyphs=identity,
      ):
          self.glyphs_name = glyphs_name
          self.glyphs_long_name = glyphs_long_name
          self.glyphs_multivalued = glyphs_multivalued
          # By default, they have the same name in both
          self.ufo_name = ufo_name or glyphs_name
          self.ufo_prefix = ufo_prefix
          self.ufo_info = ufo_info
          self.ufo_default = ufo_default
          # Value transformation functions
          self.value_to_ufo = value_to_ufo
          self.value_to_glyphs = value_to_glyphs
  
      # By default, the parameter is read from/written to:
      #  - the Glyphs object's customParameters
      #  - the UFO's info object if it has a matching attribute, else the lib
      def to_glyphs(self, glyphs, ufo):
          ufo_value = self._read_from_ufo(glyphs, ufo)
          if ufo_value is None:
              return
          glyphs_value = self.value_to_glyphs(ufo_value)
          self._write_to_glyphs(glyphs, glyphs_value)
  
      def to_ufo(self, builder, glyphs, ufo):
          glyphs_value = self._read_from_glyphs(glyphs)
          if glyphs_value is None:
              return
          ufo_value = self.value_to_ufo(glyphs_value)
          self._write_to_ufo(glyphs, ufo, ufo_value)
  
      def _read_from_glyphs(self, glyphs):
          # Try both the prefixed (long) name and the short name
          if self.glyphs_multivalued:
              getter = glyphs.get_custom_values
          else:
              getter = glyphs.get_custom_value
          # The value registered using the small name has precedence
          small_name_value = getter(self.glyphs_name)
          if small_name_value is not None:
              return small_name_value
          if self.glyphs_long_name is not None:
              return getter(self.glyphs_long_name)
          return None
  
      def _write_to_glyphs(self, glyphs, value):
          # Never write the prefixed (long) name?
          # FIXME: (jany) maybe should rather preserve the naming choice of user
          if self.glyphs_multivalued:
              glyphs.set_custom_values(self.glyphs_name, value)
          else:
              glyphs.set_custom_value(self.glyphs_name, value)
  
      def _read_from_ufo(self, glyphs, ufo):
          if self.ufo_info and ufo.has_info_attr(self.ufo_name):
              return ufo.get_info_value(self.ufo_name)
          else:
              ufo_prefix = self.ufo_prefix
              if ufo_prefix == CUSTOM_PARAM_PREFIX:
                  ufo_prefix += glyphs.sub_key
              return ufo.get_lib_value(ufo_prefix + self.ufo_name)
  
      def _write_to_ufo(self, glyphs, ufo, value):
          if self.ufo_default is not None and value == self.ufo_default:
              return
          if self.ufo_info and ufo.has_info_attr(self.ufo_name):
              # most OpenType table entries go in the info object
              ufo.set_info_value(self.ufo_name, value)
          else:
              # everything else gets dumped in the lib
              ufo_prefix = self.ufo_prefix
              if ufo_prefix == CUSTOM_PARAM_PREFIX:
                  ufo_prefix += glyphs.sub_key
              ufo.set_lib_value(ufo_prefix + self.ufo_name, value)
  
  
  KNOWN_PARAM_HANDLERS = []
  
  
  def register(handler):
      KNOWN_PARAM_HANDLERS.append(handler)
  
  
  GLYPHS_UFO_CUSTOM_PARAMS = (
      ("hheaAscender", "openTypeHheaAscender"),
      ("hheaDescender", "openTypeHheaDescender"),
      ("hheaLineGap", "openTypeHheaLineGap"),
      ("compatibleFullName", "openTypeNameCompatibleFullName"),
      ("description", "openTypeNameDescription"),
      ("license", "openTypeNameLicense"),
      ("licenseURL", "openTypeNameLicenseURL"),
      ("preferredFamilyName", "openTypeNamePreferredFamilyName"),
      ("preferredSubfamilyName", "openTypeNamePreferredSubfamilyName"),
      ("sampleText", "openTypeNameSampleText"),
      ("WWSFamilyName", "openTypeNameWWSFamilyName"),
      ("WWSSubfamilyName", "openTypeNameWWSSubfamilyName"),
      ("panose", "openTypeOS2Panose"),
      ("fsType", "openTypeOS2Type"),
      ("typoAscender", "openTypeOS2TypoAscender"),
      ("typoDescender", "openTypeOS2TypoDescender"),
      ("typoLineGap", "openTypeOS2TypoLineGap"),
      ("unicodeRanges", "openTypeOS2UnicodeRanges"),
      ("vendorID", "openTypeOS2VendorID"),
      # ('weightClass', 'openTypeOS2WeightClass'),
      # ('widthClass', 'openTypeOS2WidthClass'),
      # ('winAscent', 'openTypeOS2WinAscent'),
      # ('winDescent', 'openTypeOS2WinDescent'),
      ("vheaVertTypoAscender", "openTypeVheaVertTypoAscender"),
      ("vheaVertTypoDescender", "openTypeVheaVertTypoDescender"),
      ("vheaVertTypoLineGap", "openTypeVheaVertTypoLineGap"),
      # Postscript parameters
      ("blueScale", "postscriptBlueScale"),
      ("blueShift", "postscriptBlueShift"),
      ("isFixedPitch", "postscriptIsFixedPitch"),
      ("underlinePosition", "postscriptUnderlinePosition"),
      ("underlineThickness", "postscriptUnderlineThickness"),
  )
  for glyphs_name, ufo_name in GLYPHS_UFO_CUSTOM_PARAMS:
      register(ParamHandler(glyphs_name, ufo_name, glyphs_long_name=ufo_name))
  
  # TODO: (jany) for all the following fields, check that they are stored in a
  # meaningful Glyphs customParameter. Maybe they have short names?
  GLYPHS_UFO_CUSTOM_PARAMS_NO_SHORT_NAME = (
      "openTypeHheaCaretSlopeRun",
      "openTypeVheaCaretSlopeRun",
      "openTypeHheaCaretSlopeRise",
      "openTypeVheaCaretSlopeRise",
      "openTypeHheaCaretOffset",
      "openTypeVheaCaretOffset",
      "openTypeHeadLowestRecPPEM",
      "openTypeHeadFlags",
      "openTypeNameVersion",
      "openTypeNameUniqueID",
      # TODO: (jany) look at
      # https://forum.glyphsapp.com/t/name-table-entry-win-id4/3811/10
      # Use Name Table Entry for the next param
      "openTypeNameRecords",
      "openTypeOS2FamilyClass",
      "openTypeOS2SubscriptXSize",
      "openTypeOS2SubscriptYSize",
      "openTypeOS2SubscriptXOffset",
      "openTypeOS2SubscriptYOffset",
      "openTypeOS2SuperscriptXSize",
      "openTypeOS2SuperscriptYSize",
      "openTypeOS2SuperscriptXOffset",
      "openTypeOS2SuperscriptYOffset",
      "openTypeOS2StrikeoutSize",
      "openTypeOS2StrikeoutPosition",
      "postscriptFontName",
      "postscriptFullName",
      "postscriptSlantAngle",
      "postscriptUniqueID",
      # Should this be handled in `blue_values.py`?
      # 'postscriptFamilyBlues',
      # 'postscriptFamilyOtherBlues',
      "postscriptBlueFuzz",
      "postscriptForceBold",
      "postscriptDefaultWidthX",
      "postscriptNominalWidthX",
      "postscriptWeightName",
      "postscriptDefaultCharacter",
      "postscriptWindowsCharacterSet",
      "macintoshFONDFamilyID",
      "macintoshFONDName",
      "trademark",
      "styleMapFamilyName",
      "styleMapStyleName",
  )
  for name in GLYPHS_UFO_CUSTOM_PARAMS_NO_SHORT_NAME:
      register(ParamHandler(name))
  
  
  # TODO: (jany) handle dynamic version number replacement
  register(ParamHandler("versionString", "openTypeNameVersion"))
  
  
  class EmptyListDefaultParamHandler(ParamHandler):
      def to_glyphs(self, glyphs, ufo):
          ufo_value = self._read_from_ufo(glyphs, ufo)
          # Ingore default value == empty list
          if ufo_value is None or ufo_value == []:
              return
          glyphs_value = self.value_to_glyphs(ufo_value)
          self._write_to_glyphs(glyphs, glyphs_value)
  
  
  register(EmptyListDefaultParamHandler("postscriptFamilyBlues"))
  register(EmptyListDefaultParamHandler("postscriptFamilyOtherBlues"))
  
  
  # Convert code page numbers to OS/2 ulCodePageRange bits. Empty lists stay empty lists.
  class OS2CodePageRangesParamHandler(AbstractParamHandler):
      def to_glyphs(self, glyphs, ufo):
          ufo_codepage_bits = ufo.get_info_value("openTypeOS2CodePageRanges")
          if ufo_codepage_bits is None:
              return
  
          codepages = []
          unsupported_codepage_bits = []
          for codepage in ufo_codepage_bits:
              if codepage in REVERSE_CODEPAGE_RANGES:
                  codepages.append(REVERSE_CODEPAGE_RANGES[codepage])
              else:
                  unsupported_codepage_bits.append(codepage)
  
          glyphs.set_custom_value("codePageRanges", codepages)
          if unsupported_codepage_bits:
              glyphs.set_custom_value(
                  "codePageRangesUnsupportedBits", unsupported_codepage_bits
              )
  
      def to_ufo(self, builder, glyphs, ufo):
          codepages = glyphs.get_custom_value("codePageRanges")
          if codepages is None:
              codepages = glyphs.get_custom_value("openTypeOS2CodePageRanges")
              if codepages is None:
                  return
  
          ufo_codepage_bits = [CODEPAGE_RANGES[v] for v in codepages]
          unsupported_codepage_bits = glyphs.get_custom_value(
              "codePageRangesUnsupportedBits"
          )
          if unsupported_codepage_bits:
              ufo_codepage_bits.extend(unsupported_codepage_bits)
  
          ufo.set_info_value("openTypeOS2CodePageRanges", sorted(ufo_codepage_bits))
  
  
  register(OS2CodePageRangesParamHandler())
  
  # enforce that winAscent/Descent are positive, according to UFO spec
  for glyphs_name in ("winAscent", "winDescent"):
      ufo_name = "openTypeOS2W" + glyphs_name[1:]
      register(
          ParamHandler(
              glyphs_name,
              ufo_name,
              glyphs_long_name=ufo_name,
              value_to_ufo=abs,
              value_to_glyphs=abs,
          )
      )
  
  # The value of these could be a float, and ufoLib/defcon expect an int.
  for glyphs_name in ("weightClass", "widthClass"):
      ufo_name = "openTypeOS2W" + glyphs_name[1:]
      register(ParamHandler(glyphs_name, ufo_name, value_to_ufo=int))
  
  
  # convert Glyphs' GASP Table to UFO openTypeGaspRangeRecords
  def to_ufo_gasp_table(value):
      # XXX maybe the parser should cast the gasp values to int?
      value = {int(k): int(v) for k, v in value.items()}
      gasp_records = []
      # gasp range records must be sorted in ascending rangeMaxPPEM
      for max_ppem, gasp_behavior in sorted(value.items()):
          gasp_records.append(
              {
                  "rangeMaxPPEM": max_ppem,
                  "rangeGaspBehavior": bin_to_int_list(gasp_behavior),
              }
          )
      return gasp_records
  
  
  def to_glyphs_gasp_table(value):
      return {
          str(record["rangeMaxPPEM"]): int_list_to_bin(record["rangeGaspBehavior"])
          for record in value
      }
  
  
  register(
      ParamHandler(
          glyphs_name="GASP Table",
          ufo_name="openTypeGaspRangeRecords",
          value_to_ufo=to_ufo_gasp_table,
          value_to_glyphs=to_glyphs_gasp_table,
      )
  )
  
  register(ParamHandler(glyphs_name="Disable Last Change", ufo_name="disablesLastChange"))
  
  register(
      ParamHandler(
          # convert between Glyphs.app's and ufo2ft's equivalent parameter
          glyphs_name="Don't use Production Names",
          ufo_name=UFO2FT_USE_PROD_NAMES_KEY,
          ufo_prefix="",
          value_to_ufo=lambda value: not value,
          value_to_glyphs=lambda value: not value,
      )
  )
  
  
  class MiscParamHandler(ParamHandler):
      """Copy GSFont attributes to ufo lib"""
  
      def _read_from_glyphs(self, glyphs):
          return glyphs.get_attribute_value(self.glyphs_name)
  
      def _write_to_glyphs(self, glyphs, value):
          glyphs.set_attribute_value(self.glyphs_name, value)
  
  
  register(MiscParamHandler(glyphs_name="disablesAutomaticAlignment"))
  register(MiscParamHandler(glyphs_name="iconName"))
  
  
  class DisplayStringsParamHandler(MiscParamHandler):
      def __init__(self):
          super().__init__(glyphs_name="DisplayStrings")
  
      def to_ufo(self, builder, glyphs, ufo):
          # We test for builder here because apply_instance_data() passes None and
          # we don't want to copy-paste or subclass UFOBuilder.
          if builder is not None and builder.store_editor_state:
              super().to_ufo(builder, glyphs, ufo)
  
  
  register(DisplayStringsParamHandler())
  
  # deal with any Glyphs naming quirks here
  register(
      MiscParamHandler(
          glyphs_name="disablesNiceNames",
          ufo_name="useNiceNames",
          value_to_ufo=lambda value: int(not value),
          value_to_glyphs=lambda value: not bool(value),
      )
  )
  
  for number in ("", "1", "2", "3"):
      register(MiscParamHandler("customValue" + number, ufo_info=False))
  register(MiscParamHandler("weightValue", ufo_info=False))
  register(MiscParamHandler("widthValue", ufo_info=False))
  
  
  def append_unique(array, value):
      if value not in array:
          array.append(value)
  
  
  class OS2SelectionParamHandler(AbstractParamHandler):
      flags = {7: "Use Typo Metrics", 8: "Has WWS Names"}
  
      # Note that en empty openTypeOS2Selection list should stay an empty list, as
      # opposed to a non-existant list. In the latter case, we round-trip nothing, in the
      # former, we at least write an empty list to openTypeOS2SelectionUnsupportedBits
      # which we use to re-instate an empty list in the UFO on tripping back.
      def to_glyphs(self, glyphs, ufo):
          ufo_flags = ufo.get_info_value("openTypeOS2Selection")
          if ufo_flags is None:
              return
  
          unsupported_bits = []
          for flag in ufo_flags:
              if flag in self.flags:
                  glyphs.set_custom_value(self.flags[flag], True)
              else:
                  unsupported_bits.append(flag)
          glyphs.set_custom_value("openTypeOS2SelectionUnsupportedBits", unsupported_bits)
  
      def to_ufo(self, builder, glyphs, ufo):
          use_typo_metrics = glyphs.get_custom_value(self.flags[7])
          has_wws_name = glyphs.get_custom_value(self.flags[8])
          unsupported_bits = glyphs.get_custom_value(
              "openTypeOS2SelectionUnsupportedBits"
          )
          if not use_typo_metrics and not has_wws_name and unsupported_bits is None:
              return
  
          selection_bits = []
          if use_typo_metrics:
              selection_bits.append(7)
          if has_wws_name:
              selection_bits.append(8)
          if unsupported_bits:
              selection_bits.extend(unsupported_bits)
          ufo.set_info_value("openTypeOS2Selection", sorted(selection_bits))
  
  
  register(OS2SelectionParamHandler())
  
  
  class GlyphOrderParamHandler(AbstractParamHandler):
      """Translate between Glyphs.app's glyphOrder parameter and UFO's
      public.glyphOrder.
  
      See the GlyphOrderTest class for a thorough explanation.
      """
  
      def to_glyphs(self, glyphs, ufo):
          if glyphs.is_font():
              glyphs_glyphOrder = ufo.get_lib_value(GLYPHS_PREFIX + "glyphOrder")
              if isinstance(glyphs_glyphOrder, list):
                  glyphs.set_custom_value("glyphOrder", glyphs_glyphOrder)
              elif glyphs_glyphOrder is None:
                  ufo_glyphOrder = ufo.get_lib_value(PUBLIC_PREFIX + "glyphOrder")
                  if ufo_glyphOrder:
                      glyphs.set_custom_value("glyphOrder", ufo_glyphOrder)
  
      def to_ufo(self, builder, glyphs, ufo):
          if glyphs.is_font():
              glyphs_glyphOrder = glyphs.get_custom_value("glyphOrder")
              if glyphs_glyphOrder:
                  ufo.set_lib_value(PUBLIC_PREFIX + "glyphOrder", glyphs_glyphOrder)
              else:
                  ufo.set_lib_value(GLYPHS_PREFIX + "glyphOrder", False)
  
  
  register(GlyphOrderParamHandler())
  
  
  class FilterParamHandler(AbstractParamHandler):
      """Handler for (Pre)Filter custom paramters.
  
      This is complicated. ufo2ft grew filter modules to mimic some of Glyph's
      automatic features, but due to the impendance mismatch between the flow of
      data in Glyphs and in UFOs plus Designspaces, they need to be handled in
      two ways: once for filters that should be applied to masters and once for
      filters on instances, which should be applied only to interpolated UFOs:
  
         +------+
         |GSFont+-------------------+
         +----+-+                   |
              |                     |
            +-+-----------+       +-+----------+
            |GSFontMaster |       |GSIntance   |
            +-------------+       +------------+
             userData                    customParameters
               com...ufo2ft.filters        Filter & PreFilter
  
                  ^  |                      |  ^
       roundtrips |  |                      |  |
                  |  v                      |  |
              lib                           |  | roundtrips
                com...ufo2ft.filters        |  |
            +-----------+                   v  |
            |Master UFO |          lib
            +---+-------+            com.schriftgestaltung.customParameter...
                |
            +---+-----+        +----------+                    +-----------------+
            | Source  |        | Instance |    ------------>   |Interpolated UFO |
            +---+-----+        +-----+----+                    +-----------------+
                |                    |          goes 1 way        lib
        +-------+-----+              |     apply_instance_data()    com...ufo2ft.filters
        | Designspace +--------------+
        +-------------+
  
      The ufo2ft filters should roundtrip as-is between UFO source masters and
      GSFontMaster, because that's how we use them in the UFO workflow with 1
      master UFO = 1 final font with filters applied.
  
      The Glyphs filters defined on GSInstance should keep doing what they were
      doing already:
  
      - first be copied as-is into the designspace instance's lib, which should
        roundtrip back to Glyphs
      - then be converted to ufo2ft equivalents and put in the final interpolated
        UFOs before they are compiled into final fonts. Those should not
        roundtrip because the interpolated UFO is discarded after compilation.
  
      The handler below only handles the latter, one-way case. Since ufo2ft
      filters are a UFO lib key, they are automatically stored in a master's
      userData by another code path.
      """
  
      def to_glyphs(self, glyphs, ufo):
          pass
  
      def to_ufo(self, builder, glyphs, ufo):
          if not glyphs.is_font():
              ufo_filters = []
              for pre_filter in glyphs.get_custom_values("PreFilter"):
                  ufo_filters.append(parse_glyphs_filter(pre_filter, is_pre=True))
              for filter in glyphs.get_custom_values("Filter"):
                  ufo_filters.append(parse_glyphs_filter(filter, is_pre=False))
  
              if not ufo_filters:
                  return
              if not ufo.has_lib_key(UFO2FT_FILTERS_KEY):
                  ufo.set_lib_value(UFO2FT_FILTERS_KEY, [])
              existing = ufo.get_lib_value(UFO2FT_FILTERS_KEY)
              existing.extend(ufo_filters)
  
  
  register(FilterParamHandler())
  
  
  class ReplacePrefixParamHandler(AbstractParamHandler):
      def to_ufo(self, builder, glyphs, ufo):
          repl_map = {}
          for value in glyphs.get_custom_values("Replace Prefix"):
              prefix_name, prefix_code = re.split(r"\s*;\s*", value, 1)
              # if multiple 'Replace Prefix' custom params replace the same
              # prefix, the last wins
              repl_map[prefix_name] = prefix_code
  
          features_text = ufo._owner.features.text
  
          if not (repl_map and features_text):
              return
  
          glyph_names = set(ufo._owner.keys())
  
          ufo._owner.features.text = replace_prefixes(
              repl_map, features_text, glyph_names=glyph_names
          )
  
      def to_glyphs(self, glyphs, ufo):
          # do the same as ReplaceFeatureParamHandler.to_glyphs
          pass
  
  
  register(ReplacePrefixParamHandler())
  
  
  class ReplaceFeatureParamHandler(AbstractParamHandler):
      def to_ufo(self, builder, glyphs, ufo):
          for value in glyphs.get_custom_values("Replace Feature"):
              tag, repl = re.split(r"\s*;\s*", value, 1)
              ufo._owner.features.text = replace_feature(
                  tag, repl, ufo._owner.features.text or ""
              )
  
      def to_glyphs(self, glyphs, ufo):
          # TODO: (jany) The "Replace Feature" custom parameter can be used to
          # have one master/instance with different features than what is stored
          # in the GSFont. When going from several UFOs to one GSFont, we could
          # detect when UFOs have different features, put the common ones in
          # GSFont and replace the different ones with this custom parameter.
          # See the file `tests/builder/features_test.py`.
          pass
  
  
  register(ReplaceFeatureParamHandler())
  
  
  class ReencodeGlyphsParamHandler(AbstractParamHandler):
      """ The "Reencode Glyphs" custom parameter contains a list of
      'glyphname=unicodevalue' strings: e.g., ["smiley=E100", "logo=E101"].
      It only applies to specific instance (not to master or globally) and is
      meant to assign Unicode values to glyphs with the specied name at export
      time.
      When the Unicode value in question is already assigned to another glyph,
      the latter's Unicode value is deleted.
      When the Unicode value is left out, e.g., "f_f_i=", "f_f_j=", this will
      strip "f_f_i" and "f_f_j" of their Unicode values.
  
      This parameter handler only handles going from Glyphs to (instance) UFOs,
      and not also in the opposite direction, as the parameter isn't stored in
      the UFO lib, but directly applied to the UFO unicode values.
      """
  
      def to_ufo(self, builder, glyphs, ufo):
          # TODO Check that the wrapped glyphs object is indeed an instance, and
          # not a GSFont or GSMaster (unlikely)
          reencode_list = glyphs.get_custom_value("Reencode Glyphs")
          if not reencode_list:
              return
          ufo = ufo._owner
          cmap = {glyph.unicode: glyph.name for glyph in ufo}
          for entry in reencode_list:
              name, hexcode = entry.split("=")
              if name not in ufo:
                  continue
              if hexcode.strip() == "":
                  ufo[name].unicode = None
              else:
                  codepoint = int(hexcode, 16)
                  if codepoint in cmap:
                      previous = cmap[codepoint]
                      ufo[previous].unicode = None
                  ufo[name].unicode = codepoint
  
      def to_glyphs(self, glyphs, ufo):
          # The 'Reencode Glyphs' parameter only applies to instances, which
          # are not meant to be roundtripped. No need to handle it here.
          pass
  
  
  register(ReencodeGlyphsParamHandler())
  
  
  def to_ufo_custom_params(self, ufo, glyphs_object):
      # glyphs_module=None because we shouldn't instanciate any Glyphs classes
      glyphs_proxy = GlyphsObjectProxy(glyphs_object, glyphs_module=None)
      ufo_proxy = UFOProxy(ufo)
  
      glyphs_proxy.mark_handled(UFO_FILENAME_CUSTOM_PARAM)
  
      for handler in KNOWN_PARAM_HANDLERS:
          handler.to_ufo(self, glyphs_proxy, ufo_proxy)
  
      for param in glyphs_proxy.unhandled_custom_parameters():
          name = _normalize_custom_param_name(param.name)
          ufo.lib[CUSTOM_PARAM_PREFIX + glyphs_proxy.sub_key + name] = param.value
  
      _set_default_params(ufo)
  
  
  def to_glyphs_custom_params(self, ufo, glyphs_object):
      glyphs_proxy = GlyphsObjectProxy(glyphs_object, glyphs_module=self.glyphs_module)
      ufo_proxy = UFOProxy(ufo)
  
      # Handle known parameters
      for handler in KNOWN_PARAM_HANDLERS:
          handler.to_glyphs(glyphs_proxy, ufo_proxy)
  
      # Since all UFO `info` entries (from `fontinfo.plist`) have a registered
      # handler, the only place where we can find unexpected stuff is the `lib`.
      # See the file `tests/builder/fontinfo_test.py` for `fontinfo` coverage.
      prefix = CUSTOM_PARAM_PREFIX + glyphs_proxy.sub_key
      for name, value in ufo_proxy.unhandled_lib_items():
          name = _normalize_custom_param_name(name)
          if not name.startswith(prefix):
              continue
          name = name[len(prefix) :]
          glyphs_proxy.set_custom_value(name, value)
  
      _unset_default_params(glyphs_object)
  
  
  def _normalize_custom_param_name(name):
      """Replace curved quotes with straight quotes in a custom parameter name.
      These should be the only keys with problematic (non-ascii) characters,
      since they can be user-generated.
      """
  
      replacements = (("\u2018", "'"), ("\u2019", "'"), ("\u201C", '"'), ("\u201D", '"'))
      for orig, replacement in replacements:
          name = name.replace(orig, replacement)
      return name
  
  
  DEFAULT_PARAMETERS = (
      # ufo2ft defaults to fsType Bit 2 ("Preview & Print embedding"), while
      # Glyphs.app defaults to Bit 3 ("Editable embedding")
      ("fsType", "openTypeOS2Type", [3]),
      # Reference:
      # https://glyphsapp.com/content/1-get-started/2-manuals/
      # 1-handbook-glyphs-2-0/Glyphs-Handbook-2.3.pdf#page=200
      ("underlineThickness", "postscriptUnderlineThickness", 50),
      ("underlinePosition", "postscriptUnderlinePosition", -100),
  )
  
  
  def _set_default_params(ufo):
      """ Set Glyphs.app's default parameters when different from ufo2ft ones.
      """
      for _, ufo_name, default_value in DEFAULT_PARAMETERS:
          if getattr(ufo.info, ufo_name) is None:
              if isinstance(default_value, list):
                  # Prevent problem if the same default value list is put in
                  # several unrelated objects.
                  default_value = default_value[:]
              setattr(ufo.info, ufo_name, default_value)
  
  
  def _unset_default_params(glyphs):
      """ Unset Glyphs.app's parameters that have default values.
      FIXME: (jany) maybe this should be taken care of in the writer? and/or
          classes should have better default values?
      """
      for glyphs_name, _, default_value in DEFAULT_PARAMETERS:
          if (
              glyphs_name in glyphs.customParameters
              and glyphs.customParameters[glyphs_name] == default_value
          ):
              del glyphs.customParameters[glyphs_name]
          # These parameters can be referred to with the two names in Glyphs
          if (
              glyphs_name in glyphs.customParameters
              and glyphs.customParameters[glyphs_name] == default_value
          ):
              del glyphs.customParameters[glyphs_name]
  