from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import floatToFixedToFloat
from fontTools import ttLib
from fontTools import designspaceLib
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
from fontTools.ttLib.tables import _f_v_a_r, _g_l_y_f
from fontTools.ttLib.tables import otTables
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools import varLib
from fontTools.varLib import instancer
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib import builder
from fontTools.varLib import featureVars
from fontTools.varLib import models
import collections
from copy import deepcopy
import logging
import os
import re
import pytest


# see Tests/varLib/instancer/conftest.py for "varfont" fixture definition

TESTDATA = os.path.join(os.path.dirname(__file__), "data")


@pytest.fixture(params=[True, False], ids=["optimize", "no-optimize"])
def optimize(request):
    return request.param


@pytest.fixture
def fvarAxes():
    wght = _f_v_a_r.Axis()
    wght.axisTag = Tag("wght")
    wght.minValue = 100
    wght.defaultValue = 400
    wght.maxValue = 900
    wdth = _f_v_a_r.Axis()
    wdth.axisTag = Tag("wdth")
    wdth.minValue = 70
    wdth.defaultValue = 100
    wdth.maxValue = 100
    return [wght, wdth]


def _get_coordinates(varfont, glyphname):
    # converts GlyphCoordinates to a list of (x, y) tuples, so that pytest's
    # assert will give us a nicer diff
    return list(varfont["glyf"].getCoordinatesAndControls(glyphname, varfont)[0])


class InstantiateGvarTest(object):
    @pytest.mark.parametrize("glyph_name", ["hyphen"])
    @pytest.mark.parametrize(
        "location, expected",
        [
            pytest.param(
                {"wdth": -1.0},
                {
                    "hyphen": [
                        (27, 229),
                        (27, 310),
                        (247, 310),
                        (247, 229),
                        (0, 0),
                        (274, 0),
                        (0, 536),
                        (0, 0),
                    ]
                },
                id="wdth=-1.0",
            ),
            pytest.param(
                {"wdth": -0.5},
                {
                    "hyphen": [
                        (33.5, 229),
                        (33.5, 308.5),
                        (264.5, 308.5),
                        (264.5, 229),
                        (0, 0),
                        (298, 0),
                        (0, 536),
                        (0, 0),
                    ]
                },
                id="wdth=-0.5",
            ),
            # an axis pinned at the default normalized location (0.0) means
            # the default glyf outline stays the same
            pytest.param(
                {"wdth": 0.0},
                {
                    "hyphen": [
                        (40, 229),
                        (40, 307),
                        (282, 307),
                        (282, 229),
                        (0, 0),
                        (322, 0),
                        (0, 536),
                        (0, 0),
                    ]
                },
                id="wdth=0.0",
            ),
        ],
    )
    def test_pin_and_drop_axis(self, varfont, glyph_name, location, expected, optimize):
        instancer.instantiateGvar(varfont, location, optimize=optimize)

        assert _get_coordinates(varfont, glyph_name) == expected[glyph_name]

        # check that the pinned axis has been dropped from gvar
        assert not any(
            "wdth" in t.axes
            for tuples in varfont["gvar"].variations.values()
            for t in tuples
        )

    def test_full_instance(self, varfont, optimize):
        instancer.instantiateGvar(
            varfont, {"wght": 0.0, "wdth": -0.5}, optimize=optimize
        )

        assert _get_coordinates(varfont, "hyphen") == [
            (33.5, 229),
            (33.5, 308.5),
            (264.5, 308.5),
            (264.5, 229),
            (0, 0),
            (298, 0),
            (0, 536),
            (0, 0),
        ]

        assert "gvar" not in varfont

    def test_composite_glyph_not_in_gvar(self, varfont):
        """The 'minus' glyph is a composite glyph, which references 'hyphen' as a
        component, but has no tuple variations in gvar table, so the component offset
        and the phantom points do not change; however the sidebearings and bounding box
        do change as a result of the parent glyph 'hyphen' changing.
        """
        hmtx = varfont["hmtx"]
        vmtx = varfont["vmtx"]

        hyphenCoords = _get_coordinates(varfont, "hyphen")
        assert hyphenCoords == [
            (40, 229),
            (40, 307),
            (282, 307),
            (282, 229),
            (0, 0),
            (322, 0),
            (0, 536),
            (0, 0),
        ]
        assert hmtx["hyphen"] == (322, 40)
        assert vmtx["hyphen"] == (536, 229)

        minusCoords = _get_coordinates(varfont, "minus")
        assert minusCoords == [(0, 0), (0, 0), (422, 0), (0, 536), (0, 0)]
        assert hmtx["minus"] == (422, 40)
        assert vmtx["minus"] == (536, 229)

        location = {"wght": -1.0, "wdth": -1.0}

        instancer.instantiateGvar(varfont, location)

        # check 'hyphen' coordinates changed
        assert _get_coordinates(varfont, "hyphen") == [
            (26, 259),
            (26, 286),
            (237, 286),
            (237, 259),
            (0, 0),
            (263, 0),
            (0, 536),
            (0, 0),
        ]
        # check 'minus' coordinates (i.e. component offset and phantom points)
        # did _not_ change
        assert _get_coordinates(varfont, "minus") == minusCoords

        assert hmtx["hyphen"] == (263, 26)
        assert vmtx["hyphen"] == (536, 250)

        assert hmtx["minus"] == (422, 26)  # 'minus' left sidebearing changed
        assert vmtx["minus"] == (536, 250)  # 'minus' top sidebearing too


class InstantiateCvarTest(object):
    @pytest.mark.parametrize(
        "location, expected",
        [
            pytest.param({"wght": -1.0}, [500, -400, 150, 250], id="wght=-1.0"),
            pytest.param({"wdth": -1.0}, [500, -400, 180, 200], id="wdth=-1.0"),
            pytest.param({"wght": -0.5}, [500, -400, 165, 250], id="wght=-0.5"),
            pytest.param({"wdth": -0.3}, [500, -400, 180, 235], id="wdth=-0.3"),
        ],
    )
    def test_pin_and_drop_axis(self, varfont, location, expected):
        instancer.instantiateCvar(varfont, location)

        assert list(varfont["cvt "].values) == expected

        # check that the pinned axis has been dropped from cvar
        pinned_axes = location.keys()
        assert not any(
            axis in t.axes for t in varfont["cvar"].variations for axis in pinned_axes
        )

    def test_full_instance(self, varfont):
        instancer.instantiateCvar(varfont, {"wght": -0.5, "wdth": -0.5})

        assert list(varfont["cvt "].values) == [500, -400, 165, 225]

        assert "cvar" not in varfont


