from fontTools.varLib.models import (
    normalizeLocation,
    supportScalar,
    VariationModel,
    VariationModelError,
)
import pytest


def test_normalizeLocation():
    axes = {"wght": (100, 400, 900)}
    assert normalizeLocation({"wght": 400}, axes) == {"wght": 0.0}
    assert normalizeLocation({"wght": 100}, axes) == {"wght": -1.0}
    assert normalizeLocation({"wght": 900}, axes) == {"wght": 1.0}
    assert normalizeLocation({"wght": 650}, axes) == {"wght": 0.5}
    assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0}
    assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0}

    axes = {"wght": (0, 0, 1000)}
    assert normalizeLocation({"wght": 0}, axes) == {"wght": 0.0}
    assert normalizeLocation({"wght": -1}, axes) == {"wght": 0.0}
    assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0}
    assert normalizeLocation({"wght": 500}, axes) == {"wght": 0.5}
    assert normalizeLocation({"wght": 1001}, axes) == {"wght": 1.0}

    axes = {"wght": (0, 1000, 1000)}
    assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0}
    assert normalizeLocation({"wght": -1}, axes) == {"wght": -1.0}
    assert normalizeLocation({"wght": 500}, axes) == {"wght": -0.5}
    assert normalizeLocation({"wght": 1000}, axes) == {"wght": 0.0}
    assert normalizeLocation({"wght": 1001}, axes) == {"wght": 0.0}


@pytest.mark.parametrize(
    "axes, location, expected",
    [
        # lower != default != upper
        ({"wght": (100, 400, 900)}, {"wght": 1000}, {"wght": 1.2}),
        ({"wght": (100, 400, 900)}, {"wght": 900}, {"wght": 1.0}),
        ({"wght": (100, 400, 900)}, {"wght": 650}, {"wght": 0.5}),
        ({"wght": (100, 400, 900)}, {"wght": 400}, {"wght": 0.0}),
        ({"wght": (100, 400, 900)}, {"wght": 250}, {"wght": -0.5}),
        ({"wght": (100, 400, 900)}, {"wght": 100}, {"wght": -1.0}),
        ({"wght": (100, 400, 900)}, {"wght": 25}, {"wght": -1.25}),
        # lower == default != upper
        (
            {"wght": (400, 400, 900), "wdth": (100, 100, 150)},
            {"wght": 1000, "wdth": 200},
            {"wght": 1.2, "wdth": 2.0},
        ),
        (
            {"wght": (400, 400, 900), "wdth": (100, 100, 150)},
            {"wght": 25, "wdth": 25},
            {"wght": -0.75, "wdth": -1.5},
        ),
        # lower != default == upper
        (
            {"wght": (100, 400, 400), "wdth": (50, 100, 100)},
            {"wght": 700, "wdth": 150},
            {"wght": 1.0, "wdth": 1.0},
        ),
        (
            {"wght": (100, 400, 400), "wdth": (50, 100, 100)},
            {"wght": -50, "wdth": 25},
            {"wght": -1.5, "wdth": -1.5},
        ),
        # degenerate case with lower == default == upper, normalized location always 0
        ({"wght": (400, 400, 400)}, {"wght": 100}, {"wght": 0.0}),
        ({"wght": (400, 400, 400)}, {"wght": 400}, {"wght": 0.0}),
        ({"wght": (400, 400, 400)}, {"wght": 700}, {"wght": 0.0}),
    ],
)
def test_normalizeLocation_extrapolate(axes, location, expected):
    assert normalizeLocation(location, axes, extrapolate=True) == expected


