import sys, os, os.path, re, argparse
import defcon
from multiprocessing import Pool
from fontTools.designspaceLib import DesignSpaceDocument
from ufo2ft.filters import loadFilters
from datetime import datetime

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'tools')))
from common import getGitHash, getVersion


OPT_EDITABLE = False  # --editable


def update_version(ufo):
  version = getVersion()
  buildtag, buildtagErrs = getGitHash()
  now = datetime.utcnow()
  if buildtag == "" or len(buildtagErrs) > 0:
    buildtag = "src"
    print("warning: getGitHash() failed: %r" % buildtagErrs, file=sys.stderr)
  versionMajor, versionMinor = [int(num) for num in version.split(".")]
  ufo.info.versionMajor = versionMajor
  ufo.info.versionMinor = versionMinor
  ufo.info.year = now.year
  ufo.info.openTypeNameVersion = "Version %d.%03d;git-%s" % (versionMajor, versionMinor, buildtag)
  psFamily = re.sub(r'\s', '', ufo.info.familyName)
  psStyle = re.sub(r'\s', '', ufo.info.styleName)
  #
  # id format:
  #   version ";" "git-" git-tag ";" foundry-tag ";" ps_family "-" ps_style
  # E.g.
  #   "4.001;git-4de559246;RSMS;Inter-DisplayThinItalic"
  # Note: this should match what generated by fontmake.
  # fix-static-display-names.py depends on this format being consistent for all fonts.
  #
  if buildtag != "src":
    buildtag = "git-" + buildtag
  ufo.info.openTypeNameUniqueID = "%d.%03d;%s;%s;%s-%s" % (
    versionMajor, versionMinor,
    buildtag,
    ufo.info.openTypeOS2VendorID,
    psFamily, psStyle)
  ufo.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S")


def ufo_set_wws(ufo):
  # Fix missing WWS entries for Display fonts:
  # See https://github.com/googlefonts/glyphsLib/issues/820
  subfamily = ufo.info.styleName
  if subfamily.find("Display") == -1:
    return
  subfamily = subfamily[len("Display"):].strip()
  if subfamily == "":
    # "Display" -> "Regular"
    subfamily = "Regular"
  ufo.info.openTypeNameWWSFamilyName = "Inter Display"
  ufo.info.openTypeNameWWSSubfamilyName = subfamily


def fix_opsz_range(designspace):
  opsz_min = 1000000
  opsz_max = 0
  opsz_name = ''
  opsz_axis = None

  for opsz_axis in designspace.axes:
    if opsz_axis.tag == "opsz":
      opsz_name = opsz_axis.name
      break

  for instance in designspace.instances:
    opsz_value = instance.location[opsz_name]
    if opsz_value < opsz_min:
      opsz_min = opsz_value
    if opsz_value > opsz_max:
      opsz_max = opsz_value

  opsz_axis.minimum = opsz_min
  opsz_axis.maximum = opsz_max

  return designspace


def fix_wght_range(designspace):
  for a in designspace.axes:
    if a.tag == "wght":
      a.minimum = 100
      a.maximum = 900
      break
  return designspace


def should_decompose_glyph(g):
  if not g.components or len(g.components) == 0:
    return False

  # Does the component have non-trivial transformation? (i.e. scaled or skewed)
  # Example of no transformation: (identity matrix)
  #   (1, 0, 0, 1, 0, 0)    no scale or offset
  # Example of simple offset transformation matrix:
  #   (1, 0, 0, 1, 20, 30)  20 x offset, 30 y offset
  # Example of scaled transformation matrix:
  #   (-1.0, 0, 0.3311, 1, 1464.0, 0)  flipped x axis, sheered and offset
  # Matrix order:
  #   (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
  for cn in g.components:
    # if g.name == 'dotmacron.lc':
    #   print(f"{g.name} cn {cn.baseGlyph}", cn.transformation)
    # Check if transformation is not identity (ignoring x & y offset)
    m = cn.transformation
    if m[0] + m[1] + m[2] + m[3] != 2.0:
      return True

  return False


def copy_component_anchors(font, g):
  # do nothing if there are no components or if g has anchors already
  if not g.components or len(g.anchors) > 0:
    return

  anchor_names = set()
  for cn in g.components:
    if cn.transformation[1] != 0.0 or cn.transformation[2] != 0.0:
      print(f"TODO: support transformations with skew ({g.name})")
      return
    cn_g = font[cn.baseGlyph]
    # copy_component_anchors(font, cn_g)  # depth first
    for a in cn_g.anchors:
      # Check if there are multiple components with achors with the same name.
      # Don't copy any anchors if there are duplicate "_..." anchors
      if a.name in anchor_names and len(a.name) > 1 and a.name[0] == '_':
        return
      anchor_names.add(a.name)

  if len(anchor_names) == 0:
    return

  anchor_names.clear()
  for cn in g.components:
    for a in font[cn.baseGlyph].anchors:
      if a.name in anchor_names:
        continue
      anchor_names.add(a.name)
      a2 = defcon.Anchor(glyph=g, anchorDict=a.copy())
      m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
      a2.x += m[4] * m[0]
      a2.y += m[5] * m[3]
      g.appendAnchor(a2)


