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

# 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


@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


# 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",
)

# 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 où sont les 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 qui tient 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="reprise dans les {days} jours qui suivent la réception",
    note=(
        "Chaque marque du plan dit les mêmes mots que sa ligne dans la liste.",
        "Le logiciel de lecture les affiche, les masque ou les laisse hors du papier.",
    ),
    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",
        "Coulures de peinture sur cloison",
        "Dalle de plafond fendue au centre",
        "Cloison vitrée rayée",
        "Revêtement de sol qui se décolle",
        "Trois prises mortes au sud",
        "Luminaire manquant au-dessus",
        "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="à reprendre avant le",
    list="Les réserves, une par ligne",
    summary="Ce que les réserves représentent",
    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 reprise",
    page="Page",
    of="sur",
)

# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}


def 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 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."""
    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))

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