import itertools
import io
import os
from pathlib import Path
from typing import cast

import numpy as np
import pytest

import matplotlib as mpl
from matplotlib import ft2font
from matplotlib.testing import _gen_multi_font_text
from matplotlib.testing.decorators import image_comparison
import matplotlib.font_manager as fm
import matplotlib.path as mpath
import matplotlib.pyplot as plt


def test_ft2image_draw_rect_filled():
    width = 23
    height = 42
    for x0, y0, x1, y1 in itertools.product([1, 100], [2, 200], [4, 400], [8, 800]):
        with pytest.warns(mpl.MatplotlibDeprecationWarning):
            im = ft2font.FT2Image(width, height)
        im.draw_rect_filled(x0, y0, x1, y1)
        a = np.asarray(im)
        assert a.dtype == np.uint8
        assert a.shape == (height, width)
        if x0 == 100 or y0 == 200:
            # All the out-of-bounds starts should get automatically clipped.
            assert np.sum(a) == 0
        else:
            # Otherwise, ends are clipped to the dimension, but are also _inclusive_.
            filled = (min(x1 + 1, width) - x0) * (min(y1 + 1, height) - y0)
            assert np.sum(a) == 255 * filled


def test_ft2font_dejavu_attrs():
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    assert font.fname == file
    # Names extracted from FontForge: Font Information → PS Names tab.
    assert font.postscript_name == 'DejaVuSans'
    assert font.family_name == 'DejaVu Sans'
    assert font.style_name == 'Book'
    assert font.num_faces == 1  # Single TTF.
    assert font.num_named_instances == 0  # Not a variable font.
    assert font.num_glyphs == 6241  # From compact encoding view in FontForge.
    assert font.num_fixed_sizes == 0  # All glyphs are scalable.
    assert font.num_charmaps == 5
    # Other internal flags are set, so only check the ones we're allowed to test.
    expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT |
                      ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.KERNING |
                      ft2font.FaceFlags.GLYPH_NAMES)
    assert expected_flags in font.face_flags
    assert font.style_flags == ft2font.StyleFlags.NORMAL
    assert font.scalable
    # From FontForge: Font Information → General tab → entry name below.
    assert font.units_per_EM == 2048  # Em Size.
    assert font.underline_position == -175  # Underline position.
    assert font.underline_thickness == 90  # Underline height.
    # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below.
    assert font.ascender == 1901  # HHead Ascent.
    assert font.descender == -483  # HHead Descent.
    # Unconfirmed values.
    assert font.height == 2384
    assert font.max_advance_width == 3838
    assert font.max_advance_height == 2384
    assert font.bbox == (-2090, -948, 3673, 2524)


def test_ft2font_cm_attrs():
    file = fm.findfont('cmtt10')
    font = ft2font.FT2Font(file)
    assert font.fname == file
    # Names extracted from FontForge: Font Information → PS Names tab.
    assert font.postscript_name == 'Cmtt10'
    assert font.family_name == 'cmtt10'
    assert font.style_name == 'Regular'
    assert font.num_faces == 1  # Single TTF.
    assert font.num_named_instances == 0  # Not a variable font.
    assert font.num_glyphs == 133  # From compact encoding view in FontForge.
    assert font.num_fixed_sizes == 0  # All glyphs are scalable.
    assert font.num_charmaps == 2
    # Other internal flags are set, so only check the ones we're allowed to test.
    expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT |
                      ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES)
    assert expected_flags in font.face_flags
    assert font.style_flags == ft2font.StyleFlags.NORMAL
    assert font.scalable
    # From FontForge: Font Information → General tab → entry name below.
    assert font.units_per_EM == 2048  # Em Size.
    assert font.underline_position == -143  # Underline position.
    assert font.underline_thickness == 20  # Underline height.
    # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below.
    assert font.ascender == 1276  # HHead Ascent.
    assert font.descender == -489  # HHead Descent.
    # Unconfirmed values.
    assert font.height == 1765
    assert font.max_advance_width == 1536
    assert font.max_advance_height == 1765
    assert font.bbox == (-12, -477, 1280, 1430)


def test_ft2font_stix_bold_attrs():
    file = fm.findfont('STIXSizeTwoSym:bold')
    font = ft2font.FT2Font(file)
    assert font.fname == file
    # Names extracted from FontForge: Font Information → PS Names tab.
    assert font.postscript_name == 'STIXSizeTwoSym-Bold'
    assert font.family_name == 'STIXSizeTwoSym'
    assert font.style_name == 'Bold'
    assert font.num_faces == 1  # Single TTF.
    assert font.num_named_instances == 0  # Not a variable font.
    assert font.num_glyphs == 20  # From compact encoding view in FontForge.
    assert font.num_fixed_sizes == 0  # All glyphs are scalable.
    assert font.num_charmaps == 3
    # Other internal flags are set, so only check the ones we're allowed to test.
    expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT |
                      ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES)
    assert expected_flags in font.face_flags
    assert font.style_flags == ft2font.StyleFlags.BOLD
    assert font.scalable
    # From FontForge: Font Information → General tab → entry name below.
    assert font.units_per_EM == 1000  # Em Size.
    assert font.underline_position == -133  # Underline position.
    assert font.underline_thickness == 20  # Underline height.
    # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below.
    assert font.ascender == 2095  # HHead Ascent.
    assert font.descender == -404  # HHead Descent.
    # Unconfirmed values.
    assert font.height == 2499
    assert font.max_advance_width == 1130
    assert font.max_advance_height == 2499
    assert font.bbox == (4, -355, 1185, 2095)


