"""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 a reader 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 porte les titres qui le concernent : "
        "l'agence à côté de laquelle il se trouve, le trimestre au-dessus de lui, et "
        "l'année qui couvre les quatre trimestres. Rien n'a été écrit à la main — la "
        "feuille dit quelles lignes et quelle colonne portent ses titres, et le moteur "
        "en déduit le reste. Lu à voix haute, le premier montant de la première ligne "
        "s'annonce par la ville, le premier trimestre, l'année, puis la somme."
    ),
    branch="Agence",
    quarters=("1er trimestre", "2e trimestre", "3e trimestre", "4e trimestre"),
    every_branch="Toutes agences réunies",
    foot="Recettes par agence, page",
)

# Every language the sheet is written in.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}

# 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, hqf_pdf.Rgb.gray(0.0)))
    hairline = hqf_pdf.Stroke(0.25, 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, hqf_pdf.Rgb.gray(0.0)))

    table.push(
        hqf_pdf.Row(
            [
                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([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(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(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()
