"""Sets one line of statuses twice: above in a house font alone, below with a
second font behind it, so the characters the house font has no glyph for can be
seen appearing.

The Python twin of the `write_font_fallback` example in Rust. The house font
here is IBM Plex Sans, which draws a tick but neither a star, an umbrella nor a
cross. Set alone, those three come out as the empty box every font carries for a
character it does not have. Set through a chain, each is handed to DejaVu Sans,
which does have it, and the line is drawn in as many fonts as it takes.

The closing note counts the stretches the chain cuts the line into, and names the
two fonts as the files themselves name them.

Under the list, the house font is set alone three more times, each time told something
else to draw for a character it lacks: the box every font carries, which is what it
does unless told otherwise; nothing at all; and a question mark in its place. Each line
is labelled with the policy the font reports.
A last line sets a temperature in kelvins with stand-ins turned on: the house font has no
kelvin sign, and draws the capital K it stands for.

The foot of the page sets the ten figures twice: in the house font, which has a glyph
for each, and in the house font told to take the figures from the second font all the
same, so the second font's figures stand where the house font's would.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set
is drawn. The four symbols are not language: they stand the same in every one.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# Where the committed fonts sit.
FONT_DIR = _out.ROOT / "crates" / "hqf-pdf" / "tests" / "fonts"

# The four symbols the statuses are marked with, in the order they are drawn.
#
# The tick is in both fonts; the star, the umbrella and the cross are in the second
# alone, which is what the two settings show.
MARKS = ("✓", "★", "☂", "✗")

# What stands between two statuses on the line. A run of spaces would be set as one, so
# the statuses are held apart by a character.
GAP = " · "


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

    What is not language stays out of it: the four symbols are marks, and the two font
    names are what the files call themselves.
    """

    # The line across the top of the page.
    title: str
    # Why a document needs a font behind its own, in two sentences.
    why: str
    # The heading over the setting in the house font alone.
    alone: str
    # The heading over the setting that falls back.
    chained: str
    # The four statuses, each drawn after its mark.
    statuses: tuple[str, str, str, str]
    # The closing note, cut where the count and the two font names go into it.
    note_before: str
    note_between: str
    note_joiner: str
    note_after: str
    # The three headings over the list of marks.
    head_mark: str
    head_status: str
    head_font: str
    # The heading over the three settings of the house font alone.
    policies: str
    # What each policy is called: the box, nothing, and a character in its place,
    # `{character}` standing for the character.
    policy_names: tuple[str, str, str]
    # What stands before the line set with stand-ins.
    stand_ins: str
    # The heading over the two settings of the figures.
    figures: str
    # What stands before the figures the house font draws itself.
    own_figures: str
    # What stands before the figures the second font draws for it.
    figures_behind: str

    def line(self) -> str:
        """Return the line of statuses, each after its mark."""
        return GAP.join(f"{mark} {status}" for mark, status in zip(MARKS, self.statuses))

    def note(self, stretches: int, house: str, behind: str) -> str:
        """Return the closing note, with the count and the two font names in it."""
        return (
            f"{self.note_before}{stretches}{self.note_between}"
            f"{house}{self.note_joiner}{behind}{self.note_after}"
        )


# The page in English.
ENGLISH = Words(
    title="A font behind the font",
    why=(
        "A house font rarely covers every character a document has to draw. Without a "
        "second font behind it, a character it has no glyph for comes out as the empty "
        "box below.\nA chain hands that character to the next font instead, so the "
        "line is set in as many fonts as it takes and every mark is drawn."
    ),
    alone="The house font alone: what it has no glyph for, it cannot draw.",
    chained="The house font, with a second one behind it.",
    statuses=(
        "Paid",
        "Priority",
        "Weather hold",
        "Refused",
    ),
    note_before="The line above is cut into ",
    note_between=" stretches, drawn by two fonts: ",
    note_joiner=", then ",
    note_after=". The words are in the first; the marks it lacks are in the second.",
    head_mark="Mark",
    head_status="Status",
    head_font="Drawn by",
    policies="The house font alone, told three ways what to draw for what it lacks:",
    policy_names=(
        "the box every font carries",
        "nothing at all",
        "“{character}” in its place",
    ),
    stand_ins="told to draw what it lacks as the character it stands for",
    figures="The house font's figures, drawn once by the house font and once by the second font:",
    own_figures="its own figures",
    figures_behind="the second font's figures",
)

