"""Marks what has to come out of a statement before it is released.

The Python twin of the `write_redaction` example in Rust. A redaction is a mark, and
only a mark. It states which part of the page must go, what is to be shown in its
place, and what colour fills the gap — and all three describe the page as it will be
once a reading software has applied the mark and written the document out again.
Until then the words are still in the file under the box, and a search finds them.
That is the mistake this page is laid out to make visible.

The upper half is the statement as it is released: the entries readable, each under a
mark that covers exactly the words it hides, because every box is measured off the
line it stands on rather than guessed.

The lower half is drawn by this example, in its own ink, to show what those three
marks ask for. The first shows a word in the middle of the gap. The second repeats it
along the whole width. The third shows nothing at all.

Usage: python examples/write_redaction.py [out.pdf]
       HQF_PDF_LANG=fr python examples/write_redaction.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The margin the page is laid out inside.
MARGIN = 56.0

# How far the page runs across, between the margins.
WIDTH = 483.0

# How far the value of an entry stands from the margin.
VALUE = 160.0

# The size the entries are set in.
SIZE = 11.0

# The size the word in a gap is set in.
SHOWN_SIZE = 9.0

# How far a mark reaches above and below the baseline of the line it covers.
OVER = 3.0
UNDER = 3.0

# The most copies of a word one gap is filled with.
MOST_COPIES = 64

# Where each entry's baseline sits, as released.
ROWS = (700.0, 676.0, 652.0)

# Where each gap's baseline sits, in the half drawn to show what the marks ask for.
SHOWN_ROWS = (512.0, 488.0, 464.0)

# What each entry states. None of it is anybody's: the statement is written for this
# page.
ENTRIES = (
    "Claire Vasseur",
    "14 rue des Lilas, 59000 Lille",
    "FR76 3000 1007 9412 3456 7890 185",
)


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

    # The line at the head of the page.
    title: str
    # The heading over the statement as it is released.
    released: str
    # The heading over the half drawn to show what the marks ask for.
    applied: str
    # What each struck-out entry is called.
    labels: tuple[str, str, str]
    # The word shown in the first gap, and repeated along the second.
    shown: str
    # The paragraph the statement is released with.
    body: str
    # The line under it all.
    caption: str


# The page in English.
ENGLISH = Words(
    title="Witness statement, released in part",
    released="As it is released",
    applied="As a reading software shows it once it has applied the marks",
    labels=("Name", "Address", "Account"),
    shown="REDACTED",
    body="The statement itself is released whole. What identifies the witness "
    "is struck out above: the name shows the reason in the middle of its "
    "gap, the address repeats it along the length of a longer one, and "
    "the account is covered without a word.",
    caption="A mark says what must go. It does not take it out. Until a reading "
    "software applies these marks and writes the document out again, "
    "the words are still under the boxes and a search finds them — "
    "which is why the lower half is drawn here in this example's own "
    "ink, and is not what the file states.",
)

# The page in French.
FRENCH = Words(
    title="Déposition de témoin, communiquée en partie",
    released="Telle qu'elle est communiquée",
    applied="Telle qu'un logiciel de lecture la montre une fois les marques appliquées",
    labels=("Nom", "Adresse", "Compte"),
    shown="CAVIARDÉ",
    body="La déposition elle-même est communiquée entière. Ce qui identifie le "
    "témoin est caviardé ci-dessus : le nom montre le motif au milieu de "
    "son emplacement, l'adresse le répète sur toute la longueur d'un "
    "emplacement plus large, et le compte est couvert sans un mot.",
    caption="Une marque dit ce qui doit partir. Elle ne l'enlève pas. Tant "
    "qu'un logiciel de lecture n'a pas appliqué ces marques et réécrit "
    "le document, les mots sont toujours sous les boîtes et une "
    "recherche les trouve — c'est pourquoi la moitié basse est tracée "
    "ici par l'exemple lui-même, et n'est pas ce que le fichier dit.",
)


# 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 fitting(room: float, one: float, step: float) -> int:
    """How many copies of a word `one` wide, set `step` apart, stand in `room`."""
    copies = 0
    while copies < MOST_COPIES and copies * step + one <= room:
        copies += 1
    return copies


def paragraph(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    top: float,
    color: hqf_pdf.Rgb,
    text: str,
) -> None:
    """Writes a paragraph in a column the width of the page."""
    flow = hqf_pdf.TextFlow(font, size, leading=size * 1.4, color=color)
    lines = flow.break_lines(text, WIDTH)
    flow.draw(content, lines, MARGIN, top, WIDTH)


def gap(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    y: float,
    width: float,
    shown: str,
    align: hqf_pdf.FieldAlign,
    repeating: bool,
) -> None:
    """Draws the gap a mark asks for: the colour it states, and the words it shows in
    it, set where its alignment puts them and repeated if it asks for that."""
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.0))
    content.rect(MARGIN + VALUE, y - UNDER, width, SIZE + OVER + UNDER)
    content.fill()

    if shown:
        content.set_fill(hqf_pdf.Rgb.gray(1.0))
        one = font.measure(shown, SHOWN_SIZE)
        step = one + font.measure("   ", SHOWN_SIZE)
        left = MARGIN + VALUE
        if align == hqf_pdf.FieldAlign.Center:
            start = (width - one) * 0.5 + left
        elif align == hqf_pdf.FieldAlign.Right:
            start = left + width - one
        else:
            start = left
        room = left + width - start
        times = fitting(room, one, step) if repeating else int(one <= room)
        for copy in range(times):
            content.draw_text(font, SHOWN_SIZE, copy * step + start, y, shown)
    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("redaction.pdf", language)).stem)

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

    widths = tuple(font.measure(entry, SIZE) for entry in ENTRIES)

    content = hqf_pdf.Content()
    content.draw_text(font, 16.0, MARGIN, 780.0, words.title)

    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 9.0, MARGIN, 736.0, words.released)
    content.draw_text(font, 9.0, MARGIN, 548.0, words.applied)
    content.restore_state()

    for row, label, entry in zip(ROWS, words.labels, ENTRIES):
        content.draw_text(font, SIZE, MARGIN, row, label)
        content.draw_text(font, SIZE, MARGIN + VALUE, row, entry)
    paragraph(content, font, 10.0, 618.0, hqf_pdf.Rgb.gray(0.0), words.body)

    for row, label in zip(SHOWN_ROWS, words.labels):
        content.draw_text(font, SIZE, MARGIN, row, label)
    gap(
        content,
        font,
        SHOWN_ROWS[0],
        widths[0],
        words.shown,
        hqf_pdf.FieldAlign.Center,
        False,
    )
    gap(
        content,
        font,
        SHOWN_ROWS[1],
        widths[1],
        words.shown,
        hqf_pdf.FieldAlign.Left,
        True,
    )
    gap(content, font, SHOWN_ROWS[2], widths[2], "", hqf_pdf.FieldAlign.Left, False)

    paragraph(content, font, 8.5, 428.0, hqf_pdf.Rgb.gray(0.35), words.caption)

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

    # Each mark covers the words it hides and nothing else, so it is measured off the
    # line it stands on.
    marks = [
        hqf_pdf.RedactionAnnotation(
            MARGIN + VALUE, ROWS[struck] - UNDER, widths[struck], SIZE + OVER + UNDER
        ).fill(hqf_pdf.Rgb.gray(0.0))
        for struck in range(len(ENTRIES))
    ]
    page.add_annotation(
        marks[0].overlay_text(words.shown).aligned(hqf_pdf.FieldAlign.Center)
    )
    page.add_annotation(
        marks[1]
        .overlay_text(words.shown)
        .repeating(True)
        .aligned(hqf_pdf.FieldAlign.Left)
    )
    page.add_annotation(marks[2])

    document.add_page(page)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes, {len(ENTRIES)} entries struck out")


if __name__ == "__main__":
    main()
