"""Draws a sheet of peel-off labels: a grid of shelf labels, each carrying an article
name, its price, and the article number a till reads, closed by a row of warehouse bin
labels.

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

The grid is driven by a stock description — how many labels across and down, how big
one is, and how far apart their corners sit — so a reader retargets the sheet to their
own die-cut stock by changing six numbers. Every label is outlined and the page margins
carry cut marks, so what is drawn lines up with what is cut.

Whether the codes scan is not for the page to say, and not for an eye: a scanner says.
`scripts/check_barcode.sh` points a decoder at the rendered sheet and reads back every
article number and every bin.

Every word the page draws is held in `Words`, once per language, and `HQF_PDF_LANG`
picks which one is drawn. The prices, the article numbers, the aisle and rack codes and
what a scanner reads off the sheet are the same in both.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# A4, in points.
PAGE_WIDTH = 595.276
PAGE_HEIGHT = 841.89

# The die-cut stock the sheet is laid out for: how many labels across and down, how big
# one is, and how far apart two neighbours' corners sit. The pitch is the label plus the
# gutter the cutter leaves between two of them.
COLUMNS = 3
ROWS = 5
LABEL_WIDTH = 170.0
LABEL_HEIGHT = 120.0
COLUMN_PITCH = 178.0
ROW_PITCH = 128.0

# The top edge of the first row of labels.
SHEET_TOP = 742.0

# How long a cut mark is and how far it stands off the grid, in points.
MARK_LENGTH = 11.0
MARK_OFFSET = 5.0

# The width of the narrowest bar, in points, and the side of one Data Matrix module.
# Everything else in a code is a whole number of these.
MODULE = 1.0
MATRIX_MODULE = 3.0

# The ink the prices are drawn in, the grey of the small print, and the black the names
# and the codes are drawn in.
ACCENT = hqf_pdf.Rgb(0.11, 0.33, 0.55)
MUTED = hqf_pdf.Rgb(0.42, 0.42, 0.45)
INK = hqf_pdf.Rgb(0.1, 0.1, 0.12)


@dataclass(frozen=True)
class Ean13Article:
    """Twelve digits, without their check digit, drawn as an EAN-13 barcode."""

    digits: str


@dataclass(frozen=True)
class UpcAArticle:
    """Eleven digits, without their check digit, drawn as a UPC-A barcode."""

    digits: str


@dataclass(frozen=True)
class Shelf:
    """A shelf label's face: the price of one pack, and the article number the till
    reads.
    """

    price: float
    article: Ean13Article | UpcAArticle


@dataclass(frozen=True)
class Bin:
    """A bin label's face: where in the warehouse the stock stands, and the code a
    picker scans on arriving there.
    """

    positions: tuple[str, str, str]
    code: str


# The faces the sheet prints, in reading order: the grid fills across a row before
# dropping to the next, and ``Words.names`` names them in that order.
LABELS = [
    Shelf(18.90, Ean13Article("347000100013")),
    Shelf(11.90, Ean13Article("347000100020")),
    Shelf(2.40, Ean13Article("347000100037")),
    Shelf(6.90, Ean13Article("347000100044")),
    Shelf(2.45, Ean13Article("347000100051")),
    Shelf(3.20, Ean13Article("347000100068")),
    Shelf(8.90, Ean13Article("347000100075")),
    Shelf(5.90, Ean13Article("347000100082")),
    Shelf(3.90, Ean13Article("347000100099")),
    Shelf(4.60, Ean13Article("347000100105")),
    Shelf(1.90, Ean13Article("347000100112")),
    Shelf(6.90, Ean13Article("347000100129")),
    Shelf(9.90, UpcAArticle("01234567890")),
    Bin(("A12", "R04", "S03"), "LOC:A12-R04-S03"),
    Bin(("B07", "R11", "S02"), "LOC:B07-R11-S02"),
]


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

    What is not language stays out of it: the prices, the article numbers, the aisle and
    rack codes and what the sheet is laid out for are drawn from constants of their own
    and read the same in every language.
    """

    # The words the document's title is built from, and the words set at the head of the
    # sheet, over the grid.
    title: str
    heading: str
    # Which sheet of the run this one is.
    sheet_number: str
    # What the labels are counted in, and what stands either side of the size of one, in
    # the line under the heading.
    labels: str
    stock_before: str
    stock_after: str
    # What stands before the day the sheet was printed, and that day.
    printed_label: str
    printed_on: str
    # What each label names, in the order ``LABELS`` prints them.
    names: tuple[str, ...]
    # The line under each name: what one pack holds and what it comes to by weight or by
    # volume, and, on a bin label, what the code beneath it is.
    subtitles: tuple[str, ...]
    # What stands before each of a bin label's three position codes.
    positions: tuple[str, str, str]
    # The sentence at the foot of the sheet, cut where the day the prices hold until
    # goes into it.
    valid_before: str
    valid_until: str