# The page in French.
FRENCH = Words(
    title="Une police derrière la police",
    why=(
        "Une police d’entreprise couvre rarement tous les caractères qu’un document "
        "doit afficher. Sans seconde police derrière elle, un caractère dont elle n’a "
        "pas le glyphe sort en case vide, comme ci-dessous.\nUne chaîne, elle, confie "
        "ce caractère à la police suivante : la ligne est composée dans autant de "
        "polices qu’il faut, et chaque signe s’affiche."
    ),
    alone=(
        "La police d’entreprise seule : faute de glyphe, le caractère sort en case "
        "vide."
    ),
    chained="La police d’entreprise, avec une seconde police derrière elle.",
    statuses=(
        "Payé",
        "Prioritaire",
        "Attente météo",
        "Refusé",
    ),
    note_before="La ligne du dessus est découpée en ",
    note_between=" tronçons, composés avec deux polices : ",
    note_joiner=", puis ",
    note_after=(
        ". Les mots viennent de la première ; les signes qui lui manquent, de la "
        "seconde."
    ),
    head_mark="Signe",
    head_status="État",
    head_font="Police",
    policies=(
        "La police d’entreprise seule, avec trois consignes pour ce qui lui manque :"
    ),
    policy_names=(
        "la case que contient toute police",
        "rien du tout",
        "« {character} » à sa place",
    ),
    stand_ins=(
        "avec pour consigne de remplacer un caractère qui lui manque par celui dont il "
        "tient lieu"
    ),
    figures="La police de l’entreprise compose les dix chiffres avec les siens, puis avec ceux de la seconde police :",
    own_figures="ses propres chiffres",
    figures_behind="chiffres de la seconde police",
)

# The page in Dutch.
DUTCH = Words(
    title="Een lettertype achter het lettertype",
    why=(
        "Een huislettertype dekt zelden elk teken dat een document moet tekenen. "
        "Zonder een tweede lettertype erachter komt een teken waarvoor het geen glyph "
        "heeft eruit als het lege vakje hieronder.\nEen keten geeft dat teken door "
        "aan het volgende lettertype, dus wordt de regel gezet in zoveel lettertypen "
        "als nodig en wordt elk teken getekend."
    ),
    alone="Het huislettertype alleen: wat het niet heeft, kan het niet tekenen.",
    chained="Het huislettertype, met een tweede erachter.",
    statuses=(
        "Betaald",
        "Voorrang",
        "Weersvertraging",
        "Geweigerd",
    ),
    note_before="De regel hierboven is opgeknipt in ",
    note_between=" stukken, getekend door twee lettertypen: ",
    note_joiner=", dan ",
    note_after=". De woorden staan in het eerste; de tekens die het mist, in het tweede.",
    head_mark="Teken",
    head_status="Status",
    head_font="Getekend door",
    policies="Het huislettertype alleen, op drie manieren verteld wat het moet tekenen voor wat het mist:",
    policy_names=(
        "het vakje dat elk lettertype heeft",
        "helemaal niets",
        "„{character}” op zijn plaats",
    ),
    stand_ins="gevraagd om wat het mist te tekenen als het teken waar het voor staat",
    figures="De cijfers van het huislettertype, eerst getekend door het huislettertype zelf, daarna door het tweede lettertype:",
    own_figures="zijn eigen cijfers",
    figures_behind="die van het tweede lettertype",
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="Um tipo de letra por trás do tipo de letra",
    why=(
        "Um tipo de letra da casa raramente cobre todos os caracteres que um "
        "documento tem de desenhar. Sem um segundo por trás, um carácter para o qual "
        "não tem glifo sai como a caixa vazia abaixo.\nUma cadeia passa esse carácter "
        "ao tipo de letra seguinte, por isso a linha compõe-se com tantos tipos de "
        "letra quantos forem precisos e cada marca é desenhada."
    ),
    alone="O tipo de letra da casa sozinho: o que não tem, não o pode desenhar.",
    chained="O tipo de letra da casa, com um segundo por trás.",
    statuses=(
        "Pago",
        "Prioridade",
        "Retido pelo mau tempo",
        "Recusado",
    ),
    note_before="A linha acima é cortada em ",
    note_between=" troços, desenhados por dois tipos de letra: ",
    note_joiner=", depois ",
    note_after=". As palavras estão no primeiro; as marcas que lhe faltam, no segundo.",
    head_mark="Marca",
    head_status="Estado",
    head_font="Desenhado por",
    policies="O tipo de letra da casa sozinho, com três formas de lhe dizer o que desenhar no lugar do que lhe falta:",
    policy_names=(
        "a caixa que todo o tipo de letra leva",
        "nada",
        "«{character}» no seu lugar",
    ),
    stand_ins="pede-se-lhe que desenhe o que lhe falta como o carácter que representa",
    figures="Os dez algarismos, desenhados primeiro pelo tipo de letra da casa e depois pelo segundo tipo de letra:",
    own_figures="os seus algarismos",
    figures_behind="os do segundo tipo de letra",
)

