"""Writes a tender pack: a cover sheet, three files attached to it, and the table a
reading software shows them in.

The Python twin of the `write_portfolio` example in Rust. A document that carries
files is a portfolio when it says how to show them: which columns to lay out, what
each file states for each column, and which way the list is sorted. Without that a
reading software falls back on a list of file names, which tells a buyer nothing
about which lot came in when and for how much.

Each lot is attached as a small XML file, and each states its own lot, the day it was
sent and its total. The columns are declared once on the document, the sort is on the
day, newest first, and the cover sheet lists the files as well, so that a reading
software with no portfolio of its own still shows what is inside.

The dates the columns carry are written the way PDF writes a moment, which is what
`hqf_pdf.pdf_date` turns an ISO 8601 moment into. A file's own `modified` is stated in
ISO 8601 and converted by the library.

Two more files belong to the pack without travelling in it: a site plan kept beside the
pack, named by its path, and a survey served from an address. The columns add four the
reader fills in from the files themselves — the name, what the file is, when it last
changed, its size, the last of them hidden — and the sheet ends on what the pack states:
how it is shown, the file it opens on, the column it sorts on and which way, each column
and what it holds, and for every file what it is, its media type, what it is to the pack
and where its bytes are.

The file opens with the attachments panel shown.

The terminal counts the files named by a path beside the pack and those named by an
address, each read off what the file is made of.

Usage: python examples/write_portfolio.py [out.pdf]
       HQF_PDF_LANG=fr python examples/write_portfolio.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The margin the page is laid out inside.
MARGIN = 56.0

# How far the page runs across, between the margins.
WIDTH = 483.0

# The name each lot is attached under.
FILES = ("lot-a.xml", "lot-b.xml", "lot-c.xml")

# When each lot was sent, in ISO 8601.
SENT = (
    "2026-07-14T09:30:00+02:00",
    "2026-08-03T16:05:00+02:00",
    "2026-06-28T11:20:00+02:00",
)

# What each lot comes to, in euros.
TOTALS = (18400.0, 9250.0, 31700.0)

# The keys the columns are read under. They are not words anybody is shown: a file
# states its columns under these, and the sort names one of them.
KEYS = ("lot", "sent", "total")

# The number of the folder each lot is placed in: the two groundworks lots in the first
# folder under the root, the surfacing in the second.
FOLDER_OF = (2, 2, 3)

# The number of the root folder.
ROOT = 1

# Where each line of the list sits.
ROWS = (648.0, 626.0, 604.0)

# Where the heading of the list sits.
HEADING = 676.0

# Where each column of the list starts, from the margin.
COLUMNS = (0.0, 96.0, 300.0, 392.0)

# The path the site plan is kept at, beside the pack.
PLAN = "plans/site-plan.pdf"

# The address the ground survey is served from.
SURVEY = "https://hqf-pdf.com/examples/tender/ground-survey.csv"

# The keys of the four columns the reader fills in from the files.
READ_KEYS = ("file", "what", "changed", "size")

# Where the heading over what the pack states sits.
STATED_TOP = 500.0


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

    # The line at the head of the page.
    title: str
    # The paragraph under it.
    intro: str
    # The heading each column is shown under.
    columns: tuple[str, str, str]
    # What each lot is called.
    lots: tuple[str, str, str]
    # What each lot comes to, written the way the language writes money.
    totals: tuple[str, str, str]
    # The heading over the column of file names.
    file_heading: str
    # The line under the list.
    caption: str
    # What the file kept beside the pack is, and what the file served from an address
    # is.
    elsewhere: tuple[str, str]
    # The four columns the reader fills in from the files: name, what it is, when it
    # changed, its size.
    read_columns: tuple[str, str, str, str]
    # The heading over what the pack states.
    stated: str
    # How the pack is shown; `{view}`, `{file}`, `{key}` and `{order}` stand for them.
    shown: str
    # The three ways a pack is shown: rows, tiles, hidden.
    views: tuple[str, str, str]
    # Which way a sort runs: upward, then downward.
    orders: tuple[str, str]
    # The columns; `{columns}` stands for them.
    columns_line: str
    # The folders the files are grouped in: the root, then the two it holds.
    folders: tuple[str, str, str]
    # The folders and the files in each; `{folders}` stands for them.
    folders_line: str
    # What stands between two items of the columns line and of the folders line.
    separator: str
    # What a column holds: words, a moment, a number, the file name, what the file is,
    # when it changed, its size.
    kinds: tuple[str, ...]
    # What a hidden column is marked with.
    hidden: str
    # One file; `{file}`, `{description}`, `{media}`, `{relationship}` and `{payload}`
    # stand for them.
    file_line: str
    # What a file named without a media type is said to have.
    no_media: str
    # What a file is to the pack: its source, an alternative, its data, a supplement,
    # nothing said.
    relationships: tuple[str, str, str, str, str]
    # Where a carried file is; `{bytes}` and `{modified}` stand for them.
    carried: str
    # Where a file kept beside the pack is; `{path}` stands for it.
    beside: str
    # Where a served file is; `{address}` stands for it.
    served: str


# The page in English.
ENGLISH = Words(
    title="Tender pack: three lots",
    intro=(
        "Every lot of this tender travels in the pack as a file of its own. The "
        "document states the columns the files are shown in, what each file holds for "
        "each of them, and that the list is sorted on the day it was sent, newest "
        "first."
    ),
    columns=(
        "Lot",
        "Sent",
        "Total",
    ),
    lots=(
        "Earthworks and site clearance",
        "Drainage and services",
        "Surfacing and lining",
    ),
    totals=(
        "EUR 18,400.00",
        "EUR 9,250.00",
        "EUR 31,700.00",
    ),
    file_heading="File",
    caption=(
        "A reading software that shows a portfolio lays the three files out in these "
        "columns. One that does not still opens the pack and lists the files, which is "
        "why they are named on this sheet too."
    ),
    elsewhere=(
        "The site plan, kept beside the pack",
        "The ground survey, served from our site",
    ),
    read_columns=(
        "File",
        "What it is",
        "Changed",
        "Size",
    ),
    stated="What the pack states",
    shown="Shown as {view}, opening on {file}, sorted on the column {key}, {order}.",
    views=(
        "rows",
        "tiles",
        "nothing until asked",
    ),
    orders=(
        "oldest first",
        "newest first",
    ),
    columns_line="Columns: {columns}.",
    folders=("Tender pack", "Groundworks", "Surfacing"),
    folders_line="Folders: {folders}.",
    separator="; ",
    kinds=(
        "words",
        "a moment",
        "a number",
        "the file name",
        "what the file is",
        "when it changed",
        "its size",
    ),
    hidden="hidden",
    file_line="{file}: {description}; {media}; {relationship}; {payload}.",
    no_media="no media type",
    relationships=(
        "its source",
        "an alternative to it",
        "its data",
        "a supplement to it",
        "nothing said",
    ),
    carried="carried, {bytes} bytes, changed {modified}",
    beside="kept beside the pack at {path}",
    served="served from {address}",
)

# The page in French.
FRENCH = Words(
    title="Dossier d’appel d’offres : trois lots",
    intro=(
        "Chaque lot de cet appel d’offres voyage dans le dossier comme un fichier "
        "distinct. Le document indique les colonnes sous lesquelles les fichiers sont "
        "présentés, ce que chacun contient pour chacune d’elles, et que la liste est "
        "triée par date d’envoi, du plus récent au plus ancien."
    ),
    columns=(
        "Lot",
        "Envoi",
        "Montant",
    ),
    lots=(
        "Terrassement et dégagement du terrain",
        "Drainage et réseaux",
        "Revêtement et marquage",
    ),
    totals=(
        "18 400,00 EUR",
        "9 250,00 EUR",
        "31 700,00 EUR",
    ),
    file_heading="Fichier",
    caption=(
        "Un logiciel de lecture qui présente un portefeuille dispose les trois "
        "fichiers sous ces colonnes. Un logiciel qui ne le fait pas ouvre tout de même "
        "le dossier et en énumère les fichiers : c’est pourquoi ils sont aussi nommés "
        "sur cette feuille."
    ),
    elsewhere=(
        "Le plan du site, rangé à côté du dossier",
        "Le relevé du terrain, servi par notre site",
    ),
    read_columns=(
        "Fichier",
        "Description",
        "Modifié",
        "Taille",
    ),
    stated="Ce que le dossier indique",
    shown="Présenté {view}, ouvert sur {file}, trié selon la colonne {key}, {order}.",
    views=(
        "en lignes",
        "en vignettes",
        "seulement sur demande",
    ),
    orders=(
        "du plus ancien au plus récent",
        "du plus récent au plus ancien",
    ),
    columns_line="Colonnes : {columns}.",
    folders=("Appel d’offres", "Terrassement et réseaux", "Finitions"),
    folders_line="Sous-dossiers : {folders}.",
    separator=" ; ",
    kinds=(
        "des mots",
        "une date",
        "un nombre",
        "le nom du fichier",
        "ce qu’est le fichier",
        "sa date de modification",
        "sa taille",
    ),
    hidden="masquée",
    file_line="{file} : {description} ; {media} ; {relationship} ; {payload}.",
    no_media="aucun type de média",
    relationships=(
        "sa source",
        "une variante",
        "ses données",
        "un complément",
        "rien de précisé",
    ),
    carried="incorporé, {bytes} octets, modifié le {modified}",
    beside="rangé à côté du dossier : {path}",
    served="servi par {address}",
)

# The page in Dutch.
DUTCH = Words(
    title="Aanbestedingspakket: drie percelen",
    intro=(
        "Elk perceel van deze aanbesteding reist in het pakket mee als eigen bestand. "
        "Het document noemt de kolommen waarin de bestanden getoond worden, wat elk "
        "bestand voor elke kolom bevat, en dat de lijst gesorteerd is op "
        "verzenddatum, nieuwste eerst."
    ),
    columns=(
        "Perceel",
        "Verzonden",
        "Totaal",
    ),
    lots=(
        "Grondwerk en bouwrijp maken",
        "Riolering en nutsvoorzieningen",
        "Verharding en belijning",
    ),
    totals=(
        "EUR 18.400,00",
        "EUR 9.250,00",
        "EUR 31.700,00",
    ),
    file_heading="Bestand",
    caption=(
        "Leessoftware die een portfolio toont, zet de drie bestanden in deze "
        "kolommen. Leessoftware die dat niet doet, opent het pakket toch en somt de "
        "bestanden op, en daarom worden ze ook op dit blad genoemd."
    ),
    elsewhere=(
        "De bouwtekening, naast het pakket bewaard",
        "Het bodemonderzoek, geleverd vanaf onze site",
    ),
    read_columns=(
        "Bestand",
        "Wat het is",
        "Gewijzigd",
        "Grootte",
    ),
    stated="Wat het pakket vermeldt",
    shown="Getoond als {view}, geopend op {file}, gesorteerd op de kolom {key}, {order}.",
    views=(
        "rijen",
        "tegels",
        "niets tot erom gevraagd wordt",
    ),
    orders=(
        "oudste eerst",
        "nieuwste eerst",
    ),
    columns_line="Kolommen: {columns}.",
    folders=("Aanbestedingspakket", "Grond- en leidingwerk", "Afwerking"),
    folders_line="Mappen: {folders}.",
    separator="; ",
    kinds=(
        "woorden",
        "een moment",
        "een getal",
        "de bestandsnaam",
        "wat het bestand is",
        "wanneer het veranderde",
        "de grootte",
    ),
    hidden="verborgen",
    file_line="{file}: {description}; {media}; {relationship}; {payload}.",
    no_media="geen mediatype",
    relationships=(
        "de bron ervan",
        "een alternatief ervoor",
        "de gegevens ervan",
        "een aanvulling erop",
        "niets vermeld",
    ),
    carried="ingesloten, {bytes} bytes, gewijzigd op {modified}",
    beside="naast het pakket bewaard op {path}",
    served="geleverd vanaf {address}",
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="Dossiê de concurso: três lotes",
    intro=(
        "Cada lote deste concurso viaja no dossiê como ficheiro próprio. O documento "
        "indica as colunas em que os ficheiros são mostrados, o que cada ficheiro "
        "contém para cada uma, e que a lista é ordenada pela data de envio, a mais "
        "recente primeiro."
    ),
    columns=(
        "Lote",
        "Enviado",
        "Total",
    ),
    lots=(
        "Movimentação de terras e limpeza do terreno",
        "Drenagem e redes",
        "Pavimentação e sinalização",
    ),
    totals=(
        "18 400,00 EUR",
        "9 250,00 EUR",
        "31 700,00 EUR",
    ),
    file_heading="Ficheiro",
    caption=(
        "Um programa de leitura que mostra um portefólio dispõe os três ficheiros "
        "nestas colunas. Um que não o faça abre na mesma o dossiê e lista os "
        "ficheiros, e é por isso que também são nomeados nesta folha."
    ),
    elsewhere=(
        "A planta da obra, guardada ao lado do dossiê",
        "O levantamento do terreno, servido pelo nosso site",
    ),
    read_columns=(
        "Ficheiro",
        "O que é",
        "Alterado",
        "Tamanho",
    ),
    stated="O que o dossiê indica",
    shown="Mostrado como {view}, aberto em {file}, ordenado pela coluna {key}, {order}.",
    views=(
        "linhas",
        "mosaicos",
        "nada até ser pedido",
    ),
    orders=(
        "o mais antigo primeiro",
        "o mais recente primeiro",
    ),
    columns_line="Colunas: {columns}.",
    folders=("Dossiê de concurso", "Movimentação de terras e redes", "Pavimentação"),
    folders_line="Pastas: {folders}.",
    separator="; ",
    kinds=(
        "palavras",
        "um momento",
        "um número",
        "o nome do ficheiro",
        "o que é o ficheiro",
        "quando mudou",
        "o seu tamanho",
    ),
    hidden="oculta",
    file_line="{file}: {description}; {media}; {relationship}; {payload}.",
    no_media="sem tipo de suporte",
    relationships=(
        "a sua origem",
        "uma alternativa a ele",
        "os seus dados",
        "um complemento a ele",
        "nada indicado",
    ),
    carried="incluído, {bytes} bytes, alterado em {modified}",
    beside="guardado ao lado do dossiê em {path}",
    served="servido a partir de {address}",
)

# The page in Italian.
ITALIAN = Words(
    title="Fascicolo di gara: tre lotti",
    intro=(
        "Ogni lotto di questa gara viaggia nel fascicolo come file a sé. Il documento "
        "indica le colonne in cui si mostrano i file, cosa contiene ciascun file per "
        "ciascuna, e che l’elenco è ordinato per data di invio, dal più recente."
    ),
    columns=(
        "Lotto",
        "Inviato",
        "Totale",
    ),
    lots=(
        "Movimento terra e sgombero del cantiere",
        "Drenaggio e reti",
        "Pavimentazione e segnaletica",
    ),
    totals=(
        "18.400,00 EUR",
        "9.250,00 EUR",
        "31.700,00 EUR",
    ),
    file_heading="File",
    caption=(
        "Un programma di lettura che mostra un portfolio dispone i tre file in queste "
        "colonne. Uno che non lo fa apre comunque il fascicolo ed elenca i file, ed è "
        "per questo che sono nominati anche su questo foglio."
    ),
    elsewhere=(
        "La planimetria del cantiere, tenuta accanto al fascicolo",
        "Il rilievo del terreno, servito dal nostro sito",
    ),
    read_columns=(
        "File",
        "Che cos’è",
        "Modificato",
        "Dimensione",
    ),
    stated="Cosa indica il fascicolo",
    shown="Mostrato come {view}, aperto su {file}, ordinato sulla colonna {key}, {order}.",
    views=(
        "righe",
        "riquadri",
        "nulla finché non si chiede",
    ),
    orders=(
        "dal più vecchio",
        "dal più recente",
    ),
    columns_line="Colonne: {columns}.",
    folders=("Fascicolo di gara", "Movimento terra e reti", "Opere di completamento"),
    folders_line="Cartelle: {folders}.",
    separator="; ",
    kinds=(
        "parole",
        "un momento",
        "un numero",
        "il nome del file",
        "cos’è il file",
        "quando è cambiato",
        "la sua dimensione",
    ),
    hidden="nascosta",
    file_line="{file}: {description}; {media}; {relationship}; {payload}.",
    no_media="nessun tipo di supporto",
    relationships=(
        "la sua fonte",
        "un’alternativa a esso",
        "i suoi dati",
        "un complemento a esso",
        "nulla di detto",
    ),
    carried="incluso, {bytes} byte, modificato il {modified}",
    beside="tenuto accanto al fascicolo in {path}",
    served="servito da {address}",
)

# The page in Spanish.
SPANISH = Words(
    title="Expediente de licitación: tres lotes",
    intro=(
        "Cada lote de esta licitación viaja en el expediente como archivo propio. El "
        "documento indica las columnas en que se muestran los archivos, lo que cada "
        "archivo contiene para cada una, y que la lista se ordena por fecha de envío, "
        "el más reciente primero."
    ),
    columns=(
        "Lote",
        "Enviado",
        "Total",
    ),
    lots=(
        "Movimiento de tierras y desbroce",
        "Drenaje y redes",
        "Firme y señalización",
    ),
    totals=(
        "18.400,00 EUR",
        "9.250,00 EUR",
        "31.700,00 EUR",
    ),
    file_heading="Archivo",
    caption=(
        "Un programa de lectura que muestra una cartera coloca los tres archivos en "
        "estas columnas. Uno que no lo hace abre igualmente el expediente y enumera "
        "los archivos, por eso también se nombran en esta hoja."
    ),
    elsewhere=(
        "El plano de la obra, guardado junto al expediente",
        "El estudio del terreno, servido desde nuestro sitio",
    ),
    read_columns=(
        "Archivo",
        "Qué es",
        "Modificado",
        "Tamaño",
    ),
    stated="Lo que indica el expediente",
    shown="Mostrado como {view}, abierto en {file}, ordenado por la columna {key}, {order}.",
    views=(
        "filas",
        "mosaico",
        "nada hasta que se pida",
    ),
    orders=(
        "el más antiguo primero",
        "el más reciente primero",
    ),
    columns_line="Columnas: {columns}.",
    folders=("Expediente de licitación", "Explanación y redes", "Firme y señalización"),
    folders_line="Carpetas: {folders}.",
    separator="; ",
    kinds=(
        "palabras",
        "un momento",
        "un número",
        "el nombre del archivo",
        "qué es el archivo",
        "cuándo cambió",
        "su tamaño",
    ),
    hidden="oculta",
    file_line="{file}: {description}; {media}; {relationship}; {payload}.",
    no_media="sin tipo de soporte",
    relationships=(
        "su fuente",
        "una alternativa a él",
        "sus datos",
        "un complemento de él",
        "nada indicado",
    ),
    carried="incluido, {bytes} bytes, modificado el {modified}",
    beside="guardado junto al expediente en {path}",
    served="servido desde {address}",
)

# The page in German.
GERMAN = Words(
    title="Ausschreibungsmappe: drei Lose",
    intro=(
        "Jedes Los dieser Ausschreibung reist als eigene Datei in der Mappe. Das "
        "Dokument nennt die Spalten, in denen die Dateien gezeigt werden, was jede "
        "Datei zu jeder davon enthält, und dass die Liste nach dem Versanddatum "
        "sortiert ist, das neueste zuerst."
    ),
    columns=(
        "Los",
        "Versandt",
        "Summe",
    ),
    lots=(
        "Erdarbeiten und Baufeldräumung",
        "Entwässerung und Leitungen",
        "Belag und Markierung",
    ),
    totals=(
        "18.400,00 EUR",
        "9.250,00 EUR",
        "31.700,00 EUR",
    ),
    file_heading="Datei",
    caption=(
        "Eine Lesesoftware, die ein Portfolio anzeigt, ordnet die drei Dateien in "
        "diesen Spalten an. Eine, die das nicht tut, öffnet die Mappe trotzdem und "
        "listet die Dateien auf, darum sind sie auch auf diesem Blatt genannt."
    ),
    elsewhere=(
        "Der Lageplan, neben der Mappe aufbewahrt",
        "Die Bodenuntersuchung, von unserer Website bereitgestellt",
    ),
    read_columns=(
        "Datei",
        "Was es ist",
        "Geändert",
        "Größe",
    ),
    stated="Was die Mappe angibt",
    shown="Gezeigt als {view}, geöffnet auf {file}, sortiert nach der Spalte {key}, {order}.",
    views=(
        "Zeilen",
        "Kacheln",
        "nichts, bis es verlangt wird",
    ),
    orders=(
        "älteste zuerst",
        "neueste zuerst",
    ),
    columns_line="Spalten: {columns}.",
    folders=("Ausschreibungsmappe", "Tiefbau", "Fahrbahn"),
    folders_line="Ordner: {folders}.",
    separator="; ",
    kinds=(
        "Wörter",
        "ein Zeitpunkt",
        "eine Zahl",
        "der Dateiname",
        "was die Datei ist",
        "wann sie geändert wurde",
        "ihre Größe",
    ),
    hidden="verborgen",
    file_line="{file}: {description}; {media}; {relationship}; {payload}.",
    no_media="kein Medientyp",
    relationships=(
        "ihre Quelle",
        "eine Alternative dazu",
        "ihre Daten",
        "eine Ergänzung dazu",
        "nichts angegeben",
    ),
    carried="mitgeführt, {bytes} Bytes, geändert {modified}",
    beside="neben der Mappe aufbewahrt unter {path}",
    served="bereitgestellt von {address}",
)

# 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 day_of(moment: str) -> str:
    """The day of an ISO 8601 moment, which is what the list shows."""
    return moment.split("T")[0]


def lot_file(name: str, sent: str, total: float) -> bytes:
    """The file one lot is sent as."""
    return (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        f"<lot>\n  <name>{name}</name>\n  <sent>{sent}</sent>\n  "
        f'<total currency="EUR">{total:.2f}</total>\n</lot>\n'
    ).encode()


def paragraph(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    top: float,
    color: hqf_pdf.Rgb,
    text: str,
) -> None:
    """Writes a paragraph in a column the width of the page."""
    flow = hqf_pdf.TextFlow(font, size, leading=size * 1.4, color=color)
    lines = flow.break_lines(text, WIDTH)
    flow.draw(content, lines, MARGIN, top, WIDTH)


# The three ways a pack is shown, in the order `Words.views` names them.
VIEWS = (
    hqf_pdf.CollectionView.rows(),
    hqf_pdf.CollectionView.tiles(),
    hqf_pdf.CollectionView.hidden(),
)

# What a column may hold, in the order `Words.kinds` names it.
KINDS = (
    hqf_pdf.CollectionData.text(),
    hqf_pdf.CollectionData.date(),
    hqf_pdf.CollectionData.number(),
    hqf_pdf.CollectionData.file_name(),
    hqf_pdf.CollectionData.description(),
    hqf_pdf.CollectionData.modified(),
    hqf_pdf.CollectionData.size(),
)

# What a file may be to the pack, in the order `Words.relationships` names it.
RELATIONSHIPS = (
    hqf_pdf.Relationship.Source,
    hqf_pdf.Relationship.Alternative,
    hqf_pdf.Relationship.Data,
    hqf_pdf.Relationship.Supplement,
    hqf_pdf.Relationship.Unspecified,
)


def stated(collection, files, words: Words) -> list[str]:
    """What the pack states of `collection` and of `files`, one line each."""
    upward = collection.sorts_upward
    order = "" if upward is None else words.orders[0 if upward else 1]
    shown = (
        words.shown.replace("{view}", words.views[VIEWS.index(collection.view)])
        .replace("{file}", collection.opens_on or "")
        .replace("{key}", collection.sorted_on or "")
        .replace("{order}", order)
    )
    listed = []
    for field in collection.columns:
        named = f"{field.title} ({words.kinds[KINDS.index(field.data)]})"
        if not field.is_visible:
            named = f"{named}, {words.hidden}"
        listed.append(named)
    tree = collection.folder_tree
    grouped = []
    for folder in tree.children if tree is not None else []:
        held = [file.file_name for file in files if file.folder == folder.id]
        grouped.append(f"{folder.name} ({', '.join(held)})")
    lines = [
        shown,
        words.columns_line.replace("{columns}", words.separator.join(listed)),
        words.folders_line.replace("{folders}", words.separator.join(grouped)),
    ]
    for file in files:
        if file.data is not None:
            payload = words.carried.replace("{bytes}", str(len(file.data))).replace(
                "{modified}", day_of(file.modified)
            )
        elif file.path is not None:
            payload = words.beside.replace("{path}", file.path)
        else:
            payload = words.served.replace("{address}", file.address or "")
        media = file.media_type or words.no_media
        relationship = words.relationships[RELATIONSHIPS.index(file.relationship)]
        lines.append(
            words.file_line.replace("{file}", file.file_name)
            .replace("{description}", file.description)
            .replace("{media}", media)
            .replace("{relationship}", relationship)
            .replace("{payload}", payload)
        )
    return 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("portfolio.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    # A file named beside the document or at an address is read from PDF 2.0 on.
    document.set_version(hqf_pdf.Version.V2_0)
    font = document.add_font(hqf_pdf.Font.from_path(_out.DEFAULT_FONT))

    files = []
    for lot in range(len(FILES)):
        files.append(
            hqf_pdf.Attachment(
                FILES[lot],
                words.lots[lot],
                "application/xml",
                hqf_pdf.Relationship.Source,
                SENT[lot],
                lot_file(words.lots[lot], SENT[lot], TOTALS[lot]),
            )
            .stating(KEYS[0], hqf_pdf.CollectionValue.text(words.lots[lot]))
            .stating(KEYS[1], hqf_pdf.CollectionValue.date(hqf_pdf.pdf_date(SENT[lot])))
            .stating(KEYS[2], hqf_pdf.CollectionValue.number(TOTALS[lot]))
            .in_folder(FOLDER_OF[lot])
        )

    files.append(
        hqf_pdf.Attachment.beside(
            PLAN, words.elsewhere[0], hqf_pdf.Relationship.Supplement
        )
    )
    files.append(
        hqf_pdf.Attachment.at(SURVEY, words.elsewhere[1], hqf_pdf.Relationship.Data)
    )

    data = hqf_pdf.CollectionData
    columns = [
        hqf_pdf.CollectionField(KEYS[0], words.columns[0], data.text()),
        hqf_pdf.CollectionField(KEYS[1], words.columns[1], data.date()),
        hqf_pdf.CollectionField(KEYS[2], words.columns[2], data.number()),
    ]
    read = (data.file_name(), data.description(), data.modified(), data.size())
    for key, title, kind in zip(READ_KEYS, words.read_columns, read):
        field = hqf_pdf.CollectionField(key, title, kind)
        columns.append(field.hidden() if kind == data.size() else field)
    collection = (
        hqf_pdf.Collection(hqf_pdf.CollectionView.rows())
        .with_columns(columns)
        .sorted_by(KEYS[1], False)
        .opening_on(FILES[0])
        .with_folder_tree(
            hqf_pdf.CollectionFolder(ROOT, words.folders[0]).holding(
                [
                    hqf_pdf.CollectionFolder(FOLDER_OF[0], words.folders[1]),
                    hqf_pdf.CollectionFolder(FOLDER_OF[2], words.folders[2]),
                ]
            )
        )
    )
    said = stated(collection, files, words)
    beside = sum(1 for file in files if file.path is not None)
    served = sum(1 for file in files if file.address is not None)
    document.set_collection(collection)
    for file in files:
        document.attach(file)

    content = hqf_pdf.Content()
    content.draw_text(font, 16.0, MARGIN, 780.0, words.title)
    paragraph(content, font, 10.0, 750.0, hqf_pdf.Rgb.gray(0.0), words.intro)

    headings = (
        words.file_heading,
        words.columns[0],
        words.columns[1],
        words.columns[2],
    )
    for column, heading in zip(COLUMNS, headings):
        content.draw_text(font, 9.0, MARGIN + column, HEADING, heading)
    content.save_state()
    content.set_stroke(hqf_pdf.Rgb.gray(0.75))
    content.set_line_width(0.5)
    content.move_to(MARGIN, HEADING - 6.0)
    content.line_to(MARGIN + WIDTH, HEADING - 6.0)
    content.stroke()
    content.restore_state()

    for lot, at in enumerate(ROWS):
        cells = (FILES[lot], words.lots[lot], day_of(SENT[lot]), words.totals[lot])
        for column, cell in zip(COLUMNS, cells):
            content.draw_text(font, 10.0, MARGIN + column, at, cell)

    paragraph(content, font, 8.5, 566.0, hqf_pdf.Rgb.gray(0.35), words.caption)

    content.draw_text(font, 11.0, MARGIN, STATED_TOP, words.stated)
    top = STATED_TOP - 18.0
    for line in said:
        flow = hqf_pdf.TextFlow(font, 8.5, leading=11.0)
        lines = flow.break_lines(line, WIDTH)
        flow.draw(content, lines, MARGIN, top, WIDTH)
        top -= flow.height(lines) + 3.0

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

    document.set_open_mode(hqf_pdf.OpenMode.Attachments)

    written = document.write(out)
    print(
        f"wrote {out}: {written} bytes, {len(FILES)} lots, {beside} file kept beside the "
        f"pack and {served} served from an address"
    )


if __name__ == "__main__":
    main()
