import matplotlib._type1font as t1f
import os.path
import difflib
import pytest


def test_Type1Font():
    filename = os.path.join(os.path.dirname(__file__), 'data', 'cmr10.pfb')
    font = t1f.Type1Font(filename)
    slanted = font.transform({'slant': 1})
    condensed = font.transform({'extend': 0.5})
    with open(filename, 'rb') as fd:
        rawdata = fd.read()
    assert font.parts[0] == rawdata[0x0006:0x10c5]
    assert font.parts[1] == rawdata[0x10cb:0x897f]
    assert font.parts[2] == rawdata[0x8985:0x8ba6]
    assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
    assert font.decrypted.endswith(b'mark currentfile closefile\n')
    assert slanted.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
    assert slanted.decrypted.endswith(b'mark currentfile closefile\n')
    assert b'UniqueID 5000793' in font.parts[0]
    assert b'UniqueID 5000793' in font.decrypted
    assert font._pos['UniqueID'] == [(797, 818), (4483, 4504)]

    len0 = len(font.parts[0])
    for key in font._pos.keys():
        for pos0, pos1 in font._pos[key]:
            if pos0 < len0:
                data = font.parts[0][pos0:pos1]
            else:
                data = font.decrypted[pos0-len0:pos1-len0]
            assert data.startswith(f'/{key}'.encode('ascii'))
    assert {'FontType', 'FontMatrix', 'PaintType', 'ItalicAngle', 'RD'
            } < set(font._pos.keys())

    assert b'UniqueID 5000793' not in slanted.parts[0]
    assert b'UniqueID 5000793' not in slanted.decrypted
    assert 'UniqueID' not in slanted._pos
    assert font.prop['Weight'] == 'Medium'
    assert not font.prop['isFixedPitch']
    assert font.prop['ItalicAngle'] == 0
    assert slanted.prop['ItalicAngle'] == -45
    assert font.prop['Encoding'][5] == 'Pi'
    assert isinstance(font.prop['CharStrings']['Pi'], bytes)
    assert font._abbr['ND'] == 'ND'

    differ = difflib.Differ()
    diff = list(differ.compare(
        font.parts[0].decode('latin-1').splitlines(),
        slanted.parts[0].decode('latin-1').splitlines()))
    for line in (
         # Removes UniqueID
         '- /UniqueID 5000793 def',
         # Changes the font name
         '- /FontName /CMR10 def',
         '+ /FontName/CMR10_Slant_1000 def',
         # Alters FontMatrix
         '- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def',
         '+ /FontMatrix [0.001 0 0.001 0.001 0 0] readonly def',
         # Alters ItalicAngle
         '-  /ItalicAngle 0 def',
         '+  /ItalicAngle -45.0 def'):
        assert line in diff, 'diff to slanted font must contain %s' % line

    diff = list(differ.compare(
        font.parts[0].decode('latin-1').splitlines(),
        condensed.parts[0].decode('latin-1').splitlines()))
    for line in (
         # Removes UniqueID
         '- /UniqueID 5000793 def',
         # Changes the font name
         '- /FontName /CMR10 def',
         '+ /FontName/CMR10_Extend_500 def',
         # Alters FontMatrix
         '- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def',
         '+ /FontMatrix [0.0005 0 0 0.001 0 0] readonly def'):
        assert line in diff, 'diff to condensed font must contain %s' % line


def test_Type1Font_2():
    filename = os.path.join(os.path.dirname(__file__), 'data',
                            'Courier10PitchBT-Bold.pfb')
    font = t1f.Type1Font(filename)
    assert font.prop['Weight'] == 'Bold'
    assert font.prop['isFixedPitch']
    assert font.prop['Encoding'][65] == 'A'  # the font uses StandardEncoding
    (pos0, pos1), = font._pos['Encoding']
    assert font.parts[0][pos0:pos1] == b'/Encoding StandardEncoding'
    assert font._abbr['ND'] == '|-'


def test_tokenize():
    data = (b'1234/abc false -9.81  Foo <<[0 1 2]<0 1ef a\t>>>\n'
            b'(string with(nested\t\\) par)ens\\\\)')
    #         1           2          x    2     xx1
    # 1 and 2 are matching parens, x means escaped character
    n, w, num, kw, d = 'name', 'whitespace', 'number', 'keyword', 'delimiter'
    b, s = 'boolean', 'string'
    correct = [
        (num, 1234), (n, 'abc'), (w, ' '), (b, False), (w, ' '), (num, -9.81),
        (w, '  '), (kw, 'Foo'), (w, ' '), (d, '<<'), (d, '['), (num, 0),
        (w, ' '), (num, 1), (w, ' '), (num, 2), (d, ']'), (s, b'\x01\xef\xa0'),
        (d, '>>'), (w, '\n'), (s, 'string with(nested\t) par)ens\\')
    ]
    correct_no_ws = [x for x in correct if x[0] != w]

    def convert(tokens):
        return [(t.kind, t.value()) for t in tokens]

    assert convert(t1f._tokenize(data, False)) == correct
    assert convert(t1f._tokenize(data, True)) == correct_no_ws

    def bin_after(n):
        tokens = t1f._tokenize(data, True)
        result = []
        for _ in range(n):
            result.append(next(tokens))
        result.append(tokens.send(10))
        return convert(result)

    for n in range(1, len(correct_no_ws)):
        result = bin_after(n)
        assert result[:-1] == correct_no_ws[:n]
        assert result[-1][0] == 'binary'
        assert isinstance(result[-1][1], bytes)