def test_ft2font_valid_args():
    class PathLikeClass:
        def __init__(self, filename):
            self.filename = filename

        def __fspath__(self):
            return self.filename

    file_str = fm.findfont('DejaVu Sans')
    file_bytes = os.fsencode(file_str)

    font = ft2font.FT2Font(file_str)
    assert font.fname == file_str
    font = ft2font.FT2Font(file_bytes)
    assert font.fname == file_bytes
    font = ft2font.FT2Font(PathLikeClass(file_str))
    assert font.fname == file_str
    font = ft2font.FT2Font(PathLikeClass(file_bytes))
    assert font.fname == file_bytes


def test_ft2font_invalid_args(tmp_path):
    # filename argument.
    with pytest.raises(TypeError, match='to a font file or a binary-mode file object'):
        ft2font.FT2Font(None)
    with pytest.raises(TypeError, match='to a font file or a binary-mode file object'):
        ft2font.FT2Font(object())  # Not bytes or string, and has no read() method.
    file = tmp_path / 'invalid-font.ttf'
    file.write_text('This is not a valid font file.')
    with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'),
          file.open('rt') as fd):
        ft2font.FT2Font(fd)
    with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'),
          file.open('wt') as fd):
        ft2font.FT2Font(fd)
    with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'),
          file.open('wb') as fd):
        ft2font.FT2Font(fd)

    file = fm.findfont('DejaVu Sans')

    # hinting_factor argument.
    with pytest.raises(TypeError, match='incompatible constructor arguments'):
        ft2font.FT2Font(file, 1.3)
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='text.hinting_factor rcParam was deprecated .+ 3.11'):
        mpl.rcParams['text.hinting_factor'] = 8
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='The hinting_factor parameter was deprecated'):
        ft2font.FT2Font(file, 0)

    with pytest.raises(TypeError, match='incompatible constructor arguments'):
        # failing to be a list will fail before the 0
        ft2font.FT2Font(file, _fallback_list=(0,))
    with pytest.raises(TypeError, match='incompatible constructor arguments'):
        ft2font.FT2Font(file, _fallback_list=[0])

    # kerning_factor argument.
    with pytest.raises(TypeError, match='incompatible constructor arguments'):
        ft2font.FT2Font(file, _kerning_factor=1.3)
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='text.kerning_factor rcParam was deprecated .+ 3.11'):
        mpl.rcParams['text.kerning_factor'] = 0
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='_kerning_factor parameter was deprecated .+ 3.11'):
        ft2font.FT2Font(file, _kerning_factor=123)


