#!/usr/bin/env python3
__doc__ = 'Generate instance UFOs from a designspace document and master UFOs'

# Python 2.7 script to build instance UFOs from a designspace document
# If a file is given, all instances are built
# A particular instance to build can be specified using the -i option
#  and the 'name' attribute value for an 'instance' element in the designspace file
#  Or it can be specified using the -a and -v options
#  to specify any attribute and value pair for an 'instance' in the designspace file
#  If more than one instances matches, all will be built
# A prefix for the output path can be specified (for smith processing)
# If the location of an instance UFO matches a master's location,
#  glyphs are copied instead of calculated
#  This allows instances to build with glyphs that are not interpolatable
#  An option exists to calculate glyphs instead of copying them
# If a folder is given using an option, all instances in all designspace files are built
#  Specifying an instance to build or an output path prefix is not supported with a folder
#  Also, all glyphs will be calculated

__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2018 SIL International  (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Alan Ward'

import os, re
from mutatorMath.ufo.document import DesignSpaceDocumentReader
from mutatorMath.ufo.instance import InstanceWriter
from fontMath.mathGlyph import MathGlyph
from mutatorMath.ufo import build as build_designspace
from silfont.core import execute

argspec = [
    ('designspace_path', {'help': 'Path to designspace document (or folder of them)'}, {}),
    ('-i', '--instanceName', {'help': 'Font name for instance to build'}, {}),
    ('-a', '--instanceAttr', {'help': 'Attribute used to specify instance to build'}, {}),
    ('-v', '--instanceVal', {'help': 'Value of attribute specifying instance to build'}, {}),
    ('-f', '--folder', {'help': 'Build all designspace files in a folder','action': 'store_true'}, {}),
    ('-o', '--output', {'help': 'Prepend path to all output paths'}, {}),
    ('--forceInterpolation', {'help': 'If an instance matches a master, calculate glyphs instead of copying them',
                               'action': 'store_true'}, {}),
    ('--roundInstances', {'help': 'Apply integer rounding to all geometry when interpolating',
                           'action': 'store_true'}, {}),
    ('-l','--log',{'help': 'Log file (default: *_createinstances.log)'}, {'type': 'outfile', 'def': '_createinstances.log'}),
    ('-W','--weightfix',{'help': 'Enable RIBBI style weight fixing', 'action': 'store_true'}, {}),
]

# Class factory to wrap a subclass in a closure to store values not defined in the original class
#  that our method overrides will utilize
# The class methods will fail unless the class is generated by the factory, which is enforced by scoping
# Using class attribs or global variables would violate encapsulation even more
#  and would only allow for one instance of the class

weightClasses = {
    'bold': 700
}

def InstanceWriterCF(output_path_prefix, calc_glyphs, fix_weight):

    class LocalInstanceWriter(InstanceWriter):
        fixWeight = fix_weight

        def __init__(self, path, *args, **kw):
            if output_path_prefix:
                path = os.path.join(output_path_prefix, path)
            return super(LocalInstanceWriter, self).__init__(path, *args, **kw)

        # Override the method used to calculate glyph geometry
        # If copy_glyphs is true and the glyph being processed is in the same location
        # (has all the same axes values) as a master UFO,
        # then extract the glyph geometry directly into the target glyph.
        # FYI, in the superclass method, m = buildMutator(); m.makeInstance() returns a MathGlyph
        def _calculateGlyph(self, targetGlyphObject, instanceLocationObject, glyphMasters):
            # Search for a glyphMaster with the same location as instanceLocationObject
            found = False
            if not calc_glyphs: # i.e. if copying glyphs
                for item in glyphMasters:
                    locationObject = item['location'] # mutatorMath Location
                    if locationObject.sameAs(instanceLocationObject) == 0:
                        found = True
                        fontObject = item['font'] # defcon Font
                        glyphName = item['glyphName'] # string
                        glyphObject = MathGlyph(fontObject[glyphName])
                        glyphObject.extractGlyph(targetGlyphObject, onlyGeometry=True)
                        break

            if not found: # includes case of calc_glyphs == True
                super(LocalInstanceWriter, self)._calculateGlyph(targetGlyphObject,
                                                                 instanceLocationObject,
                                                                 glyphMasters)

        def _copyFontInfo(self, targetInfo, sourceInfo):
            super(LocalInstanceWriter, self)._copyFontInfo(targetInfo, sourceInfo)

            if getattr(self, 'fixWeight', False):
                # fixWeight is True since the --weightfix (or -W) option was specified

                # This mode is used for RIBBI font builds,
                # therefore the weight class can be determined
                # by the style name
                if self.font.info.styleMapStyleName.lower().startswith("bold"):
                    weight_class = 700
                else:
                    weight_class = 400
            else:
                # fixWeight is False (or None)

                # This mode is used for non-RIBBI font builds,
                # therefore the weight class can be determined
                # by the weight axis map in the Designspace file
                foundmap = False
                weight = int(self.locationObject["weight"])
                for map_space in self.axes["weight"]["map"]:
                    userspace = int(map_space[0])  # called input in the Designspace file
                    designspace = int(map_space[1])  # called output in the Designspace file
                    if designspace == weight:
                        weight_class = userspace
                        foundmap = True
                if not foundmap:
                    weight_class = 399 # Dummy value designed to look non-standard
                    logger.log(f'No entry in designspace axis mapping for {weight}; set to 399', 'W')
            setattr(targetInfo, 'openTypeOS2WeightClass', weight_class)

            localinfo = {}
            for k in (('openTypeNameManufacturer', None),
                      ('styleMapFamilyName', 'familyName'),
                      ('styleMapStyleName', 'styleName')):
                localinfo[k[0]] = getattr(targetInfo, k[0], (getattr(targetInfo, k[1]) if k[1] is not None else ""))
            localinfo['styleMapStyleName'] = localinfo['styleMapStyleName'].title()
            localinfo['year'] = re.sub(r'^.*?([0-9]+)\s*$', r'\1', getattr(targetInfo, 'openTypeNameUniqueID')) 
            uniqueID = "{openTypeNameManufacturer}: {styleMapFamilyName} {styleMapStyleName} {year}".format(**localinfo)
            setattr(targetInfo, 'openTypeNameUniqueID', uniqueID)

    return LocalInstanceWriter

logger = None
severe_error = False
def progress_func(state="update", action=None, text=None, tick=0):
    global severe_error
    if logger:
        if state == 'error':
            if str(action) == 'unicodes':
                logger.log("%s: %s\n%s" % (state, str(action), str(text)), 'W')
            else:
                logger.log("%s: %s\n%s" % (state, str(action), str(text)), 'E')
                severe_error = True
        else:
            logger.log("%s: %s\n%s" % (state, str(action), str(text)), 'I')

def doit(args):
    global logger
    logger = args.logger

    designspace_path = args.designspace_path
    instance_font_name = args.instanceName
    instance_attr = args.instanceAttr
    instance_val = args.instanceVal
    output_path_prefix = args.output
    calc_glyphs = args.forceInterpolation
    build_folder = args.folder
    round_instances = args.roundInstances

    if instance_font_name and (instance_attr or instance_val):
        args.logger.log('--instanceName is mutually exclusive with --instanceAttr or --instanceVal','S')
    if (instance_attr and not instance_val) or (instance_val and not instance_attr):
        args.logger.log('--instanceAttr and --instanceVal must be used together', 'S')
    if (build_folder and (instance_font_name or instance_attr or instance_val
                          or output_path_prefix or calc_glyphs)):
        args.logger.log('--folder cannot be used with options: -i, -a, -v, -o, --forceInterpolation', 'S')

    args.logger.log('Interpolating master UFOs from designspace', 'P')
    if not build_folder:
        if not os.path.isfile(designspace_path):
            args.logger.log('A designspace file (not a folder) is required', 'S')
        reader = DesignSpaceDocumentReader(designspace_path, ufoVersion=3,
                                           roundGeometry=round_instances,
                                           progressFunc=progress_func)
        # assignment to an internal object variable is a kludge, probably should use subclassing instead
        reader._instanceWriterClass = InstanceWriterCF(output_path_prefix, calc_glyphs, args.weightfix)
        if calc_glyphs:
            args.logger.log('Interpolating glyphs where an instance font location matches a master', 'P')
        if instance_font_name or instance_attr:
            key_attr = instance_attr if instance_val else 'name'
            key_val = instance_val if instance_attr else instance_font_name
            reader.readInstance((key_attr, key_val))
        else:
            reader.readInstances()
    else:
        # The below uses a utility function that's part of mutatorMath
        #  It will accept a folder and processes all designspace files there
        args.logger.log('Interpolating glyphs where an instance font location matches a master', 'P')
        build_designspace(designspace_path,
                          outputUFOFormatVersion=3, roundGeometry=round_instances,
                          progressFunc=progress_func)

    if not severe_error:
        args.logger.log('Done', 'P')
    else:
        args.logger.log('Done with severe error', 'S')

def cmd(): execute(None, doit, argspec)
if __name__ == '__main__': cmd()

# Future development might use: fonttools\Lib\fontTools\designspaceLib to read
#  the designspace file (which is the most up-to-date approach)
#  then pass that object to mutatorMath, but there's no way to do that today.


# For reference:
# from mutatorMath/ufo/__init__.py:
# 	build() is a convenience function for reading and executing a designspace file.
# 		documentPath: 				filepath to the .designspace document
# 		outputUFOFormatVersion:		ufo format for output
# 		verbose:					True / False for lots or no feedback [to log file]
# 		logPath:					filepath to a log file
# 		progressFunc:				an optional callback to report progress.
# 									see mutatorMath.ufo.tokenProgressFunc
#
# class DesignSpaceDocumentReader(object):
# def __init__(self, documentPath,
#         ufoVersion,
#         roundGeometry=False,
#         verbose=False,
#         logPath=None,
#         progressFunc=None
#         ):
#
# def readInstance(self, key, makeGlyphs=True, makeKerning=True, makeInfo=True):
# def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True):
