"""Writes a table of quarterly takings whose every figure carries the headings that
reach it.

The Python twin of the `write_table_headers` example in Rust. A figure alone says
nothing: `118 400` is a number until something says which branch and which quarter it
belongs to. The table names its heading rows and its heading column, and the engine
works out, cell by cell, which headings reach each figure — so reading software that
reads the sheet aloud announces the town, the quarter and the year before the amount.

The table runs onto a second page, and it is one table there: the heading is drawn again
at the top of the page, and the document still says there is one of it.

Every word is held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
set. The branches and their takings are not words: they stand in one table, shared by
both languages.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# Where the sheet's text begins, in points from the left edge of the page.
LEFT = 56.0
# How wide it runs.
WIDTH = 483.0
# The size the heading is set in.
TITLE_SIZE = 20.0
# The size the opening paragraph is set in.
INTRO_SIZE = 10.0
# The size a line of the table is set in.
ROW_SIZE = 9.0
# The size the foot of the page is set in.
FOOT_SIZE = 8.5
# The top of the heading's box.
TITLE_TOP = 790.0
# The top of the opening paragraph.
INTRO_TOP = 752.0
# The top of the table on the first page, under the opening paragraph.
FIRST_TOP = 668.0
# The top of the table on every page after it.
NEXT_TOP = 790.0
# How far down the page the table may reach.
FOOT = 90.0
# The baseline the foot of the page is set on.
FOOT_BASELINE = 60.0
# How wide each of the four quarter columns is, in points.
QUARTER_WIDTH = 90.0
# When the sheet was drawn up. The library never reads the clock: a document that
# stamped itself with the time would be a different file on every build.
DRAWN_UP = "2026-08-08T09:30:00+02:00"


@dataclass(frozen=True)
class Words:
    """The words the sheet is written in, one set per language."""

    # The BCP 47 tag the document states.
    lang: str
    # What stands across the head of the sheet.
    title: str
    # The paragraph under it, which says what the sheet is showing.
    intro: str
    # The heading of the column the branches stand in.
    branch: str
    # What the four quarter columns are called.
    quarters: tuple[str, str, str, str]
    # What stands against the sum of every branch.
    every_branch: str
    # What opens the foot of the page, the page number following it.
    foot: str

    def page_number(self, index: int) -> str:
        """What the foot of page `index` reads, the pages counting from zero."""
        return f"{self.foot} {index + 1}"


# The sheet in English.
ENGLISH = Words(
    lang="en-GB",
    title="Takings by branch, quarter by quarter",
    intro=(
        "Every figure below carries the headings that reach it: the branch "
        "it stands beside, the quarter it stands under, and the year over the four "
        "quarters together. Nothing here was written by hand — the sheet says which "
        "rows and which column hold its headings, and the engine works out the rest. "
        "Read aloud, the first figure of the first line is announced as the town, the "
        "first quarter, the year, and then the amount."
    ),
    branch="Branch",
    quarters=("First quarter", "Second quarter", "Third quarter", "Fourth quarter"),
    every_branch="Every branch together",
    foot="Takings by branch, page",
)

# The sheet in French.
FRENCH = Words(
    lang="fr-FR",
    title="Recettes par agence, trimestre par trimestre",
    intro=(
        ("Chaque montant ci-dessous est rattaché aux titres qui le concernent : "
         "l’agence à côté de laquelle il se trouve, le trimestre au-dessus de lui, et "
         "l’année qui coiffe les quatre trimestres. Rien n’a été écrit à la main — la "
         "feuille indique quelles lignes et quelle colonne contiennent ses titres, et "
         "le moteur en déduit le reste. Lu à voix haute, le premier montant de la "
         "première ligne est annoncé avec la ville, le premier trimestre et l’année, "
         "puis sa valeur.")
    ),
    branch="Agence",
    quarters=("1er trimestre", "2e trimestre", "3e trimestre", "4e trimestre"),
    every_branch="Toutes agences réunies",
    foot="Recettes par agence, page",
)

# The sheet in Dutch.
DUTCH = Words(
    lang="nl-NL",
    title="Opbrengst per filiaal, kwartaal na kwartaal",
    intro=(
        "Elk getal hieronder heeft de koppen die erbij horen: het filiaal waarnaast "
        "het staat, het kwartaal waaronder het staat en het jaar boven de vier "
        "kwartalen samen. Niets hier is met de hand geschreven: het blad zegt welke "
        "rijen en welke kolom de koppen bevatten, en de motor berekent de rest. "
        "Voorgelezen wordt het eerste getal van de eerste regel aangekondigd als de "
        "stad, het eerste kwartaal, het jaar en dan het bedrag."
    ),
    branch="Filiaal",
    quarters=("Eerste kwartaal", "Tweede kwartaal", "Derde kwartaal", "Vierde kwartaal"),
    every_branch="Alle filialen samen",
    foot="Opbrengst per filiaal, pagina",
)

# The sheet in Portuguese.
PORTUGUESE = Words(
    lang="pt-PT",
    title="Receitas por balcão, trimestre a trimestre",
    intro=(
        "Cada número abaixo leva os títulos que lhe chegam: o balcão ao lado do qual "
        "está, o trimestre sob o qual está e o ano por cima dos quatro trimestres "
        "juntos. Nada aqui foi escrito à mão: a folha diz que linhas e que coluna têm "
        "os seus títulos, e o motor calcula o resto. Lido em voz alta, o primeiro "
        "número da primeira linha é anunciado como a cidade, o primeiro trimestre, o "
        "ano e depois o montante."
    ),
    branch="Balcão",
    quarters=("1.º trimestre", "2.º trimestre", "3.º trimestre", "4.º trimestre"),
    every_branch="Todos os balcões",
    foot="Receitas por balcão, página",
)

# The sheet in Italian.
ITALIAN = Words(
    lang="it-IT",
    title="Incassi per filiale, trimestre per trimestre",
    intro=(
        "Ogni cifra qui sotto porta le intestazioni che la raggiungono: la filiale "
        "accanto a cui sta, il trimestre sotto cui sta e l’anno sopra i quattro "
        "trimestri insieme. Niente è stato scritto a mano: il foglio dice quali righe "
        "e quale colonna portano le sue intestazioni, e il motore calcola il resto. "
        "Letta ad alta voce, la prima cifra della prima riga è annunciata come la "
        "città, il primo trimestre, l’anno e poi l’importo."
    ),
    branch="Filiale",
    quarters=("I trimestre", "II trimestre", "III trimestre", "IV trimestre"),
    every_branch="Tutte le filiali insieme",
    foot="Incassi per filiale, pagina",
)

# The sheet in Spanish.
SPANISH = Words(
    lang="es-ES",
    title="Recaudación por sucursal, trimestre a trimestre",
    intro=(
        "Cada cifra de abajo lleva los títulos que le corresponden: la sucursal junto "
        "a la que está, el trimestre bajo el que está y el año sobre los cuatro "
        "trimestres juntos. Nada se escribió a mano: la hoja dice qué filas y qué "
        "columna tienen sus títulos, y el motor calcula el resto. Leída en voz alta, "
        "la primera cifra de la primera línea se anuncia como la ciudad, el primer "
        "trimestre, el año y luego el importe."
    ),
    branch="Sucursal",
    quarters=("1.er trimestre", "2.º trimestre", "3.er trimestre", "4.º trimestre"),
    every_branch="Todas las sucursales",
    foot="Recaudación por sucursal, página",
)

# The sheet in German.
GERMAN = Words(
    lang="de-DE",
    title="Einnahmen je Filiale, Quartal für Quartal",
    intro=(
        "Jede Zahl unten trägt die Überschriften, die sie erreichen: die Filiale, "
        "neben der sie steht, das Quartal, unter dem sie steht, und das Jahr über "
        "allen vier Quartalen. Nichts hier wurde von Hand geschrieben — die Tabelle "
        "sagt, welche Zeilen und welche Spalte ihre Überschriften enthalten, und die "
        "Engine erledigt den Rest. Beim Vorlesen wird die erste Zahl der ersten Zeile "
        "als Stadt, erstes Quartal, Jahr und dann Betrag angekündigt."
    ),
    branch="Filiale",
    quarters=("Erstes Quartal", "Zweites Quartal", "Drittes Quartal", "Viertes Quartal"),
    every_branch="Alle Filialen zusammen",
    foot="Einnahmen je Filiale, Seite",
)

# Every language the sheet is written in.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}

# The year the four quarter columns stand under.
#
# A year is not a word: both languages set the same one.
YEAR = "2026"

# What every branch took in each of the four quarters, in whole euros.
#
# Names and figures are not words either: both languages set the same table.
BRANCHES = (
    ("Ashcombe", (118_400, 126_900, 131_250, 142_800)),
    ("Barrowfield", (94_200, 88_750, 96_400, 103_900)),
    ("Coldharbour", (151_300, 148_200, 155_900, 167_400)),
    ("Dunwater", (72_600, 79_450, 81_200, 84_750)),
    ("Eastmarch", (133_800, 129_400, 138_600, 145_100)),
    ("Fernbridge", (86_950, 91_300, 89_700, 97_250)),
    ("Greyharbour", (164_500, 171_200, 168_900, 179_600)),
    ("Hollowmead", (58_300, 61_750, 64_200, 66_800)),
    ("Ivybridge", (107_600, 112_400, 109_850, 118_300)),
    ("Kelburn", (123_900, 118_700, 126_500, 134_200)),
    ("Langmere", (69_400, 73_100, 71_850, 76_900)),
    ("Marchford", (142_700, 147_300, 151_600, 158_400)),
    ("Netherby", (81_200, 84_900, 87_450, 92_100)),
    ("Oakhaven", (176_800, 182_400, 179_300, 191_700)),
    ("Penwick", (63_500, 66_200, 68_900, 71_400)),
    ("Quarrington", (98_700, 103_400, 101_250, 108_600)),
    ("Ravensmoor", (136_200, 141_800, 139_400, 148_900)),
    ("Saltmarsh", (77_300, 81_600, 79_950, 85_200)),
    ("Thornbury", (159_400, 164_900, 162_300, 173_500)),
    ("Upwell", (54_800, 57_200, 59_600, 62_100)),
    ("Vellacourt", (112_500, 117_800, 115_200, 123_700)),
    ("Westhaven", (188_300, 194_600, 191_400, 203_900)),
    ("Yarrowdale", (66_700, 69_300, 71_800, 74_500)),
    ("Applegarth", (104_200, 108_900, 106_400, 114_300)),
    ("Bramblewick", (89_600, 93_200, 91_700, 98_400)),
    ("Cotterill", (147_900, 152_600, 149_800, 160_200)),
    ("Dellmoor", (71_300, 74_800, 76_500, 79_900)),
    ("Eaglestone", (129_700, 134_200, 132_600, 141_300)),
    ("Foxholt", (83_400, 87_100, 85_600, 91_800)),
    ("Greenslade", (155_800, 161_400, 158_900, 169_700)),
)


def figure(amount: int) -> str:
    """An amount, with a space between its thousands."""
    digits = str(amount)
    out = ""
    for index, digit in enumerate(digits):
        if index > 0 and (len(digits) - index) % 3 == 0:
            out += " "
        out += digit
    return out


def totals() -> list[int]:
    """What every branch took together in each of the four quarters."""
    sums = [0, 0, 0, 0]
    for __, quarters in BRANCHES:
        for column, took in enumerate(quarters):
            sums[column] += took
    return sums


def columns() -> hqf_pdf.Columns:
    """The five columns: the branches, then one for each quarter."""
    widths = [hqf_pdf.ColumnWidth.fraction(1.0)]
    widths.extend(hqf_pdf.ColumnWidth.points(QUARTER_WIDTH) for __ in range(4))
    return hqf_pdf.Columns(widths, WIDTH)


def cell(
    font: hqf_pdf.FontHandle,
    text: str,
    align: hqf_pdf.Align,
    span: int = 1,
    span_rows: int = 1,
    valign: hqf_pdf.VAlign = hqf_pdf.VAlign.Top,
) -> hqf_pdf.Cell:
    """A cell of the table, padded the way every cell of it is."""
    return hqf_pdf.Cell(
        font,
        ROW_SIZE,
        text,
        align=align,
        valign=valign,
        padding=hqf_pdf.Padding.symmetric(6.0, 7.0),
        span=span,
        span_rows=span_rows,
    )


def takings(font: hqf_pdf.FontHandle, words: Words) -> hqf_pdf.Table:
    """The table: two heading rows, one heading column, a line per branch, and a line
    adding every branch together.

    The heading of the branch column covers both heading rows, and the year covers the
    four quarter columns. Neither of those can be read by looking up a column and along
    a row, so the engine names the headings instead and writes, against every figure,
    which of them reach it.
    """
    table = hqf_pdf.Table(columns())
    table.header(2)
    table.row_headers(1)
    table.fill(hqf_pdf.Area.header(), hqf_pdf.Rgb.gray(0.90))
    table.rule(hqf_pdf.Rule.frame(), hqf_pdf.Stroke(0.8))
    hairline = hqf_pdf.Stroke(0.25, color=hqf_pdf.Rgb.gray(0.75))
    table.rule(hqf_pdf.Rule.horizontal_other(), hairline)
    table.rule(hqf_pdf.Rule.vertical_other(), hairline)
    table.rule(
        hqf_pdf.Rule.horizontal(2), hqf_pdf.Stroke(0.8)
    )

    table.push(
        hqf_pdf.Row(
            cells=[
                cell(
                    font,
                    words.branch,
                    hqf_pdf.Align.Left,
                    span_rows=2,
                    valign=hqf_pdf.VAlign.Middle,
                ),
                cell(font, YEAR, hqf_pdf.Align.Center, span=4),
            ]
        )
    )
    table.push(
        hqf_pdf.Row(
            cells=[cell(font, name, hqf_pdf.Align.Right) for name in words.quarters]
        )
    )

    for branch, took in BRANCHES:
        line = [cell(font, branch, hqf_pdf.Align.Left)]
        line.extend(cell(font, figure(amount), hqf_pdf.Align.Right) for amount in took)
        table.push(hqf_pdf.Row(cells=line))

    total = [cell(font, words.every_branch, hqf_pdf.Align.Left)]
    total.extend(cell(font, figure(amount), hqf_pdf.Align.Right) for amount in totals())
    table.push(hqf_pdf.Row(cells=total))

    return table


def heading(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    words: Words,
    marks: hqf_pdf.Marks,
) -> hqf_pdf.StructElement:
    """Draws the heading of the first page and hands back the element standing for it."""
    flow = hqf_pdf.TextFlow(font, TITLE_SIZE)
    lines = flow.break_lines(words.title, WIDTH)
    mcid = marks.next_id()
    content.begin_tagged("H1", mcid)
    flow.draw(content, lines, LEFT, TITLE_TOP, WIDTH)
    content.end_marked()
    return hqf_pdf.StructElement("H1", children=[(0, mcid)])


def opening(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    words: Words,
    marks: hqf_pdf.Marks,
) -> hqf_pdf.StructElement:
    """Draws the opening paragraph and hands back the element standing for it."""
    flow = hqf_pdf.TextFlow(font, INTRO_SIZE, align=hqf_pdf.Align.Justify)
    lines = flow.break_lines(words.intro, WIDTH)
    mcid = marks.next_id()
    content.begin_tagged("P", mcid)
    flow.draw(content, lines, LEFT, INTRO_TOP, WIDTH)
    content.end_marked()
    return hqf_pdf.StructElement("P", children=[(0, mcid)])


def foot(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, index: int
) -> None:
    """Draws the foot of a page, which belongs to the pagination and not to what the
    document says."""
    flow = hqf_pdf.TextFlow(font, FOOT_SIZE, align=hqf_pdf.Align.Right)
    numbered = words.page_number(index)
    lines = flow.break_lines(numbered, WIDTH)
    content.begin_artifact(hqf_pdf.Artifact.Footer)
    flow.draw(content, lines, LEFT, FOOT_BASELINE, WIDTH)
    content.end_marked()


def build(words: Words, font_path: Path) -> hqf_pdf.Document:
    """Builds the whole document: the heading, the opening paragraph, the table over as
    many pages as it needs, and the branch of the tree that says what all of it is."""
    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    # The accessibility claim, which is what a buyer's validator is pointed at: it is
    # judged against the tree, the artifacts, the embedded font and the title all at
    # once.
    document.set_ua_conformance(hqf_pdf.PdfUa.One)
    document.set_metadata(
        hqf_pdf.Metadata(
            title=words.title,
            producer="hqf-pdf",
            created=DRAWN_UP,
        )
    )
    document.set_info("Title", words.title)
    font = document.add_font(hqf_pdf.Font.from_path(font_path))

    table = takings(font, words)
    boxes = table.paginate(
        hqf_pdf.TableFrame(LEFT, FIRST_TOP, FIRST_TOP - FOOT),
        hqf_pdf.TableFrame(LEFT, NEXT_TOP, NEXT_TOP - FOOT),
    )

    # One table, however many pages draw it: every box is added to the same description,
    # and the heading drawn again at the top of each page is the heading it already
    # stands for.
    described = hqf_pdf.TaggedTable("takings")
    children = []
    for index, placement in enumerate(boxes):
        content = hqf_pdf.Content()
        marks = hqf_pdf.Marks()
        if index == 0:
            children.append(heading(content, font, words, marks))
            children.append(opening(content, font, words, marks))
        described.add(placement, content, index, marks)
        foot(content, font, words, index)
        page = hqf_pdf.Page.a4()
        page.set_content(content)
        document.add_page(page)
    children.append(described.finish())
    document.set_structure(
        hqf_pdf.StructureTree(
            children=[hqf_pdf.StructElement("Document", children=children)],
            lang=words.lang,
        )
    )
    return document


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

    written = build(words, _out.font_path()).write(out)
    print(f"wrote {out}: {written} bytes")


if __name__ == "__main__":
    main()
