# Adapted from https://github.com/larsenwork/monoid
# Copyright 2015 Chase Colman (chase@colman.io)
# LICENSE: MIT
# vim: sts=4 sw=4 ts=4 et

from past.builtins import xrange

import fontforge
from itertools import compress
import os
from os.path import basename, splitext, join
import subprocess
from features import update_features

SCRIPTS = os.path.dirname(os.path.realpath(__file__))

def mkdir_p(path):
    normalized = os.path.normpath(path)
    try:
        os.makedirs(normalized)
    except OSError:
        pass

# Builder
def style(name, does):
    if not isinstance(does, list):
        does = [does]
    option(name, name, [Variation(name)] + does)

    return name

def option(abrv, name, does):
    if not isinstance(does, list):
        does = [does]
    option.operations[abrv] = does
    option.abrvs.append(abrv)
    option.names[abrv] = name

    return abrv

# Initialize the operations map, abbreviation list, and name map
option.operations = {}
option.abrvs = []
option.names = {}

def conflicting(*abrvs):
    """Wrap the abbreviations as a tuple in the option abbreviation list"""
    # Assumes last #abrvs abbreviations are conflicting options
    option.abrvs = option.abrvs[:-len(abrvs)] + [tuple(abrvs)]

def _expand_options(bitmap):
    # Apply the bitmap to the options
    opts = compress(option.abrvs, bitmap)

    # Expand the permutations for all options
    expanded = [[]]
    for opt in opts:
        if isinstance(opt, tuple):
            expanded = [items + [prmtn] for items in expanded for prmtn in opt]
        else:
            expanded = [items + [opt] for items in expanded]

    return expanded

def permutations():
    """Yields all possible permutations from the options list"""
    count = len(option.abrvs)

    # Each option is a binary choice, so we use an int as a quick bitmap.
    # To iterate over every possible permutation, all we have to do is increment
    # up to the maximum value 2^(#options)
    bitmap_max = 1 << count

    # Iterate over all possible permutations
    for i in xrange(bitmap_max):
        # Map the iteration's permutations using a bitmap
        bitmap = [i >> n & 1 for n in xrange(count)]
        for opts in _expand_options(bitmap):
            yield(int(float(i)/bitmap_max*100), opts)

def _build(dstdir, font, permutations):
    for prcnt, opts in permutations:
        # Open the original font
        fnt = fontforge.open(font)

        # Get the base name for the font
        name = splitext(basename(font))[0]

        # Build a variant name based on applied options
        variants = []

        for opt in opts:
            # Append this option to the font name
            variants.append(str(opt))
            # Run all the operations for this option
            for oper in option.operations[opt]:
                oper(fnt)

        # Update the automatic features (code ligatures)
        update_features(fnt)

        variant = '-'.join(variants) or 'Normal'
        variant_dir = join(dstdir, variant)

        print('Generating ' + variant_dir)

        mkdir_p(join(variant_dir, 'TTF'))
        mkdir_p(join(variant_dir, 'OTF'))
        mkdir_p(join(variant_dir, 'Webfonts'))

        # Output the files and cleanup
        fnt.generate(join(variant_dir, 'TTF', name + '.ttf'), flags=("opentype", "dummy-dsig"))
        fnt.generate(join(variant_dir, 'OTF', name + '.otf'), flags=("opentype", "dummy-dsig"))
        fnt.generate(join(variant_dir, 'Webfonts', name + '.svg'))
        fnt.close()

        # Output other formats and the CSS declaration
        subprocess.check_call(
            [join(SCRIPTS, 'generate-other-formats'), font],
            cwd=variant_dir
        )
        subprocess.check_call(
            [join(SCRIPTS, 'generate-css-decl'), font],
            cwd=variant_dir
        )

def build(dstdir, font):
    _build(dstdir, font, permutations())

def build_batch(dstdir, font, total_nodes, node_number):
    # Starting at (i) node_number, build option every (n) total_nodes
    _build(dstdir, font, list(permutations())[node_number::total_nodes])