# The page in Italian.
ITALIAN = Words(
    title="Un font dietro il font",
    why=(
        "Un font aziendale copre di rado ogni carattere che un documento deve "
        "disegnare. Senza un secondo font dietro, un carattere per cui non ha glifo "
        "esce come il riquadro vuoto qui sotto.\nUna catena passa quel carattere al "
        "font successivo, così la riga si compone con quanti font servono e ogni "
        "segno è disegnato."
    ),
    alone="Il font aziendale da solo: ciò per cui non ha glifo, non lo può disegnare.",
    chained="Il font aziendale, con un secondo dietro.",
    statuses=(
        "Pagato",
        "Priorità",
        "Fermo per maltempo",
        "Rifiutato",
    ),
    note_before="La riga qui sopra è tagliata in ",
    note_between=" tratti, disegnati da due font: ",
    note_joiner=", poi ",
    note_after=". Le parole sono nel primo; i segni che gli mancano, nel secondo.",
    head_mark="Segno",
    head_status="Stato",
    head_font="Disegnato da",
    policies="Il font aziendale da solo, con tre modi di dirgli cosa disegnare per ciò che gli manca:",
    policy_names=(
        "il riquadro che ogni font porta",
        "niente del tutto",
        "«{character}» al suo posto",
    ),
    stand_ins="gli si chiede di disegnare ciò che gli manca come il carattere che rappresenta",
    figures="Le cifre del font aziendale, disegnate prima dal font stesso, poi dal secondo font:",
    own_figures="le cifre del font aziendale",
    figures_behind="le cifre del secondo font",
)

# The page in Spanish.
SPANISH = Words(
    title="Una fuente detrás de la fuente",
    why=(
        "Una fuente de la casa rara vez cubre todos los caracteres que un documento "
        "tiene que dibujar. Sin una segunda fuente detrás, un carácter para el que no "
        "tiene glifo sale como la caja vacía de abajo.\nUna cadena de fuentes pasa "
        "ese carácter a la fuente siguiente, así que la línea se compone con tantas "
        "fuentes como haga falta y cada marca se dibuja."
    ),
    alone="La fuente de la casa sola: lo que no tiene, no lo puede dibujar.",
    chained="La fuente de la casa, con una segunda detrás.",
    statuses=(
        "Pagado",
        "Prioridad",
        "Retenido por mal tiempo",
        "Rechazado",
    ),
    note_before="La línea de arriba se corta en ",
    note_between=" tramos, dibujados por dos fuentes: ",
    note_joiner=", luego ",
    note_after=". Las palabras están en la primera; las marcas que le faltan, en la segunda.",
    head_mark="Marca",
    head_status="Estado",
    head_font="Dibujado por",
    policies="La fuente de la casa sola, con tres formas de decirle qué dibujar en lugar de lo que le falta:",
    policy_names=(
        "la caja que lleva toda fuente",
        "nada en absoluto",
        "«{character}» en su lugar",
    ),
    stand_ins="se le pide dibujar lo que le falta como el carácter que representa",
    figures="Las cifras, dibujadas una vez por la fuente de la casa y otra por la segunda fuente:",
    own_figures="cifras propias",
    figures_behind="cifras de la segunda fuente",
)

