"""
Tool to find wrong contour order between different masters, and
other interpolatability (or lack thereof) issues.

Call as:
$ fontemon_blender_addon.fontTools varLib.interpolatable font1 font2 ...
"""

from fontemon_blender_addon.fontTools.pens.basePen import AbstractPen, BasePen
from fontemon_blender_addon.fontTools.pens.recordingPen import RecordingPen
from fontemon_blender_addon.fontTools.pens.statisticsPen import StatisticsPen
from fontemon_blender_addon.fontTools.pens.momentsPen import OpenContourError
from collections import OrderedDict
import itertools
import sys


class PerContourPen(BasePen):
    def __init__(self, Pen, glyphset=None):
        BasePen.__init__(self, glyphset)
        self._glyphset = glyphset
        self._Pen = Pen
        self._pen = None
        self.value = []

    def _moveTo(self, p0):
        self._newItem()
        self._pen.moveTo(p0)

    def _lineTo(self, p1):
        self._pen.lineTo(p1)

    def _qCurveToOne(self, p1, p2):
        self._pen.qCurveTo(p1, p2)

    def _curveToOne(self, p1, p2, p3):
        self._pen.curveTo(p1, p2, p3)

    def _closePath(self):
        self._pen.closePath()
        self._pen = None

    def _endPath(self):
        self._pen.endPath()
        self._pen = None

    def _newItem(self):
        self._pen = pen = self._Pen()
        self.value.append(pen)


class PerContourOrComponentPen(PerContourPen):
    def addComponent(self, glyphName, transformation):
        self._newItem()
        self.value[-1].addComponent(glyphName, transformation)


def _vdiff(v0, v1):
    return tuple(b - a for a, b in zip(v0, v1))


def _vlen(vec):
    v = 0
    for x in vec:
        v += x * x
    return v


def _matching_cost(G, matching):
    return sum(G[i][j] for i, j in enumerate(matching))


def min_cost_perfect_bipartite_matching(G):
    n = len(G)
    try:
        from scipy.optimize import linear_sum_assignment

        rows, cols = linear_sum_assignment(G)
        assert (rows == list(range(n))).all()
        return list(cols), _matching_cost(G, cols)
    except ImportError:
        pass

    try:
        from munkres import Munkres

        cols = [None] * n
        for row, col in Munkres().compute(G):
            cols[row] = col
        return cols, _matching_cost(G, cols)
    except ImportError:
        pass

    if n > 6:
        raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")

    # Otherwise just brute-force
    permutations = itertools.permutations(range(n))
    best = list(next(permutations))
    best_cost = _matching_cost(G, best)
    for p in permutations:
        cost = _matching_cost(G, p)
        if cost < best_cost:
            best, best_cost = list(p), cost
    return best, best_cost


def test(glyphsets, glyphs=None, names=None):

    if names is None:
        names = glyphsets
    if glyphs is None:
        glyphs = glyphsets[0].keys()

    hist = []
    problems = OrderedDict()

    def add_problem(glyphname, problem):
        problems.setdefault(glyphname, []).append(problem)

    for glyph_name in glyphs:
        # print()
        # print(glyph_name)

        try:
            allVectors = []
            allNodeTypes = []
            for glyphset, name in zip(glyphsets, names):
                # print('.', end='')
                if glyph_name not in glyphset:
                    add_problem(glyph_name, {"type": "missing", "master": name})
                    continue
                glyph = glyphset[glyph_name]

                perContourPen = PerContourOrComponentPen(
                    RecordingPen, glyphset=glyphset
                )
                glyph.draw(perContourPen)
                contourPens = perContourPen.value
                del perContourPen

                contourVectors = []
                nodeTypes = []
                allNodeTypes.append(nodeTypes)
                allVectors.append(contourVectors)
                for ix, contour in enumerate(contourPens):
                    nodeTypes.append(
                        tuple(instruction[0] for instruction in contour.value)
                    )
                    stats = StatisticsPen(glyphset=glyphset)
                    try:
                        contour.replay(stats)
                    except OpenContourError as e:
                        add_problem(
                            glyph_name,
                            {"master": name, "contour": ix, "type": "open_path"},
                        )
                        continue
                    size = abs(stats.area) ** 0.5 * 0.5
                    vector = (
                        int(size),
                        int(stats.meanX),
                        int(stats.meanY),
                        int(stats.stddevX * 2),
                        int(stats.stddevY * 2),
                        int(stats.correlation * size),
                    )
                    contourVectors.append(vector)
                    # print(vector)

            # Check each master against the next one in the list.
            for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
                if len(m0) != len(m1):
                    add_problem(
                        glyph_name,
                        {
                            "type": "path_count",
                            "master_1": names[i],
                            "master_2": names[i + 1],
                            "value_1": len(m0),
                            "value_2": len(m1),
                        },
                    )
                if m0 == m1:
                    continue
                for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)):
                    if nodes1 == nodes2:
                        continue
                    if len(nodes1) != len(nodes2):
                        add_problem(
                            glyph_name,
                            {
                                "type": "node_count",
                                "path": pathIx,
                                "master_1": names[i],
                                "master_2": names[i + 1],
                                "value_1": len(nodes1),
                                "value_2": len(nodes2),
                            },
                        )
                        continue
                    for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)):
                        if n1 != n2:
                            add_problem(
                                glyph_name,
                                {
                                    "type": "node_incompatibility",
                                    "path": pathIx,
                                    "node": nodeIx,
                                    "master_1": names[i],
                                    "master_2": names[i + 1],
                                    "value_1": n1,
                                    "value_2": n2,
                                },
                            )
                            continue

            for i, (m0, m1) in enumerate(zip(allVectors[:-1], allVectors[1:])):
                if len(m0) != len(m1):
                    # We already reported this
                    continue
                if not m0:
                    continue
                costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
                matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
                if matching != list(range(len(m0))):
                    add_problem(
                        glyph_name,
                        {
                            "type": "contour_order",
                            "master_1": names[i],
                            "master_2": names[i + 1],
                            "value_1": list(range(len(m0))),
                            "value_2": matching,
                        },
                    )
                    break
                upem = 2048
                item_cost = round(
                    (matching_cost / len(m0) / len(m0[0])) ** 0.5 / upem * 100
                )
                hist.append(item_cost)
                threshold = 7
                if item_cost >= threshold:
                    add_problem(
                        glyph_name,
                        {
                            "type": "high_cost",
                            "master_1": names[i],
                            "master_2": names[i + 1],
                            "value_1": item_cost,
                            "value_2": threshold,
                        },
                    )

        except ValueError as e:
            add_problem(
                glyph_name,
                {"type": "math_error", "master": name, "error": e},
            )
    return problems


