"""Writes a page of two-dimensional codes whose bytes name the character set they are
written in.

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

A code that names no set is read in whatever set the scanner falls back on, so a text
with an accent in it comes back right on one scanner and wrong on the next. Each code
here is handed its payload as bytes already written in a set, and names that set ahead
of them: a QR code in Latin-1, a Data Matrix in Greek and an Aztec code in Latin-9 side
by side, then a column of PDF417 codes in Latin-2, Cyrillic, Shift JIS, Windows-1252,
ASCII and binary, each at a level of correction of its own. Under each stand the text
its bytes spell and the number the register gives the set.

Whether a code works is not a question the page can answer, and not one a human eye
can: it is answered by a scanner. `scripts/check_barcode.sh` points a decoder at the
rendered image and compares what it reads with the texts under the codes.

The head of the sheet is held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set the sheet is headed in. What the codes carry stands apart from them: a
scanner reads it, not a person, and it is the same in every language.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The QR code's text, and the Latin-1 bytes it is handed over as.
LATIN_1_TEXT = "Grüße aus Köln"
LATIN_1_BYTES = LATIN_1_TEXT.encode("latin-1")

# The Data Matrix's text, and the Greek bytes it is handed over as.
GREEK_TEXT = "Καλημέρα"
GREEK_BYTES = GREEK_TEXT.encode("iso8859-7")

# The Aztec code's text, and the Latin-9 bytes it is handed over as.
LATIN_9_TEXT = "Prix 12 €"
LATIN_9_BYTES = LATIN_9_TEXT.encode("iso8859-15")

# What the PDF417 codes carry, in the order they are set down the page.
STACKED = ("Łódź", "Москва", "TOKYO 2026", "Café 3 €", "ASCII 2026", "BIN 7")

# The side of one module of a square code, in points. Four pixels to a module at 288dpi,
# so every module's edge lands on a pixel boundary.
MODULE = 3.0

# The width of one module of a PDF417 code, in points: six pixels to a module at 288dpi.
BAR_MODULE = 1.5

# Where the bottom of the row of square codes sits, in points up from the bottom of the
# sheet.
TOP = 640.0

# Where the head of the first PDF417 code sits, in points up from the bottom of the
# sheet.
STACK_TOP = 590.0

# How far in from the left edge of the sheet the head of the page is set. The quiet zone
# of the first code starts here too, so the heading stands over the left edge of the
# codes.
MARGIN = 72.0

# Where the baseline of the heading sits, in points up from the bottom of the sheet.
HEAD_TOP = 794.0

# The size the heading is set at, in points.
HEADING = 16.0

# The size the lines under the heading are set at, in points.
BODY = 10.5

# How far under the heading the first line beneath it sits, in points.
HEAD_GAP = 24.0

# How far below one line under the heading the next one sits, in points.
LINE_STEP = 15.0


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

    # What the page says it is, at its head and in what the file says of itself.
    title: str
    # What the page says under its heading.
    lines: tuple[str, str]
    # The line under a code that names its character set; `{number}` stands for the
    # number the register gives the set.
    declared: str


# The head of the sheet in English.
ENGLISH = Words(
    title="Codes that name their character set",
    lines=(
        "A code that names no character set is read in the one the scanner assumes.",
        (
            "Each code here names its own, so Greek comes back as Greek, Cyrillic as "
            "Cyrillic."
        ),
    ),
    declared="character set {number}",
)

# The head of the sheet in French.
FRENCH = Words(
    title="Codes qui nomment leur jeu de caractères",
    lines=(
        "Un code qui ne nomme pas son jeu de caractères est lu dans celui du scanner.",
        (
            "Ici, chaque code nomme le sien : le grec se relit en grec, le "
            "cyrillique aussi."
        ),
    ),
    declared="jeu de caractères {number}",
)

# The head of the sheet in Dutch.
DUTCH = Words(
    title="Codes die hun tekenset noemen",
    lines=(
        "Een code zonder tekenset wordt gelezen in de set die de scanner aanneemt.",
        (
            "Elke code hier noemt de zijne: Grieks komt terug als Grieks, Cyrillisch "
            "als Cyrillisch."
        ),
    ),
    declared="tekenset {number}",
)

# The head of the sheet in Portuguese.
PORTUGUESE = Words(
    title="Códigos que nomeiam o seu conjunto de caracteres",
    lines=(
        "Um código sem conjunto de caracteres declarado lê-se como o leitor presumir.",
        (
            "Aqui cada código indica o seu: o grego lê-se como grego, o cirílico como "
            "cirílico."
        ),
    ),
    declared="conjunto de caracteres {number}",
)

# The head of the sheet in Italian.
ITALIAN = Words(
    title="Codici che nominano il loro set di caratteri",
    lines=(
        "Un codice senza set di caratteri dichiarato si legge in quello che suppone lo scanner.",
        (
            "Ogni codice qui nomina il suo, così il greco torna greco, il cirillico "
            "cirillico."
        ),
    ),
    declared="set di caratteri {number}",
)

# The head of the sheet in Spanish.
SPANISH = Words(
    title="Códigos que nombran su juego de caracteres",
    lines=(
        "Un código que no nombra juego de caracteres se lee en el que supone el escáner.",
        (
            "Aquí cada código nombra el suyo: el griego sale griego y el cirílico, "
            "cirílico."
        ),
    ),
    declared="juego de caracteres {number}",
)

# The head of the sheet in German.
GERMAN = Words(
    title="Codes, die ihren Zeichensatz nennen",
    lines=(
        "Ein Code ohne Zeichensatz wird in dem gelesen, den der Scanner annimmt.",
        (
            "Jeder Code nennt seinen: Griechisch kommt griechisch zurück, Kyrillisch "
            "kyrillisch."
        ),
    ),
    declared="Zeichensatz {number}",
)

# 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 head(content, handle, words) -> None:
    """Draw the heading, and the lines under it that say what the page is for."""
    content.draw_text(handle, HEADING, MARGIN, HEAD_TOP, words.title)

    y = HEAD_TOP - HEAD_GAP
    for line in words.lines:
        content.draw_text(handle, BODY, MARGIN, y, line)
        y -= LINE_STEP


def caption(content, handle, size, at, lines) -> None:
    """Set each of `lines` at `size` from `at[0]`, its baseline the drop it comes with
    under `at[1]`."""
    for line, drop in lines:
        content.draw_text(handle, size, at[0], at[1] - drop, line)


def stacked() -> list[tuple[hqf_pdf.Pdf417, hqf_pdf.Charset]]:
    """The PDF417 codes, each handed its text as bytes in a set it names, at a level of
    correction of its own."""
    charset = hqf_pdf.Charset
    built = (
        (STACKED[0].encode("iso8859-2"), charset.LATIN_2, 1),
        (STACKED[1].encode("iso8859-5"), charset.CYRILLIC, 2),
        (STACKED[2].encode(), charset.SHIFT_JIS, 3),
        (STACKED[3].encode("cp1252"), charset.WINDOWS_1252, 4),
        (STACKED[4].encode(), charset.ASCII, 6),
        (STACKED[5].encode(), charset.BINARY, 7),
    )
    return [
        (hqf_pdf.Pdf417(data, level=level, charset=named), named)
        for data, named, level in built
    ]


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

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.title)

    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    content = hqf_pdf.Content()
    content.set_fill(hqf_pdf.Rgb(0.0, 0.0, 0.0))
    head(content, handle, words)

    # The three square codes side by side, each drawn its own quiet zone in from where
    # it stands, and the next one standing clear of its caption as well as of the code
    # itself.
    def declared(charset: hqf_pdf.Charset) -> str:
        return words.declared.replace("{number}", str(charset.number))

    left = MARGIN

    qr = hqf_pdf.QrCode(
        LATIN_1_BYTES,
        charset=hqf_pdf.Charset.LATIN_1,
        correction=hqf_pdf.QrCorrection.Medium,
    )
    quiet = hqf_pdf.QrCode.quiet_zone() * MODULE
    side = qr.size * MODULE
    content.draw_qr(qr, left + quiet, TOP, side)
    named = declared(hqf_pdf.Charset.LATIN_1)
    caption(
        content, handle, 8.0, (left + quiet, TOP), ((LATIN_1_TEXT, 12.0), (named, 22.0))
    )
    widest = max(handle.measure(LATIN_1_TEXT, 8.0), handle.measure(named, 8.0))
    left += max(side, widest) + quiet * 2.0 + 24.0

    matrix = hqf_pdf.DataMatrix(GREEK_BYTES, charset=hqf_pdf.Charset.GREEK)
    quiet = hqf_pdf.DataMatrix.quiet_zone() * MODULE
    side = matrix.width * MODULE
    content.draw_data_matrix(matrix, left + quiet, TOP, side)
    named = declared(hqf_pdf.Charset.GREEK)
    caption(
        content, handle, 8.0, (left + quiet, TOP), ((GREEK_TEXT, 12.0), (named, 22.0))
    )
    widest = max(handle.measure(GREEK_TEXT, 8.0), handle.measure(named, 8.0))
    left += max(side, widest) + quiet * 2.0 + 24.0

    # An Aztec code asks for no quiet zone: the scanner finds the bullseye at its
    # centre.
    aztec = hqf_pdf.Aztec(
        LATIN_9_BYTES,
        correction=hqf_pdf.Aztec.default_correction(),
        charset=hqf_pdf.Charset.LATIN_9,
    )
    side = aztec.size * MODULE
    content.draw_aztec(aztec, left, TOP, side)
    named = declared(hqf_pdf.Charset.LATIN_9)
    caption(content, handle, 8.0, (left, TOP), ((LATIN_9_TEXT, 12.0), (named, 22.0)))

    # The PDF417 codes one above the other: a decoder reads codes set side by side less
    # surely than codes set one above the other.
    quiet = hqf_pdf.Pdf417.quiet_zone() * BAR_MODULE
    aspect = hqf_pdf.Pdf417.row_aspect()
    top = STACK_TOP
    for (code, charset), carried in zip(stacked(), STACKED):
        width = code.width * BAR_MODULE
        height = code.height * BAR_MODULE * aspect
        bottom = top - height
        content.draw_pdf417(code, MARGIN + quiet, bottom, width, height)
        named = declared(charset)
        at = (MARGIN + quiet, bottom)
        caption(content, handle, 7.0, at, ((carried, 10.0), (named, 19.0)))
        top = bottom - (quiet * 2.0 + 30.0)

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

    written = document.write(out)
    print(f"wrote {out} ({written} bytes)")


if __name__ == "__main__":
    main()
