"""Creates the snag list drawn up when premises are handed over: a plan of the floor
with every defect marked on it, the list of those defects one to a line, and a summary
of what they come to.

The Python twin of the `write_snag_list` example in Rust: the same sheet, through the
binding rather than through the library directly.

The marks on the plan are annotations, which is what sets this sheet apart from every
other example here. An annotation sits above the page rather than inside what the page
draws: reading software lists it, filters it, shows it, hides it or leaves it off the
paper, and the plan underneath is untouched either way. The plan is drawn once; the
reserves come and go over it.

Five kinds of mark are used, one per shape the standard offers for this: a cloud round a
patch that is wholly under reserve, a ring on a defect that sits at one point, an arrow
whose closed head rests on what the note is about, the outline of a defect that spreads
over an area, and a run along a defect that follows a line. Every one of them carries
the words of its line in the list, so what a screen reader speaks and what the list
prints are the same sentence.

Every mark also says who made it, when, and which trade it is put to, which a reader
shows in the title bar of the window it opens the words in. The two blocking snags each
carry such a window, and the first of them opens it as soon as the file is opened,
beside the key to the colours. The electrician's answer to that snag is a note replying
to it, and the inspectors close the snag with a second note setting it to completed,
both gathered under the snag by a reader that threads replies.

Every figure on the sheet is worked out rather than written down. A room's area comes
from the rectangle the plan draws it as, at the scale the plan is set to. A degree's
count is the number of snags carrying it, its share is that count against the total, and
the date each snag is to be made good by is the handover plus the days its degree
allows. The last of those dates is the greatest of them.

Every word the sheet prints is held in `Words`, once per language, and `HQF_PDF_LANG`
picks which one it is printed in. What is not language stays out of it: the firm, the
site, the handover, the reference, the room rectangles, the counts, the areas, the
shares and the dates read the same whichever set of words is drawn.

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

from __future__ import annotations

from dataclasses import dataclass
from datetime import date, timedelta
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The space that holds a figure and the sign beside it together.
NO_BREAK = " "

# The sheet, in points: A4 upright.
PAGE_WIDTH = 595.276
PAGE_HEIGHT = 841.890

# The margins every page keeps clear.
LEFT = 56.0
RIGHT = PAGE_WIDTH - LEFT

# Where the first baseline of a page sits, and where its foot is written.
HEAD_TOP = 786.0
FOOT = 54.0

# The steps a page comes down by: between two lines of a block, between two blocks,
# between two blocks that stand apart, and between two rows of a table.
LINE = 12.0
BLOCK = 22.0
GAP = BLOCK * 2.0
ROW = 21.0

# The sizes the sheet is set at.
FIRM_SIZE = 15.0
TITLE_SIZE = 19.0
HEADING_SIZE = 11.0
BODY = 9.0
SMALL = 8.0
TINY = 6.5

# The near-black the sheet is set in, the grey its labels take, and the rule that parts
# two blocks.
INK = hqf_pdf.Rgb(0.11, 0.12, 0.14)
MUTED = hqf_pdf.Rgb(0.42, 0.44, 0.48)
RULE = hqf_pdf.Rgb(0.78, 0.79, 0.82)

# The grey a table's head band and its every other row are filled with.
HEAD_BAND = hqf_pdf.Rgb(0.90, 0.91, 0.93)
ROW_BAND = hqf_pdf.Rgb(0.965, 0.968, 0.972)

# The floor of a room, the wall round the premises, and the partition between two rooms.
FLOOR = hqf_pdf.Rgb(0.965, 0.960, 0.945)
SHELL_INK = hqf_pdf.Rgb(0.20, 0.22, 0.26)
PARTITION_INK = hqf_pdf.Rgb(0.52, 0.54, 0.58)

# The tint the outline of a spreading defect is filled with.
FOOTPRINT_TINT = hqf_pdf.Rgb(0.98, 0.93, 0.86)

# The colour every mark of a degree is drawn in.
MINOR_INK = hqf_pdf.Rgb(0.83, 0.60, 0.05)
MAJOR_INK = hqf_pdf.Rgb(0.87, 0.35, 0.05)
BLOCKING_INK = hqf_pdf.Rgb(0.76, 0.09, 0.14)

# The width a wall, a partition and a mark are stroked with, in points.
SHELL_WIDTH_PT = 1.6
PARTITION_WIDTH = 0.9
MARK_WIDTH = 1.2

# How far the arcs of a cloud bulge, from 0 to 2.
CLOUD_BULGE = 1.0

# The firm that inspected the premises, the premises themselves, and the reference the
# sheet is filed under. The same whichever language the sheet is printed in.
FIRM = "Vaugelade & Ferrand"
SITE = "Îlot Cassiopée, 42 quai de la Fonderie, 44200 Nantes"
INSPECTOR = "M. Ravel, Y. Sombath"
REFERENCE = "RS-2026-0914-03"

# The day the premises were handed over, as a year, a month and a day.
HANDOVER = (2026, 9, 14)

# Who answers for the electrical snags, the same whichever language the sheet is printed
# in.
ELECTRICIAN = "J. Morel"

# The hour on the day of the handover the marks were made at, the hour the electrician
# answered at, and the days after the handover the answer came.
MARKED_AT = "09:30:00+02:00"
ANSWERED_AT = "14:00:00+02:00"
ANSWERED_AFTER = 4

# The scale the plan is set to, and what turns a length on the premises into a length on
# the paper: a millimetre is 72/25.4 points, so a centimetre of the premises is a tenth
# of that, divided again by the scale.
SCALE_DENOMINATOR = 300.0
POINTS_PER_INCH = 72.0
MILLIMETRES_PER_INCH = 25.4
PLAN_SCALE = 10.0 * POINTS_PER_INCH / (SCALE_DENOMINATOR * MILLIMETRES_PER_INCH)

# The premises, in centimetres: how far the shell runs across and up.
SHELL_WIDTH = 4800.0
SHELL_HEIGHT = 2600.0

# Where the plan's own origin — the inner corner of the shell nearest the bottom left —
# sits on the page, in points. It is set across the middle of the text.
PLAN_LEFT = LEFT + ((RIGHT - LEFT) - SHELL_WIDTH * PLAN_SCALE) / 2.0
PLAN_BOTTOM = 370.0

# How far a room's name sits in from its own corner, in points.
PLAN_PAD = 5.0

# How far the code of a snag sits from the mark it names, in points.
CODE_OFFSET = 3.0

# Where the window of a blocking snag opens, in points: its left edge, its foot, its
# width and its height, to the right of the key to the colours.
WINDOW = (RIGHT - 170.0, 166.0, 170.0, 54.0)

# How far the electrician's answer, and below it the note closing the snag, sit from the
# anchor of the snag they answer, in points.
ANSWER_OFFSET = (18.0, -14.0)
CLOSING_OFFSET = (18.0, -38.0)


@dataclass(frozen=True)
class Room:
    """One room of the premises: the corner nearest the plan's origin and how far it
    runs, in centimetres.

    What the room is called is a word, and is held in `Words` at the place the room has
    here.
    """

    # The corner nearest the plan's origin.
    x: float
    y: float
    # How far the room runs across, and how far it runs up.
    width: float
    height: float


# The rooms, in the order the plan lays them out.
ROOMS = (
    Room(x=0.0, y=0.0, width=1800.0, height=1100.0),
    Room(x=0.0, y=1100.0, width=1800.0, height=1500.0),
    Room(x=1800.0, y=0.0, width=3000.0, height=1700.0),
    Room(x=1800.0, y=1700.0, width=3000.0, height=900.0),
)


@dataclass(frozen=True)
class Wall:
    """One run of wall the plan draws, in centimetres, and whether it holds the premises
    in or only parts two rooms.
    """

    # Where the run begins, and where it ends.
    start: tuple[float, float]
    stop: tuple[float, float]
    # Whether it is the shell rather than a partition.
    shell: bool


# Every run of wall, cut short at each doorway so the openings show.
WALLS = (
    Wall(start=(0.0, 0.0), stop=(200.0, 0.0), shell=True),
    Wall(start=(320.0, 0.0), stop=(4800.0, 0.0), shell=True),
    Wall(start=(4800.0, 0.0), stop=(4800.0, 2600.0), shell=True),
    Wall(start=(4800.0, 2600.0), stop=(0.0, 2600.0), shell=True),
    Wall(start=(0.0, 2600.0), stop=(0.0, 0.0), shell=True),
    Wall(start=(1800.0, 0.0), stop=(1800.0, 300.0), shell=False),
    Wall(start=(1800.0, 390.0), stop=(1800.0, 1750.0), shell=False),
    Wall(start=(1800.0, 1840.0), stop=(1800.0, 2600.0), shell=False),
    Wall(start=(0.0, 1100.0), stop=(700.0, 1100.0), shell=False),
    Wall(start=(790.0, 1100.0), stop=(1800.0, 1100.0), shell=False),
    Wall(start=(1800.0, 1700.0), stop=(2600.0, 1700.0), shell=False),
    Wall(start=(2690.0, 1700.0), stop=(4800.0, 1700.0), shell=False),
)


@dataclass(frozen=True)
class Degree:
    """How badly a snag stands in the way of the premises being used."""

    # The days the trade is given to make it good, counted from the handover.
    days: int
    # Its place in the table of words.
    place: int
    # The colour every mark of this degree is drawn in.
    ink: hqf_pdf.Rgb


MINOR = Degree(days=30, place=0, ink=MINOR_INK)
MAJOR = Degree(days=21, place=1, ink=MAJOR_INK)
BLOCKING = Degree(days=7, place=2, ink=BLOCKING_INK)

# Every degree, in the order the summary sets them out: the one that holds everything up
# first.
DEGREES = (BLOCKING, MAJOR, MINOR)


@dataclass(frozen=True)
class Cloud:
    """A cloud round a patch the whole of which is under reserve, in centimetres."""

    # The corner of the patch nearest the plan's origin.
    x: float
    y: float
    # How far the patch runs across, and how far it runs up.
    width: float
    height: float


@dataclass(frozen=True)
class Ring:
    """A ring round a defect that sits at one point, in centimetres."""

    # Where the defect sits.
    x: float
    y: float
    # How far the ring stands off it.
    radius: float


@dataclass(frozen=True)
class Arrow:
    """An arrow whose head rests on what the note is about, in centimetres."""

    # The tail, where the code of the snag is written, and the head.
    start: tuple[float, float]
    stop: tuple[float, float]


@dataclass(frozen=True)
class Footprint:
    """The outline of a defect that spreads over an area, in centimetres."""

    # Its corners, in the order they are joined.
    corners: tuple[tuple[float, float], ...]


@dataclass(frozen=True)
class Run:
    """A run along a defect that follows a line, in centimetres."""

    # The points it passes through, in order.
    along: tuple[tuple[float, float], ...]


@dataclass(frozen=True)
class Snag:
    """One snag: where it was found, how badly it stands in the way, who answers for it,
    and how it is marked on the plan.

    What the defect is is a word, and is held in `Words` at the place the snag has here.
    """

    # The room it was found in, as its place in the table of rooms.
    room: int
    # How badly it stands in the way.
    degree: Degree
    # The trade that answers for it, as its place in the table of trades.
    trade: int
    # How it is marked on the plan.
    mark: Cloud | Ring | Arrow | Footprint | Run


# The corners of the one defect that spreads over an area, and the points the one that
# follows a line passes through.
SPREAD = (
    (2200.0, 300.0),
    (3400.0, 300.0),
    (3400.0, 900.0),
    (2900.0, 1120.0),
    (2200.0, 900.0),
)
ALONG = ((1810.0, 1880.0), (1810.0, 2220.0), (1810.0, 2560.0))

# The snags, in the order they were found.
SNAGS = (
    Snag(room=0, degree=MAJOR, trade=0, mark=Ring(x=260.0, y=150.0, radius=130.0)),
    Snag(
        room=0,
        degree=MINOR,
        trade=1,
        mark=Cloud(x=900.0, y=780.0, width=780.0, height=240.0),
    ),
    Snag(room=1, degree=MINOR, trade=2, mark=Ring(x=900.0, y=1850.0, radius=170.0)),
    Snag(room=1, degree=MAJOR, trade=3, mark=Run(along=ALONG)),
    Snag(room=2, degree=MAJOR, trade=4, mark=Footprint(corners=SPREAD)),
    Snag(
        room=2,
        degree=BLOCKING,
        trade=5,
        mark=Arrow(start=(4260.0, 980.0), stop=(3620.0, 130.0)),
    ),
    Snag(room=2, degree=MINOR, trade=5, mark=Ring(x=4380.0, y=1360.0, radius=160.0)),
    Snag(
        room=3,
        degree=BLOCKING,
        trade=6,
        mark=Cloud(x=2250.0, y=1780.0, width=520.0, height=520.0),
    ),
    Snag(
        room=3,
        degree=MAJOR,
        trade=7,
        mark=Arrow(start=(3540.0, 1840.0), stop=(4280.0, 2400.0)),
    ),
)

# The right edge of each of the six columns of the list, measured from the left margin,
# in points.
LIST_COLUMNS = (28.0, 124.0, 282.0, 340.0, 408.0, 483.276)

# The right edge of each of the three columns the summary counts a degree or a trade in,
# and of the four it counts a room in.
COUNT_COLUMNS = (190.0, 260.0, 330.0)
ROOM_COLUMNS = (190.0, 275.0, 345.0, 415.0)


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

    What is not language stays out of it: the firm, the site, the inspectors, the
    reference, the handover, the room rectangles, the counts, the areas, the shares and
    the dates are drawn from data of their own and read the same in every language.
    """

    # What the file says it is, and the line under the firm's name.
    title: str
    tagline: str
    # The four marks at the head of the first page.
    site: str
    handover: str
    reference: str
    inspected: str
    # What the plan is headed, and what its scale stands under.
    plan: str
    scale: str
    # The legend under the plan, and one line for each kind of mark.
    legend: str
    cloud: str
    ring: str
    arrow: str
    footprint: str
    run: str
    # The key to the colours, and how long a degree is given, worded round the number of
    # days.
    key: str
    within: str
    # What the reader is told about the marks, over two lines.
    note: tuple[str, str]
    # The three degrees, in the order their place gives them.
    degrees: tuple[str, str, str]
    # The rooms, in the order the plan lays them out.
    rooms: tuple[str, str, str, str]
    # The trades that answer for the snags.
    trades: tuple[str, str, str, str, str, str, str, str]
    # The nine snags, in the order the list holds them.
    natures: tuple[str, str, str, str, str, str, str, str, str]
    # The heads of the six columns of the list.
    number: str
    room: str
    nature: str
    degree: str
    due: str
    trade: str
    # What the date a snag is to be made good by stands under, in a sentence rather than
    # at the head of a column.
    repair: str
    # What the list and the summary are headed.
    list: str
    summary: str
    # The heads of the three tables of the summary and of their own columns.
    by_degree: str
    by_room: str
    by_trade: str
    counted: str
    share: str
    area: str
    total: str
    # The last line of the summary.
    latest: str
    # The two words the foot of every page numbers it with.
    page: str
    of: str
    # What the electrician answers the first blocking snag with, in the note replying to
    # it.
    answer: str