@pytest.mark.parametrize('name, size, skippable',
                         [('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)])
def test_ft2font_face_index(name, size, skippable):
    try:
        file = fm.findfont(name, fallback_to_default=False)
    except ValueError:
        if skippable:
            pytest.skip(r'Font {name} may be missing')
        raise
    for index in range(size):
        font = ft2font.FT2Font(file, face_index=index)
        assert font.num_faces >= size
        assert font.face_index == index
    with pytest.raises(ValueError, match='must be between'):  # out of bounds for spec
        ft2font.FT2Font(file, face_index=0x1ffff)
    with pytest.raises(RuntimeError, match='invalid argument'):  # invalid for this font
        ft2font.FT2Font(file, face_index=0xff)


def test_ft2font_clear():
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    assert font.get_num_glyphs() == 0
    assert font.get_width_height() == (0, 0)
    assert font.get_bitmap_offset() == (0, 0)
    font.set_text('ABabCDcd')
    assert font.get_num_glyphs() == 8
    assert font.get_width_height() != (0, 0)
    assert font.get_bitmap_offset() != (0, 0)
    font.clear()
    assert font.get_num_glyphs() == 0
    assert font.get_width_height() == (0, 0)
    assert font.get_bitmap_offset() == (0, 0)


def test_ft2font_set_size():
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_size(12, 72)
    font.set_text('ABabCDcd')
    orig = font.get_width_height()
    font.set_size(24, 72)
    font.set_text('ABabCDcd')
    assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)
    font.set_size(12, 144)
    font.set_text('ABabCDcd')
    assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)


def test_ft2font_features():
    # Smoke test that these are accepted as intended.
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_text('foo', features=None)  # unset
    font.set_text('foo', features=['calt', 'dlig'])  # list
    font.set_text('foo', features=('calt', 'dlig'))  # tuple
    with pytest.raises(TypeError):
        font.set_text('foo', features=123)
    with pytest.raises(TypeError):
        font.set_text('foo', features=[123, 456])


def test_ft2font_charmaps():
    def enc(name):
        # We don't expose the encoding enum from FreeType, but can generate it here.
        # For DejaVu, there are 5 charmaps, but only 2 have enum entries in FreeType.
        e = 0
        for x in name:
            e <<= 8
            e += ord(x)
        return e

    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    assert font.num_charmaps == 5

    # Unicode.
    font.select_charmap(enc('unic'))
    unic = font.get_charmap()
    font.set_charmap(0)  # Unicode platform, Unicode BMP only.
    after = font.get_charmap()
    assert len(after) <= len(unic)
    for chr, glyph in after.items():
        assert unic[chr] == glyph == font.get_char_index(chr)
    font.set_charmap(1)  # Unicode platform, modern subtable.
    after = font.get_charmap()
    assert unic == after
    font.set_charmap(3)  # Windows platform, Unicode BMP only.
    after = font.get_charmap()
    assert len(after) <= len(unic)
    for chr, glyph in after.items():
        assert unic[chr] == glyph == font.get_char_index(chr)
    font.set_charmap(4)  # Windows platform, Unicode full repertoire, modern subtable.
    after = font.get_charmap()
    assert unic == after

    # This is just a random sample from FontForge.
    glyph_names = cast(dict[str, ft2font.GlyphIndexType], {
        'non-existent-glyph-name': 0,
        'plusminus': 115,
        'Racute': 278,
        'perthousand': 2834,
        'seveneighths': 3057,
        'triagup': 3721,
        'uni01D3': 405,
        'uni0417': 939,
        'uni2A02': 4464,
        'u1D305': 5410,
        'u1F0A1': 5784,
    })
    for name, index in glyph_names.items():
        assert font.get_name_index(name) == index
        if name == 'non-existent-glyph-name':
            name = '.notdef'
        # This doesn't always apply, but it does for DejaVu Sans.
        assert font.get_glyph_name(index) == name

    # Apple Roman.
    font.select_charmap(enc('armn'))
    armn = font.get_charmap()
    font.set_charmap(2)  # Macintosh platform, Roman.
    after = font.get_charmap()
    assert armn == after
    assert len(armn) <= 256  # 8-bit encoding.
    # The first 128 characters of Apple Roman match ASCII, which also matches Unicode.
    for o in range(1, 128):
        if o not in armn or o not in unic:
            continue
        assert unic[o] == armn[o]
    # Check a couple things outside the ASCII set that are different in each charset.
    examples = [
        # (Unicode, Macintosh)
        (0x2020, 0xA0),  # Dagger.
        (0x00B0, 0xA1),  # Degree symbol.
        (0x00A3, 0xA3),  # Pound sign.
        (0x00A7, 0xA4),  # Section sign.
        (0x00B6, 0xA6),  # Pilcrow.
        (0x221E, 0xB0),  # Infinity symbol.
    ]
    for u, m in examples:
        # Though the encoding is different, the glyph should be the same.
        assert unic[u] == armn[m]

    # Out-of-range charmap indices must be rejected rather than indexing out of
    # bounds. A negative index previously passed the upper-bound-only check and
    # read face->charmaps[i] out of bounds.
    with pytest.raises(RuntimeError, match='exceeds the available number'):
        font.set_charmap(-1)
    with pytest.raises(RuntimeError, match='exceeds the available number'):
        font.set_charmap(font.num_charmaps)


_expected_sfnt_names = {
    'DejaVu Sans': {
        0: 'Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.\n'
           'Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n'
           'DejaVu changes are in public domain\n',
        1: 'DejaVu Sans',
        2: 'Book',
        3: 'DejaVu Sans',
        4: 'DejaVu Sans',
        5: 'Version 2.35',
        6: 'DejaVuSans',
        8: 'DejaVu fonts team',
        11: 'http://dejavu.sourceforge.net',
        13: 'Fonts are (c) Bitstream (see below). '
            'DejaVu changes are in public domain. '
            '''Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below)

Bitstream Vera Fonts Copyright
------------------------------

Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:

The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.

The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".

This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.

The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.

THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.

Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org. ''' '''

Arev Fonts Copyright
------------------------------

Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and
associated documentation files (the "Font Software"), to reproduce
and distribute the modifications to the Bitstream Vera Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to
the following conditions:

The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.

The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Tavmjong Bah" or the word "Arev".

This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the ''' '''
"Tavmjong Bah Arev" names.

The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.

THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Except as contained in this notice, the name of Tavmjong Bah shall not
be used in advertising or otherwise to promote the sale, use or other
dealings in this Font Software without prior written authorization
from Tavmjong Bah. For further information, contact: tavmjong @ free
. fr.''',
        14: 'http://dejavu.sourceforge.net/wiki/index.php/License',
        16: 'DejaVu Sans',
        17: 'Book',
    },
    'cmtt10': {
        0: 'Copyright (C) 1994, Basil K. Malyshev. All Rights Reserved.'
           '012BaKoMa Fonts Collection, Level-B.',
        1: 'cmtt10',
        2: 'Regular',
        3: 'FontMonger:cmtt10',
        4: 'cmtt10',
        5: '1.1/12-Nov-94',
        6: 'Cmtt10',
    },
    'STIXSizeTwoSym:bold': {
        0: 'Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the '
           'American Chemical Society, the American Institute of Physics, the American '
           'Mathematical Society, the American Physical Society, Elsevier, Inc., and '
           'The Institute of Electrical and Electronic Engineers, Inc. Portions '
           'copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright (c) 1990 by '
           'Elsevier, Inc. All rights reserved.',
        1: 'STIXSizeTwoSym',
        2: 'Bold',
        3: 'FontMaster:STIXSizeTwoSym-Bold:1.0.0',
        4: 'STIXSizeTwoSym-Bold',
        5: 'Version 1.0.0',
        6: 'STIXSizeTwoSym-Bold',
        7: 'STIX Fonts(TM) is a trademark of The Institute of Electrical and '
           'Electronics Engineers, Inc.',
        9: 'MicroPress Inc., with final additions and corrections provided by Coen '
           'Hoffman, Elsevier (retired)',
        10: 'Arie de Ruiter, who in 1995 was Head of Information Technology '
            'Development at Elsevier Science, made a proposal to the STI Pub group, an '
            'informal group of publishers consisting of representatives from the '
            'American Chemical Society (ACS), American Institute of Physics (AIP), '
            'American Mathematical Society (AMS), American Physical Society (APS), '
            'Elsevier, and Institute of Electrical and Electronics Engineers (IEEE). '
            'De Ruiter encouraged the members to consider development of a series of '
            'Web fonts, which he proposed should be called the Scientific and '
            'Technical Information eXchange, or STIX, Fonts. All STI Pub member '
            'organizations enthusiastically endorsed this proposal, and the STI Pub '
            'group agreed to embark on what has become a twelve-year project. The goal '
            'of the project was to identify all alphabetic, symbolic, and other '
            'special characters used in any facet of scientific publishing and to '
            'create a set of Unicode-based fonts that would be distributed free to '
            'every scientist, student, and other interested party worldwide. The fonts '
            'would be consistent with the emerging Unicode standard, and would permit '
            'universal representation of every character. With the release of the STIX '
            "fonts, de Ruiter's vision has been realized.",
        11: 'http://www.stixfonts.org',
        12: 'http://www.micropress-inc.com',
        13: 'As a condition for receiving these fonts at no charge, each person '
            'downloading the fonts must agree to some simple license terms. The '
            'license is based on the SIL Open Font License '
            '<http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL>. The '
            'SIL License is a free and open source license specifically designed for '
            'fonts and related software. The basic terms are that the recipient will '
            'not remove the copyright and trademark statements from the fonts and '
            'that, if the person decides to create a derivative work based on the STIX '
            'Fonts but incorporating some changes or enhancements, the derivative work '
            '("Modified Version") will carry a different name. The copyright and '
            'trademark restrictions are part of the agreement between the STI Pub '
            'companies and the typeface designer. The "renaming" restriction results '
            'from the desire of the STI Pub companies to assure that the STIX Fonts '
            'will continue to function in a predictable fashion for all that use them. '
            'No copy of one or more of the individual Font typefaces that form the '
            'STIX Fonts(TM) set may be sold by itself, but other than this one '
            'restriction, licensees are free to sell the fonts either separately or as '
            'part of a package that combines other software or fonts with this font '
            'set.',
        14: 'http://www.stixfonts.org/user_license.html',
    },
}


@pytest.mark.parametrize('font_name, expected', _expected_sfnt_names.items(),
                         ids=_expected_sfnt_names.keys())
def test_ft2font_get_sfnt(font_name, expected):
    file = fm.findfont(font_name)
    font = ft2font.FT2Font(file)
    sfnt = font.get_sfnt()
    for name, value in expected.items():
        # Macintosh, Unicode 1.0, English, name.
        assert sfnt.pop((1, 0, 0, name)) == value.encode('ascii')
        # Microsoft, Unicode, English United States, name.
        assert sfnt.pop((3, 1, 1033, name)) == value.encode('utf-16be')
    assert sfnt == {}


_expected_sfnt_tables = {
    'DejaVu Sans': {
        'invalid': None,
        'head': {
            'version': (1, 0),
            'fontRevision': (2, 22937),
            'checkSumAdjustment': -175678572,
            'magicNumber': 0x5F0F3CF5,
            'flags': 31,
            'unitsPerEm': 2048,
            'created': (0, 3514699492), 'modified': (0, 3514699492),
            'xMin': -2090, 'yMin': -948, 'xMax': 3673, 'yMax': 2524,
            'macStyle': 0,
            'lowestRecPPEM': 8,
            'fontDirectionHint': 0,
            'indexToLocFormat': 1,
            'glyphDataFormat': 0,
        },
        'maxp': {
            'version': (1, 0),
            'numGlyphs': 6241,
            'maxPoints': 852, 'maxComponentPoints': 104, 'maxTwilightPoints': 16,
            'maxContours': 43, 'maxComponentContours': 12,
            'maxZones': 2,
            'maxStorage': 153,
            'maxFunctionDefs': 64,
            'maxInstructionDefs': 0,
            'maxStackElements': 1045,
            'maxSizeOfInstructions': 534,
            'maxComponentElements': 8,
            'maxComponentDepth': 4,
        },
        'OS/2': {
            'version': 1,
            'xAvgCharWidth': 1038,
            'usWeightClass': 400, 'usWidthClass': 5,
            'fsType': 0,
            'ySubscriptXSize': 1331, 'ySubscriptYSize': 1433,
            'ySubscriptXOffset': 0, 'ySubscriptYOffset': 286,
            'ySuperscriptXSize': 1331, 'ySuperscriptYSize': 1433,
            'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 983,
            'yStrikeoutSize': 102, 'yStrikeoutPosition': 530,
            'sFamilyClass': 0,
            'panose': b'\x02\x0b\x06\x03\x03\x08\x04\x02\x02\x04',
            'ulUnicodeRange': (3875565311, 3523280383, 170156073, 67117068),
            'achVendID': b'PfEd',
            'fsSelection': 64, 'usFirstCharIndex': 32, 'usLastCharIndex': 65535,
            'sTypoAscender': 1556, 'sTypoDescender': -492, 'sTypoLineGap': 410,
            'usWinAscent': 1901, 'usWinDescent': 483,
            'ulCodePageRange': (1610613247, 3758030848),
        },
        'hhea': {
            'version': (1, 0),
            'ascent': 1901, 'descent': -483, 'lineGap': 0,
            'advanceWidthMax': 3838,
            'minLeftBearing': -2090, 'minRightBearing': -1455,
            'xMaxExtent': 3673,
            'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0,
            'metricDataFormat': 0, 'numOfLongHorMetrics': 6226,
        },
        'vhea': None,
        'post': {
            'format': (2, 0),
            'isFixedPitch': 0, 'italicAngle': (0, 0),
            'underlinePosition': -130, 'underlineThickness': 90,
            'minMemType42': 0, 'maxMemType42': 0,
            'minMemType1': 0, 'maxMemType1': 0,
        },
        'pclt': None,
    },
    'cmtt10': {
        'invalid': None,
        'head': {
            'version': (1, 0),
            'fontRevision': (1, 0),
            'checkSumAdjustment': 555110277,
            'magicNumber': 0x5F0F3CF5,
            'flags': 3,
            'unitsPerEm': 2048,
            'created': (0, 0), 'modified': (0, 0),
            'xMin': -12, 'yMin': -477, 'xMax': 1280, 'yMax': 1430,
            'macStyle': 0,
            'lowestRecPPEM': 6,
            'fontDirectionHint': 2,
            'indexToLocFormat': 1,
            'glyphDataFormat': 0,
        },
        'maxp': {
            'version': (1, 0),
            'numGlyphs': 133,
            'maxPoints': 94, 'maxComponentPoints': 0, 'maxTwilightPoints': 12,
            'maxContours': 5, 'maxComponentContours': 0,
            'maxZones': 2,
            'maxStorage': 6,
            'maxFunctionDefs': 64,
            'maxInstructionDefs': 0,
            'maxStackElements': 200,
            'maxSizeOfInstructions': 100,
            'maxComponentElements': 4,
            'maxComponentDepth': 1,
        },
        'OS/2': {
            'version': 0,
            'xAvgCharWidth': 1075,
            'usWeightClass': 400, 'usWidthClass': 5,
            'fsType': 0,
            'ySubscriptXSize': 410, 'ySubscriptYSize': 369,
            'ySubscriptXOffset': 0, 'ySubscriptYOffset': -469,
            'ySuperscriptXSize': 410, 'ySuperscriptYSize': 369,
            'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 1090,
            'yStrikeoutSize': 102, 'yStrikeoutPosition': 530,
            'sFamilyClass': 0,
            'panose': b'\x02\x0b\x05\x00\x00\x00\x00\x00\x00\x00',
            'ulUnicodeRange': (0, 0, 0, 0),
            'achVendID': b'\x00\x00\x00\x00',
            'fsSelection': 64, 'usFirstCharIndex': 32, 'usLastCharIndex': 9835,
            'sTypoAscender': 1276, 'sTypoDescender': -469, 'sTypoLineGap': 0,
            'usWinAscent': 1430, 'usWinDescent': 477,
        },
        'hhea': {
            'version': (1, 0),
            'ascent': 1276, 'descent': -489, 'lineGap': 0,
            'advanceWidthMax': 1536,
            'minLeftBearing': -12, 'minRightBearing': -29,
            'xMaxExtent': 1280,
            'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0,
            'metricDataFormat': 0, 'numOfLongHorMetrics': 133,
        },
        'vhea': None,
        'post': {
            'format': (2, 0),
            'isFixedPitch': 0, 'italicAngle': (0, 0),
            'underlinePosition': -133, 'underlineThickness': 20,
            'minMemType42': 0, 'maxMemType42': 0,
            'minMemType1': 0, 'maxMemType1': 0,
        },
        'pclt': {
            'version': (1, 0),
            'fontNumber': 2147483648,
            'pitch': 1075,
            'xHeight': 905,
            'style': 0,
            'typeFamily': 0,
            'capHeight': 1276,
            'symbolSet': 0,
            'typeFace': b'cmtt10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
            'characterComplement': b'\xff\xff\xff\xff7\xff\xff\xfe',
            'strokeWeight': 0,
            'widthType': -5,
            'serifStyle': 64,
        },
    },
    'STIXSizeTwoSym:bold': {
        'invalid': None,
        'head': {
            'version': (1, 0),
            'fontRevision': (1, 0),
            'checkSumAdjustment': 1803408080,
            'magicNumber': 0x5F0F3CF5,
            'flags': 11,
            'unitsPerEm': 1000,
            'created': (0, 3359035786), 'modified': (0, 3359035786),
            'xMin': 4, 'yMin': -355, 'xMax': 1185, 'yMax': 2095,
            'macStyle': 1,
            'lowestRecPPEM': 8,
            'fontDirectionHint': 2,
            'indexToLocFormat': 0,
            'glyphDataFormat': 0,
        },
        'maxp': {
            'version': (1, 0),
            'numGlyphs': 20,
            'maxPoints': 37, 'maxComponentPoints': 0, 'maxTwilightPoints': 0,
            'maxContours': 1, 'maxComponentContours': 0,
            'maxZones': 2,
            'maxStorage': 1,
            'maxFunctionDefs': 64,
            'maxInstructionDefs': 0,
            'maxStackElements': 64,
            'maxSizeOfInstructions': 0,
            'maxComponentElements': 0,
            'maxComponentDepth': 0,
        },
        'OS/2': {
            'version': 2,
            'xAvgCharWidth': 598,
            'usWeightClass': 700, 'usWidthClass': 5,
            'fsType': 0,
            'ySubscriptXSize': 500, 'ySubscriptYSize': 500,
            'ySubscriptXOffset': 0, 'ySubscriptYOffset': 250,
            'ySuperscriptXSize': 500, 'ySuperscriptYSize': 500,
            'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 500,
            'yStrikeoutSize': 20, 'yStrikeoutPosition': 1037,
            'sFamilyClass': 0,
            'panose': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
            'ulUnicodeRange': (3, 192, 0, 0),
            'achVendID': b'STIX',
            'fsSelection': 32, 'usFirstCharIndex': 32, 'usLastCharIndex': 10217,
            'sTypoAscender': 750, 'sTypoDescender': -250, 'sTypoLineGap': 1499,
            'usWinAscent': 2095, 'usWinDescent': 404,
            'ulCodePageRange': (2688417793, 2432565248),
            'sxHeight': 0,
            'sCapHeight': 0,
            'usDefaultChar': 0,
            'usBreakChar': 32,
            'usMaxContext': 1,
        },
        'hhea': {
            'version': (1, 0),
            'ascent': 2095, 'descent': -404, 'lineGap': 0,
            'advanceWidthMax': 1130,
            'minLeftBearing': 0, 'minRightBearing': -55,
            'xMaxExtent': 1185,
            'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0,
            'metricDataFormat': 0, 'numOfLongHorMetrics': 19,
        },
        'vhea': None,
        'post': {
            'format': (2, 0),
            'isFixedPitch': 0, 'italicAngle': (0, 0),
            'underlinePosition': -123, 'underlineThickness': 20,
            'minMemType42': 0, 'maxMemType42': 0,
            'minMemType1': 0, 'maxMemType1': 0,
        },
        'pclt': None,
    },
}


@pytest.mark.parametrize('font_name', _expected_sfnt_tables.keys())
@pytest.mark.parametrize('header', _expected_sfnt_tables['DejaVu Sans'].keys())
def test_ft2font_get_sfnt_table(font_name, header):
    file = fm.findfont(font_name)
    font = ft2font.FT2Font(file)
    assert font.get_sfnt_table(header) == _expected_sfnt_tables[font_name][header]


@pytest.mark.parametrize('left, right, unscaled, unfitted, default', [
    # These are all the same class.
    ('A', 'A', 57, 247, 256), ('A', 'À', 57, 247, 256), ('A', 'Á', 57, 247, 256),
    ('A', 'Â', 57, 247, 256), ('A', 'Ã', 57, 247, 256), ('A', 'Ä', 57, 247, 256),
    # And a few other random ones.
    ('D', 'A', -36, -156, -128), ('T', '.', -243, -1055, -1024),
    ('X', 'C', -149, -647, -640), ('-', 'J', 114, 495, 512),
])
def test_ft2font_get_kerning(left, right, unscaled, unfitted, default):
    file = fm.findfont('DejaVu Sans')
    # With unscaled, these settings should produce exact values found in FontForge.
    font = ft2font.FT2Font(file)
    font.set_size(100, 100)
    assert font.get_kerning(font.get_char_index(ord(left)),
                            font.get_char_index(ord(right)),
                            ft2font.Kerning.UNSCALED) == unscaled
    assert font.get_kerning(font.get_char_index(ord(left)),
                            font.get_char_index(ord(right)),
                            ft2font.Kerning.UNFITTED) == unfitted
    assert font.get_kerning(font.get_char_index(ord(left)),
                            font.get_char_index(ord(right)),
                            ft2font.Kerning.DEFAULT) == default
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='Use Kerning.UNSCALED instead'):
        k = ft2font.KERNING_UNSCALED
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='Use Kerning enum values instead'):
        assert font.get_kerning(font.get_char_index(ord(left)),
                                font.get_char_index(ord(right)),
                                int(k)) == unscaled
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='Use Kerning.UNFITTED instead'):
        k = ft2font.KERNING_UNFITTED
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='Use Kerning enum values instead'):
        assert font.get_kerning(font.get_char_index(ord(left)),
                                font.get_char_index(ord(right)),
                                int(k)) == unfitted
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='Use Kerning.DEFAULT instead'):
        k = ft2font.KERNING_DEFAULT
    with pytest.warns(mpl.MatplotlibDeprecationWarning,
                      match='Use Kerning enum values instead'):
        assert font.get_kerning(font.get_char_index(ord(left)),
                                font.get_char_index(ord(right)),
                                int(k)) == default