# The sheet in English.
ENGLISH = Words(
    title="Shelf label sheet, 3 × 5 die-cut",
    heading="SHELF LABELS",
    sheet_number="Sheet 3 of 8",
    labels="labels",
    stock_before="on",
    stock_after="pt die-cut stock",
    printed_label="Printed",
    printed_on="20 July 2026",
    names=(
        "Espresso beans, whole",
        "Olive oil, extra virgin",
        "Sea salt, coarse",
        "Basmati rice",
        "Dark chocolate, 70 %",
        "Orange juice, pressed",
        "Wildflower honey",
        "Green tea, loose leaf",
        "Sparkling water",
        "Sunflower seed oil",
        "Wholegrain pasta",
        "Roasted almonds",
        "Maple syrup, imported",
        "Dry goods reserve",
        "Chilled reserve",
    ),
    subtitles=(
        "1 kg pack · 18.90 EUR/kg",
        "750 ml bottle · 15.87 EUR/l",
        "1 kg pack · 2.40 EUR/kg",
        "2 kg pack · 3.45 EUR/kg",
        "100 g bar · 24.50 EUR/kg",
        "1 l carton · 3.20 EUR/l",
        "500 g jar · 17.80 EUR/kg",
        "125 g tin · 47.20 EUR/kg",
        "6 × 1 l · 0.65 EUR/l",
        "1 l bottle · 4.60 EUR/l",
        "500 g pack · 3.80 EUR/kg",
        "250 g pack · 27.60 EUR/kg",
        "250 ml bottle · 39.60 EUR/l",
        "BIN LOCATION",
        "BIN LOCATION",
    ),
    positions=("Aisle", "Rack", "Shelf"),
    valid_before="Prices valid until",
    valid_until="31 August 2026",
)

# The sheet in French.
FRENCH = Words(
    title="Planche d'étiquettes de rayon, prédécoupée 3 × 5",
    heading="ÉTIQUETTES DE RAYON",
    sheet_number="Feuille 3 sur 8",
    labels="étiquettes",
    stock_before="sur planche prédécoupée",
    stock_after="pt",
    printed_label="Imprimée le",
    printed_on="20 juillet 2026",
    names=(
        "Café en grains, espresso",
        "Huile d'olive vierge extra",
        "Gros sel de mer",
        "Riz basmati",
        "Chocolat noir 70 %",
        "Jus d'orange pressé",
        "Miel toutes fleurs",
        "Thé vert en vrac",
        "Eau gazeuse",
        "Huile de tournesol",
        "Pâtes complètes",
        "Amandes grillées",
        "Sirop d'érable importé",
        "Réserve épicerie sèche",
        "Réserve froid positif",
    ),
    subtitles=(
        "sachet de 1 kg · 18.90 EUR/kg",
        "bouteille de 750 ml · 15.87 EUR/l",
        "sachet de 1 kg · 2.40 EUR/kg",
        "sachet de 2 kg · 3.45 EUR/kg",
        "tablette de 100 g · 24.50 EUR/kg",
        "brique de 1 l · 3.20 EUR/l",
        "pot de 500 g · 17.80 EUR/kg",
        "boîte de 125 g · 47.20 EUR/kg",
        "pack de 6 × 1 l · 0.65 EUR/l",
        "bouteille de 1 l · 4.60 EUR/l",
        "paquet de 500 g · 3.80 EUR/kg",
        "sachet de 250 g · 27.60 EUR/kg",
        "bouteille de 250 ml · 39.60 EUR/l",
        "EMPLACEMENT",
        "EMPLACEMENT",
    ),
    positions=("Allée", "Travée", "Étagère"),
    valid_before="Prix valables jusqu'au",
    valid_until="31 août 2026",
)


# 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 sheet_left() -> float:
    """The left edge of the first column, which is what centres the grid on the page."""
    return (PAGE_WIDTH - ((COLUMNS - 1) * COLUMN_PITCH + LABEL_WIDTH)) / 2.0


def sheet_right() -> float:
    """The right edge of the last column."""
    return (COLUMNS - 1) * COLUMN_PITCH + sheet_left() + LABEL_WIDTH


