"""Bulleted, numbered and nested lists, each built out of the indents and one tab
stop rather than out of a list engine.

The Python twin of the `write_lists` example in Rust. A list is a paragraph whose
first line hangs out to the left: the marker sits in the room the hanging indent
opens, the tabulation after it carries the pen to the stop the body begins at,
and every line below the first starts at that same stop. Nesting is the same
block again, its marker where the block above put its body.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the page is set in. The indents are not language: the headings read the very
numbers the blocks are built from, so no language can announce a room the page
does not open.

Usage: python examples/write_lists.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_lists.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The left edge every block is set from.
X = 70.0

# The room the two lists at the top of the page each have.
NARROW = 210.0

# The left edge of the second of them.
RIGHT = 320.0

# The room the nested list at the foot of the page has.
WIDE = 460.0

# The size every list is set at.
SIZE = 10.5

# The room left between a heading and the block below it.
DROP = 4.0

# The top the two lists at the head of the page are set from.
TOP = 780.0

# The top the nested list is set from, headings and all.
NEST_TOP = 640.0

# How far into the box the bodies of the bulleted list begin.
BULLET_BODY = 14.0

# How far into the box the bodies of the numbered list begin, which is what the widest
# of its markers needs rather than what the narrowest does.
NUMBER_BODY = 22.0

# Where the markers and the bodies of the inner list stand: its markers begin where the
# outer bodies do, and its bodies one step further in again.
INNER_MARKER = NUMBER_BODY
INNER_BODY = NUMBER_BODY * 2.0


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

    # The heading over the bulleted list, on either side of the room its bodies begin
    # at.
    bulleted_before: str
    bulleted_after: str
    # The items of the bulleted list, each behind its bullet and a tabulation.
    bullets: str
    # The heading over the numbered list, on either side of the room its bodies begin
    # at.
    numbered_before: str
    numbered_after: str
    # The items of the numbered list, whose markers are wider than a bullet and so ask
    # for more room in front of the body.
    numbers: str
    # The heading over the nested list.
    nested: str
    # The outer items of the nested list, the first of which is broken down further.
    outer: tuple[str, str]
    # What the first outer item breaks down into, set again one level in.
    inner: str

    def bulleted(self) -> str:
        """The heading over the bulleted list, with the room its bodies begin at read
        off the very number the block is built from."""
        return f"{self.bulleted_before}{BULLET_BODY:.0f}{self.bulleted_after}"

    def numbered(self) -> str:
        """The heading over the numbered list, likewise."""
        return f"{self.numbered_before}{NUMBER_BODY:.0f}{self.numbered_after}"


# The page in English.
ENGLISH = Words(
    bulleted_before="Bulleted: a bullet, a tabulation, and ",
    bulleted_after=" points in front of the body.",
    bullets=(
        "•\tEvery line below the first lines up with the words, not with the bullet.\n"
        "•\tThe marker hangs in the room the negative first-line indent opens.\n"
        "•\tA long item wraps inside the block and stays inside it."
    ),
    numbered_before="Numbered: the same block, widened to ",
    numbered_after=" points for the widest marker.",
    numbers=(
        "1.\tA number hangs out to the left exactly as a bullet does.\n"
        "2.\tThe room in front is what the widest marker needs, not the narrowest.\n"
        "10.\tA two-figure number still ends before the stop the body begins at."
    ),
    nested=(
        "Nested: the inner list is the same block again, its markers standing "
        "where the outer bodies begin."
    ),
    outer=(
        "1.\tPrepare the document, the font it draws through and the page it fills.",
        "2.\tAdd each page, with the content stream it was built from.",
    ),
    inner=(
        "a.\tRead the font from its file and hand it to the document.\n"
        "b.\tName the page size, in points."
    ),
)

# The page in French.
FRENCH = Words(
    bulleted_before="À puces : une puce, une tabulation et ",
    bulleted_after=" points devant le corps.",
    bullets=(
        ("•\tToutes les lignes sous la première s’alignent sur les mots, pas sur la "
         "puce.\n•\tLe marqueur se loge dans l’espace que dégage le retrait négatif de"
         " première ligne.\n•\tUne entrée longue passe à la ligne dans le bloc et y "
         "reste.")
    ),
    numbered_before="Numérotée : le même bloc, élargi à ",
    numbered_after=" points pour le marqueur le plus large.",
    numbers=(
        ("1.\tUn numéro dépasse à gauche exactement comme une puce.\n2.\tLa place "
         "devant est celle qu’il faut au plus large des marqueurs, pas au plus "
         "étroit.\n10.\tUn numéro à deux chiffres finit toujours avant le taquet où "
         "commence le corps.")
    ),
    nested=(
        ("Imbriquée : la liste intérieure est le même bloc, ses marqueurs posés là où "
         "commencent les corps de la liste extérieure.")
    ),
    outer=(
        "1.\tPréparer le document, la police de son texte et la page qu’il remplit.",
        "2.\tAjouter chaque page, avec le flux de contenu qui la compose.",
    ),
    inner=(
        "a.\tLire la police dans son fichier et la donner au document.\n"
        "b.\tDonner la taille de la page, en points."
    ),
)

# The page in Dutch.
DUTCH = Words(
    bulleted_before="Met opsommingstekens: een teken, een tab en",
    bulleted_after=" punten voor de hoofdtekst.",
    bullets=(
        "•\tElke volgregel lijnt uit met de woorden, niet met het teken.\n•\tHet "
        "teken hangt in de ruimte die de negatieve inspringing opent.\n•\tEen lang "
        "item breekt binnen het blok af en blijft erin."
    ),
    numbered_before="Genummerd: hetzelfde blok, verbreed tot",
    numbered_after=" punten voor het breedste teken.",
    numbers=(
        "1.\tEen nummer hangt links naar buiten, net als een teken.\n2.\tDe ruimte "
        "ervoor volgt het breedste teken.\n10.\tOok een nummer van twee cijfers "
        "eindigt vóór de tabstop."
    ),
    nested=(
        "Genest: de binnenste lijst is weer hetzelfde blok, met de tekens op de plek "
        "waar de tekst van de buitenste begint."
    ),
    outer=(
        "1.\tHet document voorbereiden, het lettertype waarmee het tekent en de pagina die het vult.",
        "2.\tElke pagina toevoegen, met de inhoudsstroom waaruit ze is opgebouwd.",
    ),
    inner=(
        "a.\tHet lettertype uit zijn bestand lezen en aan het document "
        "geven.\nb.\tHet paginaformaat noemen, in punten."
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    bulleted_before="Com marcas: uma marca, uma tabulação e",
    bulleted_after=" pontos à frente do corpo.",
    bullets=(
        "•\tCada linha abaixo da primeira alinha pelas palavras, não pela "
        "marca.\n•\tA marca fica pendurada no espaço que abre o avanço negativo da "
        "primeira linha.\n•\tUm item longo muda de linha dentro do bloco e fica "
        "dentro dele."
    ),
    numbered_before="Numerada: o mesmo bloco, alargado para",
    numbered_after=" pontos para a marca mais larga.",
    numbers=(
        "1.\tUm número fica pendurado à esquerda tal como uma marca.\n2.\tO espaço à "
        "frente é o que a marca mais larga precisa, não a mais estreita.\n10.\tUm "
        "número de dois algarismos acaba ainda antes da paragem de tabulação onde "
        "começa o corpo."
    ),
    nested=(
        "Aninhada: a lista interior é de novo o mesmo bloco, com as marcas onde "
        "começam os corpos exteriores."
    ),
    outer=(
        "1.\tPreparar o documento, o tipo de letra com que desenha e a página que preenche.",
        "2.\tAcrescentar cada página, com o fluxo de conteúdo a partir do qual foi construída.",
    ),
    inner=(
        "a.\tLer o tipo de letra do seu ficheiro e entregá-lo ao "
        "documento.\nb.\tIndicar o tamanho da página, em pontos."
    ),
)

# The page in Italian.
ITALIAN = Words(
    bulleted_before="Puntato: un pallino, una tabulazione e",
    bulleted_after=" punti davanti al corpo.",
    bullets=(
        "•\tOgni riga sotto la prima si allinea alle parole, non al pallino.\n•\tIl "
        "segno pende nello spazio aperto dal rientro negativo della prima "
        "riga.\n•\tUna voce lunga va a capo dentro il blocco e ci resta."
    ),
    numbered_before="Numerato: lo stesso blocco, allargato a",
    numbered_after=" punti per il segno più largo.",
    numbers=(
        "1.\tUn numero pende a sinistra proprio come un pallino.\n2.\tLo spazio "
        "davanti è quello che serve al segno più largo, non al più stretto.\n10.\tUn "
        "numero a due cifre finisce comunque prima del fermo dove inizia il corpo."
    ),
    nested=(
        "Annidato: l’elenco interno è di nuovo lo stesso blocco, con i segni dove "
        "iniziano i corpi esterni."
    ),
    outer=(
        "1.\tPreparare il documento, il font con cui disegna e la pagina che riempie.",
        "2.\tAggiungere ogni pagina, con il flusso di contenuto da cui è stata costruita.",
    ),
    inner=(
        "a.\tLeggere il font dal suo file e passarlo al documento.\nb.\tIndicare il "
        "formato della pagina, in punti."
    ),
)

# The page in Spanish.
SPANISH = Words(
    bulleted_before="Con viñetas: una viñeta, un tabulador y ",
    bulleted_after=" puntos delante del cuerpo.",
    bullets=(
        "•\tCada línea bajo la primera se alinea con las palabras, no con la "
        "viñeta.\n•\tLa marca cuelga en el hueco que abre la sangría negativa de la "
        "primera línea.\n•\tUn elemento largo pasa de línea dentro del bloque y se "
        "queda en él."
    ),
    numbered_before="Numerada: el mismo bloque, ensanchado a ",
    numbered_after=" puntos para la marca más ancha.",
    numbers=(
        "1.\tUn número cuelga a la izquierda igual que una viñeta.\n2.\tEl hueco "
        "delante es el que necesita la marca más ancha, no la más estrecha.\n10.\tUn "
        "número de dos cifras sigue acabando antes del tope donde empieza el cuerpo."
    ),
    nested=(
        "Anidada: la lista interior es otra vez el mismo bloque, con sus marcas donde "
        "empiezan los cuerpos exteriores."
    ),
    outer=(
        "1.\tPreparar el documento, la fuente con que se dibuja y la página que llena.",
        "2.\tAñadir cada página, con el flujo de contenido del que se construyó.",
    ),
    inner=(
        "a.\tLeer la fuente de su archivo y dársela al documento.\nb.\tNombrar el "
        "tamaño de página, en puntos."
    ),
)

# The page in German.
GERMAN = Words(
    bulleted_before="Mit Aufzählungszeichen: ein Aufzählungspunkt, ein Tabulator und ",
    bulleted_after=" Punkt vor dem Text.",
    bullets=(
        "•\tJede Folgezeile richtet sich an den Wörtern aus, nicht am "
        "Zeichen.\n•\tDas Zeichen hängt im Raum, den der negative Erstzeileneinzug "
        "öffnet.\n•\tEin langer Eintrag bricht im Block um und bleibt darin."
    ),
    numbered_before="Nummeriert: derselbe Block, verbreitert auf ",
    numbered_after=" Punkt für das breiteste Zeichen.",
    numbers=(
        "1.\tEine Zahl hängt genau wie ein Zeichen nach links heraus.\n2.\tDer Raum "
        "davor richtet sich nach dem breitesten Zeichen.\n10.\tAuch eine zweistellige "
        "Zahl endet vor dem Tabstopp des Texts."
    ),
    nested=(
        "Verschachtelt: Die innere Liste ist wieder derselbe Block, ihre Zeichen "
        "stehen dort, wo die äußeren Texte beginnen."
    ),
    outer=(
        "1.\tDas Dokument vorbereiten, die Schrift, mit der es zeichnet, und die Seite, die es füllt.",
        "2.\tJede Seite hinzufügen, mit dem Inhaltsstrom, aus dem sie gebaut wurde.",
    ),
    inner=(
        "a.\tDie Schrift aus ihrer Datei lesen und dem Dokument übergeben.\nb.\tDie "
        "Seitengröße nennen, in Punkt."
    ),
)

# 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 a_list(
    handle: hqf_pdf.FontHandle, marker: float, body: float
) -> hqf_pdf.TextFlow:
    """A list block whose markers begin `marker` points into the box and whose
    bodies begin `body` points into it.

    One call sets the three settings a list is made of, and there is no list engine
    under them: the left indent holds every line at the body, the first-line indent
    hangs that one line back out to the marker, and the single ruler stop is what
    the tabulation after the marker carries the pen to.
    """
    return hqf_pdf.TextFlow(handle, SIZE, leading=14.0).hanging_list(marker, body)


def heading(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    top: float,
    x: float,
    width: float,
    label: str,
) -> float:
    """Draws a heading across `width` and hands back the top of the block that goes
    under it, however many lines the heading itself took."""
    flow = hqf_pdf.TextFlow(handle, 8.0)
    lines = flow.break_lines(label, width)
    flow.draw(content, lines, x, top, width)
    return top - flow.height(lines) - DROP


def block(
    content: hqf_pdf.Content,
    flow: hqf_pdf.TextFlow,
    text: str,
    x: float,
    top: float,
    width: float,
) -> float:
    """Draws one block at `top` and hands back the top of whatever follows it."""
    lines = flow.break_lines(text, width)
    flow.draw(content, lines, x, top, width)
    return top - flow.height(lines)


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("lists.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    content = hqf_pdf.Content()

    # A bullet is narrow, so the body begins close in.
    top = heading(content, handle, TOP, X, NARROW, words.bulleted())
    block(content, a_list(handle, 0.0, BULLET_BODY), words.bullets, X, top, NARROW)

    # "10." is wider than "1.", and the room in front is what the widest of them needs.
    top = heading(content, handle, TOP, RIGHT, NARROW, words.numbered())
    block(
        content, a_list(handle, 0.0, NUMBER_BODY), words.numbers, RIGHT, top, NARROW
    )

    # Nesting is the same block again with its marker where the block above put its
    # body, each one set from the foot of the one before it.
    top = heading(content, handle, NEST_TOP, X, WIDE, words.nested)
    outer = a_list(handle, 0.0, NUMBER_BODY)
    inner = a_list(handle, INNER_MARKER, INNER_BODY)

    for index, item in enumerate(words.outer):
        top = block(content, outer, item, X, top, WIDE)
        if index == 0:
            top = block(content, inner, words.inner, X, top, WIDE)

    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()