# Operations
## NOTE:
## All operations return a closure with the 1st argument being a fontforge.font
def Line(ascent, descent):
    """Sets the ascent and/or descent of the font's line"""
    def line_op(fnt):
        fnt.os2_winascent = fnt.os2_typoascent = fnt.hhea_ascent = ascent
        fnt.os2_windescent = descent
        fnt.os2_typodescent = fnt.hhea_descent = -descent
    return line_op

def Bearing(left=0, right=0):
    """Adjusts the left and/or right bearings of all glyphs"""
    def bearing_op(fnt):
        for glyph in fnt.glyphs():
            if left != 0:
                glyph.left_side_bearing += left
            if right != 0:
                glyph.right_side_bearing += right
    return bearing_op

def Swap(glyph1, glyph2):
    """Swaps the places of two glyphs"""
    def swap_op(fnt):
        # Unlike selections, glyph layer data is returned as a copy
        swp = fnt[glyph1].foreground
        fnt[glyph1].foreground = fnt[glyph2].foreground
        fnt[glyph2].foreground = swp
    return swap_op

def SwapLookup(target_lookup):
    """Swaps the places of glyphs based on an OpenType lookup table"""
    def swaplookup_op(fnt):
        # Get every subtable for every matching lookup
        lookups = [i for i in fnt.gsub_lookups if fnt.getLookupInfo(i)[2][0][0] == target_lookup]
        subtables = []
        for lookup in lookups:
            for subtable in fnt.getLookupSubtables(lookup):
                subtables.append(subtable)

        for glyph in fnt.glyphs():
            subbed = False

            for subtable in subtables:
                posSub = glyph.getPosSub(subtable)
                if not subbed and posSub and posSub[0][1] == "Substitution":
                    subbed = True # Don't double tap if there are duplicates

                    sub = posSub[0][2]
                    swp = glyph.foreground
                    glyph.foreground = fnt[sub].foreground
                    fnt[sub].foreground = swp

    return swaplookup_op

def DropCAltAndLiga():
    """Removes Contextual Alternates and Ligatures"""
    def dropcaltandliga_op(fnt):
        for lookup in fnt.gsub_lookups:
            if fnt.getLookupInfo(lookup)[0] in ['gsub_ligature', 'gsub_contextchain']:
                fnt.removeLookup(lookup)

    return dropcaltandliga_op

def Variation(name):
    """Changes the subfamily/variation of the font"""
    def variation_op(fnt):
        # Get the SFNT information as dictionary {property: value}
        # where English (US) is the language... Here be dragons.
        #
        #                                      o
        #                                     /\
        #                                    /::\
        #                                   /::::\
        #                     ,a_a         /\::::/\
        #                    {/ ''\_      /\ \::/\ \
        #                    {\ ,_oo)    /\ \ \/\ \ \
        #                    {/  (_^____/  \ \ \ \ \ \
        #          .=.      {/ \___)))*)    \ \ \ \ \/
        #         (.=.`\   {/   /=;  ~/      \ \ \ \/
        #             \ `\{/(   \/\  /        \ \ \/
        #              \  `. `\  ) )           \ \/
        #               \    // /_/_            \/
        #                '==''---))))
        sfnt_dict = {sfnt[1]: sfnt[2] for sfnt in fnt.sfnt_names if sfnt[0] == 'English (US)'}

        fnt.familyname = sfnt_dict['Family'] + ' ' + name
        fnt.fullname = fnt.familyname + ' ' + sfnt_dict['SubFamily']
        fnt.fontname = fnt.fullname.replace(' ', '-')

        fnt.appendSFNTName('English (US)', 'Family', fnt.familyname)
        fnt.appendSFNTName('English (US)', 'Fullname', fnt.fullname)
        fnt.appendSFNTName('English (US)', 'PostScriptName', fnt.fontname)
        fnt.appendSFNTName('English (US)', 'SubFamily', sfnt_dict['SubFamily'])
        fnt.appendSFNTName('English (US)', 'UniqueID', sfnt_dict['UniqueID'] + ' : ' + name)
    return variation_op