# The sheet in English.
ENGLISH = Words(
    title="Snag list",
    tagline="Building surveyors",
    site="SITE",
    handover="HANDOVER",
    reference="REFERENCE",
    inspected="INSPECTED BY",
    plan="The floor, and where each snag sits",
    scale="Scale",
    legend="What each mark means",
    cloud="Cloud — the whole of the patch it runs round is under reserve.",
    ring="Ring — a defect that sits at one point.",
    arrow="Arrow — the head rests on what the note is about.",
    footprint="Outline — a defect that spreads over an area of its own.",
    run="Run — a defect that follows a line, a crack, a joint or a skirting.",
    key="What each colour means",
    within="made good within {days} days of the handover",
    note=(
        "Every mark on the plan says the same words as its line in the list.",
        "Reading software shows them, hides them or leaves them off the paper.",
    ),
    degrees=("Minor", "Major", "Blocking"),
    rooms=("Reception", "Meeting room", "Open office", "Service core"),
    trades=(
        "Joinery",
        "Painting",
        "Plastering",
        "Glazing",
        "Floor covering",
        "Electrics",
        "Plumbing",
        "Air handling",
    ),
    natures=(
        "Entrance door rubs on the floor",
        "Paint runs on the end partition",
        "Ceiling tile cracked over table",
        "Glazed partition scratched",
        "Floor covering lifting at joints",
        "Three sockets dead on south run",
        "Luminaire missing over copier",
        "Water at the foot of the riser",
        "Extract grille not connected",
    ),
    number="NO.",
    room="ROOM",
    nature="WHAT WAS FOUND",
    degree="DEGREE",
    due="MAKE GOOD BY",
    trade="TRADE",
    repair="to be made good by",
    list="The snags, one to a line",
    summary="What the snags come to",
    by_degree="Gathered by degree",
    by_room="Gathered by room",
    by_trade="Gathered by trade",
    counted="SNAGS",
    share="SHARE",
    area="AREA",
    total="TOTAL",
    latest="The last of the dates the work is to be made good by",
    page="Page",
    of="of",
    answer="South run rewired: the three sockets are live again.",
)

