"""Writes a document reading software shows nothing of until a password is typed in.

The Python twin of the `write_locked` example in Rust: the same document, through the
binding rather than through the library directly.

Every string and every stream in the file is ciphertext, locked with AES under a
two-hundred-and-fifty-six-bit key. The user password is set, so reading software asks for it before it shows a single page, and the password is what the key is worked out from: reading software given the wrong one shows nothing at all. The page states the password, so that
the file can be opened by whoever is handed it.

Once it is open, the file grants printing, at the resolution the page was drawn at, and
reading aloud; it withholds copying, changing and taking pages out — as requests reading software honours, not as locks. The author's password lifts them and opens the file as
well, and the page states that one too.

The document that opens with nothing typed is `write_restricted`, and this page names it
in words rather than by that name.

The page is written in the language `HQF_PDF_LANG` names. The passwords are not: they are strings typed into reading software, and a translated password opens nothing.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# How far in from the left edge of the sheet every line is set, in points.
LEFT = 72.0

# The size the heading is set at, in points.
HEADING_SIZE = 18.0

# Where the baseline of the first line of the heading sits, in points up from the foot
# of the sheet.
HEADING_BASELINE = 760.0

# How far one line of the heading sits below the one before it, in points.
HEADING_LEADING = 22.0

# The size the body is set at, in points.
BODY_SIZE = 11.0

# Where the baseline of the first line of the body sits, in points up from the foot of
# the sheet.
FIRST_BASELINE = 700.0

# How far one line of the body sits below the one before it, in points.
LEADING = 18.0

# The size the heading over each closing section is set at, in points.
SECTION_SIZE = 13.0

# Where the baseline of the heading over the other document sits, in points up from the
# foot of the sheet.
COMPANION_HEADING_BASELINE = 520.0

# Where the baseline of the first line about the other document sits, in points up from
# the foot of the sheet.
COMPANION_BASELINE = 492.0

# Where the baseline of the heading over the closing note sits, in points up from the
# foot of the sheet.
NOTE_HEADING_BASELINE = 420.0

# Where the baseline of the first line of the note sits, in points up from the foot of
# the sheet.
NOTE_BASELINE = 392.0

# The password reading software asks for before it shows a page, which the page states
# so that the file can be opened at all. It stands outside the words: a password is
# typed into reading software, and a translated one opens nothing.
USER_PASSWORD = "the reader"

# The author's password, which the page states so that what the file withholds can be
# lifted. It stands outside the words for the same reason the other one does.
OWNER_PASSWORD = "the owner"

# The thirty-two bytes the file key is built from.
#
# They are fixed here, so that the example writes the same file on every run and one
# build can be compared with the last. A program takes them from its operating system —
# `os.urandom(32)`. A file locked under a seed anybody can read is a file anybody opens.
SEED = bytes.fromhex(
    "00112233445566778899aabbccddeeff0f1e2d3c4b5a69788796a5b4c3d2e1f0"
)


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

    The passwords are not among them: they are typed into reading software, not translated.
    """

    # What the document is called, set over two lines at the head of the page and joined
    # by a space in what the file says of itself.
    heading: tuple[str, ...]
    # The body of the page, one line to a line.
    body: tuple[str, ...]
    # What stands before the password the file asks for.
    user_password_label: str
    # What stands before the author's password.
    password_label: str
    # What stands over the lines about the other document.
    companion_heading: str
    # What the other document is, and how it differs from this one.
    companion: tuple[str, ...]
    # What stands over the closing note.
    note_heading: str
    # The closing note, which says where the key comes from.
    note: tuple[str, ...]

    def title(self) -> str:
        """What the file says of itself: the two lines of the heading, in a row."""
        return " ".join(self.heading)


