from fontbakery.callable import check
from fontbakery.checkrunner import ERROR, FAIL, PASS, WARN, INFO
from fontbakery.message import Message
from fontbakery.constants import (PriorityLevel,
                                  NameID,
                                  PlatformID,
                                  WindowsEncodingID,
                                  WindowsLanguageID)
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import

profile_imports = [
    ('.shared_conditions', ('glyph_metrics_stats', ))
]


@check(
  id = 'com.adobe.fonts/check/name/empty_records',
  rationale = """
    Check the name table for empty records, as this can cause problems in Adobe apps.
  """
)
def com_adobe_fonts_check_name_empty_records(ttFont):
    """Check name table for empty records."""
    failed = False
    for name_record in ttFont['name'].names:
        name_string = name_record.toUnicode().strip()
        if len(name_string) == 0:
            failed = True
            name_key = tuple([name_record.platformID, name_record.platEncID,
                             name_record.langID, name_record.nameID])
            yield FAIL,\
                  Message("empty-record",
                          f'"name" table record with key={name_key} is'
                          f' empty and should be removed.')
    if not failed:
        yield PASS, ("No empty name table records found.")


@check(
  id = 'com.google.fonts/check/name/no_copyright_on_description',
  misc_metadata = {
    'priority': PriorityLevel.CRITICAL
  }
)
def com_google_fonts_check_name_no_copyright_on_description(ttFont):
  """Description strings in the name table must not contain copyright info."""
  failed = False
  for name in ttFont['name'].names:
    if 'opyright' in name.string.decode(name.getEncoding())\
       and name.nameID == NameID.DESCRIPTION:
      failed = True

  if failed:
    yield FAIL,\
          Message("copyright-on-description",
                  f"Some namerecords with"
                  f" ID={NameID.DESCRIPTION} (NameID.DESCRIPTION)"
                  f" containing copyright info should be removed"
                  f" (perhaps these were added by a longstanding"
                  f" FontLab Studio 5.x bug that copied"
                  f" copyright notices to them.)")
  else:
    yield PASS, ("Description strings in the name table"
                 " do not contain any copyright string.")