# The sheet in French.
FRENCH = Words(
    title="Liste de réserves",
    tagline="Cabinet d’expertise du bâtiment",
    site="CHANTIER",
    handover="RÉCEPTION",
    reference="RÉFÉRENCE",
    inspected="VISITE FAITE PAR",
    plan="Le plateau et l’emplacement des réserves",
    scale="Échelle",
    legend="Ce que dit chaque marque",
    cloud="Nuage — toute la surface qu’il entoure est sous réserve.",
    ring="Cercle — un défaut localisé en un point.",
    arrow="Flèche — la pointe se pose sur ce qui est en cause.",
    footprint="Contour — un défaut qui s’étend sur une surface.",
    run="Filet — un défaut qui suit une ligne : fissure, joint ou plinthe.",
    key="Ce que dit chaque couleur",
    within="à lever dans les {days} jours suivant la réception",
    note=(
        "Chaque marque du plan reprend les mots de sa ligne dans la liste.",
        "Le logiciel de lecture les affiche, les masque ou ne les imprime pas.",
    ),
    degrees=("Mineure", "Majeure", "Bloquante"),
    rooms=("Accueil", "Salle de réunion", "Plateau ouvert", "Locaux techniques"),
    trades=(
        "Menuiserie",
        "Peinture",
        "Plâtrerie",
        "Vitrerie",
        "Sols souples",
        "Électricité",
        "Plomberie",
        "Ventilation",
    ),
    natures=(
        "Porte d’entrée qui frotte au sol",
        "Peinture coulée, cloison du fond",
        "Dalle fendue au-dessus de la table",
        "Cloison vitrée rayée",
        "Sol souple décollé aux joints",
        "Trois prises hors tension côté sud",
        "Luminaire absent côté copieur",
        "Eau au pied de la colonne",
        "Grille d’extraction non raccordée",
    ),
    number="N°",
    room="LOCAL",
    nature="CE QUI A ÉTÉ RELEVÉ",
    degree="DEGRÉ",
    due="LEVÉE AVANT LE",
    trade="CORPS D’ÉTAT",
    repair="à lever avant le",
    list="Les réserves, une par ligne",
    summary="Bilan des réserves",
    by_degree="Par degré",
    by_room="Par local",
    by_trade="Par corps d’état",
    counted="RÉSERVES",
    share="PART",
    area="SURFACE",
    total="TOTAL",
    latest="La dernière des dates de levée",
    page="Page",
    of="sur",
    answer="Ligne sud recâblée : les trois prises sont de nouveau sous tension.",
)