def check_digit(digits: str) -> int:
    """The last digit of an EAN-13 or UPC-A number: the one that makes the weighted sum
    of the whole a multiple of ten. Read from the right, the digit just before it weighs
    three and the weights alternate from there.
    """
    weighted = sum(
        int(digit) * 3 if index % 2 == 0 else int(digit)
        for index, digit in enumerate(reversed(digits))
        if digit.isdigit()
    )
    return (10 - weighted % 10) % 10


def full_number(digits: str) -> str:
    """The number as it is printed under the bars: the digits given, followed by the
    check digit they imply.
    """
    return f"{digits}{check_digit(digits)}"


def price_text(value: float) -> str:
    """A price, as a shelf label writes it: "18.90 EUR"."""
    return f"{value:.2f} EUR"


def text(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    x: float,
    y: float,
    color: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text, left-aligned, in a colour of its own."""
    content.set_fill(color)
    content.draw_text(font, size, x, y, s)


def text_right(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    right: float,
    y: float,
    color: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text whose right edge sits at ``right``."""
    text(content, font, size, right - font.measure(s, size), y, color, s)


def text_centred(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    centre: float,
    y: float,
    color: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text centred on ``centre``."""
    text(content, font, size, centre - font.measure(s, size) / 2.0, y, color, s)


def line(
    content: hqf_pdf.Content,
    start: tuple[float, float],
    end: tuple[float, float],
    width: float,
    color: hqf_pdf.Rgb,
) -> None:
    """A straight line between two points, in the given ink and thickness."""
    content.set_stroke(color)
    content.set_line_width(width)
    content.move_to(start[0], start[1])
    content.line_to(end[0], end[1])
    content.stroke()


def cut_marks(content: hqf_pdf.Content) -> None:
    """Draws the cut marks: a tick in the margin against every column edge and every row
    edge, which is what a guillotine or a die is registered against.
    """
    left = sheet_left()
    grid_bottom = SHEET_TOP - ((ROWS - 1) * ROW_PITCH + LABEL_HEIGHT)
    grid_right = sheet_right()
    ink = hqf_pdf.Rgb.gray(0.45)

    for column in range(COLUMNS):
        label_left = column * COLUMN_PITCH + left
        for x in (label_left, label_left + LABEL_WIDTH):
            above = SHEET_TOP + MARK_OFFSET
            below = grid_bottom - MARK_OFFSET
            line(content, (x, above), (x, above + MARK_LENGTH), 0.5, ink)
            line(content, (x, below), (x, below - MARK_LENGTH), 0.5, ink)

    for row in range(ROWS):
        label_top = -row * ROW_PITCH + SHEET_TOP
        for y in (label_top, label_top - LABEL_HEIGHT):
            outer = left - MARK_OFFSET
            far = grid_right + MARK_OFFSET
            line(content, (outer, y), (outer - MARK_LENGTH, y), 0.5, ink)
            line(content, (far, y), (far + MARK_LENGTH, y), 0.5, ink)


def outline(content: hqf_pdf.Content, x: float, bottom: float) -> None:
    """Draws one label's die-cut outline, from its lower-left corner."""
    content.set_stroke(hqf_pdf.Rgb.gray(0.74))
    content.set_line_width(0.4)
    content.rect(x, bottom, LABEL_WIDTH, LABEL_HEIGHT)
    content.stroke()


def shelf_face(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    x: float,
    bottom: float,
    subtitle: str,
    shelf: Shelf,
) -> None:
    """Draws a shelf label's face: the price, the price per unit, and the article number
    as bars with its digits under them.
    """
    text(content, font, 7.5, x + 12.0, bottom + 92.0, MUTED, subtitle)
    text(content, font, 18.0, x + 12.0, bottom + 68.0, ACCENT, price_text(shelf.price))

    printed = full_number(shelf.article.digits)
    if isinstance(shelf.article, Ean13Article):
        code: hqf_pdf.Ean13 | hqf_pdf.UpcA = hqf_pdf.Ean13(printed)
    else:
        code = hqf_pdf.UpcA(printed)

    # A barcode whose margin a neighbouring label eats does not scan. The bars are
    # centred in the label, and what the centring leaves either side is held against the
    # quiet zone the symbology asks for.
    bars_width = code.module_count * MODULE
    side_margin = (LABEL_WIDTH - bars_width) / 2.0
    quiet = hqf_pdf.Ean13.quiet_zone() * MODULE
    if side_margin < quiet:
        raise ValueError("the label is narrower than the barcode and its quiet zone")

    # Bars are read by the contrast between them and the paper: they are drawn in black
    # whatever ink the price above them was set in.
    bars_left = x + side_margin
    content.set_fill(hqf_pdf.Rgb.gray(0.0))
    if isinstance(code, hqf_pdf.Ean13):
        content.draw_ean13(code, bars_left, bottom + 22.0, bars_width, 30.0)
    else:
        content.draw_upc_a(code, bars_left, bottom + 22.0, bars_width, 30.0)

    text_centred(content, font, 7.5, x + LABEL_WIDTH / 2.0, bottom + 11.0, INK, printed)


def bin_face(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    corner: tuple[float, float],
    words: Words,
    subtitle: str,
    bin_label: Bin,
) -> None:
    """Draws a bin label's face: the Data Matrix code a picker scans, and beside it the
    aisle, rack and shelf a human reads.
    """
    x, bottom = corner
    text(content, font, 7.5, x + 12.0, bottom + 92.0, MUTED, subtitle)

    code = hqf_pdf.DataMatrix(bin_label.code)
    content.set_fill(hqf_pdf.Rgb.gray(0.0))
    side = code.size * MATRIX_MODULE
    quiet = hqf_pdf.DataMatrix.quiet_zone() * MATRIX_MODULE

    # The quiet zone is the caller's: the code is drawn that far inside the label's left
    # margin, and the placement lines start clear of it.
    content.draw_datamatrix(code, x + 12.0 + quiet, bottom + 26.0, side)

    lines_left = x + 12.0 + (quiet * 2.0 + side) + 12.0
    for index, (label, placed) in enumerate(zip(words.positions, bin_label.positions)):
        text(
            content,
            font,
            9.0,
            lines_left,
            -index * 13.0 + bottom + 70.0,
            INK,
            f"{label} {placed}",
        )

    text(content, font, 7.0, x + 12.0, bottom + 11.0, INK, bin_label.code)


def header(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the head of the sheet: who printed it, what stock it is laid out for, and
    how many labels it carries.
    """
    left = sheet_left()
    right = sheet_right()

    text(content, font, 13.0, left, 800.0, INK, "HQF Development")
    text(
        content, font, 7.5, left, 788.0, MUTED, "High Quality Foundations · www.hqf.fr"
    )

    text_right(content, font, 16.0, right, 799.0, ACCENT, words.heading)
    text_right(
        content,
        font,
        8.0,
        right,
        785.0,
        MUTED,
        f"{words.sheet_number} · {len(LABELS)} {words.labels} · "
        f"{COLUMNS} × {ROWS} {words.stock_before} "
        f"{LABEL_WIDTH:.0f} × {LABEL_HEIGHT:.0f} {words.stock_after}",
    )
    text_right(
        content,
        font,
        8.0,
        right,
        773.0,
        MUTED,
        f"{words.printed_label} {words.printed_on}",
    )

    line(content, (left, 762.0), (right, 762.0), 1.0, ACCENT)


def footer(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the foot of the sheet: how long the prices hold, and who to send the sheet
    back to.
    """
    left = sheet_left()
    right = sheet_right()

    line(content, (left, 78.0), (right, 78.0), 0.5, hqf_pdf.Rgb.gray(0.8))
    mentions = (
        f"{words.valid_before} {words.valid_until} · HQF Development · "
        "42 lot les Genêts, 13480 Calas, France · www.hqf.fr"
    )
    text_centred(content, font, 7.0, (left + right) / 2.0, 66.0, MUTED, mentions)


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

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

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

    content = hqf_pdf.Content()
    header(content, font, words)
    cut_marks(content)

    # The grid fills across a row before dropping to the next, so a label's place on the
    # page follows from its place in the list and the two pitches.
    left = sheet_left()
    for index, face in enumerate(LABELS):
        x = index % COLUMNS * COLUMN_PITCH + left
        bottom = -(index // COLUMNS) * ROW_PITCH + SHEET_TOP - LABEL_HEIGHT
        subtitle = words.subtitles[index]

        outline(content, x, bottom)
        text(content, font, 10.5, x + 12.0, bottom + 104.0, INK, words.names[index])

        if isinstance(face, Shelf):
            shelf_face(content, font, x, bottom, subtitle, face)
        else:
            bin_face(content, font, (x, bottom), words, subtitle, face)

    footer(content, font, words)

    page = hqf_pdf.Page(PAGE_WIDTH, PAGE_HEIGHT)
    page.set_content(content)
    document.add_page(page)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes, {len(LABELS)} labels")


if __name__ == "__main__":
    main()