# The page in English.
ENGLISH = Words(
    heading=("A document that asks for a password", "before it opens"),
    body=(
        "This file shows nothing until a password is typed in. Type the",
        "wrong one and no page appears at all.",
        "Everything written in it is scrambled, under a key of two",
        "hundred and fifty-six bits, and the password is what that key",
        "is worked out from.",
        "Once it is open, it asks the reading software to let it be",
        "printed and read out loud, but not copied or changed.",
    ),
    user_password_label="The password this document asks for:",
    password_label="The author's password opens it too, and lifts the request:",
    companion_heading="The other document of this pair",
    companion=(
        "A second document was written beside this one. That one opens",
        "on its own, with nothing to type in, and only asks that it not",
        "be copied or changed.",
    ),
    note_heading="Where the key comes from",
    note=(
        "The thirty-two bytes the key is built from are fixed in this",
        "example, so that it writes the same file on every run and one",
        "build can be compared with the last. A program takes them from",
        "its operating system: a seed anybody can read locks nothing.",
    ),
)

# The page in French.
FRENCH = Words(
    heading=(
        "Un document qui demande un mot de passe",
        "pour s’ouvrir",
    ),
    body=(
        "Ce fichier ne montre rien tant qu’un mot de passe n’a pas été",
        "saisi. Saisissez-en un faux et aucune page ne s’affiche.",
        "Tout ce qu’il contient est chiffré avec une clé de deux cent",
        "cinquante-six bits, et c’est le mot de passe qui permet de",
        "retrouver cette clé.",
        "Une fois ouvert, il demande au logiciel de lecture de laisser",
        "imprimer et lire à voix haute, mais ni copier ni modifier.",
    ),
    user_password_label="Le mot de passe que ce document demande :",
    password_label=("Le mot de passe de l’auteur l’ouvre aussi, et lève les "
                    "restrictions :"),
    companion_heading="L’autre document de la paire",
    companion=(
        "Un second document a été créé avec celui-ci. Lui s’ouvre",
        "tout seul, sans rien saisir, et demande seulement qu’on ne le",
        "copie ni ne le modifie.",
    ),
    note_heading="D’où vient la clé",
    note=(
        "Les trente-deux octets dont la clé est tirée sont figés dans cet",
        "exemple, pour qu’il produise le même fichier à chaque exécution et",
        "qu’on puisse le comparer au précédent. Un programme les demande à son",
        "système d’exploitation : une graine lisible par tous ne protège rien.",
    ),
)