# The sheet in Dutch.
DUTCH = Words(
    title="Opleverlijst",
    tagline="Bouwkundig adviseurs",
    site="LOCATIE",
    handover="OPLEVERING",
    reference="REFERENTIE",
    inspected="GEÏNSPECTEERD DOOR",
    plan="De verdieping, en waar elk gebrek zit",
    scale="Schaal",
    legend="Wat elke markering betekent",
    cloud="Wolk: het hele vlak dat ze omsluit, staat onder voorbehoud.",
    ring="Ring: een gebrek op één punt.",
    arrow="Pijl: de punt wijst naar waar de notitie over gaat.",
    footprint="Omtrek: een gebrek dat zich over een eigen vlak uitstrekt.",
    run="Lijn: een gebrek dat een lijn volgt, een scheur, een voeg of een plint.",
    key="Wat elke kleur betekent",
    within="hersteld binnen {days} dagen na oplevering",
    note=(
        "Elke markering op de plattegrond zegt dezelfde woorden als haar regel in de lijst.",
        "Leessoftware toont ze, verbergt ze of laat ze van het papier weg.",
    ),
    degrees=("Klein", "Groot", "Blokkerend"),
    rooms=("Receptie", "Vergaderruimte", "Kantoortuin", "Technische kern"),
    trades=(
        "Timmerwerk",
        "Schilderwerk",
        "Stucwerk",
        "Beglazing",
        "Vloerbedekking",
        "Elektra",
        "Sanitair",
        "Ventilatie",
    ),
    natures=(
        "Toegangsdeur sleept over de vloer",
        "Verflopers op de achterwand",
        "Plafondplaat gebarsten boven tafel",
        "Glazen wand bekrast",
        "Vloer komt los bij de naden",
        "Drie stopcontacten zonder stroom",
        "Geen lamp boven de kopieerder",
        "Water onder aan de stijgleiding",
        "Afzuigrooster niet aangesloten",
    ),
    number="NR.",
    room="RUIMTE",
    nature="WAT ER GEVONDEN IS",
    degree="ERNST",
    due="GEREED OP",
    trade="VAKGEBIED",
    repair="te herstellen vóór",
    list="De gebreken, één per regel",
    summary="Waar de gebreken op neerkomen",
    by_degree="Gegroepeerd per ernst",
    by_room="Gegroepeerd per ruimte",
    by_trade="Gegroepeerd per vakgebied",
    counted="GEBREKEN",
    share="AANDEEL",
    area="GEBIED",
    total="TOTAAL",
    latest="De laatste van de data waarop het werk hersteld moet zijn",
    page="Pagina",
    of="van",
    answer="Zuidgroep opnieuw bedraad: de drie stopcontacten staan weer onder spanning.",
)

# The sheet in Portuguese.
PORTUGUESE = Words(
    title="Lista de reservas",
    tagline="Peritos de construção",
    site="OBRA",
    handover="ENTREGA",
    reference="REFERÊNCIA",
    inspected="INSPECIONADO POR",
    plan="O piso, e onde fica cada reserva",
    scale="Escala",
    legend="O que significa cada marca",
    cloud="Nuvem: toda a zona que contorna está sob reserva.",
    ring="Anel: um defeito situado num ponto.",
    arrow="Seta: a ponta assenta naquilo de que a nota fala.",
    footprint="Contorno: um defeito que se estende por uma zona própria.",
    run="Percurso: um defeito que segue uma linha, uma fissura, uma junta ou um rodapé.",
    key="O que significa cada cor",
    within="corrigido nos {days} dias seguintes à entrega",
    note=(
        "Cada marca na planta diz as mesmas palavras que a sua linha na lista.",
        "O programa de leitura mostra-as, oculta-as ou deixa-as fora do papel.",
    ),
    degrees=("Ligeira", "Grave", "Bloqueante"),
    rooms=("Receção", "Sala de reuniões", "Open space", "Núcleo técnico"),
    trades=(
        "Carpintaria",
        "Pintura",
        "Estuque",
        "Vidros",
        "Piso",
        "Eletricidade",
        "Canalização",
        "Climatização",
    ),
    natures=(
        "A porta de entrada roça no chão",
        "Escorridos na divisória do fundo",
        "Placa de teto rachada sobre a mesa",
        "Divisória envidraçada riscada",
        "Piso a levantar nas juntas",
        "Três tomadas sem corrente (sul)",
        "Falta a luz sobre a fotocopiadora",
        "Água na base da coluna montante",
        "Grelha de extração por ligar",
    ),
    number="N.º",
    room="DIVISÃO",
    nature="O QUE FOI ENCONTRADO",
    degree="GRAU",
    due="CORRIGIR ATÉ",
    trade="ESPECIALIDADE",
    repair="a corrigir até",
    list="As reservas, uma por linha",
    summary="A que somam as reservas",
    by_degree="Agrupadas por grau",
    by_room="Agrupadas por divisão",
    by_trade="Agrupadas por especialidade",
    counted="RESERVAS",
    share="PARTE",
    area="ZONA",
    total="TOTAL",
    latest="A última das datas até às quais o trabalho deve ser corrigido",
    page="Página",
    of="de",
    answer="Troço sul reinstalado: as três tomadas voltaram a ter corrente.",
)

# The sheet in Italian.
ITALIAN = Words(
    title="Elenco delle riserve",
    tagline="Periti edili",
    site="CANTIERE",
    handover="CONSEGNA",
    reference="RIFERIMENTO",
    inspected="ISPEZIONATO DA",
    plan="La pianta, e dove sta ogni riserva",
    scale="Scala",
    legend="Cosa significa ogni segno",
    cloud="Nuvola: tutta l’area che circonda è sotto riserva.",
    ring="Anello: un difetto in un punto.",
    arrow="Freccia: la punta indica ciò di cui parla la nota.",
    footprint="Contorno: un difetto che si estende su un’area propria.",
    run="Percorso: un difetto che segue una linea, una crepa, un giunto o un battiscopa.",
    key="Cosa significa ogni colore",
    within="da sistemare entro {days} giorni dalla consegna",
    note=(
        "Ogni segno sulla pianta dice le stesse parole della sua riga nell’elenco.",
        "Il programma di lettura li mostra, li nasconde o li lascia fuori dalla carta.",
    ),
    degrees=("Lieve", "Grave", "Bloccante"),
    rooms=("Reception", "Sala riunioni", "Open space", "Nucleo tecnico"),
    trades=(
        "Falegnameria",
        "Tinteggiatura",
        "Intonaco",
        "Vetrate",
        "Pavimento",
        "Elettrico",
        "Idraulica",
        "Climatizzazione",
    ),
    natures=(
        "La porta d’ingresso sfrega a terra",
        "Colature sulla parete di fondo",
        "Pannello incrinato sopra il tavolo",
        "Parete vetrata graffiata",
        "Pavimento che si solleva ai giunti",
        "Tre prese senza corrente, linea sud",
        "Manca la luce sulla fotocopiatrice",
        "Acqua alla base del montante",
        "Griglia di estrazione non collegata",
    ),
    number="N.",
    room="LOCALE",
    nature="COSA SI È TROVATO",
    degree="GRADO",
    due="SCADENZA",
    trade="IMPRESA",
    repair="da sistemare entro il",
    list="Le riserve, una per riga",
    summary="A cosa ammontano le riserve",
    by_degree="Raggruppate per grado",
    by_room="Raggruppate per locale",
    by_trade="Raggruppate per impresa",
    counted="RISERVE",
    share="QUOTA",
    area="ZONA",
    total="TOTALE",
    latest="L’ultima delle date entro cui il lavoro va sistemato",
    page="Pagina",
    of="di",
    answer="Linea sud ricablata: le tre prese sono di nuovo sotto tensione.",
)