class InstantiateMVARTest(object):
    @pytest.mark.parametrize(
        "location, expected",
        [
            pytest.param(
                {"wght": 1.0},
                {"strs": 100, "undo": -200, "unds": 150, "xhgt": 530},
                id="wght=1.0",
            ),
            pytest.param(
                {"wght": 0.5},
                {"strs": 75, "undo": -150, "unds": 100, "xhgt": 515},
                id="wght=0.5",
            ),
            pytest.param(
                {"wght": 0.0},
                {"strs": 50, "undo": -100, "unds": 50, "xhgt": 500},
                id="wght=0.0",
            ),
            pytest.param(
                {"wdth": -1.0},
                {"strs": 20, "undo": -100, "unds": 50, "xhgt": 500},
                id="wdth=-1.0",
            ),
            pytest.param(
                {"wdth": -0.5},
                {"strs": 35, "undo": -100, "unds": 50, "xhgt": 500},
                id="wdth=-0.5",
            ),
            pytest.param(
                {"wdth": 0.0},
                {"strs": 50, "undo": -100, "unds": 50, "xhgt": 500},
                id="wdth=0.0",
            ),
        ],
    )
    def test_pin_and_drop_axis(self, varfont, location, expected):
        mvar = varfont["MVAR"].table
        # initially we have two VarData: the first contains deltas associated with 3
        # regions: 1 with only wght, 1 with only wdth, and 1 with both wght and wdth
        assert len(mvar.VarStore.VarData) == 2
        assert mvar.VarStore.VarRegionList.RegionCount == 3
        assert mvar.VarStore.VarData[0].VarRegionCount == 3
        assert all(len(item) == 3 for item in mvar.VarStore.VarData[0].Item)
        # The second VarData has deltas associated only with 1 region (wght only).
        assert mvar.VarStore.VarData[1].VarRegionCount == 1
        assert all(len(item) == 1 for item in mvar.VarStore.VarData[1].Item)

        instancer.instantiateMVAR(varfont, location)

        for mvar_tag, expected_value in expected.items():
            table_tag, item_name = MVAR_ENTRIES[mvar_tag]
            assert getattr(varfont[table_tag], item_name) == expected_value

        # check that regions and accompanying deltas have been dropped
        num_regions_left = len(mvar.VarStore.VarRegionList.Region)
        assert num_regions_left < 3
        assert mvar.VarStore.VarRegionList.RegionCount == num_regions_left
        assert mvar.VarStore.VarData[0].VarRegionCount == num_regions_left
        # VarData subtables have been merged
        assert len(mvar.VarStore.VarData) == 1

    @pytest.mark.parametrize(
        "location, expected",
        [
            pytest.param(
                {"wght": 1.0, "wdth": 0.0},
                {"strs": 100, "undo": -200, "unds": 150},
                id="wght=1.0,wdth=0.0",
            ),
            pytest.param(
                {"wght": 0.0, "wdth": -1.0},
                {"strs": 20, "undo": -100, "unds": 50},
                id="wght=0.0,wdth=-1.0",
            ),
            pytest.param(
                {"wght": 0.5, "wdth": -0.5},
                {"strs": 55, "undo": -145, "unds": 95},
                id="wght=0.5,wdth=-0.5",
            ),
            pytest.param(
                {"wght": 1.0, "wdth": -1.0},
                {"strs": 50, "undo": -180, "unds": 130},
                id="wght=0.5,wdth=-0.5",
            ),
        ],
    )
    def test_full_instance(self, varfont, location, expected):
        instancer.instantiateMVAR(varfont, location)

        for mvar_tag, expected_value in expected.items():
            table_tag, item_name = MVAR_ENTRIES[mvar_tag]
            assert getattr(varfont[table_tag], item_name) == expected_value

        assert "MVAR" not in varfont


class InstantiateHVARTest(object):
    # the 'expectedDeltas' below refer to the VarData item deltas for the "hyphen"
    # glyph in the PartialInstancerTest-VF.ttx test font, that are left after
    # partial instancing
    @pytest.mark.parametrize(
        "location, expectedRegions, expectedDeltas",
        [
            ({"wght": -1.0}, [{"wdth": (-1.0, -1.0, 0)}], [-59]),
            ({"wght": 0}, [{"wdth": (-1.0, -1.0, 0)}], [-48]),
            ({"wght": 1.0}, [{"wdth": (-1.0, -1.0, 0)}], [7]),
            (
                {"wdth": -1.0},
                [
                    {"wght": (-1.0, -1.0, 0.0)},
                    {"wght": (0.0, 0.6099854, 1.0)},
                    {"wght": (0.6099854, 1.0, 1.0)},
                ],
                [-11, 31, 51],
            ),
            ({"wdth": 0}, [{"wght": (0.6099854, 1.0, 1.0)}], [-4]),
        ],
    )
    def test_partial_instance(self, varfont, location, expectedRegions, expectedDeltas):
        instancer.instantiateHVAR(varfont, location)

        assert "HVAR" in varfont
        hvar = varfont["HVAR"].table
        varStore = hvar.VarStore

        regions = varStore.VarRegionList.Region
        fvarAxes = [a for a in varfont["fvar"].axes if a.axisTag not in location]
        regionDicts = [reg.get_support(fvarAxes) for reg in regions]
        assert len(regionDicts) == len(expectedRegions)
        for region, expectedRegion in zip(regionDicts, expectedRegions):
            assert region.keys() == expectedRegion.keys()
            for axisTag, support in region.items():
                assert support == pytest.approx(expectedRegion[axisTag])

        assert len(varStore.VarData) == 1
        assert varStore.VarData[0].ItemCount == 2

        assert hvar.AdvWidthMap is not None
        advWithMap = hvar.AdvWidthMap.mapping

        assert advWithMap[".notdef"] == advWithMap["space"]
        varIdx = advWithMap[".notdef"]
        # these glyphs have no metrics variations in the test font
        assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == (
            [0] * varStore.VarData[0].VarRegionCount
        )

        varIdx = advWithMap["hyphen"]
        assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == expectedDeltas

    def test_full_instance(self, varfont):
        instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})

        assert "HVAR" not in varfont

    def test_partial_instance_keep_empty_table(self, varfont):
        # Append an additional dummy axis to fvar, for which the current HVAR table
        # in our test 'varfont' contains no variation data.
        # Instancing the other two wght and wdth axes should leave HVAR table empty,
        # to signal there are variations to the glyph's advance widths.
        fvar = varfont["fvar"]
        axis = _f_v_a_r.Axis()
        axis.axisTag = "TEST"
        fvar.axes.append(axis)

        instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})

        assert "HVAR" in varfont

        varStore = varfont["HVAR"].table.VarStore

        assert varStore.VarRegionList.RegionCount == 0
        assert not varStore.VarRegionList.Region
        assert varStore.VarRegionList.RegionAxisCount == 1