# The page in Dutch.
DUTCH = Words(
    heading=("Een document dat om een wachtwoord vraagt", "voordat het opengaat"),
    body=(
        "Dit bestand toont niets tot er een wachtwoord is ingetypt.",
        "Met het verkeerde verschijnt er geen enkele pagina.",
        "Alles wat erin staat, is versleuteld, met een sleutel van",
        "tweehonderdzesenvijftig bits, en die sleutel wordt uit het",
        "wachtwoord berekend.",
        "Eenmaal geopend vraagt het de leessoftware om afdrukken",
        "en voorlezen toe te staan, maar kopiëren en wijzigen niet.",
    ),
    user_password_label="Het wachtwoord dat dit document vraagt:",
    password_label="Het wachtwoord van de auteur opent het ook, en heft het verzoek op:",
    companion_heading="Het andere document van dit paar",
    companion=(
        "Naast dit document is een tweede geschreven. Dat opent",
        "vanzelf, zonder iets in te typen, en vraagt alleen dat het niet",
        "gekopieerd of gewijzigd wordt.",
    ),
    note_heading="Waar de sleutel vandaan komt",
    note=(
        "De tweeëndertig bytes waaruit de sleutel wordt opgebouwd, liggen in dit",
        "voorbeeld vast, zodat het bij elke run hetzelfde bestand schrijft en een",
        "build met de vorige vergeleken kan worden. Een programma haalt ze uit",
        "zijn besturingssysteem: een startwaarde die iedereen kan lezen, sluit niets af.",
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    heading=("Um documento que pede uma palavra-passe", "antes de abrir"),
    body=(
        "Este ficheiro não mostra nada até se escrever uma palavra-passe.",
        "Com a errada não aparece nenhuma página.",
        "Tudo o que nele está escrito está cifrado, com uma chave de duzentos",
        "e cinquenta e seis bits, e a palavra-passe é aquilo a partir do qual",
        "essa chave é calculada.",
        "Uma vez aberto, pede ao programa de leitura que o deixe",
        "imprimir e ler em voz alta, mas não copiar nem alterar.",
    ),
    user_password_label="A palavra-passe que este documento pede:",
    password_label="A palavra-passe do autor também o abre, e levanta o pedido:",
    companion_heading="O outro documento deste par",
    companion=(
        "Ao lado deste foi escrito um segundo documento. Esse abre",
        "sem palavra-passe, e só pede que não seja copiado",
        "nem alterado.",
    ),
    note_heading="De onde vem a chave",
    note=(
        "Os trinta e dois bytes a partir dos quais a chave é construída são fixos neste",
        "exemplo, para que escreva o mesmo ficheiro a cada execução e uma",
        "compilação possa ser comparada com a anterior. Um programa tira-os do",
        "seu sistema operativo: uma semente que qualquer um pode ler não fecha nada.",
    ),
)

# The page in Italian.
ITALIAN = Words(
    heading=("Un documento che chiede una password", "prima di aprirsi"),
    body=(
        "Questo file non mostra nulla finché non si digita una password.",
        "Con quella sbagliata non appare nessuna pagina.",
        "Tutto ciò che contiene è cifrato, con una chiave di",
        "duecentocinquantasei bit, e la password è ciò da cui",
        "quella chiave viene calcolata.",
        "Una volta aperto, chiede al programma di lettura di lasciarlo",
        "stampare e leggere ad alta voce, ma non copiare né modificare.",
    ),
    user_password_label="La password che chiede questo documento:",
    password_label="Anche la password dell’autore lo apre, e toglie la richiesta:",
    companion_heading="L’altro documento di questa coppia",
    companion=(
        "Accanto a questo è stato scritto un secondo documento. Quello si apre",
        "da solo, senza digitare nulla, e chiede soltanto di non essere",
        "copiato né modificato.",
    ),
    note_heading="Da dove viene la chiave",
    note=(
        "I trentadue byte da cui si costruisce la chiave sono fissi in questo",
        "esempio, così scrive lo stesso file a ogni esecuzione e una",
        "compilazione si può confrontare con la precedente. Un programma li prende dal",
        "suo sistema operativo: un seme che chiunque può leggere non chiude nulla.",
    ),
)

# The page in Spanish.
SPANISH = Words(
    heading=("Un documento que pide una contraseña", "antes de abrirse"),
    body=(
        "Este archivo no muestra nada hasta que se teclea una contraseña.",
        "Con la equivocada no aparece ninguna página.",
        "Todo lo escrito en él está cifrado, con una clave de doscientos",
        "cincuenta y seis bits, y esa clave se calcula a partir",
        "de la contraseña.",
        "Una vez abierto, pide al programa de lectura que deje",
        "imprimirlo y leerlo en voz alta, pero no copiarlo ni cambiarlo.",
    ),
    user_password_label="La contraseña que pide este documento:",
    password_label="La contraseña del autor también lo abre, y anula la petición:",
    companion_heading="El otro documento de esta pareja",
    companion=(
        "Junto a este se escribió un segundo documento. Ese se abre",
        "solo, sin teclear nada, y solo pide que no se copie",
        "ni se cambie.",
    ),
    note_heading="De dónde sale la clave",
    note=(
        "Los treinta y dos bytes con que se construye la clave son fijos en este",
        "ejemplo, para que escriba el mismo archivo en cada ejecución y una",
        "compilación pueda compararse con la anterior. Un programa real los toma de",
        "su sistema operativo: una semilla que cualquiera puede leer no cierra nada.",
    ),
)

# The page in German.
GERMAN = Words(
    heading=("Ein Dokument, das nach einem Passwort fragt", "bevor es sich öffnet"),
    body=(
        "Diese Datei zeigt nichts, bis ein Passwort eingegeben ist. Wer",
        "das falsche eingibt, sieht überhaupt keine Seite.",
        "Alles darin ist verschlüsselt, unter einem Schlüssel von",
        "zweihundertsechsundfünfzig Bit, und das Passwort ist das,",
        "woraus dieser Schlüssel berechnet wird.",
        "Einmal geöffnet, bittet es die Lesesoftware, es drucken und",
        "vorlesen, aber nicht kopieren oder ändern zu lassen.",
    ),
    user_password_label="Das Passwort, das dieses Dokument verlangt:",
    password_label="Das Passwort des Autors öffnet es auch und hebt die Bitte auf:",
    companion_heading="Das andere Dokument dieses Paares",
    companion=(
        "Neben diesem wurde ein zweites Dokument geschrieben. Es öffnet",
        "sich von selbst, ohne dass etwas einzugeben ist, und bittet nur",
        "darum, nicht kopiert oder geändert zu werden.",
    ),
    note_heading="Woher der Schlüssel kommt",
    note=(
        "Die zweiunddreißig Bytes, aus denen der Schlüssel gebaut wird,",
        "sind in diesem Beispiel fest, damit es bei jedem Lauf dieselbe",
        "Datei schreibt. Ein Programm holt sie von seinem Betriebssystem:",
        "Ein Startwert, den jeder lesen kann, schließt nichts ab.",
    ),
)

# 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 lines(words: Words) -> list[str]:
    """What the body says.

    Its last two lines state the password the file asks for and the author's.
    """
    return [
        *words.body,
        f"{words.user_password_label} {USER_PASSWORD}",
        f"{words.password_label} {OWNER_PASSWORD}",
    ]


def encryption() -> hqf_pdf.Encryption:
    """What the document is locked with.

    The password reading software asks for, the author's, and what is allowed once it is open:
    printing at the resolution the page was drawn at, and reading aloud.
    """
    return (
        hqf_pdf.Encryption(SEED)
        .user_password(USER_PASSWORD)
        .owner_password(OWNER_PASSWORD)
        .permissions(
            hqf_pdf.Permissions()
            .printing()
            .printing_at_full_resolution()
        )
    )


def drawing(words: Words) -> list[tuple[str, float, float]]:
    """Every line the page draws, in the order they are drawn.

    Each is what it says, the size it is set at, and where its baseline sits in points
    up from the foot of the sheet.
    """
    drawn = []

    baseline = HEADING_BASELINE
    for line in words.heading:
        drawn.append((line, HEADING_SIZE, baseline))
        baseline -= HEADING_LEADING

    baseline = FIRST_BASELINE
    for line in lines(words):
        drawn.append((line, BODY_SIZE, baseline))
        baseline -= LEADING

    drawn.append(
        (words.companion_heading, SECTION_SIZE, COMPANION_HEADING_BASELINE)
    )
    baseline = COMPANION_BASELINE
    for line in words.companion:
        drawn.append((line, BODY_SIZE, baseline))
        baseline -= LEADING

    drawn.append((words.note_heading, SECTION_SIZE, NOTE_HEADING_BASELINE))
    baseline = NOTE_BASELINE
    for line in words.note:
        drawn.append((line, BODY_SIZE, baseline))
        baseline -= LEADING
    return drawn


def page(
    handle: hqf_pdf.FontHandle, drawn: list[tuple[str, float, float]]
) -> hqf_pdf.Page:
    """The page: every line set by its own origin, each in an object of its own."""
    content = hqf_pdf.Content()
    for line, size, baseline in drawn:
        content.draw_text(handle, size, LEFT, baseline, line)

    result = hqf_pdf.Page.a4()
    result.set_content(content)
    return result


def document(words: Words, face: Path) -> hqf_pdf.Document:
    """The document and its one page, before it is locked."""
    result = hqf_pdf.Document()
    result.set_license(_licence.licensed())
    result.set_info("Title", words.title())
    handle = result.add_font(hqf_pdf.Font.from_path(face))
    result.add_page(page(handle, drawing(words)))
    return result


def written(path: Path, doc: hqf_pdf.Document) -> None:
    """Writes `doc` to `path`, making the directory it goes in if it is not there."""
    path.parent.mkdir(parents=True, exist_ok=True)
    print(f"wrote {path} ({doc.write(path)} bytes)")


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

    doc = document(words, _out.font_path())
    doc.set_encryption(encryption())
    written(Path(out), doc)


if __name__ == "__main__":
    main()