# The sheet in Spanish.
SPANISH = Words(
    title="Lista de reservas",
    tagline="Peritos de edificación",
    site="OBRA",
    handover="ENTREGA",
    reference="REFERENCIA",
    inspected="INSPECCIONADO POR",
    plan="La planta, y dónde está cada reserva",
    scale="Escala",
    legend="Qué significa cada marca",
    cloud="Nube: todo el recinto que rodea está bajo reserva.",
    ring="Anillo: un defecto situado en un punto.",
    arrow="Flecha: la punta señala aquello de lo que habla la nota.",
    footprint="Contorno: un defecto que se extiende por una zona propia.",
    run="Recorrido: un defecto que sigue una línea, una grieta, una junta o un rodapié.",
    key="Qué significa cada color",
    within="subsanado en los {days} días siguientes a la entrega",
    note=(
        "Cada marca del plano dice lo mismo que su línea de la lista.",
        "El programa de lectura las muestra, las oculta o las deja fuera del papel.",
    ),
    degrees=("Leve", "Grave", "Bloqueante"),
    rooms=("Recepción", "Sala de reuniones", "Oficina diáfana", "Núcleo técnico"),
    trades=(
        "Carpintería",
        "Pintura",
        "Yesería",
        "Acristalamiento",
        "Suelo",
        "Electricidad",
        "Fontanería",
        "Climatización",
    ),
    natures=(
        "La puerta de entrada roza el suelo",
        "Chorretones en el tabique del fondo",
        "Placa de techo rajada sobre la mesa",
        "Mampara acristalada rayada",
        "Suelo levantado en las juntas",
        "Tres enchufes sin corriente (sur)",
        "Sin lámpara sobre la fotocopiadora",
        "Agua al pie del montante",
        "Rejilla de extracción sin conectar",
    ),
    number="N.º",
    room="ESTANCIA",
    nature="QUÉ SE ENCONTRÓ",
    degree="GRADO",
    due="FECHA LÍMITE",
    trade="GREMIO",
    repair="a subsanar antes del",
    list="Las reservas, una por línea",
    summary="Lo que suman las reservas",
    by_degree="Agrupadas por grado",
    by_room="Agrupadas por estancia",
    by_trade="Agrupadas por gremio",
    counted="RESERVAS",
    share="PARTE",
    area="ZONA",
    total="TOTAL",
    latest="La última de las fechas en que debe subsanarse el trabajo",
    page="Página",
    of="de",
    answer="Tramo sur recableado: los tres enchufes vuelven a tener corriente.",
)

# The sheet in German.
GERMAN = Words(
    title="Mängelliste",
    tagline="Bausachverständige",
    site="BAUSTELLE",
    handover="ÜBERGABE",
    reference="REFERENZ",
    inspected="GEPRÜFT VON",
    plan="Das Geschoss, und wo jeder Mangel liegt",
    scale="Maßstab",
    legend="Was jede Markierung bedeutet",
    cloud="Wolke — die ganze umrandete Fläche steht unter Vorbehalt.",
    ring="Kreis — ein Mangel an einem Punkt.",
    arrow="Pfeil — die Spitze zeigt auf das, worum es in der Notiz geht.",
    footprint="Umriss — ein Mangel, der sich über eine eigene Fläche erstreckt.",
    run="Linienzug — ein Mangel entlang einer Linie, eines Risses, einer Fuge oder einer Sockelleiste.",
    key="Was jede Farbe bedeutet",
    within="binnen {days} Tagen nach der Übergabe zu beheben",
    note=(
        "Jede Markierung im Plan sagt dieselben Worte wie ihre Zeile in der Liste.",
        "Eine Lesesoftware zeigt sie, verbirgt sie oder lässt sie auf dem Papier weg.",
    ),
    degrees=("Gering", "Erheblich", "Sperrend"),
    rooms=("Empfang", "Besprechung", "Großraumbüro", "Technik"),
    trades=(
        "Tischler",
        "Maler",
        "Putz",
        "Verglasung",
        "Bodenbelag",
        "Elektro",
        "Sanitär",
        "Lüftung",
    ),
    natures=(
        "Eingangstür schleift am Boden",
        "Farbläufer an Endtrennwand",
        "Deckenplatte über Tisch gerissen",
        "Glastrennwand zerkratzt",
        "Bodenbelag hebt sich an Fugen",
        "Drei Steckdosen Süd ohne Strom",
        "Leuchte über Kopierer fehlt",
        "Wasser am Fuß des Schachts",
        "Abluftgitter nicht angeschlossen",
    ),
    number="NR.",
    room="RAUM",
    nature="BEFUND",
    degree="GRAD",
    due="BEHEBEN BIS",
    trade="GEWERK",
    repair="zu beheben bis",
    list="Die Mängel, einer je Zeile",
    summary="Was die Mängel ergeben",
    by_degree="Nach Grad",
    by_room="Nach Raum",
    by_trade="Nach Gewerk",
    counted="MÄNGEL",
    share="ANTEIL",
    area="BEREICH",
    total="SUMME",
    latest="Der letzte der Termine, bis zu denen die Mängel zu beheben sind",
    page="Seite",
    of="von",
    answer="Südstrang neu verdrahtet: Die drei Steckdosen sind wieder unter Strom.",
)

# 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 grouped(digits: str) -> str:
    """``digits``, its thousands parted by a no-break space, which is how every language
    this example is written in parts them.
    """
    out = []
    for index, digit in enumerate(digits):
        if index > 0 and (len(digits) - index) % 3 == 0:
            out.append(NO_BREAK)
        out.append(digit)
    return "".join(out)


def counted(value: int) -> str:
    """A count, written as the sheet writes one."""
    return grouped(str(value))


def measured(value: float) -> str:
    """A measurement, written with one decimal, its thousands parted by a no-break space
    and its decimal by a point.
    """
    written_out = f"{value:.1f}"
    whole, _, fraction = written_out.partition(".")
    return f"{grouped(whole)}.{fraction}"