def test_tokenize_errors():
    with pytest.raises(ValueError):
        list(t1f._tokenize(b'1234 (this (string) is unterminated\\)', True))
    with pytest.raises(ValueError):
        list(t1f._tokenize(b'/Foo<01234', True))
    with pytest.raises(ValueError):
        list(t1f._tokenize(b'/Foo<01234abcg>/Bar', True))


def test_overprecision():
    # We used to output too many digits in FontMatrix entries and
    # ItalicAngle, which could make Type-1 parsers unhappy.
    filename = os.path.join(os.path.dirname(__file__), 'data', 'cmr10.pfb')
    font = t1f.Type1Font(filename)
    slanted = font.transform({'slant': .167})
    lines = slanted.parts[0].decode('ascii').splitlines()
    matrix, = (line[line.index('[')+1:line.index(']')]
               for line in lines if '/FontMatrix' in line)
    angle, = (word
              for line in lines if '/ItalicAngle' in line
              for word in line.split() if word[0] in '-0123456789')
    # the following used to include 0.00016700000000000002
    assert matrix == '0.001 0 0.000167 0.001 0 0'
    # and here we had -9.48090361795083
    assert angle == '-9.4809'


def test_encrypt_decrypt_roundtrip():
    data = b'this is my plaintext \0\1\2\3'
    encrypted = t1f.Type1Font._encrypt(data, 'eexec')
    decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec')
    assert encrypted != decrypted
    assert data == decrypted


def _write_pfa(path, private):
    """Write a minimal font whose eexec-encrypted part is *private*."""
    plaintext = b'/FontName /X def\n/FontBBox [0 0 1 1] def\n' + private
    enc = t1f.Type1Font._encrypt(plaintext, 'eexec').hex().encode()
    path.write_bytes(b'%!PS-AdobeFont-1.0: X 001.000\neexec\n' + enc + b'\n'
                     + b'0' * 512 + b'\ncleartomark\n')
    return str(path)


def test_Subrs_no_preallocation(tmp_path):
    # Regression test for #31962: a font declaring a huge /Subrs count must not
    # cause a large allocation sized from that (untrusted) count before the
    # body is parsed. Here the body is empty, so parsing must fail without
    # first allocating a count-sized array.
    import tracemalloc
    path = _write_pfa(tmp_path / 'x.pfa', b'/Subrs 5000000 array\n')

    tracemalloc.start()
    with pytest.raises(RuntimeError, match='Incomplete /Subrs'):
        t1f.Type1Font(path)
    _, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    # The pre-allocation regressed to ~40 MB ([None] * count) before failing.
    assert peak < 5 * 1024 * 1024


def _write_subrs_pfa(path, indices):
    """Write a font with a two-element /Subrs array declared at *indices*."""
    return _write_pfa(path, (
        b'/Subrs 2 array\n'
        + b''.join(b'dup %d 5 RD \x00\x01\x02\x03\x04 NP\n' % index
                   for index in indices)
        + b'ND\n'
        b'/CharStrings 1 begin\n'
        b'/.notdef 5 RD \x00\x01\x02\x03\x04 ND\n'
        b'end\n'
    ))


def test_Subrs_indices(tmp_path):
    font = t1f.Type1Font(_write_subrs_pfa(tmp_path / 'x.pfa', (0, 1)))
    assert len(font.prop['Subrs']) == 2


@pytest.mark.parametrize('indices', [(0, 5), (0, 0), (-1, 1)])
def test_Subrs_bad_indices(tmp_path, indices):
    # The declared indices must cover 0 to count-1 exactly, so neither an index
    # past the end nor a duplicate may reach the returned array.
    with pytest.raises(RuntimeError, match='indices do not cover'):
        t1f.Type1Font(_write_subrs_pfa(tmp_path / 'x.pfa', indices))


@pytest.mark.parametrize('private, section', [
    (b'/CharStrings 1 begin\n/.notdef 5 RD \x00\x01\x02\x03\x04 ND\n',
     'CharStrings'),
    (b'/CharStrings 1\n', 'CharStrings'),
    (b'/CharStrings 1 begin\n', 'CharStrings'),
    (b'/Encoding 1 array\ndup 0 /.notdef put\n', 'Encoding'),
    (b'/OtherSubrs [ {} {} \n', 'OtherSubrs'),
])
def test_incomplete_sections(tmp_path, private, section):
    # A font that ends in the middle of a section used to raise a bare
    # StopIteration out of the parser, which says nothing about the file.
    path = _write_pfa(tmp_path / 'x.pfa', private)
    with pytest.raises(RuntimeError, match=f'Incomplete /{section}'):
        t1f.Type1Font(path)