class InstantiateItemVariationStoreTest(object):
    def test_VarRegion_get_support(self):
        axisOrder = ["wght", "wdth", "opsz"]
        regionAxes = {"wdth": (-1.0, -1.0, 0.0), "wght": (0.0, 1.0, 1.0)}
        region = builder.buildVarRegion(regionAxes, axisOrder)

        assert len(region.VarRegionAxis) == 3
        assert region.VarRegionAxis[2].PeakCoord == 0

        fvarAxes = [SimpleNamespace(axisTag=axisTag) for axisTag in axisOrder]

        assert region.get_support(fvarAxes) == regionAxes

    @pytest.fixture
    def varStore(self):
        return builder.buildVarStore(
            builder.buildVarRegionList(
                [
                    {"wght": (-1.0, -1.0, 0)},
                    {"wght": (0, 0.5, 1.0)},
                    {"wght": (0.5, 1.0, 1.0)},
                    {"wdth": (-1.0, -1.0, 0)},
                    {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
                    {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)},
                    {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
                ],
                ["wght", "wdth"],
            ),
            [
                builder.buildVarData([0, 1, 2], [[100, 100, 100], [100, 100, 100]]),
                builder.buildVarData(
                    [3, 4, 5, 6], [[100, 100, 100, 100], [100, 100, 100, 100]]
                ),
            ],
        )

    @pytest.mark.parametrize(
        "location, expected_deltas, num_regions",
        [
            ({"wght": 0}, [[0, 0], [0, 0]], 1),
            ({"wght": 0.25}, [[50, 50], [0, 0]], 1),
            ({"wdth": 0}, [[0, 0], [0, 0]], 3),
            ({"wdth": -0.75}, [[0, 0], [75, 75]], 3),
            ({"wght": 0, "wdth": 0}, [[0, 0], [0, 0]], 0),
            ({"wght": 0.25, "wdth": 0}, [[50, 50], [0, 0]], 0),
            ({"wght": 0, "wdth": -0.75}, [[0, 0], [75, 75]], 0),
        ],
    )
    def test_instantiate_default_deltas(
        self, varStore, fvarAxes, location, expected_deltas, num_regions
    ):
        defaultDeltas = instancer.instantiateItemVariationStore(
            varStore, fvarAxes, location
        )

        defaultDeltaArray = []
        for varidx, delta in sorted(defaultDeltas.items()):
            major, minor = varidx >> 16, varidx & 0xFFFF
            if major == len(defaultDeltaArray):
                defaultDeltaArray.append([])
            assert len(defaultDeltaArray[major]) == minor
            defaultDeltaArray[major].append(delta)

        assert defaultDeltaArray == expected_deltas
        assert varStore.VarRegionList.RegionCount == num_regions


class TupleVarStoreAdapterTest(object):
    def test_instantiate(self):
        regions = [
            {"wght": (-1.0, -1.0, 0)},
            {"wght": (0.0, 1.0, 1.0)},
            {"wdth": (-1.0, -1.0, 0)},
            {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
            {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
        ]
        axisOrder = ["wght", "wdth"]
        tupleVarData = [
            [
                TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]),
                TupleVariation({"wght": (0.0, 1.0, 1.0)}, [30, 90]),
                TupleVariation(
                    {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100]
                ),
                TupleVariation(
                    {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120]
                ),
            ],
            [
                TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]),
                TupleVariation(
                    {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55]
                ),
                TupleVariation(
                    {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75]
                ),
            ],
        ]
        adapter = instancer._TupleVarStoreAdapter(
            regions, axisOrder, tupleVarData, itemCounts=[2, 2]
        )

        defaultDeltaArray = adapter.instantiate({"wght": 0.5})

        assert defaultDeltaArray == [[15, 45], [0, 0]]
        assert adapter.regions == [{"wdth": (-1.0, -1.0, 0)}]
        assert adapter.tupleVarData == [
            [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-30, -60])],
            [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])],
        ]

    def test_rebuildRegions(self):
        regions = [
            {"wght": (-1.0, -1.0, 0)},
            {"wght": (0.0, 1.0, 1.0)},
            {"wdth": (-1.0, -1.0, 0)},
            {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
            {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
        ]
        axisOrder = ["wght", "wdth"]
        variations = []
        for region in regions:
            variations.append(TupleVariation(region, [100]))
        tupleVarData = [variations[:3], variations[3:]]
        adapter = instancer._TupleVarStoreAdapter(
            regions, axisOrder, tupleVarData, itemCounts=[1, 1]
        )

        adapter.rebuildRegions()

        assert adapter.regions == regions

        del tupleVarData[0][2]
        tupleVarData[1][0].axes = {"wght": (-1.0, -0.5, 0)}
        tupleVarData[1][1].axes = {"wght": (0, 0.5, 1.0)}

        adapter.rebuildRegions()

        assert adapter.regions == [
            {"wght": (-1.0, -1.0, 0)},
            {"wght": (0.0, 1.0, 1.0)},
            {"wght": (-1.0, -0.5, 0)},
            {"wght": (0, 0.5, 1.0)},
        ]

    def test_roundtrip(self, fvarAxes):
        regions = [
            {"wght": (-1.0, -1.0, 0)},
            {"wght": (0, 0.5, 1.0)},
            {"wght": (0.5, 1.0, 1.0)},
            {"wdth": (-1.0, -1.0, 0)},
            {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
            {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)},
            {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
        ]
        axisOrder = [axis.axisTag for axis in fvarAxes]

        itemVarStore = builder.buildVarStore(
            builder.buildVarRegionList(regions, axisOrder),
            [
                builder.buildVarData(
                    [0, 1, 2, 4, 5, 6],
                    [[10, -20, 30, -40, 50, -60], [70, -80, 90, -100, 110, -120]],
                ),
                builder.buildVarData(
                    [3, 4, 5, 6], [[5, -15, 25, -35], [45, -55, 65, -75]]
                ),
            ],
        )

        adapter = instancer._TupleVarStoreAdapter.fromItemVarStore(
            itemVarStore, fvarAxes
        )

        assert adapter.tupleVarData == [
            [
                TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]),
                TupleVariation({"wght": (0, 0.5, 1.0)}, [-20, -80]),
                TupleVariation({"wght": (0.5, 1.0, 1.0)}, [30, 90]),
                TupleVariation(
                    {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100]
                ),
                TupleVariation(
                    {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [50, 110]
                ),
                TupleVariation(
                    {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120]
                ),
            ],
            [
                TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]),
                TupleVariation(
                    {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55]
                ),
                TupleVariation(
                    {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [25, 65]
                ),
                TupleVariation(
                    {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75]
                ),
            ],
        ]
        assert adapter.itemCounts == [data.ItemCount for data in itemVarStore.VarData]
        assert adapter.regions == regions
        assert adapter.axisOrder == axisOrder

        itemVarStore2 = adapter.asItemVarStore()

        assert [
            reg.get_support(fvarAxes) for reg in itemVarStore2.VarRegionList.Region
        ] == regions

        assert itemVarStore2.VarDataCount == 2
        assert itemVarStore2.VarData[0].VarRegionIndex == [0, 1, 2, 4, 5, 6]
        assert itemVarStore2.VarData[0].Item == [
            [10, -20, 30, -40, 50, -60],
            [70, -80, 90, -100, 110, -120],
        ]
        assert itemVarStore2.VarData[1].VarRegionIndex == [3, 4, 5, 6]
        assert itemVarStore2.VarData[1].Item == [[5, -15, 25, -35], [45, -55, 65, -75]]


def makeTTFont(glyphOrder, features):
    font = ttLib.TTFont()
    font.setGlyphOrder(glyphOrder)
    addOpenTypeFeaturesFromString(font, features)
    font["name"] = ttLib.newTable("name")
    return font


def _makeDSAxesDict(axes):
    dsAxes = collections.OrderedDict()
    for axisTag, axisValues in axes:
        axis = designspaceLib.AxisDescriptor()
        axis.name = axis.tag = axis.labelNames["en"] = axisTag
        axis.minimum, axis.default, axis.maximum = axisValues
        dsAxes[axis.tag] = axis
    return dsAxes


def makeVariableFont(masters, baseIndex, axes, masterLocations):
    vf = deepcopy(masters[baseIndex])
    dsAxes = _makeDSAxesDict(axes)
    fvar = varLib._add_fvar(vf, dsAxes, instances=())
    axisTags = [axis.axisTag for axis in fvar.axes]
    normalizedLocs = [models.normalizeLocation(m, dict(axes)) for m in masterLocations]
    model = models.VariationModel(normalizedLocs, axisOrder=axisTags)
    varLib._merge_OTL(vf, model, masters, axisTags)
    return vf


def makeParametrizedVF(glyphOrder, features, values, increments):
    # Create a test VF with given glyphs and parametrized OTL features.
    # The VF is built from 9 masters (3 x 3 along wght and wdth), with
    # locations hard-coded and base master at wght=400 and wdth=100.
    # 'values' is a list of initial values that are interpolated in the
    # 'features' string, and incremented for each subsequent master by the
    # given 'increments' (list of 2-tuple) along the two axes.
    assert values and len(values) == len(increments)
    assert all(len(i) == 2 for i in increments)
    masterLocations = [
        {"wght": 100, "wdth": 50},
        {"wght": 100, "wdth": 100},
        {"wght": 100, "wdth": 150},
        {"wght": 400, "wdth": 50},
        {"wght": 400, "wdth": 100},  # base master
        {"wght": 400, "wdth": 150},
        {"wght": 700, "wdth": 50},
        {"wght": 700, "wdth": 100},
        {"wght": 700, "wdth": 150},
    ]
    n = len(values)
    values = list(values)
    masters = []
    for _ in range(3):
        for _ in range(3):
            master = makeTTFont(glyphOrder, features=features % tuple(values))
            masters.append(master)
            for i in range(n):
                values[i] += increments[i][1]
        for i in range(n):
            values[i] += increments[i][0]
    baseIndex = 4
    axes = [("wght", (100, 400, 700)), ("wdth", (50, 100, 150))]
    vf = makeVariableFont(masters, baseIndex, axes, masterLocations)
    return vf


@pytest.fixture
def varfontGDEF():
    glyphOrder = [".notdef", "f", "i", "f_i"]
    features = (
        "feature liga { sub f i by f_i;} liga;"
        "table GDEF { LigatureCaretByPos f_i %d; } GDEF;"
    )
    values = [100]
    increments = [(+30, +10)]
    return makeParametrizedVF(glyphOrder, features, values, increments)


@pytest.fixture
def varfontGPOS():
    glyphOrder = [".notdef", "V", "A"]
    features = "feature kern { pos V A %d; } kern;"
    values = [-80]
    increments = [(-10, -5)]
    return makeParametrizedVF(glyphOrder, features, values, increments)


@pytest.fixture
def varfontGPOS2():
    glyphOrder = [".notdef", "V", "A", "acutecomb"]
    features = (
        "markClass [acutecomb] <anchor 150 -10> @TOP_MARKS;"
        "feature mark {"
        "  pos base A <anchor %d 450> mark @TOP_MARKS;"
        "} mark;"
        "feature kern {"
        "  pos V A %d;"
        "} kern;"
    )
    values = [200, -80]
    increments = [(+30, +10), (-10, -5)]
    return makeParametrizedVF(glyphOrder, features, values, increments)


class InstantiateOTLTest(object):
    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": -1.0}, 110),  # -60
            ({"wght": 0}, 170),
            ({"wght": 0.5}, 200),  # +30
            ({"wght": 1.0}, 230),  # +60
            ({"wdth": -1.0}, 160),  # -10
            ({"wdth": -0.3}, 167),  # -3
            ({"wdth": 0}, 170),
            ({"wdth": 1.0}, 180),  # +10
        ],
    )
    def test_pin_and_drop_axis_GDEF(self, varfontGDEF, location, expected):
        vf = varfontGDEF
        assert "GDEF" in vf

        instancer.instantiateOTL(vf, location)

        assert "GDEF" in vf
        gdef = vf["GDEF"].table
        assert gdef.Version == 0x00010003
        assert gdef.VarStore
        assert gdef.LigCaretList
        caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0]
        assert caretValue.Format == 3
        assert hasattr(caretValue, "DeviceTable")
        assert caretValue.DeviceTable.DeltaFormat == 0x8000
        assert caretValue.Coordinate == expected

    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": -1.0, "wdth": -1.0}, 100),  # -60 - 10
            ({"wght": -1.0, "wdth": 0.0}, 110),  # -60
            ({"wght": -1.0, "wdth": 1.0}, 120),  # -60 + 10
            ({"wght": 0.0, "wdth": -1.0}, 160),  # -10
            ({"wght": 0.0, "wdth": 0.0}, 170),
            ({"wght": 0.0, "wdth": 1.0}, 180),  # +10
            ({"wght": 1.0, "wdth": -1.0}, 220),  # +60 - 10
            ({"wght": 1.0, "wdth": 0.0}, 230),  # +60
            ({"wght": 1.0, "wdth": 1.0}, 240),  # +60 + 10
        ],
    )
    def test_full_instance_GDEF(self, varfontGDEF, location, expected):
        vf = varfontGDEF
        assert "GDEF" in vf

        instancer.instantiateOTL(vf, location)

        assert "GDEF" in vf
        gdef = vf["GDEF"].table
        assert gdef.Version == 0x00010000
        assert not hasattr(gdef, "VarStore")
        assert gdef.LigCaretList
        caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0]
        assert caretValue.Format == 1
        assert not hasattr(caretValue, "DeviceTable")
        assert caretValue.Coordinate == expected

    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": -1.0}, -85),  # +25
            ({"wght": 0}, -110),
            ({"wght": 1.0}, -135),  # -25
            ({"wdth": -1.0}, -105),  # +5
            ({"wdth": 0}, -110),
            ({"wdth": 1.0}, -115),  # -5
        ],
    )
    def test_pin_and_drop_axis_GPOS_kern(self, varfontGPOS, location, expected):
        vf = varfontGPOS
        assert "GDEF" in vf
        assert "GPOS" in vf

        instancer.instantiateOTL(vf, location)

        gdef = vf["GDEF"].table
        gpos = vf["GPOS"].table
        assert gdef.Version == 0x00010003
        assert gdef.VarStore

        assert gpos.LookupList.Lookup[0].LookupType == 2  # PairPos
        pairPos = gpos.LookupList.Lookup[0].SubTable[0]
        valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
        assert valueRec1.XAdvDevice
        assert valueRec1.XAdvDevice.DeltaFormat == 0x8000
        assert valueRec1.XAdvance == expected

    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": -1.0, "wdth": -1.0}, -80),  # +25 + 5
            ({"wght": -1.0, "wdth": 0.0}, -85),  # +25
            ({"wght": -1.0, "wdth": 1.0}, -90),  # +25 - 5
            ({"wght": 0.0, "wdth": -1.0}, -105),  # +5
            ({"wght": 0.0, "wdth": 0.0}, -110),
            ({"wght": 0.0, "wdth": 1.0}, -115),  # -5
            ({"wght": 1.0, "wdth": -1.0}, -130),  # -25 + 5
            ({"wght": 1.0, "wdth": 0.0}, -135),  # -25
            ({"wght": 1.0, "wdth": 1.0}, -140),  # -25 - 5
        ],
    )
    def test_full_instance_GPOS_kern(self, varfontGPOS, location, expected):
        vf = varfontGPOS
        assert "GDEF" in vf
        assert "GPOS" in vf

        instancer.instantiateOTL(vf, location)

        assert "GDEF" not in vf
        gpos = vf["GPOS"].table

        assert gpos.LookupList.Lookup[0].LookupType == 2  # PairPos
        pairPos = gpos.LookupList.Lookup[0].SubTable[0]
        valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
        assert not hasattr(valueRec1, "XAdvDevice")
        assert valueRec1.XAdvance == expected

    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": -1.0}, (210, -85)),  # -60, +25
            ({"wght": 0}, (270, -110)),
            ({"wght": 0.5}, (300, -122)),  # +30, -12
            ({"wght": 1.0}, (330, -135)),  # +60, -25
            ({"wdth": -1.0}, (260, -105)),  # -10, +5
            ({"wdth": -0.3}, (267, -108)),  # -3, +2
            ({"wdth": 0}, (270, -110)),
            ({"wdth": 1.0}, (280, -115)),  # +10, -5
        ],
    )
    def test_pin_and_drop_axis_GPOS_mark_and_kern(
        self, varfontGPOS2, location, expected
    ):
        vf = varfontGPOS2
        assert "GDEF" in vf
        assert "GPOS" in vf

        instancer.instantiateOTL(vf, location)

        v1, v2 = expected
        gdef = vf["GDEF"].table
        gpos = vf["GPOS"].table
        assert gdef.Version == 0x00010003
        assert gdef.VarStore
        assert gdef.GlyphClassDef

        assert gpos.LookupList.Lookup[0].LookupType == 4  # MarkBasePos
        markBasePos = gpos.LookupList.Lookup[0].SubTable[0]
        baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0]
        assert baseAnchor.Format == 3
        assert baseAnchor.XDeviceTable
        assert baseAnchor.XDeviceTable.DeltaFormat == 0x8000
        assert not baseAnchor.YDeviceTable
        assert baseAnchor.XCoordinate == v1
        assert baseAnchor.YCoordinate == 450

        assert gpos.LookupList.Lookup[1].LookupType == 2  # PairPos
        pairPos = gpos.LookupList.Lookup[1].SubTable[0]
        valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
        assert valueRec1.XAdvDevice
        assert valueRec1.XAdvDevice.DeltaFormat == 0x8000
        assert valueRec1.XAdvance == v2

    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": -1.0, "wdth": -1.0}, (200, -80)),  # -60 - 10, +25 + 5
            ({"wght": -1.0, "wdth": 0.0}, (210, -85)),  # -60, +25
            ({"wght": -1.0, "wdth": 1.0}, (220, -90)),  # -60 + 10, +25 - 5
            ({"wght": 0.0, "wdth": -1.0}, (260, -105)),  # -10, +5
            ({"wght": 0.0, "wdth": 0.0}, (270, -110)),
            ({"wght": 0.0, "wdth": 1.0}, (280, -115)),  # +10, -5
            ({"wght": 1.0, "wdth": -1.0}, (320, -130)),  # +60 - 10, -25 + 5
            ({"wght": 1.0, "wdth": 0.0}, (330, -135)),  # +60, -25
            ({"wght": 1.0, "wdth": 1.0}, (340, -140)),  # +60 + 10, -25 - 5
        ],
    )
    def test_full_instance_GPOS_mark_and_kern(self, varfontGPOS2, location, expected):
        vf = varfontGPOS2
        assert "GDEF" in vf
        assert "GPOS" in vf

        instancer.instantiateOTL(vf, location)

        v1, v2 = expected
        gdef = vf["GDEF"].table
        gpos = vf["GPOS"].table
        assert gdef.Version == 0x00010000
        assert not hasattr(gdef, "VarStore")
        assert gdef.GlyphClassDef

        assert gpos.LookupList.Lookup[0].LookupType == 4  # MarkBasePos
        markBasePos = gpos.LookupList.Lookup[0].SubTable[0]
        baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0]
        assert baseAnchor.Format == 1
        assert not hasattr(baseAnchor, "XDeviceTable")
        assert not hasattr(baseAnchor, "YDeviceTable")
        assert baseAnchor.XCoordinate == v1
        assert baseAnchor.YCoordinate == 450

        assert gpos.LookupList.Lookup[1].LookupType == 2  # PairPos
        pairPos = gpos.LookupList.Lookup[1].SubTable[0]
        valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
        assert not hasattr(valueRec1, "XAdvDevice")
        assert valueRec1.XAdvance == v2