def share(count: int, total: int) -> str:
    """What ``count`` out of ``total`` comes to, as a share of a hundred rounded to the
    nearest tenth.
    """
    tenths = (count * 1000 + total // 2) // total
    return f"{grouped(str(tenths // 10))}.{tenths % 10}{NO_BREAK}%"


def code(place: int) -> str:
    """The code the sheet knows the snag at ``place`` by."""
    return f"S-{place + 1:02d}"


def area(room: Room) -> float:
    """The floor area of ``room``, in square metres."""
    return room.width * room.height / 10_000.0


def total_area() -> float:
    """The floor area of the whole premises, in square metres."""
    return sum(area(room) for room in ROOMS)


def found_in(place: int) -> int:
    """How many snags were found in the room at ``place``."""
    return sum(1 for snag in SNAGS if snag.room == place)


def answered_for(place: int) -> int:
    """How many snags the trade at ``place`` answers for."""
    return sum(1 for snag in SNAGS if snag.trade == place)


def found_at(degree: Degree) -> int:
    """How many snags carry ``degree``."""
    return sum(1 for snag in SNAGS if snag.degree == degree)


def handover() -> date:
    """The day the premises were handed over."""
    return date(HANDOVER[0], HANDOVER[1], HANDOVER[2])


def due(degree: Degree) -> date:
    """The day a snag of ``degree`` is to be made good by: the handover plus the days
    that degree allows.
    """
    return handover() + timedelta(days=degree.days)


def latest() -> date:
    """The last of the days the snags are to be made good by."""
    last = handover()
    for snag in SNAGS:
        last = max(last, due(snag.degree))
    return last


def marked_at() -> str:
    """The moment the marks were made: the handover, at the hour they were made."""
    return f"{written(handover())}T{MARKED_AT}"


def answered_at() -> str:
    """The moment the electrician answered, some days after the handover."""
    return f"{written(handover() + timedelta(days=ANSWERED_AFTER))}T{ANSWERED_AT}"


def first_blocking() -> int:
    """The place of the first blocking snag in the list: the one whose window opens with
    the file, and the one the electrician answers.
    """
    return next(place for place, snag in enumerate(SNAGS) if snag.degree == BLOCKING)


def window(place: int, snag: Snag) -> hqf_pdf.Popup | None:
    """The window a snag's words open in, for a blocking snag: open with the file for the
    first of them, closed for the others. Any other snag has none.
    """
    if snag.degree != BLOCKING:
        return None
    x, y, width, height = WINDOW
    popup = hqf_pdf.Popup(x, y, width, height)
    return popup.open() if place == first_blocking() else popup


def written(day: date) -> str:
    """A day, written from its largest unit to its smallest, which is how every language
    this example is written in writes one on a sheet like this.
    """
    return f"{day.year:04d}-{day.month:02d}-{day.day:02d}"


def allowed(degree: Degree, words: Words) -> str:
    """How long a degree is given, in that language's words."""
    return words.within.replace("{days}", grouped(str(degree.days)))


def across(centimetres: float) -> float:
    """How far a length on the premises runs on the page."""
    return centimetres * PLAN_SCALE


def at_x(centimetres: float) -> float:
    """Where a length on the premises falls on the page, across."""
    return PLAN_LEFT + across(centimetres)


def at_y(centimetres: float) -> float:
    """Where a length on the premises falls on the page, up."""
    return PLAN_BOTTOM + across(centimetres)


def plotted(corners: tuple[tuple[float, float], ...]) -> list[tuple[float, float]]:
    """The corners of a mark, as they fall on the page."""
    return [(at_x(x), at_y(y)) for x, y in corners]


def anchor(mark: Cloud | Ring | Arrow | Footprint | Run) -> tuple[float, float]:
    """Where the code of the snag is written, in centimetres."""
    if isinstance(mark, Cloud):
        return (mark.x, mark.y + mark.height)
    if isinstance(mark, Ring):
        return (mark.x + mark.radius, mark.y + mark.radius)
    if isinstance(mark, Arrow):
        return mark.start
    if isinstance(mark, Footprint):
        return mark.corners[0]
    return mark.along[0]


def text(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    x: float,
    y: float,
    color: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text, left-aligned, in a colour of its own."""
    content.set_fill(color)
    content.draw_text(font, size, x, y, s)


def text_right(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    right: float,
    y: float,
    color: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text whose right edge sits at ``right``."""
    text(content, font, size, right - font.measure(s, size), y, color, s)


def rule(content: hqf_pdf.Content, y: float) -> None:
    """Draws a rule across the width of the text."""
    content.set_stroke(RULE)
    content.set_line_width(0.6)
    content.move_to(LEFT, y)
    content.line_to(RIGHT, y)
    content.stroke()


def band(content: hqf_pdf.Content, y: float, height: float, color: hqf_pdf.Rgb) -> None:
    """Fills a band the width of the text, from ``y`` up by ``height``."""
    content.set_fill(color)
    content.rect(LEFT, y, RIGHT - LEFT, height)
    content.fill()


def head(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, top: float
) -> float:
    """Draws the firm and what the sheet is, and hands back the baseline it ends on."""
    y = top
    text(content, font, FIRM_SIZE, LEFT, y, INK, FIRM)
    text_right(content, font, SMALL, RIGHT, y, MUTED, REFERENCE)
    y -= LINE
    text(content, font, SMALL, LEFT, y, MUTED, words.tagline)
    y -= BLOCK + LINE
    text(content, font, TITLE_SIZE, LEFT, y, INK, words.title)
    return y


def marks(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, top: float
) -> float:
    """Draws the four marks that say which premises the sheet is about, and hands back
    the baseline it ends on.
    """
    y = top
    for label, value in (
        (words.site, SITE),
        (words.handover, written(handover())),
        (words.inspected, INSPECTOR),
        (words.reference, REFERENCE),
    ):
        text(content, font, TINY, LEFT, y, MUTED, label)
        text(content, font, BODY, LEFT + 96.0, y, INK, value)
        y -= LINE + 2.0
    return y


def floor(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the floor: the rooms it is cut into, the wall round it, and what each room
    is called and comes to.
    """
    for room in ROOMS:
        content.set_fill(FLOOR)
        content.rect(at_x(room.x), at_y(room.y), across(room.width), across(room.height))
        content.fill()

    for wall in WALLS:
        color = SHELL_INK if wall.shell else PARTITION_INK
        width = SHELL_WIDTH_PT if wall.shell else PARTITION_WIDTH
        content.set_stroke(color)
        content.set_line_width(width)
        content.move_to(at_x(wall.start[0]), at_y(wall.start[1]))
        content.line_to(at_x(wall.stop[0]), at_y(wall.stop[1]))
        content.stroke()

    for room, name in zip(ROOMS, words.rooms):
        top = at_y(room.y + room.height) - PLAN_PAD - SMALL
        text(content, font, SMALL, at_x(room.x) + PLAN_PAD, top, INK, name)
        measure = f"{measured(area(room))} m²"
        text(content, font, TINY, at_x(room.x) + PLAN_PAD, top - LINE, MUTED, measure)


def codes(content: hqf_pdf.Content, font: hqf_pdf.FontHandle) -> None:
    """Draws the code of every snag beside the mark that stands for it."""
    for place, snag in enumerate(SNAGS):
        x, y = anchor(snag.mark)
        text(
            content,
            font,
            TINY,
            at_x(x) + CODE_OFFSET,
            at_y(y) + CODE_OFFSET,
            snag.degree.ink,
            code(place),
        )


def spoken(place: int, snag: Snag, words: Words) -> str:
    """The words reading software speaks in place of a mark: the same line the list
    prints, run together.
    """
    return (
        f"{code(place)} — {words.rooms[snag.room]} — {words.natures[place]} — "
        f"{words.degrees[snag.degree.place]} — {words.repair} "
        f"{written(due(snag.degree))}"
    )


def marked(place: int, snag: Snag, words: Words):
    """The mark one snag wears on the plan, with who made it, when, the trade it is put
    to, and the window its words open in.
    """
    mark = (
        shaped(place, snag, words)
        .author(INSPECTOR)
        .subject(words.trades[snag.trade])
        .created(marked_at())
    )
    opens = window(place, snag)
    return mark if opens is None else mark.popup(opens)


def answers(words: Words) -> list[hqf_pdf.StickyNoteAnnotation]:
    """The two notes answering the first blocking snag: the electrician's answer, and
    below it the inspectors' note setting the snag to completed.
    """
    answered = first_blocking()
    x, y = anchor(SNAGS[answered].mark)
    x, y = at_x(x), at_y(y)
    return [
        hqf_pdf.StickyNoteAnnotation(
            x + ANSWER_OFFSET[0], y + ANSWER_OFFSET[1], words.answer
        )
        .icon(hqf_pdf.NoteIcon.Check)
        .author(ELECTRICIAN)
        .created(answered_at())
        .in_reply_to(code(answered)),
        hqf_pdf.StickyNoteAnnotation.state_change(
            x + CLOSING_OFFSET[0],
            y + CLOSING_OFFSET[1],
            code(answered),
            hqf_pdf.AnnotationState.Completed,
            INSPECTOR,
        ),
    ]


def shaped(place: int, snag: Snag, words: Words):
    """The shape one snag is marked by on the plan."""
    ink = snag.degree.ink
    border = hqf_pdf.AnnotationBorder.solid(MARK_WIDTH)
    says = spoken(place, snag, words)
    named = code(place)
    mark = snag.mark

    if isinstance(mark, Cloud):
        return (
            hqf_pdf.SquareAnnotation(
                at_x(mark.x), at_y(mark.y), across(mark.width), across(mark.height)
            )
            .effect(hqf_pdf.BorderEffect.cloudy(CLOUD_BULGE))
            .color(ink)
            .border(border)
            .name(named)
            .contents(says)
        )
    if isinstance(mark, Ring):
        return (
            hqf_pdf.CircleAnnotation(
                at_x(mark.x - mark.radius),
                at_y(mark.y - mark.radius),
                across(mark.radius * 2.0),
                across(mark.radius * 2.0),
            )
            .color(ink)
            .border(border)
            .name(named)
            .contents(says)
        )
    if isinstance(mark, Arrow):
        return (
            hqf_pdf.LineAnnotation(
                at_x(mark.start[0]),
                at_y(mark.start[1]),
                at_x(mark.stop[0]),
                at_y(mark.stop[1]),
            )
            .endings(hqf_pdf.LineEnding.None_, hqf_pdf.LineEnding.ClosedArrow)
            .interior(ink)
            .color(ink)
            .border(border)
            .name(named)
            .contents(says)
        )
    if isinstance(mark, Footprint):
        return (
            hqf_pdf.PolygonAnnotation(plotted(mark.corners))
            .interior(FOOTPRINT_TINT)
            .color(ink)
            .border(border)
            .name(named)
            .contents(says)
        )
    return (
        hqf_pdf.PolyLineAnnotation(plotted(mark.along))
        .endings(hqf_pdf.LineEnding.Butt, hqf_pdf.LineEnding.Butt)
        .color(ink)
        .border(border)
        .name(named)
        .contents(says)
    )


def legend(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, top: float
) -> float:
    """Draws what each kind of mark means and what each colour means, and hands back the
    baseline it ends on.
    """
    y = top
    text(content, font, HEADING_SIZE, LEFT, y, INK, words.legend)
    y -= BLOCK
    for line in (words.cloud, words.ring, words.arrow, words.footprint, words.run):
        text(content, font, SMALL, LEFT, y, INK, line)
        y -= LINE + 2.0

    y -= BLOCK - LINE
    text(content, font, HEADING_SIZE, LEFT, y, INK, words.key)
    y -= BLOCK
    for degree in DEGREES:
        content.set_fill(degree.ink)
        content.rect(LEFT, y - 0.5, SMALL, SMALL)
        content.fill()
        named = f"{words.degrees[degree.place]} — {allowed(degree, words)}"
        text(content, font, SMALL, LEFT + SMALL + 6.0, y, INK, named)
        y -= LINE + 2.0

    y -= BLOCK - LINE
    for line in words.note:
        text(content, font, SMALL, LEFT, y, MUTED, line)
        y -= LINE
    return y


def foot(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, sheet: int
) -> None:
    """Draws the rule and the numbering every page ends on."""
    rule(content, FOOT + LINE)
    named = f"{words.title} · {REFERENCE}"
    text(content, font, TINY, LEFT, FOOT, MUTED, named)
    numbered = f"{words.page} {counted(sheet + 1)} {words.of} {counted(len(SHEETS))}"
    text_right(content, font, TINY, RIGHT, FOOT, MUTED, numbered)


def banner(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, heading: str) -> float:
    """Draws the short head the pages after the first carry, and hands back the baseline
    it ends on.
    """
    text(content, font, SMALL, LEFT, HEAD_TOP, MUTED, FIRM)
    text_right(content, font, SMALL, RIGHT, HEAD_TOP, MUTED, REFERENCE)
    rule(content, HEAD_TOP - 8.0)
    y = HEAD_TOP - BLOCK - LINE
    text(content, font, TITLE_SIZE, LEFT, y, INK, heading)
    return y - GAP


def list_row(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    y: float,
    size: float,
    color: hqf_pdf.Rgb,
    cells: tuple[str, ...],
) -> None:
    """Draws one row of a table, each cell against the right edge of its column, save
    the first two, which are set from the left.
    """
    left = LEFT
    for place, cell in enumerate(cells):
        right = LEFT + LIST_COLUMNS[place]
        if place in (3, 4):
            text_right(content, font, size, right - 6.0, y, color, cell)
        else:
            text(content, font, size, left, y, color, cell)
        left = right + 6.0


def plan_heading(words: Words) -> str:
    """What the plan is headed: what it shows, the scale it is set to, and how far the
    premises run.
    """
    return (
        f"{words.plan} — {words.scale} 1:{SCALE_DENOMINATOR:.0f} — "
        f"{measured(SHELL_WIDTH / 100.0)} m × {measured(SHELL_HEIGHT / 100.0)} m"
    )


def plan_sheet(font: hqf_pdf.FontHandle, words: Words, sheet: int) -> hqf_pdf.Page:
    """Draws the plan, the marks over it and what they mean."""
    content = hqf_pdf.Content()
    page = hqf_pdf.Page(PAGE_WIDTH, PAGE_HEIGHT)

    y = head(content, font, words, HEAD_TOP)
    y = marks(content, font, words, y - BLOCK - LINE)

    rule(content, y - 6.0)
    text(content, font, HEADING_SIZE, LEFT, y - BLOCK, INK, plan_heading(words))

    floor(content, font, words)
    codes(content, font)

    rule(content, PLAN_BOTTOM - BLOCK)
    legend(content, font, words, PLAN_BOTTOM - GAP)
    foot(content, font, words, sheet)

    for place, snag in enumerate(SNAGS):
        page.add_annotation(marked(place, snag, words))
    for note in answers(words):
        page.add_annotation(note)

    page.set_content(content)
    return page


def list_sheet(font: hqf_pdf.FontHandle, words: Words, sheet: int) -> hqf_pdf.Page:
    """Draws the snags, one to a line."""
    content = hqf_pdf.Content()
    page = hqf_pdf.Page(PAGE_WIDTH, PAGE_HEIGHT)

    top = banner(content, font, words.list)

    band(content, top - 6.0, ROW, HEAD_BAND)
    heads = (
        words.number,
        words.room,
        words.nature,
        words.degree,
        words.due,
        words.trade,
    )
    list_row(content, font, top, TINY, MUTED, heads)

    y = top - ROW
    for place, snag in enumerate(SNAGS):
        if place % 2 == 1:
            band(content, y - 6.0, ROW, ROW_BAND)
        cells = (
            code(place),
            words.rooms[snag.room],
            words.natures[place],
            words.degrees[snag.degree.place],
            written(due(snag.degree)),
            words.trades[snag.trade],
        )
        list_row(content, font, y, SMALL, INK, cells)
        y -= ROW

    rule(content, y + ROW - 8.0)
    text(
        content,
        font,
        BODY,
        LEFT,
        y - 6.0,
        INK,
        f"{words.total} — {counted(len(SNAGS))}",
    )

    y -= GAP
    for line in words.note:
        text(content, font, SMALL, LEFT, y, MUTED, line)
        y -= LINE

    foot(content, font, words, sheet)
    page.set_content(content)
    return page


def gathered(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    words: Words,
    top: float,
    heading: str,
    column: str,
    rows: tuple[tuple[str, int, hqf_pdf.Rgb | None], ...],
) -> float:
    """Draws a table that gathers the snags under a name, one row to a name, and hands
    back the baseline it ends on. A row that carries a colour is set behind a swatch of
    it.
    """
    all_snags = len(SNAGS)
    y = top
    text(content, font, HEADING_SIZE, LEFT, y, INK, heading)

    y -= BLOCK
    band(content, y - 6.0, ROW, HEAD_BAND)
    text(content, font, TINY, LEFT + 6.0, y, MUTED, column)
    counts = LEFT + COUNT_COLUMNS[1]
    shares = LEFT + COUNT_COLUMNS[2]
    text_right(content, font, TINY, counts, y, MUTED, words.counted)
    text_right(content, font, TINY, shares, y, MUTED, words.share)

    y -= ROW
    for name, found, swatch in rows:
        left = LEFT + 6.0
        if swatch is not None:
            content.set_fill(swatch)
            content.rect(left, y - 0.5, SMALL, SMALL)
            content.fill()
            left += SMALL + 6.0
        text(content, font, BODY, left, y, INK, name)
        text_right(content, font, BODY, counts, y, INK, counted(found))
        text_right(content, font, BODY, shares, y, INK, share(found, all_snags))
        y -= ROW

    rule(content, y + ROW - 8.0)
    text(content, font, BODY, LEFT + 6.0, y, INK, words.total)
    text_right(content, font, BODY, counts, y, INK, counted(all_snags))
    return y


def by_room(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, top: float
) -> float:
    """Draws the table that gathers the snags room by room, with what each room covers,
    and hands back the baseline it ends on.
    """
    all_snags = len(SNAGS)
    y = top
    text(content, font, HEADING_SIZE, LEFT, y, INK, words.by_room)

    y -= BLOCK
    band(content, y - 6.0, ROW, HEAD_BAND)
    text(content, font, TINY, LEFT + 6.0, y, MUTED, words.room)
    for place, column in enumerate((words.area, words.counted, words.share)):
        right = LEFT + ROOM_COLUMNS[place + 1]
        text_right(content, font, TINY, right, y, MUTED, column)

    y -= ROW
    for place, name in enumerate(words.rooms):
        found = found_in(place)
        covered = f"{measured(area(ROOMS[place]))} m²"
        text(content, font, BODY, LEFT + 6.0, y, INK, name)
        text_right(content, font, BODY, LEFT + ROOM_COLUMNS[1], y, INK, covered)
        text_right(content, font, BODY, LEFT + ROOM_COLUMNS[2], y, INK, counted(found))
        shared = share(found, all_snags)
        text_right(content, font, BODY, LEFT + ROOM_COLUMNS[3], y, INK, shared)
        y -= ROW

    rule(content, y + ROW - 8.0)
    covered = f"{measured(total_area())} m²"
    text(content, font, BODY, LEFT + 6.0, y, INK, words.total)
    text_right(content, font, BODY, LEFT + ROOM_COLUMNS[1], y, INK, covered)
    text_right(content, font, BODY, LEFT + ROOM_COLUMNS[2], y, INK, counted(all_snags))
    return y


def summary_sheet(font: hqf_pdf.FontHandle, words: Words, sheet: int) -> hqf_pdf.Page:
    """Draws what the snags come to: the count and the share of each degree, then of
    each room, then of each trade, then the last of the dates.
    """
    content = hqf_pdf.Content()
    page = hqf_pdf.Page(PAGE_WIDTH, PAGE_HEIGHT)

    y = banner(content, font, words.summary)

    degrees = tuple(
        (words.degrees[degree.place], found_at(degree), degree.ink)
        for degree in DEGREES
    )
    y = gathered(
        content, font, words, y, words.by_degree, words.degree, degrees
    )

    y -= GAP
    y = by_room(content, font, words, y)

    trades = tuple(
        (name, answered_for(place), None) for place, name in enumerate(words.trades)
    )
    y -= GAP
    y = gathered(content, font, words, y, words.by_trade, words.trade, trades)

    y -= GAP
    band(content, y - 8.0, ROW + 6.0, HEAD_BAND)
    text(content, font, BODY, LEFT + 6.0, y, INK, words.latest)
    last = written(latest())
    text_right(content, font, HEADING_SIZE, RIGHT - 6.0, y - 1.0, INK, last)

    foot(content, font, words, sheet)
    page.set_content(content)
    return page


# The pages the sheet runs to, in order. The foot of each reads its own place and this
# table's length, so a page added here numbers itself.
SHEETS = (plan_sheet, list_sheet, summary_sheet)


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

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", f"{words.title} {REFERENCE}")

    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    for sheet, draw in enumerate(SHEETS):
        document.add_page(draw(font, words, sheet))

    size = document.write(out)
    print(
        f"wrote {out}: {size} bytes, {counted(len(SNAGS))} snags marked on "
        f"{measured(total_area())} m², last date {written(latest())}"
    )


if __name__ == "__main__":
    main()
