"""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 pend dans la place qu'ouvre 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éros : le même bloc, élargi à ",
    numbered_after=" points pour le marqueur le plus large.",
    numbers=(
        "1.\tUn numéro pend à gauche exactement comme une puce.\n"
        "2.\tLa place devant est celle qu'il faut au plus large des marqueurs, "
        "pas au plus étroit.\n"
        "10.\tUn numéro à deux chiffres finit encore avant le taquet où commence "
        "le corps."
    ),
    nested=(
        "Imbriqué : 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 qui le dessine et la page qu'il remplit.",
        "2.\tAjouter chaque page, avec le contenu dont elle a été composée.",
    ),
    inner=(
        "a.\tLire la police dans son fichier et la donner au document.\n"
        "b.\tDonner la taille de la page, en points."
    ),
)

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


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