class InstantiateAvarTest(object):
    @pytest.mark.parametrize("location", [{"wght": 0.0}, {"wdth": 0.0}])
    def test_pin_and_drop_axis(self, varfont, location):
        instancer.instantiateAvar(varfont, location)

        assert set(varfont["avar"].segments).isdisjoint(location)

    def test_full_instance(self, varfont):
        instancer.instantiateAvar(varfont, {"wght": 0.0, "wdth": 0.0})

        assert "avar" not in varfont

    @staticmethod
    def quantizeF2Dot14Floats(mapping):
        return {
            floatToFixedToFloat(k, 14): floatToFixedToFloat(v, 14)
            for k, v in mapping.items()
        }

    # the following values come from NotoSans-VF.ttf
    DFLT_WGHT_MAPPING = {
        -1.0: -1.0,
        -0.6667: -0.7969,
        -0.3333: -0.5,
        0: 0,
        0.2: 0.18,
        0.4: 0.38,
        0.6: 0.61,
        0.8: 0.79,
        1.0: 1.0,
    }

    DFLT_WDTH_MAPPING = {-1.0: -1.0, -0.6667: -0.7, -0.3333: -0.36664, 0: 0, 1.0: 1.0}

    @pytest.fixture
    def varfont(self):
        fvarAxes = ("wght", (100, 400, 900)), ("wdth", (62.5, 100, 100))
        avarSegments = {
            "wght": self.quantizeF2Dot14Floats(self.DFLT_WGHT_MAPPING),
            "wdth": self.quantizeF2Dot14Floats(self.DFLT_WDTH_MAPPING),
        }
        varfont = ttLib.TTFont()
        varfont["name"] = ttLib.newTable("name")
        varLib._add_fvar(varfont, _makeDSAxesDict(fvarAxes), instances=())
        avar = varfont["avar"] = ttLib.newTable("avar")
        avar.segments = avarSegments
        return varfont

    @pytest.mark.parametrize(
        "axisLimits, expectedSegments",
        [
            pytest.param(
                {"wght": (100, 900)},
                {"wght": DFLT_WGHT_MAPPING, "wdth": DFLT_WDTH_MAPPING},
                id="wght=100:900",
            ),
            pytest.param(
                {"wght": (400, 900)},
                {
                    "wght": {
                        -1.0: -1.0,
                        0: 0,
                        0.2: 0.18,
                        0.4: 0.38,
                        0.6: 0.61,
                        0.8: 0.79,
                        1.0: 1.0,
                    },
                    "wdth": DFLT_WDTH_MAPPING,
                },
                id="wght=400:900",
            ),
            pytest.param(
                {"wght": (100, 400)},
                {
                    "wght": {
                        -1.0: -1.0,
                        -0.6667: -0.7969,
                        -0.3333: -0.5,
                        0: 0,
                        1.0: 1.0,
                    },
                    "wdth": DFLT_WDTH_MAPPING,
                },
                id="wght=100:400",
            ),
            pytest.param(
                {"wght": (400, 800)},
                {
                    "wght": {
                        -1.0: -1.0,
                        0: 0,
                        0.25: 0.22784,
                        0.50006: 0.48103,
                        0.75: 0.77214,
                        1.0: 1.0,
                    },
                    "wdth": DFLT_WDTH_MAPPING,
                },
                id="wght=400:800",
            ),
            pytest.param(
                {"wght": (400, 700)},
                {
                    "wght": {
                        -1.0: -1.0,
                        0: 0,
                        0.3334: 0.2951,
                        0.66675: 0.623,
                        1.0: 1.0,
                    },
                    "wdth": DFLT_WDTH_MAPPING,
                },
                id="wght=400:700",
            ),
            pytest.param(
                {"wght": (400, 600)},
                {
                    "wght": {-1.0: -1.0, 0: 0, 0.5: 0.47363, 1.0: 1.0},
                    "wdth": DFLT_WDTH_MAPPING,
                },
                id="wght=400:600",
            ),
            pytest.param(
                {"wdth": (62.5, 100)},
                {
                    "wght": DFLT_WGHT_MAPPING,
                    "wdth": {
                        -1.0: -1.0,
                        -0.6667: -0.7,
                        -0.3333: -0.36664,
                        0: 0,
                        1.0: 1.0,
                    },
                },
                id="wdth=62.5:100",
            ),
            pytest.param(
                {"wdth": (70, 100)},
                {
                    "wght": DFLT_WGHT_MAPPING,
                    "wdth": {
                        -1.0: -1.0,
                        -0.8334: -0.85364,
                        -0.4166: -0.44714,
                        0: 0,
                        1.0: 1.0,
                    },
                },
                id="wdth=70:100",
            ),
            pytest.param(
                {"wdth": (75, 100)},
                {
                    "wght": DFLT_WGHT_MAPPING,
                    "wdth": {-1.0: -1.0, -0.49994: -0.52374, 0: 0, 1.0: 1.0},
                },
                id="wdth=75:100",
            ),
            pytest.param(
                {"wdth": (77, 100)},
                {
                    "wght": DFLT_WGHT_MAPPING,
                    "wdth": {-1.0: -1.0, -0.54346: -0.56696, 0: 0, 1.0: 1.0},
                },
                id="wdth=77:100",
            ),
            pytest.param(
                {"wdth": (87.5, 100)},
                {"wght": DFLT_WGHT_MAPPING, "wdth": {-1.0: -1.0, 0: 0, 1.0: 1.0}},
                id="wdth=87.5:100",
            ),
        ],
    )
    def test_limit_axes(self, varfont, axisLimits, expectedSegments):
        instancer.instantiateAvar(varfont, axisLimits)

        newSegments = varfont["avar"].segments
        expectedSegments = {
            axisTag: self.quantizeF2Dot14Floats(mapping)
            for axisTag, mapping in expectedSegments.items()
        }
        assert newSegments == expectedSegments

    @pytest.mark.parametrize(
        "invalidSegmentMap",
        [
            pytest.param({0.5: 0.5}, id="missing-required-maps-1"),
            pytest.param({-1.0: -1.0, 1.0: 1.0}, id="missing-required-maps-2"),
            pytest.param(
                {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.6: 0.4, 1.0: 1.0},
                id="retrograde-value-maps",
            ),
        ],
    )
    def test_drop_invalid_segment_map(self, varfont, invalidSegmentMap, caplog):
        varfont["avar"].segments["wght"] = invalidSegmentMap

        with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
            instancer.instantiateAvar(varfont, {"wght": (100, 400)})

        assert "Invalid avar" in caplog.text
        assert "wght" not in varfont["avar"].segments

    def test_isValidAvarSegmentMap(self):
        assert instancer._isValidAvarSegmentMap("FOOO", {})
        assert instancer._isValidAvarSegmentMap("FOOO", {-1.0: -1.0, 0: 0, 1.0: 1.0})
        assert instancer._isValidAvarSegmentMap(
            "FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 1.0: 1.0}
        )
        assert instancer._isValidAvarSegmentMap(
            "FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.7: 0.5, 1.0: 1.0}
        )


