"""Writes a page about text as a program receives it, before a page sets it.

The Python twin of the `write_text_sources` example in Rust: the same page, through the
binding rather than through the library directly.

The first section writes one line as bytes seven ways — UTF-8 behind its byte-order
mark, UTF-16 with its mark, both orders of UTF-16 and of UTF-32, and UTF-32 with no mark
— and sets what `decode` reads back from each, beside how many bytes it was and the
first four of them. A lone half of a UTF-16 surrogate pair is handed over last, and the
page says it was refused.

The second section holds one template kept in plain ASCII, with a named reference, a
numeric one, one by glyph name, a backslash escape and a reference that names nothing,
and sets what `resolve_references` makes of it reading nothing, each kind alone, and all
four.

The third writes one word with its accent as a letter of its own after the `e`, then as
`to_nfc` composes it, each with its code points and whether `is_nfc` finds it composed.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set the
page is written in. The template and the word stay as they are in every language.

Usage: python examples/write_text_sources.py [out.pdf]
       HQF_PDF_LANG=fr python examples/write_text_sources.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The font the page is written in.
DEFAULT_FONT = _out.ROOT / "crates" / "hqf-pdf" / "tests" / "fonts" / "DejaVuSans.ttf"

# How far in from the left edge of the sheet every line starts, in points.
LEFT = 60.0

# Where the second column of a section starts, in points from the left edge.
SECOND_COLUMN = 240.0

# Where the third column of the first section starts, in points from the left edge.
THIRD_COLUMN = 390.0

# Where the baseline of the title sits, in points up from the foot of the sheet.
TITLE_BASELINE = 780.0

# The size the title is set at, in points.
TITLE_SIZE = 16.0

# The size a section's heading is set at, in points.
HEADING_SIZE = 12.0

# The size every other line is set at, in points.
BODY_SIZE = 9.5

# How far one line sits below the one before it, in points.
LEADING = 16.0

# How far a heading sits below the line before it, in points.
HEADING_GAP = 34.0

# The template of the second section: a named reference, a numeric one, one by glyph
# name, a backslash escape and a reference that names nothing.
TEMPLATE = r"5 &euro; &#x2014; &:checkmark; \u{3A9} &nosuch;"

# The word of the third section, its accent written after the `e` as a letter of its
# own.
APART = "café"


@dataclass(frozen=True)
class Words:
    """Every word the page says, in one language."""

    # The line at the head of the page, and what the file says of itself.
    title: str
    # The heading of the first section.
    bytes_heading: str
    # The line the first section writes as bytes.
    sample: str
    # What each way of writing it is called, in the order `formats` gives them.
    formats: tuple[str, ...]
    # What follows a way's name; `{count}` and `{opening}` stand for how many bytes it
    # took and the first four of them.
    bytes: str
    # What the page says when the lone half of a surrogate pair is refused.
    refused: str
    # What the page says if it is read all the same.
    accepted: str
    # The heading of the second section.
    references_heading: str
    # What stands before the template as it is written.
    written: str
    # What each reading is called: nothing read, each kind alone, all four.
    readings: tuple[str, ...]
    # The heading of the third section.
    nfc_heading: str
    # What the word taken apart and the word composed are called.
    forms: tuple[str, ...]
    # What the page says of a word `is_nfc` finds composed, and of one it does not.
    composed: tuple[str, ...]


# The page in English.
ENGLISH = Words(
    title="Text as it arrives, before a page sets it",
    bytes_heading="One line written as bytes seven ways, and read back",
    sample="Price: 5 € — paid ✓",
    formats=(
        "UTF-8 behind its mark",
        "UTF-16 with its mark",
        "UTF-16, big end first",
        "UTF-16, little end first",
        "UTF-32 with no mark",
        "UTF-32, big end first",
        "UTF-32, little end first",
    ),
    bytes="{count} bytes, {opening}",
    refused="Half a surrogate pair on its own, D8 00, is refused.",
    accepted="Half a surrogate pair on its own, D8 00, is read.",
    references_heading="One template in plain letters, read six ways",
    written="As written",
    readings=(
        "Reading nothing",
        "Numeric references",
        "Named references",
        "Glyph names",
        "Backslash escapes",
        "All four",
    ),
    nfc_heading="One accented letter, written two ways",
    forms=("Taken apart", "Composed"),
    composed=("composed", "not composed"),
)

# The page in French.
FRENCH = Words(
    title="Le texte tel qu’il arrive, avant qu’une page le compose",
    bytes_heading="Une ligne écrite en octets de sept façons, et relue",
    sample="Prix : 5 € — payé ✓",
    formats=(
        "UTF-8 précédé de sa marque",
        "UTF-16 avec sa marque",
        "UTF-16, poids fort d’abord",
        "UTF-16, poids faible d’abord",
        "UTF-32 sans marque",
        "UTF-32, poids fort d’abord",
        "UTF-32, poids faible d’abord",
    ),
    bytes="{count} octets, {opening}",
    refused="Une demi-paire de substitution isolée, D8 00, est refusée.",
    accepted="Une demi-paire de substitution isolée, D8 00, est lue.",
    references_heading="Un modèle en lettres simples, lu de six façons",
    written="Tel qu’il est écrit",
    readings=(
        "Sans rien lire",
        "Références numériques",
        "Références nommées",
        "Noms de glyphes",
        "Échappements",
        "Les quatre",
    ),
    nfc_heading="Une lettre accentuée, écrite de deux façons",
    forms=("Décomposée", "Composée"),
    composed=("composée", "non composée"),
)

# The page in Dutch.
DUTCH = Words(
    title="Tekst zoals hij binnenkomt, voordat een pagina hem zet",
    bytes_heading="Eén regel op zeven manieren als bytes geschreven, en teruggelezen",
    sample="Prijs: € 5 — betaald ✓",
    formats=(
        "UTF-8 achter zijn markering",
        "UTF-16 met zijn markering",
        "UTF-16, grote kant eerst",
        "UTF-16, kleine kant eerst",
        "UTF-32 zonder markering",
        "UTF-32, grote kant eerst",
        "UTF-32, kleine kant eerst",
    ),
    bytes="{count} bytes, {opening}",
    refused="Een half surrogaatpaar op zichzelf, D8 00, wordt geweigerd.",
    accepted="Een half surrogaatpaar op zichzelf, D8 00, wordt gelezen.",
    references_heading="Eén sjabloon in gewone letters, op zes manieren gelezen",
    written="Zoals geschreven",
    readings=(
        "Niets lezend",
        "Numerieke verwijzingen",
        "Benoemde verwijzingen",
        "Glyphnamen",
        "Escapes met backslash",
        "Alle vier",
    ),
    nfc_heading="Eén letter met accent, op twee manieren geschreven",
    forms=("Uit elkaar gehaald", "Samengesteld"),
    composed=("samengesteld", "niet samengesteld"),
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="Texto tal como chega, antes de uma página o compor",
    bytes_heading="Uma linha escrita como bytes de sete formas, e relida",
    sample="Preço: 5 € — pago ✓",
    formats=(
        "UTF-8 atrás da sua marca",
        "UTF-16 com a sua marca",
        "UTF-16, byte mais significativo primeiro",
        "UTF-16, byte menos significativo primeiro",
        "UTF-32 sem marca",
        "UTF-32, byte mais significativo primeiro",
        "UTF-32, byte menos significativo primeiro",
    ),
    bytes="{count} bytes, {opening}",
    refused="Meio par de substitutos sozinho, D8 00, é recusado.",
    accepted="Meio par de substitutos sozinho, D8 00, é lido.",
    references_heading="Um modelo em letras simples, lido de seis formas",
    written="Tal como escrito",
    readings=(
        "Sem ler nada",
        "Referências numéricas",
        "Referências com nome",
        "Nomes de glifo",
        "Escapes com barra invertida",
        "Os quatro",
    ),
    nfc_heading="Uma letra acentuada, escrita de duas formas",
    forms=("Decomposta", "Composta"),
    composed=("composta", "não composta"),
)

# The page in Italian.
ITALIAN = Words(
    title="Il testo come arriva, prima che una pagina lo componga",
    bytes_heading="Una riga scritta come byte in sette modi, e riletta",
    sample="Prezzo: 5 € — pagato ✓",
    formats=(
        "UTF-8 dietro il suo segno",
        "UTF-16 con il suo segno",
        "UTF-16, prima il byte più significativo",
        "UTF-16, prima il byte meno significativo",
        "UTF-32 senza segno",
        "UTF-32, prima il byte più significativo",
        "UTF-32, prima il byte meno significativo",
    ),
    bytes="{count} byte, {opening}",
    refused="Mezza coppia surrogata da sola, D8 00, è rifiutata.",
    accepted="Mezza coppia surrogata da sola, D8 00, è letta.",
    references_heading="Un modello in lettere semplici, letto in sei modi",
    written="Come è scritto",
    readings=(
        "Senza leggere nulla",
        "Riferimenti numerici",
        "Riferimenti con nome",
        "Nomi di glifo",
        "Sequenze con barra rovesciata",
        "Tutti e quattro",
    ),
    nfc_heading="Una lettera accentata, scritta in due modi",
    forms=("Scomposta", "Composta"),
    composed=("composta", "non composta"),
)

# The page in Spanish.
SPANISH = Words(
    title="Texto tal como llega, antes de que una página lo componga",
    bytes_heading="Una línea escrita como bytes de siete formas, y leída de nuevo",
    sample="Precio: 5 € — pagado ✓",
    formats=(
        "UTF-8 tras su marca",
        "UTF-16 con su marca",
        "UTF-16, byte más significativo primero",
        "UTF-16, byte menos significativo primero",
        "UTF-32 sin marca",
        "UTF-32, byte más significativo primero",
        "UTF-32, byte menos significativo primero",
    ),
    bytes="{count} bytes, {opening}",
    refused="Un sustituto sin pareja, D8 00, se rechaza.",
    accepted="Un sustituto sin pareja, D8 00, se lee.",
    references_heading="Una plantilla en letras simples, leída de seis formas",
    written="Tal como se escribió",
    readings=(
        "Sin leer nada",
        "Referencias numéricas",
        "Referencias con nombre",
        "Nombres de glifo",
        "Escapes con barra invertida",
        "Las cuatro",
    ),
    nfc_heading="Una letra acentuada, escrita de dos formas",
    forms=("Descompuesta", "Compuesta"),
    composed=("compuesta", "no compuesta"),
)

# The page in German.
GERMAN = Words(
    title="Text, wie er ankommt, bevor eine Seite ihn setzt",
    bytes_heading="Eine Zeile, auf sieben Arten als Bytes geschrieben und zurückgelesen",
    sample="Preis: 5 € — bezahlt ✓",
    formats=(
        "UTF-8 mit vorangestellter BOM",
        "UTF-16 mit BOM",
        "UTF-16, Big Endian",
        "UTF-16, Little Endian",
        "UTF-32 ohne BOM",
        "UTF-32, Big Endian",
        "UTF-32, Little Endian",
    ),
    bytes="{count} Bytes, {opening}",
    refused="Ein halbes Surrogatpaar für sich, D8 00, wird abgelehnt.",
    accepted="Ein halbes Surrogatpaar für sich, D8 00, wird gelesen.",
    references_heading="Eine Vorlage in einfachen Buchstaben, auf sechs Arten gelesen",
    written="Wie geschrieben",
    readings=(
        "Nichts auflösen",
        "Numerische Referenzen",
        "Benannte Referenzen",
        "Glyphennamen",
        "Backslash-Maskierungen",
        "Alle vier",
    ),
    nfc_heading="Ein Buchstabe mit Akzent, auf zwei Arten geschrieben",
    forms=("Zerlegt", "Zusammengesetzt"),
    composed=("zusammengesetzt", "nicht zusammengesetzt"),
)

# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}


def formats(text: str) -> list[tuple[hqf_pdf.TextFormat, bytes]]:
    """`text` written as bytes each way the first section shows, with the form each is
    read back in."""
    return [
        (hqf_pdf.TextFormat.Utf8, b"\xef\xbb\xbf" + text.encode("utf-8")),
        (hqf_pdf.TextFormat.Utf16, b"\xff\xfe" + text.encode("utf-16-le")),
        (hqf_pdf.TextFormat.Utf16Be, text.encode("utf-16-be")),
        (hqf_pdf.TextFormat.Utf16Le, text.encode("utf-16-le")),
        (hqf_pdf.TextFormat.Utf32, text.encode("utf-32-be")),
        (hqf_pdf.TextFormat.Utf32Be, text.encode("utf-32-be")),
        (hqf_pdf.TextFormat.Utf32Le, text.encode("utf-32-le")),
    ]


def opening(data: bytes) -> str:
    """The first four of `data`, in hexadecimal, and an ellipsis."""
    return " ".join(f"{byte:02X}" for byte in data[:4]) + " …"


# The ways the second section reads the template, in the order `readings` names them, as
# the kinds each turns on.
READINGS = (
    {},
    {"numeric": True},
    {"named": True},
    {"glyph_names": True},
    {"escapes": True},
    {"numeric": True, "named": True, "glyph_names": True, "escapes": True},
)


def code_points(text: str) -> str:
    """The code points of `text`, as `U+` and four hexadecimal digits each."""
    return " ".join(f"U+{ord(character):04X}" for character in text)


def lines(words: Words) -> list[tuple[str, float, float, float]]:
    """Every line the page sets, in order: what it says, the size it is set at, how far
    from the left edge it starts, and where its baseline sits."""
    drawn = [(words.title, TITLE_SIZE, LEFT, TITLE_BASELINE)]
    baseline = TITLE_BASELINE - HEADING_GAP

    drawn.append((words.bytes_heading, HEADING_SIZE, LEFT, baseline))
    for (form, data), name in zip(formats(words.sample), words.formats):
        baseline -= LEADING
        said = words.bytes.replace("{count}", str(len(data))).replace(
            "{opening}", opening(data)
        )
        drawn.append((name, BODY_SIZE, LEFT, baseline))
        drawn.append((said, BODY_SIZE, SECOND_COLUMN, baseline))
        drawn.append((hqf_pdf.decode(data, form), BODY_SIZE, THIRD_COLUMN, baseline))
    baseline -= LEADING
    try:
        hqf_pdf.decode(b"\xd8\x00", hqf_pdf.TextFormat.Utf16Be)
        verdict = words.accepted
    except hqf_pdf.BadArgument:
        verdict = words.refused
    drawn.append((verdict, BODY_SIZE, LEFT, baseline))

    baseline -= HEADING_GAP
    drawn.append((words.references_heading, HEADING_SIZE, LEFT, baseline))
    baseline -= LEADING
    drawn.append((words.written, BODY_SIZE, LEFT, baseline))
    drawn.append((TEMPLATE, BODY_SIZE, SECOND_COLUMN, baseline))
    for kinds, name in zip(READINGS, words.readings):
        baseline -= LEADING
        drawn.append((name, BODY_SIZE, LEFT, baseline))
        drawn.append(
            (
                hqf_pdf.resolve_references(TEMPLATE, **kinds),
                BODY_SIZE,
                SECOND_COLUMN,
                baseline,
            )
        )

    baseline -= HEADING_GAP
    drawn.append((words.nfc_heading, HEADING_SIZE, LEFT, baseline))
    composed = hqf_pdf.to_nfc(APART)
    for word, form in zip((APART, composed), words.forms):
        baseline -= LEADING
        state = words.composed[0] if hqf_pdf.is_nfc(word) else words.composed[1]
        drawn.append((f"{form}, {state}", BODY_SIZE, LEFT, baseline))
        drawn.append((code_points(word), BODY_SIZE, SECOND_COLUMN, baseline))
    return drawn


def page(font: hqf_pdf.FontHandle, words: Words) -> hqf_pdf.Page:
    """The page: every line set by its own origin, each in a text object of its own."""
    content = hqf_pdf.Content()
    for text, size, x, y in lines(words):
        content.draw_text(font, size, x, y, text)
    result = hqf_pdf.Page.a4()
    result.set_content(content)
    return result


def main() -> None:
    language = _language.from_environment()
    words = _language.words_of(WORDS, language)

    # A named file is written as named; the default one carries the language, so the two
    # languages do not overwrite each other in `tmp/`.
    out = _out.output_path(Path(_language.file_name("text_sources.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.title)
    font = document.add_font(hqf_pdf.Font.from_path(DEFAULT_FONT))
    document.add_page(page(font, words))

    written = document.write(out)
    print(f"wrote {out} ({written} bytes)")


if __name__ == "__main__":
    main()