def test_ft2font_set_text():
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_size(12, 72)
    xys = font.set_text('')
    np.testing.assert_array_equal(xys, np.empty((0, 2)))
    assert font.get_width_height() == (0, 0)
    assert font.get_num_glyphs() == 0
    assert font.get_descent() == 0
    assert font.get_bitmap_offset() == (0, 0)
    # This string uses all the kerning pairs defined for test_ft2font_get_kerning.
    xys = font.set_text('AADAT.XC-J')
    np.testing.assert_array_equal(
        xys,
        [(0, 0), (533, 0), (1045, 0), (1608, 0), (2060, 0), (2417, 0), (2609, 0),
         (3065, 0), (3577, 0), (3940, 0)])
    assert font.get_width_height() == (4196, 768)
    assert font.get_num_glyphs() == 10
    assert font.get_descent() == 192
    assert font.get_bitmap_offset() == (6, 0)


@pytest.mark.parametrize(
    'input',
    [
        [1, 2, 3],
        [(1, 2)],
        [('en', 'foo', 2)],
        [('en', 1, 'foo')],
    ],
    ids=[
        'nontuple',
        'wrong length',
        'wrong start type',
        'wrong end type',
    ],
)
def test_ft2font_language_invalid(input):
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    with pytest.raises(TypeError):
        font.set_text('foo', language=input)


