#!/usr/bin/env python3
"""
Generates src/libmime/pdf_glyphs_defs.hxx.

Sources of truth:
  * StandardEncoding / MacRomanEncoding glyph names: fontTools.encodings
  * glyph name -> codepoint: fontTools.agl (Adobe Glyph List)
  * WinAnsiEncoding: Python's cp1252 codec plus the deviations documented in
    ISO 32000-1 Annex D.2 (see WINANSI_DEVIATIONS below)

Run:  python3 gen_pdf_glyphs.py > src/libmime/pdf_glyphs_defs.hxx
"""

import sys
from fontTools.encodings.StandardEncoding import StandardEncoding
from fontTools.encodings.MacRoman import MacRoman
from fontTools import agl

NOTDEF = ".notdef"

# ISO 32000-1 Annex D.2 notes on WinAnsiEncoding, applied on top of cp1252:
#   - codes undefined in cp1252 map to bullet
#   - 0xA0 behaves as a plain space and 0xAD as a plain hyphen; text extraction
#     wants the plain forms, not NBSP/SOFT HYPHEN
WINANSI_DEVIATIONS = {
    0x81: 0x2022,
    0x8D: 0x2022,
    0x8F: 0x2022,
    0x90: 0x2022,
    0x9D: 0x2022,
    0xA0: 0x0020,
    0xAD: 0x002D,
}


def utf8_escape(cp):
    """Codepoint -> C string literal body with hex escapes."""
    if cp is None:
        return None
    return "".join("\\x%02x" % b for b in chr(cp).encode("utf-8"))


def names_to_codepoints(names):
    out = []
    for n in names:
        if n == NOTDEF or not n:
            out.append((n, None))
            continue
        try:
            uni = agl.toUnicode(n)
        except Exception:
            uni = ""
        out.append((n, ord(uni) if len(uni) == 1 else None))
    return out


def win_ansi_table():
    """(glyph name, codepoint) per code; names come back from the AGL reverse map."""
    out = []
    for code in range(256):
        cp = WINANSI_DEVIATIONS.get(code)
        if cp is None:
            try:
                cp = ord(bytes([code]).decode("cp1252"))
            except UnicodeDecodeError:
                cp = 0x2022  # unused code -> bullet, per Annex D.2
        name = agl.UV2AGL.get(cp)
        out.append((name, cp))
    return out


def drop_controls(rows):
    """Annex D defines codes from 0x20 up; cp1252 additionally decodes 0x00-0x1f
    and 0x7f to control characters, which are never glyphs. Text structure comes
    from the content stream operators, not from control bytes inside strings."""
    out = []
    for code, (name, cp) in enumerate(rows):
        if code < 0x20 or code == 0x7F:
            out.append((NOTDEF, None))
        else:
            out.append((name, cp))
    return out


def emit_table(sym, rows, comment):
    print("/* %s */" % comment)
    print("static const glyph_utf8 %s[256] = {" % sym)
    for code, (name, cp) in enumerate(rows):
        label = name if name else NOTDEF
        if cp is None:
            print('\t/* %3d 0x%02x %-18s */ {"", 0},' % (code, code, label))
        else:
            nbytes = len(chr(cp).encode("utf-8"))
            # glyph_utf8::bytes is 4 wide and a literal brings its own NUL, so
            # anything above the BMP would not fit this initialiser form
            assert nbytes <= 3, "%s does not fit a string literal initialiser" % label
            print('\t/* %3d 0x%02x %-18s */ {"%s", %d},'
                  % (code, code, label, utf8_escape(cp), nbytes))
    print("};")
    print()


def main():
    std = drop_controls(names_to_codepoints(StandardEncoding))
    mac = drop_controls(names_to_codepoints(MacRoman))
    win = drop_controls(win_ansi_table())

    print("""/*-
 * Copyright 2026 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Generated by utils/gen_pdf_glyphs.py -- do not edit by hand.
 *
 * Simple-font encoding tables from ISO 32000-1 Annex D, as UTF-8 byte
 * sequences indexed by character code. A zero length entry is .notdef: the
 * code has no glyph in that encoding and produces no output.
 */

#ifndef RSPAMD_PDF_GLYPHS_DEFS_HXX
#define RSPAMD_PDF_GLYPHS_DEFS_HXX

#include "pdf_glyphs.hxx"

namespace rspamd::mime::pdf {
""")

    emit_table("standard_encoding_utf8", std, "StandardEncoding (Annex D.2)")
    emit_table("win_ansi_encoding_utf8", win, "WinAnsiEncoding (Annex D.2), i.e. cp1252 with Annex D deviations")
    emit_table("mac_roman_encoding_utf8", mac, "MacRomanEncoding (Annex D.2)")

    # Glyph names reachable from the three base encodings: enough to resolve the
    # /Differences arrays that simple fonts actually use. Full AGL is a follow-up.
    names = {}
    for rows in (std, win, mac):
        for name, cp in rows:
            if name and name != NOTDEF and cp is not None:
                names[name] = cp

    print("/*")
    print(" * Glyph name -> codepoint, covering every name used by the three base")
    print(" * encodings above (%d names), sorted for binary search. Names outside" % len(names))
    print(" * this set are resolved algorithmically (uniXXXX / uXXXXXX) or left")
    print(" * unmapped.")
    print(" */")
    print("struct pdf_glyph_name_def {")
    print("\tconst char *name;")
    print("\tchar32_t uc;")
    print("};")
    print()
    print("static const pdf_glyph_name_def pdf_glyph_names[] = {")
    for name, cp in sorted(names.items()):
        print('\t{"%s", 0x%04x},' % (name, cp))
    print("};")
    print()
    print("}// namespace rspamd::mime::pdf")
    print()
    print("#endif")


if __name__ == "__main__":
    main()
