"""A sheet that measures what it says, and asks not to be resized.

The Python twin of the `write_true_to_scale` example in Rust. A drawing printed to be
measured is worth nothing if the printer shrinks it to fit its own margins. This page
carries three rules — centimetres, inches and points — and states, in the file itself,
that it is to print at the size it was drawn and that reading software is not to offer
anything else. That last part arrived with PDF 2.0, so the file declares 2.0: nothing is
raised on the caller's behalf, and a file that asked for it while declaring less would
be refused rather than written.

It shows the two other things a caller decides about the file rather than about
its pages: how hard its streams are squeezed, and where the bytes go — here
straight into the file, a piece at a time, rather than into memory first.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the page is captioned in. What a rule is called is not one of them: the
length is worked out from the rule itself and only the unit is a word, so no
language can name a rule a length it does not run. The entries set beside what
the file asks for are not words either — they are what the file writes, spelled
the same way whoever reads the page.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# A4, in points.
SHEET = (595.276, 841.890)

# How far in from the edge of the sheet everything is set.
MARGIN = 64.0

# How far in from the left edge the entries the file writes stand, leaving the asking to
# their left.
ENTRY = 290.0

# One centimetre, in points.
CM = 72.0 / 2.54

# One inch, in points.
INCH = 72.0

# The three rules, from the top of the page down: the baseline they sit on, how far
# apart their ticks stand, how many steps they run for, how often a tick stands tall and
# carries a number, and what one step is worth in the unit named.
RULES = (
    (590.0, CM, 16, 1, 1),
    (500.0, INCH, 6, 1, 1),
    (410.0, 10.0, 20, 5, 10),
)

# The entries the file writes to ask for what it asks. They are read off the file rather
# than off the page, and stand the same way in every language.
WRITTEN = (
    "/PrintScaling /None",
    "/Enforce [ /PrintScaling ]",
    "/OpenAction [ page /Fit ]",
)


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

    # The title in the band across the head of the page.
    title: str
    # The three lines under the band.
    intro: tuple[str, str, str]
    # The unit each of the three rules measures in. The length itself is worked out from
    # the rule, so a rule cannot be named a length it does not run.
    units: tuple[str, str, str]
    # The heading over what the file asks for.
    asks: str
    # What the file asks for, one line per entry of `WRITTEN`.
    asked: tuple[str, str, str]
    # The two closing paragraphs, set with a blank line between them.
    closing: tuple[tuple[str, str, str], tuple[str, str, str, str]]

    def rule_name(self, index: int, steps: int, worth: int) -> str:
        """The length the rule at `index` runs, then the unit it runs in."""
        return f"{steps * worth} {self.units[index]}"


# The page in English.
ENGLISH = Words(
    title="True to scale",
    intro=(
        "Print this page at its own size and the three rules below measure what",
        "they say. Hold a ruler against them to see whether whatever printed it",
        "did as the file asked.",
    ),
    units=("centimetres", "inches", "points, at 72 to the inch"),
    asks="What this file asks for, and what it writes to ask",
    asked=(
        "Print every page at the size it was drawn.",
        "Do not offer to print it at any other size.",
        "Open on the whole sheet, rules and all.",
    ),
    closing=(
        (
            "The second of those entries arrived with PDF 2.0, so this file "
            "declares 2.0 on",
            "its first line. Nothing is raised on the caller's behalf: a file "
            "that asked for",
            "it while declaring less would be refused rather than quietly written.",
        ),
        (
            "Two more things were decided about the file rather than about its "
            "pages. Its",
            "streams were squeezed as small as the compressor goes, which costs "
            "time and",
            "saves bytes. And it went into the file a piece at a time as it was "
            "made, rather",
            "than being built whole in memory and written at the end.",
        ),
    ),
)

# The page in French.
FRENCH = Words(
    title="À l’échelle réelle",
    intro=(
        "Imprimez cette page à sa taille réelle : les trois règles ci-dessous",
        "mesurent ce qu’elles annoncent. Posez-y un double décimètre pour voir si",
        "l’impression a respecté ce que le fichier demandait.",
    ),
    units=("centimètres", "pouces", "points, 72 au pouce"),
    asks="Ce que ce fichier demande, et ce qu’il écrit pour le demander",
    asked=(
        "Imprimer chaque page à sa taille réelle.",
        "Ne proposer aucune autre taille d’impression.",
        "Ouvrir sur la feuille entière, règles comprises.",
    ),
    closing=(
        (
            ("La deuxième de ces entrées est apparue avec PDF 2.0 : ce fichier déclare"
             " donc"),
            "2.0 dès sa première ligne. Aucune version n’est relevée d’office : un",
            ("fichier qui la demanderait en déclarant moins serait refusé, non écrit "
             "en silence."),
        ),
        (
            "Deux autres réglages concernent le fichier plutôt que ses pages.",
            "Ses flux ont été compressés au niveau le plus fort, ce qui",
            ("coûte du temps mais fait gagner des octets. Et le fichier a été écrit "
             "morceau"),
            ("par morceau, à mesure de sa production, au lieu d’être monté entier en "
             "mémoire."),
        ),
    ),
)

# The page in Dutch.
DUTCH = Words(
    title="Op ware grootte",
    intro=(
        "Druk deze pagina op haar eigen formaat af en de drie linialen hieronder meten wat",
        "ze zeggen. Leg er een liniaal naast om te zien of wat haar afdrukte,",
        "deed wat het bestand vroeg.",
    ),
    units=("centimeter", "inch", "punten, 72 per inch"),
    asks="Wat dit bestand vraagt, en wat het schrijft om het te vragen",
    asked=(
        "Elke pagina op ware grootte afdrukken.",
        "Geen ander afdrukformaat aanbieden.",
        "Openen op het hele blad, linialen en al.",
    ),
    closing=(
        (
            "De tweede van die gegevens kwam met PDF 2.0, dus verklaart dit bestand "
            "2.0 op",
            "zijn eerste regel. Niets wordt stilzwijgend verhoogd: een bestand dat "
            "erom vroeg",
            "en minder verklaarde, zou geweigerd worden in plaats van stilletjes geschreven.",
        ),
        (
            "Nog twee dingen werden over het bestand beslist en niet over zijn "
            "pagina’s. Zijn",
            "stromen zijn zo klein gecomprimeerd als de compressor kan, wat tijd kost "
            "en",
            "bytes spaart. En het ging stukje voor stukje het bestand in terwijl het "
            "gemaakt werd,",
            "in plaats van in zijn geheel in het geheugen gebouwd en aan het einde geschreven.",
        ),
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="À escala real",
    intro=(
        "Imprima esta página ao seu próprio tamanho e as três réguas abaixo medem o",
        "que dizem. Encoste-lhes uma régua para ver se o programa que a imprimiu",
        "fez o que o ficheiro pedia.",
    ),
    units=("centímetros", "polegadas", "pontos, a 72 por polegada"),
    asks="O que este ficheiro pede, e o que escreve para o pedir",
    asked=(
        "Imprimir cada página em tamanho real.",
        "Não propor imprimi-la a nenhum outro tamanho.",
        "Abrir na folha inteira, réguas incluídas.",
    ),
    closing=(
        (
            "A segunda destas entradas chegou com o PDF 2.0, por isso este ficheiro "
            "declara 2.0 na",
            "sua primeira linha. A biblioteca nunca sobe a versão por si: um ficheiro "
            "que o pedisse",
            "declarando menos seria recusado em vez de escrito em silêncio.",
        ),
        (
            "Decidiram-se mais duas coisas sobre o ficheiro e não sobre as suas "
            "páginas. Os seus",
            "fluxos foram comprimidos o mais que o compressor permite, o que custa "
            "tempo e",
            "poupa bytes. E foi escrito uma peça de cada vez, à medida que era feito, "
            "em vez",
            "de ser construído inteiro em memória e escrito no fim.",
        ),
    ),
)

# The page in Italian.
ITALIAN = Words(
    title="In scala reale",
    intro=(
        "Stampate questa pagina alla sua dimensione e i tre righelli qui sotto misurano ciò",
        "che dicono. Accostatele un righello per vedere se ciò che l’ha stampata",
        "ha fatto quanto chiedeva il file.",
    ),
    units=("centimetri", "pollici", "punti, 72 per pollice"),
    asks="Cosa chiede questo file, e cosa scrive per chiederlo",
    asked=(
        "Stampare ogni pagina a grandezza reale.",
        "Non proporre di stamparla a un’altra dimensione.",
        "Aprire sul foglio intero, righelli compresi.",
    ),
    closing=(
        (
            "La seconda di queste voci è arrivata con PDF 2.0, così questo file "
            "dichiara 2.0 nella",
            "sua prima riga. Niente è alzato per conto di chi chiama: un file che la "
            "chiedesse",
            "dichiarando meno sarebbe rifiutato anziché scritto in silenzio.",
        ),
        (
            "Altre due cose sono state decise sul file e non sulle sue pagine. I suoi",
            "flussi sono compressi al massimo consentito dal compressore, il che "
            "costa tempo e",
            "risparmia byte. Ed è entrato nel file un pezzo alla volta, mentre si "
            "faceva, anziché",
            "essere costruito intero in memoria e scritto alla fine.",
        ),
    ),
)

# The page in Spanish.
SPANISH = Words(
    title="A escala real",
    intro=(
        "Imprima esta página a su propio tamaño y las tres reglas de abajo miden lo",
        "que dicen. Póngales una regla al lado para ver si lo que la imprimió",
        "hizo lo que pedía el archivo.",
    ),
    units=("centímetros", "pulgadas", "puntos, a 72 por pulgada"),
    asks="Lo que pide este archivo, y lo que escribe para pedirlo",
    asked=(
        "Imprimir cada página al tamaño en que se dibujó.",
        "No ofrecer imprimirla a ningún otro tamaño.",
        "Abrir en la hoja entera, reglas incluidas.",
    ),
    closing=(
        (
            "La segunda de esas entradas llegó con PDF 2.0, así que este archivo "
            "declara 2.0 en",
            "su primera línea. La versión nunca se sube por su cuenta: un archivo que "
            "lo pidiera",
            "declarando menos se rechazaría en vez de escribirse en silencio.",
        ),
        (
            "Se decidieron dos cosas más sobre el archivo y no sobre sus páginas. Sus",
            "flujos se comprimieron tanto como da el compresor, lo que cuesta tiempo y",
            "ahorra bytes. Y se escribió pieza a pieza a medida que se hacía, en vez",
            "de construirse entero en memoria y escribirse al final.",
        ),
    ),
)

# The page in German.
GERMAN = Words(
    title="Maßstabsgetreu",
    intro=(
        "Diese Seite in ihrer eigenen Größe drucken, und die drei Linien unten",
        "messen, was sie angeben. Ein Lineal daneben zeigt, ob das, was sie",
        "gedruckt hat, getan hat, was die Datei verlangte.",
    ),
    units=("Zentimeter", "Zoll", "Punkt, 72 pro Zoll"),
    asks="Was diese Datei verlangt, und was sie dafür schreibt",
    asked=(
        "Jede Seite in Originalgröße drucken.",
        "Keine andere Druckgröße anbieten.",
        "Auf dem ganzen Bogen öffnen, samt Linien.",
    ),
    closing=(
        (
            "Der zweite dieser Einträge kam mit PDF 2.0, also gibt diese Datei in "
            "ihrer",
            "ersten Zeile 2.0 an. Nichts wird für den Aufrufer angehoben: Eine Datei, "
            "die",
            "es verlangte und weniger angäbe, würde abgelehnt, nicht still geschrieben.",
        ),
        (
            "Zwei weitere Dinge wurden über die Datei entschieden statt über ihre "
            "Seiten.",
            "Ihre Datenströme wurden so klein gepresst, wie es der Kompressor vermag, "
            "was Zeit",
            "kostet und Bytes spart. Und sie ging Stück für Stück in die Datei, wie "
            "sie",
            "entstand, statt ganz im Speicher gebaut und am Ende geschrieben zu werden.",
        ),
    ),
)

# 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 rule(content, font, y, step, steps, tall, worth, name):
    """Draw a rule: a baseline, a tick at every step, the tall ones numbered.

    The ticks run up from the baseline and the numbers sit under it, so nothing a
    ruler is laid against is covered.
    """
    content.save_state()
    content.set_stroke(hqf_pdf.Color.gray(0.1))
    content.set_line_width(0.6)
    content.move_to(MARGIN, y)
    content.line_to(MARGIN + step * steps, y)
    content.stroke()

    for index in range(steps + 1):
        x = MARGIN + step * index
        height = 14.0 if index % tall == 0 else 7.0
        content.move_to(x, y)
        content.line_to(x, y + height)
        content.stroke()
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.35))
    for index in range(0, steps + 1, tall):
        x = MARGIN + step * index
        content.draw_text(font, 8.0, x - 2.0, y - 12.0, str(index * worth))
    content.set_fill(hqf_pdf.Color.gray(0.1))
    content.draw_text(font, 10.0, MARGIN, y + 22.0, name)
    content.restore_state()


def head(content, font, words):
    """Draw the band across the head of the page and what stands under it."""
    content.save_state()
    content.set_fill(hqf_pdf.Rgb(0.16, 0.20, 0.26))
    content.rect(0.0, SHEET[1] - 96.0, SHEET[0], 96.0)
    content.fill()
    content.set_fill(hqf_pdf.Rgb(1.0, 1.0, 1.0))
    content.draw_text(font, 20.0, MARGIN, SHEET[1] - 58.0, words.title)
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.25))
    y = SHEET[1] - 140.0
    for line in words.intro:
        content.draw_text(font, 11.0, MARGIN, y, line)
        y -= 18.0
    content.restore_state()


def asks(content, font, words):
    """Draw what the file asks for, beside the entries it writes to ask."""
    content.save_state()
    content.set_stroke(hqf_pdf.Color.gray(0.82))
    content.set_line_width(0.4)
    content.move_to(MARGIN, 350.0)
    content.line_to(SHEET[0] - MARGIN, 350.0)
    content.stroke()
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.1))
    content.draw_text(font, 13.0, MARGIN, 322.0, words.asks)
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.3))
    y = 294.0
    for asked, written in zip(words.asked, WRITTEN):
        content.draw_text(font, 11.0, MARGIN, y, asked)
        content.draw_text(font, 10.0, MARGIN + ENTRY, y, written)
        y -= 20.0
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.45))
    y = 212.0
    for paragraph in words.closing:
        for line in paragraph:
            content.draw_text(font, 10.5, MARGIN, y, line)
            y -= 17.0
        y -= 17.0
    content.restore_state()


def sheet(font, words):
    """The page: the band across its head, the three rules, and what it asks for."""
    content = hqf_pdf.Content()
    head(content, font, words)
    for index, (y, step, steps, tall, worth) in enumerate(RULES):
        rule(
            content,
            font,
            y,
            step,
            steps,
            tall,
            worth,
            words.rule_name(index, steps, worth),
        )
    asks(content, font, words)

    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.5))
    content.draw_text(font, 8.0, MARGIN, 62.0, "HQF Development")
    content.restore_state()
    return content


def main():
    """Write the sheet, and say where it went."""
    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("scale.pdf", language)).stem)

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

    page = hqf_pdf.Page(SHEET[0], SHEET[1])
    page.set_content(sheet(font, words))
    document.add_page(page)

    # Asking for the size the pages print at is one entry; taking the choice away from
    # the reader is a second, which PDF 2.0 brought and which the file must therefore
    # declare itself as.
    document.set_viewer_preferences(
        hqf_pdf.ViewerPreferences().lock_actual_size()
    )
    document.set_open_action(
        hqf_pdf.OpenAction(0).view(hqf_pdf.PageFit.whole_page())
    )
    document.set_version(hqf_pdf.Version.V2_0)
    document.set_compression_level(hqf_pdf.Compression.smallest())

    with open(out, "wb") as file:
        document.write_to(file)

    written = out.stat().st_size
    print(
        f"wrote {out}: {written} bytes, one sheet that prints at the size it "
        f"was drawn"
    )


if __name__ == "__main__":
    main()