def test_ft2font_language():
    # This is just a smoke test.
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_text('foo')
    font.set_text('foo', language='en')
    font.set_text('foo', language=[('en', 1, 2)])


def test_ft2font_loading():
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_size(12, 72)
    for glyph in [font.load_char(ord('M')),
                  font.load_glyph(font.get_char_index(ord('M')))]:
        assert glyph is not None
        assert glyph.width == 576
        assert glyph.height == 576
        assert glyph.horiBearingX == 0
        assert glyph.horiBearingY == 576
        assert glyph.horiAdvance == 640
        assert glyph.linearHoriAdvance == 678528
        assert glyph.vertBearingX == -384
        assert glyph.vertBearingY == 64
        assert glyph.vertAdvance == 832
        assert glyph.bbox == (54, 0, 574, 576)
    assert font.get_num_glyphs() == 2  # Both count as loaded.
    # But neither has been placed anywhere.
    assert font.get_width_height() == (0, 0)
    assert font.get_descent() == 0
    assert font.get_bitmap_offset() == (0, 0)


def test_ft2font_drawing():
    expected_str = (
        '          ',
        '11    11  ',
        '11    11  ',
        '1 1  1 1  ',
        '1 1  1 1  ',
        '1 1  1 1  ',
        '1  11  1  ',
        '1  11  1  ',
        '1      1  ',
        '1      1  ',
        '          ',
    )
    expected = np.array([
        [int(c) for c in line.replace(' ', '0')] for line in expected_str
    ])
    expected *= 255
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_size(12, 72)
    font.set_text('M')
    font.draw_glyphs_to_bitmap(antialiased=False)
    image = font.get_image()
    np.testing.assert_array_equal(image, expected)
    font = ft2font.FT2Font(file)
    font.set_size(12, 72)
    glyph = font.load_char(ord('M'))
    image = np.zeros(expected.shape, np.uint8)
    font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False)
    np.testing.assert_array_equal(image, expected)