# The page in German.
GERMAN = Words(
    title="Eine Schrift hinter der Schrift",
    why=(
        "Eine Hausschrift deckt selten jedes Zeichen ab, das ein Dokument zeichnen "
        "muss. Ohne eine zweite Schrift dahinter erscheint ein Zeichen, für das sie "
        "keinen Glyphen hat, als das leere Kästchen unten.\nEine Kette reicht dieses "
        "Zeichen stattdessen an die nächste Schrift weiter, sodass die Zeile in so "
        "vielen Schriften wie nötig gesetzt wird und jedes Zeichen gezeichnet wird."
    ),
    alone="Die Hausschrift allein: Wofür sie keinen Glyphen hat, das kann sie nicht zeichnen.",
    chained="Die Hausschrift mit einer zweiten dahinter.",
    statuses=(
        "Bezahlt",
        "Vorrang",
        "Wetterbedingt angehalten",
        "Abgelehnt",
    ),
    note_before="Die Zeile oben ist in ",
    note_between=" Abschnitte geteilt, von zwei Schriften gezeichnet: ",
    note_joiner=", dann ",
    note_after=". Die Wörter stehen in der ersten; die Zeichen, die ihr fehlen, in der zweiten.",
    head_mark="Zeichen",
    head_status="Status",
    head_font="Gezeichnet von",
    policies="Die Hausschrift allein, auf drei Arten angewiesen, was sie für Fehlendes zeichnen soll:",
    policy_names=(
        "das Kästchen, das jede Schrift mitbringt",
        "gar nichts",
        "„{character}“ an seiner Stelle",
    ),
    stand_ins="angewiesen, Fehlendes als das Zeichen zu zeichnen, für das es steht",
    figures="Die Hausschrift, einmal mit ihren eigenen Ziffern, einmal mit denen der zweiten Schrift:",
    own_figures="eigene Ziffern",
    figures_behind="Ziffern der zweiten Schrift",
)

# 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}

# The left edge of everything on the page.
X = 60.0

# The width every block is broken to.
ROOM = 475.0

# The baseline the title sits on.
TITLE_TOP = 780.0

# The top of the block that says why the page matters.
WHY_TOP = 750.0

# The top of the heading over the setting in the house font alone.
ALONE_TOP = 650.0

# The top of the heading over the setting that falls back.
CHAINED_TOP = 560.0

# The top of the closing note.
NOTE_TOP = 470.0

# How far a setting sits below the heading over it.
UNDER_HEADING = 34.0

# The size the statuses are set in.
LINE_SIZE = 15.0

# The top of the list that names the font behind each mark.
LIST_TOP = 400.0

# How far apart two rows of that list sit.
ROW_DROP = 18.0

# Where the three columns of that list begin.
COLUMNS = (X, X + 46.0, X + 176.0)

# The top of the heading over the three settings of the house font alone.
POLICIES_TOP = 280.0

# The character the third of those settings draws in place of a missing one.
STAND_IN = "?"

# The line set with stand-ins: a temperature in kelvins, written with the kelvin sign
# the house font has no glyph for.
KELVINS = "273 \u212a"

# The top of the heading over the two settings of the figures.
FIGURES_TOP = 140.0

# The figures set twice at the foot of the page: the house font has a glyph for each.
FIGURES = "0123456789"