def copy_anchors_from_components(font, g):
  # We use two passes here, to deduplicate anchors which appear in several components.
  # Two assumptions are made:
  # 1. Insertion order of Python dict() is retained (true for Python >=3.7)
  # 2. Base components are listed first, mark/accent comonents later.
  #    e.g. in /Ecircumflex, /E comes before /circumflexcomb, so we use "top"
  #    anchor from /circumflexcomb rather than /E

  # skip certain glyphs
  if len(g.unicodes) == 0 or len(g.anchors) > 0 or not g.components:
    return

  add_anchors = dict()
  names_to_copy = set(('top', 'bottom'))

  for cn in g.components:
    checked_cn_skew = False
    #print(f"    [{g.name}] cn {cn.baseGlyph}")
    cn_g = font[cn.baseGlyph]
    copy_anchors_from_components(font, cn_g)  # depth first
    for a in cn_g.anchors:
      if a.name not in names_to_copy:
        continue
      #print(f"    [{g.name}] use anchor {a.name}")
      m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
      if not checked_cn_skew:
        checked_cn_skew = True
        if m[1] != 0.0 or m[2] != 0.0:
          #print(f"TODO: skewed components ({cn_g.name} used by {g.name})")
          return
      a2 = defcon.Anchor(glyph=g, anchorDict=a.copy())
      a2.x += m[4] * m[0]
      a2.y += m[5] * m[3]
      add_anchors[a.name] = a2

  for a in add_anchors.values():
    #print(f"    [{g.name}] append anchor {a.name}")
    g.appendAnchor(a)


def find_glyphs_to_decompose(designspace_source):
  glyph_names = set()
  # print("find_glyphs_to_decompose inspecting %r" % designspace_source.name)
  font = defcon.Font(designspace_source.path)
  for g in font:
    # copy_anchors_from_components(font, g)
    if should_decompose_glyph(g):
      glyph_names.add(g.name)
  font.save(designspace_source.path)
  return list(glyph_names)


def set_ufo_filter(ufo, **filter_dict):
  filters = ufo.lib.setdefault("com.github.googlei18n.ufo2ft.filters", [])
  for i in range(len(filters)):
    if filters[i].get("name") == filter_dict["name"]:
      filters[i] = filter_dict
      return
  filters.append(filter_dict)


def del_ufo_filter(ufo, name):
  filters = ufo.lib.get("com.github.googlei18n.ufo2ft.filters")
  if not filters:
    return
  for i in range(len(filters)):
    if filters[i].get("name") == name:
      filters.pop(i)
      return


def update_source_ufo(ufo_file, glyphs_to_decompose):
  print(f"update {os.path.basename(ufo_file)}")

  ufo = defcon.Font(ufo_file)
  update_version(ufo)

  set_ufo_filter(ufo, name="decomposeComponents", include=glyphs_to_decompose)

  # decompose now, up front, instead of later when compiling fonts
  if not OPT_EDITABLE:
    preFilters, postFilters = loadFilters(ufo)
    for filter in preFilters:
      filter(ufo)
    for filter in postFilters:
      filter(ufo)
    # del_ufo_filter(ufo, "decomposeComponents")
    del ufo.lib["com.github.googlei18n.ufo2ft.filters"]

  ufo_set_wws(ufo) # Fix missing WWS entries for Display fonts
  ufo.save(ufo_file)


def update_sources(designspace):
  with Pool() as p:
    sources = [source for source in designspace.sources]
    # sources = [s for s in sources if s.name == "Inter Thin"] # DEBUG
    glyphs_to_decompose = set()
    for glyph_names in p.map(find_glyphs_to_decompose, sources):
      glyphs_to_decompose.update(glyph_names)
    glyphs_to_decompose = list(glyphs_to_decompose)
    # print("glyphs marked to be decomposed: %s" % ', '.join(glyphs_to_decompose))
    source_files = list(set([s.path for s in sources]))
    p.starmap(update_source_ufo, [(path, glyphs_to_decompose) for path in source_files])
  return designspace


def main(argv):
  ap = argparse.ArgumentParser(description=
    'Fixup designspace and source UFOs after they are generated by fontmake from Glyphs source')
  ap.add_argument('--editable', action='store_true',
    help="Generate UFOs suitable for further editing (don't apply filters)")
  ap.add_argument("designspace", help="Path to designspace file")

  args = ap.parse_args()
  OPT_EDITABLE = args.editable

  designspace = DesignSpaceDocument.fromfile(args.designspace)
  designspace = fix_opsz_range(designspace)
  designspace = fix_wght_range(designspace)
  designspace = update_sources(designspace)
  designspace.write(args.designspace)


if __name__ == '__main__':
  main(sys.argv)