def test_supportScalar():
    assert supportScalar({}, {}) == 1.0
    assert supportScalar({"wght": 0.2}, {}) == 1.0
    assert supportScalar({"wght": 0.2}, {"wght": (0, 2, 3)}) == 0.1
    assert supportScalar({"wght": 2.5}, {"wght": (0, 2, 4)}) == 0.75
    assert supportScalar({"wght": 3}, {"wght": (0, 2, 2)}) == 0.0
    assert (
        supportScalar(
            {"wght": 3},
            {"wght": (0, 2, 2)},
            extrapolate=True,
            axisRanges={"wght": (0, 2)},
        )
        == 1.5
    )
    assert (
        supportScalar(
            {"wght": -1},
            {"wght": (0, 2, 2)},
            extrapolate=True,
            axisRanges={"wght": (0, 2)},
        )
        == -0.5
    )
    assert (
        supportScalar(
            {"wght": 3},
            {"wght": (0, 1, 2)},
            extrapolate=True,
            axisRanges={"wght": (0, 2)},
        )
        == -1.0
    )
    assert (
        supportScalar(
            {"wght": -1},
            {"wght": (0, 1, 2)},
            extrapolate=True,
            axisRanges={"wght": (0, 2)},
        )
        == -1.0
    )
    assert (
        supportScalar(
            {"wght": 2},
            {"wght": (0, 0.75, 1)},
            extrapolate=True,
            axisRanges={"wght": (0, 1)},
        )
        == -4.0
    )
    with pytest.raises(TypeError):
        supportScalar(
            {"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True, axisRanges=None
        )


def test_model_extrapolate():
    locations = [{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}]
    model = VariationModel(locations, extrapolate=True)
    masterValues = [100, 200, 300, 400]
    testLocsAndValues = [
        ({"a": -1, "b": -1}, -200),
        ({"a": -1, "b": 0}, 0),
        ({"a": -1, "b": 1}, 200),
        ({"a": -1, "b": 2}, 400),
        ({"a": 0, "b": -1}, -100),
        ({"a": 0, "b": 0}, 100),
        ({"a": 0, "b": 1}, 300),
        ({"a": 0, "b": 2}, 500),
        ({"a": 1, "b": -1}, 0),
        ({"a": 1, "b": 0}, 200),
        ({"a": 1, "b": 1}, 400),
        ({"a": 1, "b": 2}, 600),
        ({"a": 2, "b": -1}, 100),
        ({"a": 2, "b": 0}, 300),
        ({"a": 2, "b": 1}, 500),
        ({"a": 2, "b": 2}, 700),
    ]
    for loc, expectedValue in testLocsAndValues:
        assert expectedValue == model.interpolateFromMasters(loc, masterValues)


@pytest.mark.parametrize(
    "numLocations, numSamples",
    [
        pytest.param(127, 509, marks=pytest.mark.slow),
        (31, 251),
    ],
)
def test_modeling_error(numLocations, numSamples):
    # https://github.com/fonttools/fonttools/issues/2213
    locations = [{"axis": float(i) / numLocations} for i in range(numLocations)]
    masterValues = [100.0 if i else 0.0 for i in range(numLocations)]

    model = VariationModel(locations)

    for i in range(numSamples):
        loc = {"axis": float(i) / numSamples}
        scalars = model.getScalars(loc)

        deltas_float = model.getDeltas(masterValues)
        deltas_round = model.getDeltas(masterValues, round=round)

        expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars)
        actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars)

        err = abs(actual - expected)
        assert err <= 0.5, (i, err)

        # This is how NOT to round deltas.
        # deltas_late_round = [round(d) for d in deltas_float]
        # bad = model.interpolateFromDeltasAndScalars(deltas_late_round, scalars)
        # err_bad = abs(bad - expected)
        # if err != err_bad:
        #    print("{:d}	{:.2}	{:.2}".format(i, err, err_bad))


locationsA = [{}, {"wght": 1}, {"wdth": 1}]
locationsB = [{}, {"wght": 1}, {"wdth": 1}, {"wght": 1, "wdth": 1}]
locationsC = [
    {},
    {"wght": 0.5},
    {"wght": 1},
    {"wdth": 1},
    {"wght": 1, "wdth": 1},
]