@check(
  id = 'com.google.fonts/check/monospace',
  conditions = ['glyph_metrics_stats',
                'is_ttf'],
  rationale = """
    There are various metadata in the OpenType spec to specify if a font is monospaced or not. If the font is not trully monospaced, then no monospaced metadata should be set (as sometimes they mistakenly are...)

    Monospace fonts must:

    * post.isFixedWidth "Set to 0 if the font is proportionally spaced, non-zero if the font is not proportionally spaced (monospaced)"
      www.microsoft.com/typography/otspec/post.htm

    * hhea.advanceWidthMax must be correct, meaning no glyph's width value is greater.
      www.microsoft.com/typography/otspec/hhea.htm

    * OS/2.panose.bProportion must be set to 9 (monospace). Spec says: "The PANOSE definition contains ten digits each of which currently describes up to sixteen variations. Windows uses bFamilyType, bSerifStyle and bProportion in the font mapper to determine family type. It also uses bProportion to determine if the font is monospaced."
      www.microsoft.com/typography/otspec/os2.htm#pan
      monotypecom-test.monotype.de/services/pan2

    * OS/2.xAverageWidth must be set accurately.
      "OS/2.xAverageWidth IS used when rendering monospaced fonts, at least by Windows GDI"
      http://typedrawers.com/discussion/comment/15397/#Comment_15397

    Also we should report an error for glyphs not of average width
  """
)
def com_google_fonts_check_monospace(ttFont, glyph_metrics_stats):
  """Checking correctness of monospaced metadata."""
  from fontbakery.constants import (IsFixedWidth,
                                    PANOSE_Proportion)
  failed = False
  # Note: These values are read from the dict here only to
  # reduce the max line length in the check implementation below:
  seems_monospaced = glyph_metrics_stats["seems_monospaced"]
  most_common_width = glyph_metrics_stats["most_common_width"]
  width_max = glyph_metrics_stats['width_max']

  if ttFont['hhea'].advanceWidthMax != width_max:
    failed = True
    yield FAIL,\
          Message("bad-advanceWidthMax",
                  f"Value of hhea.advanceWidthMax"
                  f" should be set to {width_max}"
                  f" but got {ttFont['hhea'].advanceWidthMax} instead.")
  if seems_monospaced:
    if ttFont['post'].isFixedPitch == IsFixedWidth.NOT_MONOSPACED:
      failed = True
      yield FAIL,\
            Message("mono-bad-post-isFixedPitch",
                    f"On monospaced fonts, the value of post.isFixedPitch"
                    f" must be set to a non-zero value"
                    f" (meaning 'fixed width monospaced'),"
                    f" but got {ttFont['post'].isFixedPitch} instead.")

    if ttFont['OS/2'].panose.bProportion != PANOSE_Proportion.MONOSPACED:
      failed = True
      yield FAIL,\
            Message("mono-bad-panose-proportion",
                    f"On monospaced fonts, the value of"
                    f" OS/2.panose.bProportion"
                    f" must be set to {PANOSE_Proportion.MONOSPACED}"
                    f" (proportion: monospaced),"
                    f" but got {ttFont['OS/2'].panose.bProportion} instead.")

    num_glyphs = len(ttFont['glyf'].glyphs)
    unusually_spaced_glyphs = [
        g for g in ttFont['glyf'].glyphs
        if g not in ['.notdef', '.null', 'NULL'] and
        ttFont['hmtx'].metrics[g][0] != most_common_width
    ]
    outliers_ratio = float(len(unusually_spaced_glyphs)) / num_glyphs
    if outliers_ratio > 0:
      failed = True
      yield WARN,\
            Message("mono-outliers",
                    f"Font is monospaced"
                    f" but {len(unusually_spaced_glyphs)} glyphs"
                    f" ({100.0 * outliers_ratio}%)"
                    f" have a different width."
                    f" You should check the widths of:"
                    f" {unusually_spaced_glyphs}")
    if not failed:
      yield PASS,\
            Message("mono-good",
                    "Font is monospaced and all related metadata look good.")
  else:
    # it is a non-monospaced font, so lets make sure
    # that all monospace-related metadata is properly unset.

    if ttFont['post'].isFixedPitch != IsFixedWidth.NOT_MONOSPACED:
      failed = True
      yield FAIL,\
            Message("bad-post-isFixedPitch",
                    f"On non-monospaced fonts,"
                    f" the post.isFixedPitch value must be set to"
                    f" {IsFixedWidth.NOT_MONOSPACED} (not monospaced),"
                    f" but got {ttFont['post'].isFixedPitch} instead.")

    if ttFont['OS/2'].panose.bProportion == PANOSE_Proportion.MONOSPACED:
      failed = True
      yield FAIL,\
            Message("bad-panose-proportion",
                    f"On non-monospaced fonts,"
                    f" the OS/2.panose.bProportion value can be set to"
                    f" any value except 9 (proportion: monospaced)"
                    f" which is the bad value we got in this font.")
    if not failed:
      yield PASS,\
            Message("good",
                    "Font is not monospaced and"
                    " all related metadata look good.")


@check(
  id = 'com.google.fonts/check/name/line_breaks'
)
def com_google_fonts_check_name_line_breaks(ttFont):
  """Name table entries should not contain line-breaks."""
  failed = False
  for name in ttFont["name"].names:
    string = name.string.decode(name.getEncoding())
    if "\n" in string:
      failed = True
      yield FAIL,\
            Message("line-break",
                    f"Name entry {NameID(name.nameID).name}"
                    f" on platform {PlatformID(name.platformID).name}"
                    f" contains a line-break.")
  if not failed:
    yield PASS, ("Name table entries are all single-line"
                 " (no line-breaks found).")