class InstantiateFvarTest(object):
    @pytest.mark.parametrize(
        "location, instancesLeft",
        [
            (
                {"wght": 400.0},
                ["Regular", "SemiCondensed", "Condensed", "ExtraCondensed"],
            ),
            (
                {"wght": 100.0},
                ["Thin", "SemiCondensed Thin", "Condensed Thin", "ExtraCondensed Thin"],
            ),
            (
                {"wdth": 100.0},
                [
                    "Thin",
                    "ExtraLight",
                    "Light",
                    "Regular",
                    "Medium",
                    "SemiBold",
                    "Bold",
                    "ExtraBold",
                    "Black",
                ],
            ),
            # no named instance at pinned location
            ({"wdth": 90.0}, []),
        ],
    )
    def test_pin_and_drop_axis(self, varfont, location, instancesLeft):
        instancer.instantiateFvar(varfont, location)

        fvar = varfont["fvar"]
        assert {a.axisTag for a in fvar.axes}.isdisjoint(location)

        for instance in fvar.instances:
            assert set(instance.coordinates).isdisjoint(location)

        name = varfont["name"]
        assert [
            name.getDebugName(instance.subfamilyNameID) for instance in fvar.instances
        ] == instancesLeft

    def test_full_instance(self, varfont):
        instancer.instantiateFvar(varfont, {"wght": 0.0, "wdth": 0.0})

        assert "fvar" not in varfont