class VariationModelTest(object):
    @pytest.mark.parametrize(
        "locations, axisOrder, sortedLocs, supports, deltaWeights",
        [
            (
                [
                    {"wght": 0.55, "wdth": 0.0},
                    {"wght": -0.55, "wdth": 0.0},
                    {"wght": -1.0, "wdth": 0.0},
                    {"wght": 0.0, "wdth": 1.0},
                    {"wght": 0.66, "wdth": 1.0},
                    {"wght": 0.66, "wdth": 0.66},
                    {"wght": 0.0, "wdth": 0.0},
                    {"wght": 1.0, "wdth": 1.0},
                    {"wght": 1.0, "wdth": 0.0},
                ],
                ["wght"],
                [
                    {},
                    {"wght": -0.55},
                    {"wght": -1.0},
                    {"wght": 0.55},
                    {"wght": 1.0},
                    {"wdth": 1.0},
                    {"wdth": 1.0, "wght": 1.0},
                    {"wdth": 1.0, "wght": 0.66},
                    {"wdth": 0.66, "wght": 0.66},
                ],
                [
                    {},
                    {"wght": (-1.0, -0.55, 0)},
                    {"wght": (-1.0, -1.0, -0.55)},
                    {"wght": (0, 0.55, 1.0)},
                    {"wght": (0.55, 1.0, 1.0)},
                    {"wdth": (0, 1.0, 1.0)},
                    {"wdth": (0, 1.0, 1.0), "wght": (0, 1.0, 1.0)},
                    {"wdth": (0, 1.0, 1.0), "wght": (0, 0.66, 1.0)},
                    {"wdth": (0, 0.66, 1.0), "wght": (0, 0.66, 1.0)},
                ],
                [
                    {},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0, 4: 1.0, 5: 1.0},
                    {
                        0: 1.0,
                        3: 0.7555555555555555,
                        4: 0.24444444444444444,
                        5: 1.0,
                        6: 0.66,
                    },
                    {
                        0: 1.0,
                        3: 0.7555555555555555,
                        4: 0.24444444444444444,
                        5: 0.66,
                        6: 0.43560000000000004,
                        7: 0.66,
                    },
                ],
            ),
            (
                [
                    {},
                    {"bar": 0.5},
                    {"bar": 1.0},
                    {"foo": 1.0},
                    {"bar": 0.5, "foo": 1.0},
                    {"bar": 1.0, "foo": 1.0},
                ],
                None,
                [
                    {},
                    {"bar": 0.5},
                    {"bar": 1.0},
                    {"foo": 1.0},
                    {"bar": 0.5, "foo": 1.0},
                    {"bar": 1.0, "foo": 1.0},
                ],
                [
                    {},
                    {"bar": (0, 0.5, 1.0)},
                    {"bar": (0.5, 1.0, 1.0)},
                    {"foo": (0, 1.0, 1.0)},
                    {"bar": (0, 0.5, 1.0), "foo": (0, 1.0, 1.0)},
                    {"bar": (0.5, 1.0, 1.0), "foo": (0, 1.0, 1.0)},
                ],
                [
                    {},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0, 1: 1.0, 3: 1.0},
                    {0: 1.0, 2: 1.0, 3: 1.0},
                ],
            ),
            (
                [
                    {},
                    {"foo": 0.25},
                    {"foo": 0.5},
                    {"foo": 0.75},
                    {"foo": 1.0},
                    {"bar": 0.25},
                    {"bar": 0.75},
                    {"bar": 1.0},
                ],
                None,
                [
                    {},
                    {"bar": 0.25},
                    {"bar": 0.75},
                    {"bar": 1.0},
                    {"foo": 0.25},
                    {"foo": 0.5},
                    {"foo": 0.75},
                    {"foo": 1.0},
                ],
                [
                    {},
                    {"bar": (0.0, 0.25, 1.0)},
                    {"bar": (0.25, 0.75, 1.0)},
                    {"bar": (0.75, 1.0, 1.0)},
                    {"foo": (0.0, 0.25, 1.0)},
                    {"foo": (0.25, 0.5, 1.0)},
                    {"foo": (0.5, 0.75, 1.0)},
                    {"foo": (0.75, 1.0, 1.0)},
                ],
                [
                    {},
                    {0: 1.0},
                    {0: 1.0, 1: 0.3333333333333333},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0, 4: 0.6666666666666666},
                    {0: 1.0, 4: 0.3333333333333333, 5: 0.5},
                    {0: 1.0},
                ],
            ),
            (
                [
                    {},
                    {"foo": 0.25},
                    {"foo": 0.5},
                    {"foo": 0.75},
                    {"foo": 1.0},
                    {"bar": 0.25},
                    {"bar": 0.75},
                    {"bar": 1.0},
                ],
                None,
                [
                    {},
                    {"bar": 0.25},
                    {"bar": 0.75},
                    {"bar": 1.0},
                    {"foo": 0.25},
                    {"foo": 0.5},
                    {"foo": 0.75},
                    {"foo": 1.0},
                ],
                [
                    {},
                    {"bar": (0, 0.25, 1.0)},
                    {"bar": (0.25, 0.75, 1.0)},
                    {"bar": (0.75, 1.0, 1.0)},
                    {"foo": (0, 0.25, 1.0)},
                    {"foo": (0.25, 0.5, 1.0)},
                    {"foo": (0.5, 0.75, 1.0)},
                    {"foo": (0.75, 1.0, 1.0)},
                ],
                [
                    {},
                    {0: 1.0},
                    {0: 1.0, 1: 0.3333333333333333},
                    {0: 1.0},
                    {0: 1.0},
                    {0: 1.0, 4: 0.6666666666666666},
                    {0: 1.0, 4: 0.3333333333333333, 5: 0.5},
                    {0: 1.0},
                ],
            ),
        ],
    )
    def test_init(self, locations, axisOrder, sortedLocs, supports, deltaWeights):
        model = VariationModel(locations, axisOrder=axisOrder)

        assert model.locations == sortedLocs
        assert model.supports == supports
        assert model.deltaWeights == deltaWeights

    def test_init_duplicate_locations(self):
        with pytest.raises(VariationModelError, match="Locations must be unique."):
            VariationModel(
                [
                    {"foo": 0.0, "bar": 0.0},
                    {"foo": 1.0, "bar": 1.0},
                    {"bar": 1.0, "foo": 1.0},
                ]
            )

    @pytest.mark.parametrize(
        "locations, axisOrder, masterValues, instanceLocation, expectedValue, masterScalars",
        [
            (
                [
                    {},
                    {"axis_A": 1.0},
                    {"axis_B": 1.0},
                    {"axis_A": 1.0, "axis_B": 1.0},
                    {"axis_A": 0.5, "axis_B": 1.0},
                    {"axis_A": 1.0, "axis_B": 0.5},
                ],
                ["axis_A", "axis_B"],
                [
                    0,
                    10,
                    20,
                    70,
                    50,
                    60,
                ],
                {
                    "axis_A": 0.5,
                    "axis_B": 0.5,
                },
                37.5,
                [0.25, 0.0, 0.0, -0.25, 0.5, 0.5],
            ),
        ],
    )
    def test_interpolation(
        self,
        locations,
        axisOrder,
        masterValues,
        instanceLocation,
        expectedValue,
        masterScalars,
    ):
        model = VariationModel(locations, axisOrder=axisOrder)

        interpolatedValue = model.interpolateFromMasters(instanceLocation, masterValues)
        assert interpolatedValue == expectedValue

        assert masterScalars == model.getMasterScalars(instanceLocation)

        assert model.interpolateFromValuesAndScalars(
            masterValues, masterScalars
        ) == pytest.approx(interpolatedValue)

    @pytest.mark.parametrize(
        "masterLocations, location, expected",
        [
            (locationsA, {"wght": 0, "wdth": 0}, [1, 0, 0]),
            (
                locationsA,
                {"wght": 0.5, "wdth": 0},
                [0.5, 0.5, 0],
            ),
            (locationsA, {"wght": 1, "wdth": 0}, [0, 1, 0]),
            (
                locationsA,
                {"wght": 0, "wdth": 0.5},
                [0.5, 0, 0.5],
            ),
            (locationsA, {"wght": 0, "wdth": 1}, [0, 0, 1]),
            (locationsA, {"wght": 1, "wdth": 1}, [-1, 1, 1]),
            (
                locationsA,
                {"wght": 0.5, "wdth": 0.5},
                [0, 0.5, 0.5],
            ),
            (
                locationsA,
                {"wght": 0.75, "wdth": 0.75},
                [-0.5, 0.75, 0.75],
            ),
            (
                locationsB,
                {"wght": 1, "wdth": 1},
                [0, 0, 0, 1],
            ),
            (
                locationsB,
                {"wght": 0.5, "wdth": 0},
                [0.5, 0.5, 0, 0],
            ),
            (
                locationsB,
                {"wght": 1, "wdth": 0.5},
                [0, 0.5, 0, 0.5],
            ),
            (
                locationsB,
                {"wght": 0.5, "wdth": 0.5},
                [0.25, 0.25, 0.25, 0.25],
            ),
            (
                locationsC,
                {"wght": 0.5, "wdth": 0},
                [0, 1, 0, 0, 0],
            ),
            (
                locationsC,
                {"wght": 0.25, "wdth": 0},
                [0.5, 0.5, 0, 0, 0],
            ),
            (
                locationsC,
                {"wght": 0.75, "wdth": 0},
                [0, 0.5, 0.5, 0, 0],
            ),
            (
                locationsC,
                {"wght": 0.5, "wdth": 1},
                [-0.5, 1, -0.5, 0.5, 0.5],
            ),
            (
                locationsC,
                {"wght": 0.75, "wdth": 1},
                [-0.25, 0.5, -0.25, 0.25, 0.75],
            ),
        ],
    )
    def test_getMasterScalars(self, masterLocations, location, expected):
        model = VariationModel(masterLocations)
        assert model.getMasterScalars(location) == expected