@check(
  id = 'com.google.fonts/check/name/match_familyname_fullfont'
)
def com_google_fonts_check_name_match_familyname_fullfont(ttFont):
  """Does full font name begin with the font family name?"""
  from fontbakery.utils import get_name_entry_strings
  familyname = get_name_entry_strings(ttFont, NameID.FONT_FAMILY_NAME)
  fullfontname = get_name_entry_strings(ttFont, NameID.FULL_FONT_NAME)

  if len(familyname) == 0:
    yield FAIL,\
          Message("no-font-family-name",
                  "Font lacks a NameID.FONT_FAMILY_NAME"
                  " entry in the 'name' table.")
  elif len(fullfontname) == 0:
    yield FAIL,\
          Message("no-full-font-name",
                  "Font lacks a NameID.FULL_FONT_NAME"
                  " entry in the 'name' table.")
  else:
    # we probably should check all found values are equivalent.
    # and, in that case, then performing the rest of the check
    # with only the first occurences of the name entries
    # will suffice:
    fullfontname = fullfontname[0]
    familyname = familyname[0]

    if not fullfontname.startswith(familyname):
      yield FAIL,\
            Message("does-not",
                    f"On the 'name' table, the full font name"
                    f" (NameID {NameID.FULL_FONT_NAME}"
                    f" - FULL_FONT_NAME: '{familyname}')"
                    f" does not begin with font family name"
                    f" (NameID {NameID.FONT_FAMILY_NAME}"
                    f" - FONT_FAMILY_NAME: '{fullfontname}')")
    else:
      yield PASS, "Full font name begins with the font family name."


@check(
  id = 'com.google.fonts/check/family_naming_recommendations'
)
def com_google_fonts_check_family_naming_recommendations(ttFont):
  """Font follows the family naming recommendations?"""
  # See http://forum.fontlab.com/index.php?topic=313.0
  import re
  from fontbakery.utils import get_name_entry_strings
  bad_entries = []

  # <Postscript name> may contain only a-zA-Z0-9
  # and one hyphen
  bad_psname = re.compile("[^A-Za-z0-9-]")
  for string in get_name_entry_strings(ttFont,
                                       NameID.POSTSCRIPT_NAME):
    if bad_psname.search(string):
      bad_entries.append({
          'field':
          'PostScript Name',
          'value':
          string,
          'rec': ('May contain only a-zA-Z0-9'
                  ' characters and an hyphen.')
      })
    if string.count('-') > 1:
      bad_entries.append({
          'field': 'Postscript Name',
          'value': string,
          'rec': ('May contain not more'
                  ' than a single hyphen')
      })

  for string in get_name_entry_strings(ttFont,
                                       NameID.FULL_FONT_NAME):
    if len(string) >= 64:
      bad_entries.append({
          'field': 'Full Font Name',
          'value': string,
          'rec': 'exceeds max length (63)'
      })

  for string in get_name_entry_strings(ttFont,
                                       NameID.POSTSCRIPT_NAME):
    if len(string) >= 30:
      bad_entries.append({
          'field': 'PostScript Name',
          'value': string,
          'rec': 'exceeds max length (29)'
      })

  for string in get_name_entry_strings(ttFont,
                                       NameID.FONT_FAMILY_NAME):
    if len(string) >= 32:
      bad_entries.append({
          'field': 'Family Name',
          'value': string,
          'rec': 'exceeds max length (31)'
      })

  for string in get_name_entry_strings(ttFont,
                                       NameID.FONT_SUBFAMILY_NAME):
    if len(string) >= 32:
      bad_entries.append({
          'field': 'Style Name',
          'value': string,
          'rec': 'exceeds max length (31)'
      })

  for string in get_name_entry_strings(ttFont,
                                       NameID.TYPOGRAPHIC_FAMILY_NAME):
    if len(string) >= 32:
      bad_entries.append({
          'field': 'OT Family Name',
          'value': string,
          'rec': 'exceeds max length (31)'
      })

  for string in get_name_entry_strings(ttFont,
                                       NameID.TYPOGRAPHIC_SUBFAMILY_NAME):
    if len(string) >= 32:
      bad_entries.append({
          'field': 'OT Style Name',
          'value': string,
          'rec': 'exceeds max length (31)'
      })

  if len(bad_entries) > 0:
    table = "| Field | Value | Recommendation |\n"
    table += "|:----- |:----- |:-------------- |\n"
    for bad in bad_entries:
      table += "| {} | {} | {} |\n".format(bad["field"],
                                           bad["value"],
                                           bad["rec"])
    yield INFO,\
          Message("bad-entries",
                  f"Font does not follow "
                  f"some family naming recommendations:\n"
                  f"\n"
                  f"{table}")
  else:
    yield PASS, "Font follows the family naming recommendations."