class InstantiateSTATTest(object):
    @pytest.mark.parametrize(
        "location, expected",
        [
            ({"wght": 400}, ["Regular", "Condensed", "Upright", "Normal"]),
            ({"wdth": 100}, ["Thin", "Regular", "Black", "Upright", "Normal"]),
        ],
    )
    def test_pin_and_drop_axis(self, varfont, location, expected):
        instancer.instantiateSTAT(varfont, location)

        stat = varfont["STAT"].table
        designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis}

        assert designAxes == {"wght", "wdth", "ital"}

        name = varfont["name"]
        valueNames = []
        for axisValueTable in stat.AxisValueArray.AxisValue:
            valueName = name.getDebugName(axisValueTable.ValueNameID)
            valueNames.append(valueName)

        assert valueNames == expected

    def test_skip_table_no_axis_value_array(self, varfont):
        varfont["STAT"].table.AxisValueArray = None

        instancer.instantiateSTAT(varfont, {"wght": 100})

        assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
        assert varfont["STAT"].table.AxisValueArray is None

    def test_skip_table_axis_value_array_empty(self, varfont):
        varfont["STAT"].table.AxisValueArray.AxisValue = []

        instancer.instantiateSTAT(varfont, {"wght": 100})

        assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
        assert not varfont["STAT"].table.AxisValueArray.AxisValue

    def test_skip_table_no_design_axes(self, varfont):
        stat = otTables.STAT()
        stat.Version = 0x00010001
        stat.populateDefaults()
        assert not stat.DesignAxisRecord
        assert not stat.AxisValueArray
        varfont["STAT"].table = stat

        instancer.instantiateSTAT(varfont, {"wght": 100})

        assert not varfont["STAT"].table.DesignAxisRecord

    @staticmethod
    def get_STAT_axis_values(stat):
        axes = stat.DesignAxisRecord.Axis
        result = []
        for axisValue in stat.AxisValueArray.AxisValue:
            if axisValue.Format == 1:
                result.append((axes[axisValue.AxisIndex].AxisTag, axisValue.Value))
            elif axisValue.Format == 3:
                result.append(
                    (
                        axes[axisValue.AxisIndex].AxisTag,
                        (axisValue.Value, axisValue.LinkedValue),
                    )
                )
            elif axisValue.Format == 2:
                result.append(
                    (
                        axes[axisValue.AxisIndex].AxisTag,
                        (
                            axisValue.RangeMinValue,
                            axisValue.NominalValue,
                            axisValue.RangeMaxValue,
                        ),
                    )
                )
            elif axisValue.Format == 4:
                result.append(
                    tuple(
                        (axes[rec.AxisIndex].AxisTag, rec.Value)
                        for rec in axisValue.AxisValueRecord
                    )
                )
            else:
                raise AssertionError(axisValue.Format)
        return result

    def test_limit_axes(self, varfont2):
        instancer.instantiateSTAT(varfont2, {"wght": (400, 500), "wdth": (75, 100)})

        assert len(varfont2["STAT"].table.AxisValueArray.AxisValue) == 5
        assert self.get_STAT_axis_values(varfont2["STAT"].table) == [
            ("wght", (400.0, 700.0)),
            ("wght", 500.0),
            ("wdth", (93.75, 100.0, 100.0)),
            ("wdth", (81.25, 87.5, 93.75)),
            ("wdth", (68.75, 75.0, 81.25)),
        ]

    def test_limit_axis_value_format_4(self, varfont2):
        stat = varfont2["STAT"].table

        axisValue = otTables.AxisValue()
        axisValue.Format = 4
        axisValue.AxisValueRecord = []
        for tag, value in (("wght", 575), ("wdth", 90)):
            rec = otTables.AxisValueRecord()
            rec.AxisIndex = next(
                i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag
            )
            rec.Value = value
            axisValue.AxisValueRecord.append(rec)
        stat.AxisValueArray.AxisValue.append(axisValue)

        instancer.instantiateSTAT(varfont2, {"wght": (100, 600)})

        assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue

        instancer.instantiateSTAT(varfont2, {"wdth": (62.5, 87.5)})

        assert axisValue not in varfont2["STAT"].table.AxisValueArray.AxisValue

    def test_unknown_axis_value_format(self, varfont2, caplog):
        stat = varfont2["STAT"].table
        axisValue = otTables.AxisValue()
        axisValue.Format = 5
        stat.AxisValueArray.AxisValue.append(axisValue)

        with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
            instancer.instantiateSTAT(varfont2, {"wght": 400})

        assert "Unknown AxisValue table format (5)" in caplog.text
        assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue


def test_setMacOverlapFlags():
    flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND
    flagOverlapSimple = _g_l_y_f.flagOverlapSimple

    glyf = ttLib.newTable("glyf")
    glyf.glyphOrder = ["a", "b", "c"]
    a = _g_l_y_f.Glyph()
    a.numberOfContours = 1
    a.flags = [0]
    b = _g_l_y_f.Glyph()
    b.numberOfContours = -1
    comp = _g_l_y_f.GlyphComponent()
    comp.flags = 0
    b.components = [comp]
    c = _g_l_y_f.Glyph()
    c.numberOfContours = 0
    glyf.glyphs = {"a": a, "b": b, "c": c}

    instancer.setMacOverlapFlags(glyf)

    assert a.flags[0] & flagOverlapSimple != 0
    assert b.components[0].flags & flagOverlapCompound != 0


def _strip_ttLibVersion(string):
    return re.sub(' ttLibVersion=".*"', "", string)


@pytest.fixture
def varfont2():
    f = ttLib.TTFont(recalcTimestamp=False)
    f.importXML(os.path.join(TESTDATA, "PartialInstancerTest2-VF.ttx"))
    return f


@pytest.fixture
def varfont3():
    f = ttLib.TTFont(recalcTimestamp=False)
    f.importXML(os.path.join(TESTDATA, "PartialInstancerTest3-VF.ttx"))
    return f


def _dump_ttx(ttFont):
    # compile to temporary bytes stream, reload and dump to XML
    tmp = BytesIO()
    ttFont.save(tmp)
    tmp.seek(0)
    ttFont2 = ttLib.TTFont(tmp, recalcBBoxes=False, recalcTimestamp=False)
    s = StringIO()
    ttFont2.saveXML(s, newlinestr="\n")
    return _strip_ttLibVersion(s.getvalue())