def test_ft2font_get_path():
    file = fm.findfont('DejaVu Sans')
    font = ft2font.FT2Font(file)
    font.set_size(12, 72)
    vertices, codes = font.get_path()
    assert vertices.shape == (0, 2)
    assert codes.shape == (0, )
    font.load_char(ord('M'))
    vertices, codes = font.get_path()
    expected_vertices = np.array([
        (0.843750, 9.000000), (2.609375, 9.000000),  # Top left.
        (4.906250, 2.875000),  # Top of midpoint.
        (7.218750, 9.000000), (8.968750, 9.000000),  # Top right.
        (8.968750, 0.000000), (7.843750, 0.000000),  # Bottom right.
        (7.843750, 7.906250),  # Point under top right.
        (5.531250, 1.734375), (4.296875, 1.734375),  # Bar under midpoint.
        (1.984375, 7.906250),  # Point under top left.
        (1.984375, 0.000000), (0.843750, 0.000000),  # Bottom left.
        (0.843750, 9.000000),  # Back to top left corner.
        (0.000000, 0.000000),
    ])
    np.testing.assert_array_equal(vertices, expected_vertices)
    expected_codes = np.full(expected_vertices.shape[0], mpath.Path.LINETO,
                             dtype=mpath.Path.code_type)
    expected_codes[0] = mpath.Path.MOVETO
    expected_codes[-1] = mpath.Path.CLOSEPOLY
    np.testing.assert_array_equal(codes, expected_codes)