@check(
  id = 'com.google.fonts/check/name/rfn'
)
def com_google_fonts_check_name_rfn(ttFont):
  """Name table strings must not contain the string 'Reserved Font Name'."""
  failed = False
  for entry in ttFont["name"].names:
    string = entry.toUnicode()
    if "reserved font name" in string.lower():
      yield WARN,\
            Message("rfn",
                    f'Name table entry ("{string}")'
                    f' contains "Reserved Font Name".'
                    f' This is an error except in a few specific rare cases.')
      failed = True
  if not failed:
    yield PASS, ('None of the name table strings'
                 ' contain "Reserved Font Name".')


@check(
  id = 'com.adobe.fonts/check/name/postscript_vs_cff',
  conditions = ['is_cff'],
  rationale = """
    The PostScript name entries in the font's 'name' table should match the FontName string in the 'CFF ' table.

    The 'CFF ' table has a lot of information that is duplicated in other tables. This information should be consistent across tables, because there's no guarantee which table an app will get the data from.
  """,
)
def com_adobe_fonts_check_name_postscript_vs_cff(ttFont):
  """CFF table FontName must match name table ID 6 (PostScript name)."""
  cff_names = ttFont['CFF '].cff.fontNames
  if len(cff_names) != 1:
    yield ERROR, ("Unexpected number of font names in CFF table.")
    return

  passed = True
  cff_name = cff_names[0]
  for entry in ttFont['name'].names:
    if entry.nameID == NameID.POSTSCRIPT_NAME:
      postscript_name = entry.toUnicode()
      if postscript_name != cff_name:
        passed = False
        yield FAIL,\
              Message("mismatch",
                      f"Name table PostScript name '{postscript_name}' "
                      f"does not match CFF table FontName '{cff_name}'.")

  if passed:
    yield PASS, ("Name table PostScript name matches CFF table FontName.")


@check(
  id = 'com.adobe.fonts/check/name/postscript_name_consistency',
  conditions = ['not is_cff'],  # e.g. TTF or CFF2
  rationale = """
    The PostScript name entries in the font's 'name' table should be consistent across platforms.

    This is the TTF/CFF2 equivalent of the CFF 'postscript_name_cff_vs_name' check.
  """,
)
def com_adobe_fonts_check_name_postscript_name_consistency(ttFont):
  """Name table ID 6 (PostScript name) must be consistent across platforms."""
  postscript_names = set()
  for entry in ttFont['name'].names:
    if entry.nameID == NameID.POSTSCRIPT_NAME:
      postscript_name = entry.toUnicode()
      postscript_names.add(postscript_name)

  if len(postscript_names) > 1:
    yield FAIL,\
          Message("inconsistency",
                  f'Entries in the "name" table for ID 6'
                  f' (PostScript name) are not consistent.'
                  f' Names found: {sorted(postscript_names)}.')
  else:
    yield PASS, ('Entries in the "name" table for ID 6 '
                 '(PostScript name) are consistent.')


@check(
  id = 'com.adobe.fonts/check/family/max_4_fonts_per_family_name',
  rationale = """
    Per the OpenType spec:
    'The Font Family name ... should be shared among at most four fonts that differ only in weight or style ...'
  """,
)
def com_adobe_fonts_check_family_max_4_fonts_per_family_name(ttFonts):
  """Verify that each group of fonts with the same nameID 1
  has maximum of 4 fonts"""
  from collections import Counter
  from fontbakery.utils import get_name_entry_strings

  family_names = list()
  for ttFont in ttFonts:
    names_list = get_name_entry_strings(ttFont,
                                        NameID.FONT_FAMILY_NAME,
                                        PlatformID.WINDOWS,
                                        WindowsEncodingID.UNICODE_BMP,
                                        WindowsLanguageID.ENGLISH_USA)
    # names_list may contain multiple entries.
    # We use set() below to remove the duplicates and only store
    # the unique family name(s) used for a given font
    names_set = set(names_list)
    family_names.extend(names_set)

  passed = True
  counter = Counter(family_names)
  for family_name, count in counter.items():
    if count > 4:
      passed = False
      yield FAIL,\
            Message("too-many",
                    f"Family '{family_name}' has {count} fonts"
                    f" (should be 4 or fewer).")
  if passed:
    yield PASS, ("There were no more than 4 fonts per family name.")
