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


@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]

    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 the "
            "reader draws the line.",
        ),
        (
            "Worked out: a program that cuts the input into bands",
            "  — the reader 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.",
    ),
)

# The page in French.
FRENCH = Words(
    title="Ce qu'une fonction fait dire à 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 lecteur trace "
            "la droite.",
        ),
        (
            "Calculé : un programme qui découpe l'entrée en bandes",
            "  — le lecteur exécute ceci.",
        ),
        (
            "Une encre, montrée par un programme quand la presse n'est pas là",
            "  — le noir traîne, puis rattrape.",
        ),
        (
            "Cousu : la table sur la première moitié, deux extrémités sur la seconde",
            "Un morceau écrit comme un flux est un objet à part, et la couture "
            "pointe dessus.",
        ),
    ),
    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 en étapes, pas en texte, donc ce qui arrive "
        "au fichier est ce qui peut s'écrire.",
    ),
)

# 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 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 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 the
    # reader 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)

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