def main(args=None):
    """Test for interpolatability issues between fonts"""
    import argparse

    parser = argparse.ArgumentParser(
        "fontemon_blender_addon.fontTools varLib.interpolatable",
        description=main.__doc__,
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output report in JSON format",
    )
    parser.add_argument(
        "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF/UFO files"
    )

    args = parser.parse_args(args)
    glyphs = None
    # glyphs = ['uni08DB', 'uniFD76']
    # glyphs = ['uni08DE', 'uni0034']
    # glyphs = ['uni08DE', 'uni0034', 'uni0751', 'uni0753', 'uni0754', 'uni08A4', 'uni08A4.fina', 'uni08A5.fina']

    from os.path import basename

    names = [basename(filename).rsplit(".", 1)[0] for filename in args.inputs]

    fonts = []
    for filename in args.inputs:
        if filename.endswith(".ufo"):
            from fontemon_blender_addon.fontTools.ufoLib import UFOReader

            fonts.append(UFOReader(filename))
        else:
            from fontemon_blender_addon.fontTools.ttLib import TTFont

            fonts.append(TTFont(filename))

    glyphsets = [font.getGlyphSet() for font in fonts]
    problems = test(glyphsets, glyphs=glyphs, names=names)
    if args.json:
        import json

        print(json.dumps(problems))
    else:
        for glyph, glyph_problems in problems.items():
            print(f"Glyph {glyph} was not compatible: ")
            for p in glyph_problems:
                if p["type"] == "missing":
                    print("    Glyph was missing in master %s" % p["master"])
                if p["type"] == "open_path":
                    print("    Glyph has an open path in master %s" % p["master"])
                if p["type"] == "path_count":
                    print(
                        "    Path count differs: %i in %s, %i in %s"
                        % (p["value_1"], p["master_1"], p["value_2"], p["master_2"])
                    )
                if p["type"] == "node_count":
                    print(
                        "    Node count differs in path %i: %i in %s, %i in %s"
                        % (
                            p["path"],
                            p["value_1"],
                            p["master_1"],
                            p["value_2"],
                            p["master_2"],
                        )
                    )
                if p["type"] == "node_incompatibility":
                    print(
                        "    Node %o incompatible in path %i: %s in %s, %s in %s"
                        % (
                            p["node"],
                            p["path"],
                            p["value_1"],
                            p["master_1"],
                            p["value_2"],
                            p["master_2"],
                        )
                    )
                if p["type"] == "contour_order":
                    print(
                        "    Contour order differs: %s in %s, %s in %s"
                        % (
                            p["value_1"],
                            p["master_1"],
                            p["value_2"],
                            p["master_2"],
                        )
                    )
                if p["type"] == "high_cost":
                    print(
                        "    Interpolation has high cost: cost of %s to %s = %i, threshold %i"
                        % (
                            p["master_1"],
                            p["master_2"],
                            p["value_1"],
                            p["value_2"],
                        )
                    )
    if problems:
        return problems


if __name__ == "__main__":
    import sys

    problems = main()
    sys.exit(int(bool(problems)))