def policy_name(policy, words) -> str:
    """What `policy` is called, in `words`."""
    if policy == hqf_pdf.MissingGlyph.drop():
        return words.policy_names[1]
    if policy == hqf_pdf.MissingGlyph.replace(STAND_IN):
        return words.policy_names[2].replace("{character}", STAND_IN)
    return words.policy_names[0]


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

    out = _out.output_path(Path(_language.file_name("font_fallback.pdf", language)).stem)

    house = hqf_pdf.Font.from_path(FONT_DIR / "IBMPlexSans-Regular.otf")
    told = (
        house.missing_glyph,
        hqf_pdf.MissingGlyph.drop(),
        hqf_pdf.MissingGlyph.replace(STAND_IN),
    )

    behind = hqf_pdf.Font.from_path(FONT_DIR / "DejaVuSans.ttf")
    house_name = house.postscript_name
    behind_name = behind.postscript_name

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    alone = [document.add_font(house.with_missing_glyph(policy)) for policy in told]
    plain = document.add_font(house)
    spare = document.add_font(behind)
    chain = plain.falling_back_to(spare)

    content = hqf_pdf.Content()

    title = hqf_pdf.TextFlow(plain, 18.0)
    title.draw(content, title.break_lines(words.title, ROOM), X, TITLE_TOP, ROOM)

    # The block that says why the page matters is set through the chain: it is ordinary
    # prose, and the chain draws ordinary prose exactly as the house font alone would.
    why = hqf_pdf.TextFlow(chain, 10.0, leading=14.0)
    why.draw(content, why.break_lines(words.why, ROOM), X, WHY_TOP, ROOM)

    # The same line twice: once in the house font on its own, once through the chain.
    # Only the font differs.
    for top, heading, handle in (
        (ALONE_TOP, words.alone, plain),
        (CHAINED_TOP, words.chained, chain),
    ):
        label = hqf_pdf.TextFlow(plain, 9.0)
        label.draw(content, label.break_lines(heading, ROOM), X, top, ROOM)

        statuses = hqf_pdf.TextFlow(handle, LINE_SIZE)
        statuses.draw(
            content,
            statuses.break_lines(line, ROOM),
            X,
            top - UNDER_HEADING,
            ROOM,
        )

    # What the chain made of the line, counted off the chain itself rather than written
    # down: the note says what the page above it holds.
    note = words.note(len(chain.runs(line)), house_name, behind_name)
    closing = hqf_pdf.TextFlow(chain, 10.0, leading=14.0)
    closing.draw(content, closing.break_lines(note, ROOM), X, NOTE_TOP, ROOM)

    # Which font drew which mark, read off the chain: a mark the house font has is drawn
    # by it, and the rest by the font behind it.
    head_row = hqf_pdf.TextFlow(plain, 9.0)
    for column, head in zip(COLUMNS, (words.head_mark, words.head_status, words.head_font)):
        head_row.draw(content, head_row.break_lines(head, ROOM), column, LIST_TOP, ROOM)

    for row, (mark, status) in enumerate(zip(MARKS, words.statuses)):
        runs = chain.runs(mark)
        drawn_by = house_name if runs and runs[0].font == plain.name else behind_name
        top = LIST_TOP - ROW_DROP * float(row + 1)
        for column, cell in zip(COLUMNS, (mark, status, drawn_by)):
            flow = hqf_pdf.TextFlow(chain, 10.0)
            flow.draw(content, flow.break_lines(cell, ROOM), column, top, ROOM)

    heading = hqf_pdf.TextFlow(plain, 9.0)
    heading_lines = heading.break_lines(words.policies, ROOM)
    heading.draw(content, heading_lines, X, POLICIES_TOP, ROOM)
    top = POLICIES_TOP - 24.0
    for handle in alone:
        label = hqf_pdf.TextFlow(plain, 9.0)
        named = policy_name(handle.missing_glyph, words)
        label.draw(content, label.break_lines(named, 150.0), X, top, 150.0)
        statuses = hqf_pdf.TextFlow(handle, 11.0)
        statuses.draw(
            content,
            statuses.break_lines(line, ROOM - 160.0),
            X + 160.0,
            top,
            ROOM - 160.0,
        )
        top -= 24.0

    label = hqf_pdf.TextFlow(plain, 9.0)
    label.draw(content, label.break_lines(words.stand_ins, 150.0), X, top, 150.0)
    kelvins = hqf_pdf.TextFlow(plain.with_stand_ins(), 11.0)
    kelvins.draw(content, kelvins.break_lines(KELVINS, ROOM - 160.0), X + 160.0, top, ROOM - 160.0)

    # The ten figures set twice: in the house font, and in the house font told to take
    # them from the second font though it has a glyph for each.
    heading = hqf_pdf.TextFlow(plain, 9.0)
    heading.draw(content, heading.break_lines(words.figures, ROOM), X, FIGURES_TOP, ROOM)
    taken = plain.with_characters_drawn_by("0", "9", spare)
    top = FIGURES_TOP - 24.0
    for said, handle in ((words.own_figures, plain), (words.figures_behind, taken)):
        label = hqf_pdf.TextFlow(plain, 9.0)
        label.draw(content, label.break_lines(said, 150.0), X, top, 150.0)
        figures = hqf_pdf.TextFlow(handle, 11.0)
        figures.draw(content, figures.break_lines(FIGURES, ROOM - 160.0), X + 160.0, top, ROOM - 160.0)
        top -= 24.0

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

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


if __name__ == "__main__":
    main()