@pytest.mark.parametrize('fmt', ['pdf', 'png', 'ps', 'raw', 'svg'])
def test_fallback_smoke(fmt):
    fonts, test_str = _gen_multi_font_text()
    plt.rcParams['font.size'] = 16
    fig = plt.figure(figsize=(4.75, 1.85))
    fig.text(0.5, 0.5, test_str,
             horizontalalignment='center', verticalalignment='center')

    fig.savefig(io.BytesIO(), format=fmt)


@pytest.mark.parametrize("font_list",
                          [['DejaVu Serif', 'DejaVu Sans'],
                           ['DejaVu Sans Mono']],
                         ids=["two fonts", "one font"])
def test_fallback_missing(recwarn, font_list):
    fig = plt.figure()
    fig.text(.5, .5, "Hello 🙃 World!", family=font_list)
    fig.canvas.draw()
    assert all(isinstance(warn.message, UserWarning) for warn in recwarn)
    # not sure order is guaranteed on the font listing so
    assert recwarn[0].message.args[0].startswith(
           "Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)")
    assert all([font in recwarn[0].message.args[0] for font in font_list])


@image_comparison(['last_resort'], style='mpl20')
def test_fallback_last_resort(recwarn):
    fig = plt.figure(figsize=(3, 0.5))
    fig.text(.5, .5, "Hello 🙃 World!", size=24,
             horizontalalignment='center', verticalalignment='center')
    fig.canvas.draw()
    assert all(isinstance(warn.message, UserWarning) for warn in recwarn)
    assert recwarn[0].message.args[0].startswith(
           "Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)")


def test__layout():
    fonts, test_str = _gen_multi_font_text()
    # Add some glyphs that don't exist in either font to check the Last Resort fallback.
    missing_glyphs = '\n几个汉字'
    test_str += missing_glyphs

    ft = fm.get_font(
        fm.fontManager._find_fonts_by_props(fm.FontProperties(family=fonts))
    )
    for substr in test_str.split('\n'):
        for item in ft._layout(substr, ft2font.LoadFlags.DEFAULT):
            if item.char in missing_glyphs:
                assert Path(item.ft_object.fname).name == 'LastResortHE-Regular.ttf'
            elif ord(item.char) > 127:
                assert Path(item.ft_object.fname).name == 'DejaVuSans.ttf'
            else:
                assert Path(item.ft_object.fname).name == 'cmr10.ttf'
