"""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 the reader 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 exacte",
    intro=(
        "Imprimez cette page à sa taille et les trois règles ci-dessous mesurent",
        "ce qu'elles annoncent. Posez un mètre dessus pour voir si ce qui l'a",
        "imprimée a fait ce que le fichier demandait.",
    ),
    units=("centimètres", "pouces", "points, à 72 pour un pouce"),
    asks="Ce que ce fichier demande, et ce qu'il écrit pour le demander",
    asked=(
        "Imprimer chaque page à la taille dessinée.",
        "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 arrivée avec PDF 2.0, donc ce fichier "
            "annonce",
            "2.0 sur sa première ligne. Rien n'est relevé à la place de l'appelant "
            ": un",
            "fichier qui la demanderait en annonçant moins serait refusé, pas écrit.",
        ),
        (
            "Deux autres choses ont été décidées sur le fichier plutôt que sur ses "
            "pages.",
            "Ses flux ont été serrés aussi petit que le compresseur sait le faire, "
            "ce qui",
            "coûte du temps et gagne des octets. Et il est parti dans le fichier "
            "morceau",
            "par morceau à mesure, au lieu d'être monté entier en mémoire puis écrit.",
        ),
    ),
)

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