def _get_expected_instance_ttx(
    name, *locations, overlap=instancer.OverlapMode.KEEP_AND_SET_FLAGS
):
    filename = f"{name}-VF-instance-{','.join(str(loc) for loc in locations)}"
    if overlap == instancer.OverlapMode.KEEP_AND_DONT_SET_FLAGS:
        filename += "-no-overlap-flags"
    elif overlap == instancer.OverlapMode.REMOVE:
        filename += "-no-overlaps"
    with open(
        os.path.join(TESTDATA, "test_results", f"{filename}.ttx"),
        "r",
        encoding="utf-8",
    ) as fp:
        return _strip_ttLibVersion(fp.read())


class InstantiateVariableFontTest(object):
    @pytest.mark.parametrize(
        "wght, wdth",
        [(100, 100), (400, 100), (900, 100), (100, 62.5), (400, 62.5), (900, 62.5)],
    )
    def test_multiple_instancing(self, varfont2, wght, wdth):
        partial = instancer.instantiateVariableFont(varfont2, {"wght": wght})
        instance = instancer.instantiateVariableFont(partial, {"wdth": wdth})

        expected = _get_expected_instance_ttx("PartialInstancerTest2", wght, wdth)

        assert _dump_ttx(instance) == expected

    def test_default_instance(self, varfont2):
        instance = instancer.instantiateVariableFont(
            varfont2, {"wght": None, "wdth": None}
        )

        expected = _get_expected_instance_ttx("PartialInstancerTest2", 400, 100)

        assert _dump_ttx(instance) == expected

    @pytest.mark.parametrize(
        "overlap, wght",
        [
            (instancer.OverlapMode.KEEP_AND_DONT_SET_FLAGS, 400),
            (instancer.OverlapMode.REMOVE, 400),
            (instancer.OverlapMode.REMOVE, 700),
        ],
    )
    def test_overlap(self, varfont3, wght, overlap):
        pytest.importorskip("pathops")

        location = {"wght": wght}

        instance = instancer.instantiateVariableFont(
            varfont3, location, overlap=overlap
        )

        expected = _get_expected_instance_ttx(
            "PartialInstancerTest3", wght, overlap=overlap
        )

        assert _dump_ttx(instance) == expected


def _conditionSetAsDict(conditionSet, axisOrder):
    result = {}
    for cond in conditionSet.ConditionTable:
        assert cond.Format == 1
        axisTag = axisOrder[cond.AxisIndex]
        result[axisTag] = (cond.FilterRangeMinValue, cond.FilterRangeMaxValue)
    return result


def _getSubstitutions(gsub, lookupIndices):
    subs = {}
    for index, lookup in enumerate(gsub.LookupList.Lookup):
        if index in lookupIndices:
            for subtable in lookup.SubTable:
                subs.update(subtable.mapping)
    return subs


def makeFeatureVarsFont(conditionalSubstitutions):
    axes = set()
    glyphs = set()
    for region, substitutions in conditionalSubstitutions:
        for box in region:
            axes.update(box.keys())
        glyphs.update(*substitutions.items())

    varfont = ttLib.TTFont()
    varfont.setGlyphOrder(sorted(glyphs))

    fvar = varfont["fvar"] = ttLib.newTable("fvar")
    fvar.axes = []
    for axisTag in sorted(axes):
        axis = _f_v_a_r.Axis()
        axis.axisTag = Tag(axisTag)
        fvar.axes.append(axis)

    featureVars.addFeatureVariations(varfont, conditionalSubstitutions)

    return varfont


class InstantiateFeatureVariationsTest(object):
    @pytest.mark.parametrize(
        "location, appliedSubs, expectedRecords",
        [
            ({"wght": 0}, {}, [({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"})]),
            (
                {"wght": -1.0},
                {},
                [
                    ({"cntr": (0, 0.25)}, {"uni0061": "uni0041"}),
                    ({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"}),
                ],
            ),
            (
                {"wght": 1.0},
                {"uni0024": "uni0024.nostroke"},
                [
                    (
                        {"cntr": (0.75, 1.0)},
                        {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
                    )
                ],
            ),
            (
                {"cntr": 0},
                {},
                [
                    ({"wght": (-1.0, -0.45654)}, {"uni0061": "uni0041"}),
                    ({"wght": (0.20886, 1.0)}, {"uni0024": "uni0024.nostroke"}),
                ],
            ),
            (
                {"cntr": 1.0},
                {"uni0041": "uni0061"},
                [
                    (
                        {"wght": (0.20886, 1.0)},
                        {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
                    )
                ],
            ),
        ],
    )
    def test_partial_instance(self, location, appliedSubs, expectedRecords):
        font = makeFeatureVarsFont(
            [
                ([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}),
                ([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}),
                (
                    [{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}],
                    {"uni0061": "uni0041"},
                ),
            ]
        )

        instancer.instantiateFeatureVariations(font, location)

        gsub = font["GSUB"].table
        featureVariations = gsub.FeatureVariations

        assert featureVariations.FeatureVariationCount == len(expectedRecords)

        axisOrder = [a.axisTag for a in font["fvar"].axes if a.axisTag not in location]
        for i, (expectedConditionSet, expectedSubs) in enumerate(expectedRecords):
            rec = featureVariations.FeatureVariationRecord[i]
            conditionSet = _conditionSetAsDict(rec.ConditionSet, axisOrder)

            assert conditionSet == expectedConditionSet

            subsRecord = rec.FeatureTableSubstitution.SubstitutionRecord[0]
            lookupIndices = subsRecord.Feature.LookupListIndex
            substitutions = _getSubstitutions(gsub, lookupIndices)

            assert substitutions == expectedSubs

        appliedLookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex

        assert _getSubstitutions(gsub, appliedLookupIndices) == appliedSubs

    @pytest.mark.parametrize(
        "location, appliedSubs",
        [
            ({"wght": 0, "cntr": 0}, None),
            ({"wght": -1.0, "cntr": 0}, {"uni0061": "uni0041"}),
            ({"wght": 1.0, "cntr": 0}, {"uni0024": "uni0024.nostroke"}),
            ({"wght": 0.0, "cntr": 1.0}, {"uni0041": "uni0061"}),
            (
                {"wght": 1.0, "cntr": 1.0},
                {"uni0041": "uni0061", "uni0024": "uni0024.nostroke"},
            ),
            ({"wght": -1.0, "cntr": 0.3}, None),
        ],
    )
    def test_full_instance(self, location, appliedSubs):
        font = makeFeatureVarsFont(
            [
                ([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}),
                ([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}),
                (
                    [{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}],
                    {"uni0061": "uni0041"},
                ),
            ]
        )

        instancer.instantiateFeatureVariations(font, location)

        gsub = font["GSUB"].table
        assert not hasattr(gsub, "FeatureVariations")

        if appliedSubs:
            lookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex
            assert _getSubstitutions(gsub, lookupIndices) == appliedSubs
        else:
            assert not gsub.FeatureList.FeatureRecord

    def test_unsupported_condition_format(self, caplog):
        font = makeFeatureVarsFont(
            [
                (
                    [{"wdth": (-1.0, -0.5), "wght": (0.5, 1.0)}],
                    {"dollar": "dollar.nostroke"},
                )
            ]
        )
        featureVariations = font["GSUB"].table.FeatureVariations
        rec1 = featureVariations.FeatureVariationRecord[0]
        assert len(rec1.ConditionSet.ConditionTable) == 2
        rec1.ConditionSet.ConditionTable[0].Format = 2

        with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
            instancer.instantiateFeatureVariations(font, {"wdth": 0})

        assert (
            "Condition table 0 of FeatureVariationRecord 0 "
            "has unsupported format (2); ignored"
        ) in caplog.text

        # check that record with unsupported condition format (but whose other
        # conditions do not reference pinned axes) is kept as is
        featureVariations = font["GSUB"].table.FeatureVariations
        assert featureVariations.FeatureVariationRecord[0] is rec1
        assert len(rec1.ConditionSet.ConditionTable) == 2
        assert rec1.ConditionSet.ConditionTable[0].Format == 2

    def test_GSUB_FeatureVariations_is_None(self, varfont2):
        varfont2["GSUB"].table.Version = 0x00010001
        varfont2["GSUB"].table.FeatureVariations = None
        tmp = BytesIO()
        varfont2.save(tmp)
        varfont = ttLib.TTFont(tmp)

        # DO NOT raise an exception when the optional 'FeatureVariations' attribute is
        # present but is set to None (e.g. with GSUB 1.1); skip and do nothing.
        assert varfont["GSUB"].table.FeatureVariations is None
        instancer.instantiateFeatureVariations(varfont, {"wght": 400, "wdth": 100})
        assert varfont["GSUB"].table.FeatureVariations is None


class LimitTupleVariationAxisRangesTest:
    def check_limit_single_var_axis_range(self, var, axisTag, axisRange, expected):
        result = instancer.limitTupleVariationAxisRange(var, axisTag, axisRange)
        print(result)

        assert len(result) == len(expected)
        for v1, v2 in zip(result, expected):
            assert v1.coordinates == pytest.approx(v2.coordinates)
            assert v1.axes.keys() == v2.axes.keys()
            for k in v1.axes:
                p, q = v1.axes[k], v2.axes[k]
                assert p == pytest.approx(q)

    @pytest.mark.parametrize(
        "var, axisTag, newMax, expected",
        [
            (
                TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
                "wdth",
                0.5,
                [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
                "wght",
                0.5,
                [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [50, 50])],
            ),
            (
                TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
                "wght",
                0.8,
                [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])],
            ),
            (
                TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
                "wght",
                1.0,
                [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
            ),
            (TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), "wght", 0.0, []),
            (TupleVariation({"wght": (0.5, 1.0, 1.0)}, [100, 100]), "wght", 0.4, []),
            (
                TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
                "wght",
                0.5,
                [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
                "wght",
                0.4,
                [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])],
            ),
            (
                TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
                "wght",
                0.6,
                [TupleVariation({"wght": (0.0, 0.833334, 1.666667)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]),
                "wght",
                0.4,
                [
                    TupleVariation({"wght": (0.0, 0.5, 1.99994)}, [100, 100]),
                    TupleVariation({"wght": (0.5, 1.0, 1.0)}, [8.33333, 8.33333]),
                ],
            ),
            (
                TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]),
                "wght",
                0.5,
                [TupleVariation({"wght": (0.0, 0.4, 1.99994)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (0.5, 0.5, 1.0)}, [100, 100]),
                "wght",
                0.5,
                [TupleVariation({"wght": (1.0, 1.0, 1.0)}, [100, 100])],
            ),
        ],
    )
    def test_positive_var(self, var, axisTag, newMax, expected):
        axisRange = instancer.NormalizedAxisRange(0, newMax)
        self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)

    @pytest.mark.parametrize(
        "var, axisTag, newMin, expected",
        [
            (
                TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
                "wdth",
                -0.5,
                [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
                "wght",
                -0.5,
                [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [50, 50])],
            ),
            (
                TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
                "wght",
                -0.8,
                [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])],
            ),
            (
                TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
                "wght",
                -1.0,
                [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
            ),
            (TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), "wght", 0.0, []),
            (
                TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [100, 100]),
                "wght",
                -0.4,
                [],
            ),
            (
                TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
                "wght",
                -0.5,
                [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
                "wght",
                -0.4,
                [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])],
            ),
            (
                TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
                "wght",
                -0.6,
                [TupleVariation({"wght": (-1.666667, -0.833334, 0.0)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]),
                "wght",
                -0.4,
                [
                    TupleVariation({"wght": (-2.0, -0.5, -0.0)}, [100, 100]),
                    TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [8.33333, 8.33333]),
                ],
            ),
            (
                TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]),
                "wght",
                -0.5,
                [TupleVariation({"wght": (-2.0, -0.4, 0.0)}, [100, 100])],
            ),
            (
                TupleVariation({"wght": (-1.0, -0.5, -0.5)}, [100, 100]),
                "wght",
                -0.5,
                [TupleVariation({"wght": (-1.0, -1.0, -1.0)}, [100, 100])],
            ),
        ],
    )
    def test_negative_var(self, var, axisTag, newMin, expected):
        axisRange = instancer.NormalizedAxisRange(newMin, 0)
        self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)


