"""The four kinds of PDF function, and what each is good for.

The Python twin of the `write_functions` example in Rust. A gradient, a spot
ink, a transfer curve: each is a number going in and a tuple of numbers coming
out, and the file says how. Four panels: a spectrum read off a table of samples,
a stepped ramp worked out by a stack program, an ink whose tint transform is a
program of its own, and the table and a pair of endpoints stitched end to end.
Each program is printed under what it draws.

The second page runs every other operator of the calculator language, each in a program
of its own that draws a grey ramp, printed under it as the file carries it. The third
samples one ramp at every depth a table can hold, fills it in by a cubic rather than a
line, reads a table back to front with its outputs narrowed, and joins the three other
kinds of function under one range.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the page is captioned in. The two programs are not language: what is printed
under the second and third panels is the very working the file carries, and it is
spelled the same way whoever reads the page.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The left edge of everything, and how wide a panel runs.
LEFT = 70.0
WIDTH = 455.0
# How tall a painted bar is.
BAR = 74.0

# The foot of each of the four panels, down the page.
SPECTRUM_FOOT = 632.0
STEPPED_FOOT = 476.0
INK_FOOT = 310.0
JOINED_FOOT = 150.0

# The seven colours the spectrum is read off, red round to violet.
SPECTRUM = [
    [0.85, 0.12, 0.12],
    [0.95, 0.55, 0.10],
    [0.95, 0.85, 0.15],
    [0.15, 0.65, 0.25],
    [0.10, 0.60, 0.75],
    [0.15, 0.20, 0.75],
    [0.45, 0.15, 0.60],
]

# The left edge of each tint swatch, and how wide one runs.
TINT_X = [70.0, 135.0, 200.0, 265.0, 330.0, 395.0, 460.0]
SWATCH = 59.0

# The tints the ink is shown at, and what each of them is called.
TINTS = [
    (0.1, "10%"),
    (0.25, "25%"),
    (0.4, "40%"),
    (0.55, "55%"),
    (0.7, "70%"),
    (0.85, "85%"),
    (1.0, "100%"),
]

# The working printed under the stepped ramp, which is the program the file carries
# rather than anything a language writes.
STEPPED_PROGRAM = "{ 7 mul cvi dup 6 gt { pop 6 } if 6 div dup 1 exch sub 0.25 }"
# The working printed under the ink, likewise.
INK_PROGRAM = "{ dup 0.9 mul exch dup 0.6 mul exch 3 exp 0.4 mul 0 exch }"

# The ink the tint ramp is drawn in, which is a name a press knows the ink by rather
# than a word a language writes.
INK = "Corporate Blue"


# The top of the first strip of the second page, and how far each strip stands under the
# one before.
OPERATOR_TOP = 720.0
OPERATOR_STEP = 52.0

# The five samples the third page's ramp is read off, black to white.
RAMP = [0.0, 0.3, 0.45, 0.8, 1.0]

# How far each strip of the third page stands under the one before.
SAMPLE_STEP = 52.0


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

    # The title over the page.
    title: str
    # The two lines under the title.
    intro: tuple[str, str]
    # The caption over each of the four panels, and the note under it. The two notes
    # that stand under a program hold only what follows it.
    captions: tuple[
        tuple[str, str], tuple[str, str], tuple[str, str], tuple[str, str]
    ]
    # The two lines of the closing note.
    closing: tuple[str, str]
    # The second page's title, and the line under it.
    operators: tuple[str, str]
    # The third page's title, and the line under it.
    samples: tuple[str, str]
    # What the three last strips of the third page show.
    sample_notes: tuple[str, str, str]

    def stepped_note(self) -> str:
        """The note under the stepped ramp: the program the file carries, then what
        the caption says about it."""
        return f"{STEPPED_PROGRAM}{self.captions[1][1]}"

    def ink_note(self) -> str:
        """The note under the ink, likewise."""
        return f"{INK_PROGRAM}{self.captions[2][1]}"


# The page in English.
ENGLISH = Words(
    title="What a function says a colour does",
    intro=(
        (
            "A gradient, an ink, a curve: one number goes in and a colour comes out, "
            "and the file says how."
        ),
        (
            "It can say it by naming two ends, by tabulating the answer, or by writing "
            "out the working."
        ),
    ),
    captions=(
        (
            "Read off a table: seven colours, sixteen bits apiece",
            (
                "The table holds what the colour is at seven points; between them "
                "reading software draws the line."
            ),
        ),
        (
            "Worked out: a program that cuts the input into bands",
            "  — reading software runs this.",
        ),
        (
            "One ink, shown through a program where the press is not to hand",
            "  — the black lags, then catches up.",
        ),
        (
            "Stitched: the table over the first half, two endpoints over the second",
            (
                "A piece written as a stream stands as an object of its own, and the "
                "joint points at it."
            ),
        ),
    ),
    closing=(
        (
            "A table and a program are both written as streams, which is what lets "
            "them hold as much as they do."
        ),
        (
            "The program is built here as steps, not as text, so what reaches the file "
            "is what can be written."
        ),
    ),
    operators=(
        "The rest of the calculator, one operator a strip",
        "Each strip is a grey ramp worked out by the program printed under it.",
    ),
    samples=(
        "One ramp of five samples, at every depth a table holds",
        (
            "The fewer bits a sample takes, the fewer greys it can say: one bit says "
            "black or white."
        ),
    ),
    sample_notes=(
        "Filled in by a cubic rather than a straight line",
        "Read back to front, its outputs narrowed to 0.2 to 0.8",
        "Three kinds of function joined end to end, under a range of 0.2 to 0.8",
    ),
)

# The page in French.
FRENCH = Words(
    title="Comment une fonction décrit une couleur",
    intro=(
        (
            "Un dégradé, une encre, une courbe : un nombre entre, une couleur sort, et "
            "le fichier dit comment."
        ),
        (
            "Il peut le dire en nommant deux extrémités, en tabulant la réponse, ou en "
            "écrivant le calcul."
        ),
    ),
    captions=(
        (
            "Lu dans une table : sept couleurs, seize bits chacune",
            (
                "La table dit la couleur en sept points ; entre eux, le logiciel de "
                "lecture trace la droite."
            ),
        ),
        (
            "Calculé : un programme qui découpe l’entrée en bandes",
            "  — le logiciel de lecture exécute ceci.",
        ),
        (
            "Une encre, rendue par un programme en l’absence de presse",
            "  — le noir tarde, puis se rattrape.",
        ),
        (
            (
                "Assemblé : la table sur la première moitié, deux extrémités sur la "
                "seconde"
            ),
            (
                "Un morceau écrit en flux devient un objet distinct, auquel "
                "l’assemblage renvoie."
            ),
        ),
    ),
    closing=(
        (
            "Une table et un programme s’écrivent tous deux comme des flux, ce qui "
            "leur permet d’en contenir autant."
        ),
        (
            "Le programme est construit ici par étapes, non en texte : n’arrive au "
            "fichier que ce qui peut s’y écrire."
        ),
    ),
    operators=(
        "Le reste de la calculatrice, un opérateur par bande",
        (
            "Chaque bande est un dégradé de gris que calcule le programme imprimé "
            "dessous."
        ),
    ),
    samples=(
        "Un dégradé de cinq échantillons, à chaque profondeur de table",
        (
            "Moins un échantillon a de bits, moins il offre de gris : un seul bit donne "
            "noir ou blanc."
        ),
    ),
    sample_notes=(
        "Interpolé par une cubique plutôt que par une droite",
        "Lu à rebours, ses sorties ramenées entre 0,2 et 0,8",
        "Trois sortes de fonctions mises bout à bout, bornées entre 0,2 et 0,8",
    ),
)

# The page in Dutch.
DUTCH = Words(
    title="Wat een functie zegt dat een kleur doet",
    intro=(
        (
            "Een verloop, een inkt, een curve: er gaat een getal in en er komt een "
            "kleur uit, en het bestand zegt hoe."
        ),
        (
            "Dat kan met twee uiteinden, met het antwoord in een tabel of met de "
            "uitgeschreven berekening."
        ),
    ),
    captions=(
        (
            "Uit een tabel gelezen: zeven kleuren, zestien bits per stuk",
            (
                "De tabel bevat de kleur op zeven punten; daartussen trekt de "
                "leessoftware de lijn."
            ),
        ),
        (
            "Berekend: een programma dat de invoer in banden verdeelt",
            "  — dit voert de leessoftware uit.",
        ),
        (
            "Eén inkt, getoond via een programma wanneer er geen pers is",
            "  — het zwart blijft achter en haalt dan in.",
        ),
        (
            "Aan elkaar gestikt: de tabel over de eerste helft, twee eindpunten over de tweede",
            (
                "Een als stroom geschreven stuk is een eigen object, en de naad wijst "
                "ernaar."
            ),
        ),
    ),
    closing=(
        (
            "Een tabel en een programma worden allebei als stroom geschreven, en "
            "daardoor kunnen ze zoveel bevatten."
        ),
        (
            "Het programma bestaat hier uit stappen, niet uit tekst: in het bestand "
            "komt wat geschreven kan worden."
        ),
    ),
    operators=(
        "De rest van de rekenmachine, één operator per strook",
        "Elke strook is een grijsverloop berekend door het programma dat eronder staat.",
    ),
    samples=(
        "Vijf monsters, op elke diepte die een tabel toelaat",
        (
            "Hoe minder bits per monster, hoe minder grijstinten: één bit geeft "
            "alleen zwart of wit."
        ),
    ),
    sample_notes=(
        "Ingevuld met een kubische curve in plaats van een rechte lijn",
        "Achterstevoren gelezen, met de uitvoer beperkt tot tussen 0,2 en 0,8",
        "Drie soorten functie achter elkaar gezet, binnen een bereik van 0,2 tot 0,8",
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="O que uma função diz que uma cor faz",
    intro=(
        (
            "Um degradé, uma tinta, uma curva: entra um número e sai uma cor, e o "
            "ficheiro diz como."
        ),
        (
            "Pode dizê-lo nomeando dois extremos, tabelando a resposta ou escrevendo "
            "o cálculo."
        ),
    ),
    captions=(
        (
            "Lido de uma tabela: sete cores, dezasseis bits cada uma",
            (
                "A tabela guarda a cor em sete pontos; entre eles, o programa de "
                "leitura traça a linha."
            ),
        ),
        (
            "Calculado: um programa que corta a entrada em faixas",
            "  — é o programa de leitura que o executa.",
        ),
        (
            "Uma tinta, mostrada através de um programa quando a gráfica não está à mão",
            "  — o preto atrasa-se e depois recupera.",
        ),
        (
            "Cosido: a tabela na primeira metade, dois extremos na segunda",
            (
                "Uma peça escrita como fluxo é um objeto próprio, e a junção aponta "
                "para ele."
            ),
        ),
    ),
    closing=(
        (
            "Uma tabela e um programa escrevem-se ambos como fluxos, e é isso que "
            "lhes permite guardar tanto."
        ),
        (
            "O programa constrói-se aqui como passos, não como texto: o que chega ao "
            "ficheiro é o que pode ser escrito."
        ),
    ),
    operators=(
        "O resto da calculadora, um operador por faixa",
        "Cada faixa é uma rampa de cinzentos calculada pelo programa impresso por baixo.",
    ),
    samples=(
        "Cinco amostras, em cada profundidade que uma tabela admite",
        (
            "Quanto menos bits uma amostra ocupa, menos cinzentos pode dizer: um bit "
            "diz preto ou branco."
        ),
    ),
    sample_notes=(
        "Preenchido por uma cúbica em vez de uma reta",
        "Lido de trás para a frente, com as saídas reduzidas de 0,2 a 0,8",
        "Três tipos de função unidos em sequência, num intervalo de 0,2 a 0,8",
    ),
)

# The page in Italian.
ITALIAN = Words(
    title="Ciò che una funzione dice che fa un colore",
    intro=(
        (
            "Un gradiente, un inchiostro, una curva: entra un numero ed esce un "
            "colore, e il file dice come."
        ),
        (
            "Può dirlo nominando due estremi, tabulando la risposta o scrivendo il "
            "calcolo."
        ),
    ),
    captions=(
        (
            "Letto da una tabella: sette colori, sedici bit ciascuno",
            (
                "La tabella tiene il colore in sette punti; tra questi il programma "
                "di lettura traccia la linea."
            ),
        ),
        (
            "Calcolato: un programma che taglia l’ingresso in fasce",
            "  — questo lo esegue il programma di lettura.",
        ),
        (
            "Un inchiostro, mostrato con un programma quando la macchina da stampa non è a portata",
            "  — il nero resta indietro, poi recupera.",
        ),
        (
            "Cucito: la tabella sulla prima metà, due estremi sulla seconda",
            (
                "Un pezzo scritto come flusso è un oggetto a sé, e la giunzione "
                "rimanda a esso."
            ),
        ),
    ),
    closing=(
        (
            "Una tabella e un programma si scrivono entrambi come flussi, ed è questo "
            "che permette loro di contenere tanto."
        ),
        (
            "Il programma qui si costruisce a passi, non come testo, così ciò che "
            "arriva al file è ciò che si può scrivere."
        ),
    ),
    operators=(
        "Il resto della calcolatrice, un operatore per striscia",
        "Ogni striscia è una scala di grigi calcolata dal programma stampato sotto.",
    ),
    samples=(
        "Cinque campioni, a ogni profondità che una tabella ammette",
        (
            "Meno bit occupa un campione, meno grigi può dire: un bit dice nero o "
            "bianco."
        ),
    ),
    sample_notes=(
        "Riempito con una cubica anziché con una retta",
        "Letto al contrario, con le uscite ristrette da 0,2 a 0,8",
        "Tre tipi di funzione uniti in fila, su un intervallo da 0,2 a 0,8",
    ),
)

# The page in Spanish.
SPANISH = Words(
    title="Lo que una función dice que hace un color",
    intro=(
        (
            "Un degradado, una tinta, una curva: entra un número y sale un color, y "
            "el archivo dice cómo."
        ),
        (
            "Puede decirlo nombrando dos extremos, tabulando la respuesta o "
            "escribiendo el cálculo."
        ),
    ),
    captions=(
        (
            "Leído de una tabla: siete colores, dieciséis bits cada uno",
            (
                "La tabla guarda el color en siete puntos; entre ellos, el programa "
                "de lectura traza la línea."
            ),
        ),
        (
            "Calculado: un programa que corta la entrada en bandas",
            "  — esto lo ejecuta el programa de lectura.",
        ),
        (
            "Una tinta, mostrada con un programa cuando la imprenta no está a mano",
            "  — el negro se retrasa y luego se pone al día.",
        ),
        (
            "Cosido: la tabla en la primera mitad, dos extremos en la segunda",
            (
                "Una pieza escrita como flujo es un objeto propio, y la unión apunta "
                "a él."
            ),
        ),
    ),
    closing=(
        (
            "Una tabla y un programa se escriben ambos como flujos, y por eso pueden "
            "guardar tanto."
        ),
        (
            "El programa se construye aquí con pasos, no con texto: lo que llega al "
            "archivo es lo que puede escribirse."
        ),
    ),
    operators=(
        "El resto de la calculadora, un operador por franja",
        "Cada franja es una rampa de grises calculada por el programa impreso debajo.",
    ),
    samples=(
        "Cinco muestras, en cada profundidad que admite una tabla",
        (
            "Cuantos menos bits ocupa una muestra, menos grises puede decir: un bit "
            "dice negro o blanco."
        ),
    ),
    sample_notes=(
        "Rellenado con una cúbica en vez de una recta",
        "Leído del revés, con sus salidas reducidas al intervalo de 0,2 a 0,8",
        "Tres tipos de función unidos uno tras otro, en un rango de 0,2 a 0,8",
    ),
)

# The page in German.
GERMAN = Words(
    title="Was eine Funktion über eine Farbe sagt",
    intro=(
        (
            "Verlauf, Farbe, Kurve: Eine Zahl geht hinein, eine Farbe heraus, und die "
            "Datei sagt, wie."
        ),
        (
            "Sie sagt es mit zwei Enden, mit einer Tabelle der Antworten oder mit dem "
            "Rechenweg."
        ),
    ),
    captions=(
        (
            "Aus einer Tabelle gelesen: sieben Farben, je sechzehn Bit",
            (
                "Die Tabelle enthält die Farbe an sieben Punkten; dazwischen zieht "
                "die Lesesoftware die Linie."
            ),
        ),
        (
            "Ausgerechnet: ein Programm, das die Eingabe in Bänder teilt",
            "  — die Lesesoftware führt dies aus.",
        ),
        (
            "Eine Farbe, durch ein Programm gezeigt, wo die Druckmaschine nicht zur Hand ist",
            "  — das Schwarz hinkt nach und holt dann auf.",
        ),
        (
            "Zusammengesetzt: die Tabelle über die erste Hälfte, zwei Endpunkte über die zweite",
            (
                "Ein als Datenstrom geschriebenes Stück ist ein eigenes Objekt, und "
                "die Naht verweist darauf."
            ),
        ),
    ),
    closing=(
        (
            "Tabelle und Programm sind beide Datenströme, darum fassen sie so viel."
        ),
        (
            "Das Programm wird aus Schritten gebaut, nicht aus Text: In die Datei "
            "kommt nur Schreibbares."
        ),
    ),
    operators=(
        "Die übrigen Operatoren des Rechners, einer je Streifen",
        "Jeder Streifen ist ein Graukeil, berechnet vom Programm, das darunter gedruckt ist.",
    ),
    samples=(
        "Ein Keil aus fünf Werten, in jeder Tiefe, die eine Tabelle fasst",
        (
            "Je weniger Bit ein Wert hat, desto weniger Grautöne: Ein Bit sagt "
            "schwarz oder weiß."
        ),
    ),
    sample_notes=(
        "Mit einer kubischen Kurve statt einer Geraden gefüllt",
        "Rückwärts gelesen, die Ausgaben auf 0,2 bis 0,8 verengt",
        "Drei Arten von Funktion aneinandergefügt, in einem Bereich von 0,2 bis 0,8",
    ),
)

# 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 caption(content, font, base, heading, note):
    """Draw a caption over a panel whose foot is at base, and the note under it."""
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.2))
    content.draw_text(font, 11.0, LEFT, base + BAR + 10.0, heading)
    content.set_fill(hqf_pdf.Rgb.gray(0.4))
    content.draw_text(font, 8.5, LEFT, base - 26.0, note)
    content.restore_state()


def bar(content, shading, y):
    """Paint a gradient across the width of the page, its foot at y."""
    content.save_state()
    content.rect(LEFT, y, WIDTH, BAR)
    content.clip()
    content.end_path()
    content.draw_shading(shading)
    content.restore_state()


def tint_ramp(content, font, ink, y):
    """Draw the ink at every tint, each swatch labelled with how much goes down."""
    for x, (tint, name) in zip(TINT_X, TINTS):
        content.save_state()
        content.set_fill_space(ink)
        content.set_fill_components([tint])
        content.rect(x, y, SWATCH, BAR)
        content.fill()
        content.restore_state()

        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.35))
        content.draw_text(font, 7.5, x, y - 11.0, name)
        content.restore_state()


def spectrum_table():
    """The table the spectrum is read off: every colour of SPECTRUM, red first."""
    return [value for colour in SPECTRUM for value in colour]


def stepped():
    """The program that steps a ramp: the input is cut into seven bands, the last
    of which takes the input's own top end, and the band runs one output up while
    it runs another down."""
    return [
        hqf_pdf.Op.integer(7),
        hqf_pdf.Op.multiply(),
        hqf_pdf.Op.to_integer(),
        hqf_pdf.Op.duplicate(),
        hqf_pdf.Op.integer(6),
        hqf_pdf.Op.greater(),
        hqf_pdf.Op.if_([hqf_pdf.Op.pop(), hqf_pdf.Op.integer(6)]),
        hqf_pdf.Op.integer(6),
        hqf_pdf.Op.divide(),
        hqf_pdf.Op.duplicate(),
        hqf_pdf.Op.integer(1),
        hqf_pdf.Op.exchange(),
        hqf_pdf.Op.subtract(),
        hqf_pdf.Op.real(0.25),
    ]


def ink_transform():
    """The program an ink is shown through: the tint runs the cyan and the magenta
    plates straight up, leaves the yellow one empty, and lets the black one lag
    behind until the ink is nearly solid."""
    return [
        hqf_pdf.Op.duplicate(),
        hqf_pdf.Op.real(0.9),
        hqf_pdf.Op.multiply(),
        hqf_pdf.Op.exchange(),
        hqf_pdf.Op.duplicate(),
        hqf_pdf.Op.real(0.6),
        hqf_pdf.Op.multiply(),
        hqf_pdf.Op.exchange(),
        hqf_pdf.Op.integer(3),
        hqf_pdf.Op.power(),
        hqf_pdf.Op.real(0.4),
        hqf_pdf.Op.multiply(),
        hqf_pdf.Op.integer(0),
        hqf_pdf.Op.exchange(),
    ]


def spectrum_function():
    """A fresh table of the spectrum, sixteen bits to a sample."""
    return hqf_pdf.Function.sampled(
        [(0.0, 1.0)],
        [(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
        [len(SPECTRUM)],
        spectrum_table(),
        depth=hqf_pdf.SampleDepth.Bits16,
    )


def draw(content, font, words, gradients, ink):
    """Draw the page: the title, the four panels and what is said under them."""
    spectrum, steps, joined = gradients

    content.draw_text(font, 16.0, LEFT, 786.0, words.title)
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 10.0, LEFT, 767.0, words.intro[0])
    content.draw_text(font, 10.0, LEFT, 753.0, words.intro[1])
    content.restore_state()

    bar(content, spectrum, SPECTRUM_FOOT)
    caption(
        content, font, SPECTRUM_FOOT, words.captions[0][0], words.captions[0][1]
    )

    bar(content, steps, STEPPED_FOOT)
    caption(content, font, STEPPED_FOOT, words.captions[1][0], words.stepped_note())

    tint_ramp(content, font, ink, INK_FOOT)
    caption(content, font, INK_FOOT, words.captions[2][0], words.ink_note())

    bar(content, joined, JOINED_FOOT)
    caption(content, font, JOINED_FOOT, words.captions[3][0], words.captions[3][1])

    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.45))
    content.draw_text(font, 9.0, LEFT, 96.0, words.closing[0])
    content.draw_text(font, 9.0, LEFT, 82.0, words.closing[1])
    content.restore_state()



def operator_programs():
    """The programs of the second page: every operator of the calculator language the
    first page leaves out, each turning the position along a strip into a grey from
    black to white."""
    return [
        [
            hqf_pdf.Op.real(0.5),
            hqf_pdf.Op.subtract(),
            hqf_pdf.Op.abs(),
            hqf_pdf.Op.integer(2),
            hqf_pdf.Op.multiply(),
        ],
        [
            hqf_pdf.Op.integer(10),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.integer(3),
            hqf_pdf.Op.integer_divide(),
            hqf_pdf.Op.integer(3),
            hqf_pdf.Op.divide(),
        ],
        [hqf_pdf.Op.negate(), hqf_pdf.Op.integer(1), hqf_pdf.Op.add()],
        [
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.round(),
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.ceiling(),
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.truncate(),
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.divide(),
        ],
        [hqf_pdf.Op.sqrt()],
        [
            hqf_pdf.Op.integer(360),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.sin(),
            hqf_pdf.Op.integer(1),
            hqf_pdf.Op.add(),
            hqf_pdf.Op.integer(2),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(360),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.cos(),
            hqf_pdf.Op.integer(1),
            hqf_pdf.Op.add(),
            hqf_pdf.Op.integer(2),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(1),
            hqf_pdf.Op.atan(),
            hqf_pdf.Op.integer(45),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.real(1.718282),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.integer(1),
            hqf_pdf.Op.add(),
            hqf_pdf.Op.natural_log(),
        ],
        [
            hqf_pdf.Op.integer(9),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.integer(1),
            hqf_pdf.Op.add(),
            hqf_pdf.Op.log10(),
        ],
        [
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.to_real(),
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.integer(12),
            hqf_pdf.Op.and_(),
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.integer(3),
            hqf_pdf.Op.or_(),
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.integer(9),
            hqf_pdf.Op.xor(),
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.not_(),
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.and_(),
            hqf_pdf.Op.integer(15),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.integer(1),
            hqf_pdf.Op.exchange(),
            hqf_pdf.Op.shift(),
            hqf_pdf.Op.integer(16),
            hqf_pdf.Op.divide(),
        ],
        [
            hqf_pdf.Op.integer(4),
            hqf_pdf.Op.multiply(),
            hqf_pdf.Op.to_integer(),
            hqf_pdf.Op.integer(2),
            hqf_pdf.Op.equal(),
            hqf_pdf.Op.if_else([hqf_pdf.Op.integer(1)], [hqf_pdf.Op.integer(0)]),
        ],
        [
            hqf_pdf.Op.real(0.5),
            hqf_pdf.Op.less(),
            hqf_pdf.Op.if_else([hqf_pdf.Op.integer(0)], [hqf_pdf.Op.integer(1)]),
        ],
        [
            hqf_pdf.Op.real(0.25),
            hqf_pdf.Op.less_or_equal(),
            hqf_pdf.Op.if_else([hqf_pdf.Op.real(0.2)], [hqf_pdf.Op.real(0.8)]),
        ],
        [
            hqf_pdf.Op.real(0.75),
            hqf_pdf.Op.greater_or_equal(),
            hqf_pdf.Op.if_else([hqf_pdf.Op.integer(1)], [hqf_pdf.Op.real(0.4)]),
        ],
        [
            hqf_pdf.Op.true_(),
            hqf_pdf.Op.if_else(
                [hqf_pdf.Op.integer(1), hqf_pdf.Op.exchange(), hqf_pdf.Op.subtract()],
                [hqf_pdf.Op.real(0.5), hqf_pdf.Op.multiply()],
            ),
        ],
        [
            hqf_pdf.Op.false_(),
            hqf_pdf.Op.if_else(
                [hqf_pdf.Op.integer(1), hqf_pdf.Op.exchange(), hqf_pdf.Op.subtract()],
                [hqf_pdf.Op.real(0.5), hqf_pdf.Op.multiply()],
            ),
        ],
    ]


def strip(document, content, at, size, function):
    """A grey strip `size` wide and tall, its foot at `at`, painted by `function` from
    its left edge to its right."""
    x, y = at
    width, height = size
    shading = document.add_shading(
        hqf_pdf.Axial((x, y), (x + width, y), hqf_pdf.DeviceSpace.Gray, function)
    )
    content.save_state()
    content.rect(x, y, width, height)
    content.clip()
    content.end_path()
    content.draw_shading(shading)
    content.restore_state()
    content.save_state()
    content.set_stroke(hqf_pdf.Rgb.gray(0.75))
    content.set_line_width(0.5)
    content.rect(x, y, width, height)
    content.stroke()
    content.restore_state()


def operators_page(document, font, words):
    """Draw the second page: every other operator, each in a program of its own, two
    columns of strips with the program printed under each."""
    content = hqf_pdf.Content()
    content.draw_text(font, 16.0, LEFT, 786.0, words.operators[0])
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 10.0, LEFT, 767.0, words.operators[1])
    content.restore_state()

    programs = operator_programs()
    per_column = (len(programs) + 1) // 2
    for place, body in enumerate(programs):
        x = LEFT if place < per_column else LEFT + 235.0
        y = OPERATOR_TOP - (place % per_column) * OPERATOR_STEP
        calculation = hqf_pdf.Calculation([(0.0, 1.0)], [(0.0, 1.0)], body)
        program = calculation.program()
        strip(document, content, (x, y), (220.0, 16.0), calculation)
        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.3))
        content.draw_text(font, 6.5, x, y - 10.0, program)
        content.restore_state()
    return content


def samples_page(document, font, words):
    """Draw the third page: one ramp sampled at every depth a table holds, then filled
    in by a cubic, read back to front, and a joint clipped to a range."""
    content = hqf_pdf.Content()
    content.draw_text(font, 16.0, LEFT, 786.0, words.samples[0])
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 10.0, LEFT, 767.0, words.samples[1])
    content.restore_state()

    def ramp(**shape):
        return hqf_pdf.Function.sampled([(0.0, 1.0)], [(0.0, 1.0)], [5], RAMP, **shape)

    depths = (
        ("BitsPerSample 1", hqf_pdf.SampleDepth.Bits1),
        ("BitsPerSample 2", hqf_pdf.SampleDepth.Bits2),
        ("BitsPerSample 4", hqf_pdf.SampleDepth.Bits4),
        ("BitsPerSample 8", hqf_pdf.SampleDepth.Bits8),
        ("BitsPerSample 12", hqf_pdf.SampleDepth.Bits12),
        ("BitsPerSample 16", hqf_pdf.SampleDepth.Bits16),
        ("BitsPerSample 24", hqf_pdf.SampleDepth.Bits24),
        ("BitsPerSample 32", hqf_pdf.SampleDepth.Bits32),
    )
    y = 720.0
    for label, depth in depths:
        strip(document, content, (LEFT, y), (WIDTH, 22.0), ramp(depth=depth))
        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.3))
        content.draw_text(font, 7.5, LEFT, y - 11.0, label)
        content.restore_state()
        y -= SAMPLE_STEP

    # The same five samples filled in by a cubic, then read back to front: the input
    # runs over the table from its last point to its first, and the outputs are narrowed
    # to the middle of the grey.
    cubic = ramp(order=hqf_pdf.SampleOrder.Cubic)
    reversed_ramp = ramp(
        order=hqf_pdf.SampleOrder.Linear, encode=[(4.0, 0.0)], decode=[(0.2, 0.8)]
    )

    # A joint of the three other kinds, under a range that keeps its outputs to the
    # middle of the grey.
    joint = hqf_pdf.Function.stitching(
        (0.0, 1.0),
        [
            hqf_pdf.Function.exponential((0.0, 1.0), 2.0),
            hqf_pdf.Calculation(
                [(0.0, 1.0)],
                [(0.0, 1.0)],
                [
                    hqf_pdf.Op.real(0.5),
                    hqf_pdf.Op.multiply(),
                    hqf_pdf.Op.real(0.5),
                    hqf_pdf.Op.add(),
                ],
            ),
            ramp(),
        ],
        [1.0 / 3.0, 2.0 / 3.0],
        [(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
        range=[(0.2, 0.8)],
    )

    for function, note in zip((cubic, reversed_ramp, joint), words.sample_notes):
        strip(document, content, (LEFT, y), (WIDTH, 22.0), function)
        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.3))
        content.draw_text(font, 7.5, LEFT, y - 11.0, note)
        content.restore_state()
        y -= SAMPLE_STEP
    return content


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

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    # A spectrum no pair of endpoints could describe: seven colours on a grid, and
    # reading software draws the line between one point and the next.
    spectrum = document.add_shading(
        hqf_pdf.Axial(
            (LEFT, SPECTRUM_FOOT),
            (LEFT + WIDTH, SPECTRUM_FOOT),
            hqf_pdf.DeviceSpace.Rgb,
            spectrum_function(),
        )
    )

    # A ramp with corners in it: the program cuts the input into bands, which no
    # interpolation between endpoints can do.
    steps = document.add_shading(
        hqf_pdf.Axial(
            (LEFT, STEPPED_FOOT),
            (LEFT + WIDTH, STEPPED_FOOT),
            hqf_pdf.DeviceSpace.Rgb,
            hqf_pdf.Calculation([(0.0, 1.0)], [(0.0, 1.0)] * 3, stepped()),
        )
    )

    ink = document.add_color_space(
        hqf_pdf.Separation(
            INK,
            hqf_pdf.DeviceSpace.Cmyk,
            hqf_pdf.Calculation(
                [(0.0, 1.0)], [(0.0, 1.0)] * 4, ink_transform()
            ),
        )
    )

    # The table over the first half of the run, and a fade into the dark over the
    # second: a joint holds pieces of whatever kind, and the one that is a stream stands
    # as an object of its own.
    joined = document.add_shading(
        hqf_pdf.Axial(
            (LEFT, JOINED_FOOT),
            (LEFT + WIDTH, JOINED_FOOT),
            hqf_pdf.DeviceSpace.Rgb,
            hqf_pdf.Function.stitching(
                (0.0, 1.0),
                [
                    spectrum_function(),
                    hqf_pdf.Function.exponential(
                        (0.0, 1.0),
                        1.0,
                        ([0.45, 0.15, 0.60], [0.06, 0.05, 0.12]),
                    ),
                ],
                [0.5],
                [(0.0, 1.0), (0.0, 1.0)],
            ),
        )
    )

    content = hqf_pdf.Content()
    draw(content, font, words, (spectrum, steps, joined), ink)

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

    page = hqf_pdf.Page.a4()
    page.set_content(operators_page(document, font, words))
    document.add_page(page)

    page = hqf_pdf.Page.a4()
    page.set_content(samples_page(document, font, words))
    document.add_page(page)

    written = document.write(out)
    print(
        f"wrote {out}: {written} bytes, a table of {len(SPECTRUM)} colours, "
        "two programs and a joint"
    )


if __name__ == "__main__":
    main()