@pytest.mark.parametrize(
    "oldRange, newRange, expected",
    [
        ((1.0, -1.0), (-1.0, 1.0), None),  # invalid oldRange min > max
        ((0.6, 1.0), (0, 0.5), None),
        ((-1.0, -0.6), (-0.5, 0), None),
        ((0.4, 1.0), (0, 0.5), (0.8, 1.0)),
        ((-1.0, -0.4), (-0.5, 0), (-1.0, -0.8)),
        ((0.4, 1.0), (0, 0.4), (1.0, 1.0)),
        ((-1.0, -0.4), (-0.4, 0), (-1.0, -1.0)),
        ((-0.5, 0.5), (-0.4, 0.4), (-1.0, 1.0)),
        ((0, 1.0), (-1.0, 0), (0, 0)),  # or None?
        ((-1.0, 0), (0, 1.0), (0, 0)),  # or None?
    ],
)
def test_limitFeatureVariationConditionRange(oldRange, newRange, expected):
    condition = featureVars.buildConditionTable(0, *oldRange)

    result = instancer._limitFeatureVariationConditionRange(
        condition, instancer.NormalizedAxisRange(*newRange)
    )

    assert result == expected


@pytest.mark.parametrize(
    "limits, expected",
    [
        (["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}),
        (["wght=400:900"], {"wght": (400, 900)}),
        (["slnt=11.4"], {"slnt": pytest.approx(11.399994)}),
        (["ABCD=drop"], {"ABCD": None}),
    ],
)
def test_parseLimits(limits, expected):
    assert instancer.parseLimits(limits) == expected


@pytest.mark.parametrize(
    "limits", [["abcde=123", "=0", "wght=:", "wght=1:", "wght=abcd", "wght=x:y"]]
)
def test_parseLimits_invalid(limits):
    with pytest.raises(ValueError, match="invalid location format"):
        instancer.parseLimits(limits)


def test_normalizeAxisLimits_tuple(varfont):
    normalized = instancer.normalizeAxisLimits(varfont, {"wght": (100, 400)})
    assert normalized == {"wght": (-1.0, 0)}


def test_normalizeAxisLimits_unsupported_range(varfont):
    with pytest.raises(NotImplementedError, match="Unsupported range"):
        instancer.normalizeAxisLimits(varfont, {"wght": (401, 700)})


def test_normalizeAxisLimits_no_avar(varfont):
    del varfont["avar"]

    normalized = instancer.normalizeAxisLimits(varfont, {"wght": (400, 500)})

    assert normalized["wght"] == pytest.approx((0, 0.2), 1e-4)


def test_normalizeAxisLimits_missing_from_fvar(varfont):
    with pytest.raises(ValueError, match="not present in fvar"):
        instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000})


def test_sanityCheckVariableTables(varfont):
    font = ttLib.TTFont()
    with pytest.raises(ValueError, match="Missing required table fvar"):
        instancer.sanityCheckVariableTables(font)

    del varfont["glyf"]

    with pytest.raises(ValueError, match="Can't have gvar without glyf"):
        instancer.sanityCheckVariableTables(varfont)


def test_main(varfont, tmpdir):
    fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf")
    varfont.save(fontfile)
    args = [fontfile, "wght=400"]

    # exits without errors
    assert instancer.main(args) is None


def test_main_exit_nonexistent_file(capsys):
    with pytest.raises(SystemExit):
        instancer.main([""])
    captured = capsys.readouterr()

    assert "No such file ''" in captured.err


def test_main_exit_invalid_location(varfont, tmpdir, capsys):
    fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf")
    varfont.save(fontfile)

    with pytest.raises(SystemExit):
        instancer.main([fontfile, "wght:100"])
    captured = capsys.readouterr()

    assert "invalid location format" in captured.err


def test_main_exit_multiple_limits(varfont, tmpdir, capsys):
    fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf")
    varfont.save(fontfile)

    with pytest.raises(SystemExit):
        instancer.main([fontfile, "wght=400", "wght=90"])
    captured = capsys.readouterr()

    assert "Specified multiple limits for the same axis" in captured.err
