"""A sheet a printer can run without asking the sender a single question.

The Python twin of the `write_print_ready` example in Rust. The finished page is
smaller than the paper it is drawn on: the ink runs past the blade so that a cut
a hair off still lands in colour, the marks stand outside the ink so the blade
takes them away, and every colour is said in the four printing inks rather than
in the light of a screen. The page names the three boxes that say all of it.

Hand a press profile on the command line and the file claims PDF/X-4. Without one
it claims nothing, and says on the page why.

The sheet is written in the language `HQF_PDF_LANG` names. What the file writes is
not: `MediaBox`, `BleedBox`, `TrimBox`, `Title` and `Trapped` are the entries a press
looks up, `Custom` is the name ISO 32000-1 asks for where the condition is not a
registered one, and `sRGB IEC61966-2.1` is the profile's own name. The page states
each of them beside what it means.

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

from __future__ import annotations

import sys
from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The date the document says it was made on, so that two runs of the example produce the
# same bytes.
MADE = "2026-07-25T10:00:00+02:00"

# How far past the blade the ink runs.
BLEED_OVER = 9.0
# How much paper is left outside the ink for the marks.
MARK_ROOM = 27.0
# How long one arm of a corner mark is.
MARK_LENGTH = 18.0
# How much paper stands outside the finished page on every side.
MARGIN = BLEED_OVER + MARK_ROOM

# The finished page: where the blade falls, in the sheet's own coordinates.
TRIM = (MARGIN, MARGIN, 400.0, 620.0)
# The whole sheet: the finished page, the ink that runs past it, and the room the marks
# stand in.
SHEET = (TRIM[2] + MARGIN + MARGIN, TRIM[3] + MARGIN + MARGIN)

# The left edge of everything drawn inside the finished page.
LEFT = 56.0
# How far right of the left edge the second column of a stated pair starts.
COLUMN = 112.0

# The head of the sheet: the flood, its title and the line under it.
BAND_BOTTOM = 545.0
# The foot of the sheet: the bar the footer is written on.
BAR_TOP = 52.0

# The width of one ink swatch, the gap to the next, their bottom edge and their height.
SWATCH = 64.0
SWATCH_GAP = 10.0
SWATCH_Y = 292.0
SWATCH_HEIGHT = 42.0

# The house sending the sheet, which the footer names before saying what the sheet is.
HOUSE = "HQF Development"

# The size the sheet's heading is set at, in points.
HEADING_SIZE = 19.0
# The size the line under that heading is set at, in points.
STRAPLINE = 9.0
# The size the line in the margin is set at, in points.
SLUG = 6.0
# The size a panel's heading is set at, in points.
PANEL = 10.5
# The size a panel's body is set at, in points.
PANEL_BODY = 8.0
# The size a stated pair is set at, in points.
PAIR = 8.5
# The size the name under a swatch is set at, in points.
SWATCH_NAME = 7.5

# Where the first line of the closing note sits, and how far below one line the next one
# does, in points.
CLOSING_TOP = 104.0
CLOSING_STEP = 12.0

# What each ink swatch is made of, in the order `Words.inks` names them.
MIXES = ["100 0 0 0", "0 100 0 0", "0 0 100 0", "0 0 0 100", "100 100 100 100"]

# What the file writes for each of the three boxes and each of the two entries the page
# states, in the order they are set. These are the names a press looks up, and they are
# the same in every language.
BOXES = ["MediaBox", "BleedBox", "TrimBox"]
ENTRIES = ["Title", "Trapped"]

# What the file writes for the trapping entry, which the page states beside what it
# means.
TRAPPED = "No"

# What the caller says the condition is called when a profile is handed over on the
# command line. ISO 32000-1 table 365 asks for `Custom` where the condition is not one
# of the registered characterizations, which a profile read off a disk is not.
CUSTOM = "Custom"
# What the condition is called when the library's own profile is used, and the profile's
# own name, which is not a description of it.
BUILT_IN = ("sRGB", "sRGB IEC61966-2.1")


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

    What is not language stays out of it: the entries a press looks up, the numbers the
    boxes measure, the four ink mixes, and the profile's own name.
    """

    # What the job is called, on the page and in what the file says of itself.
    title: str
    # What the sheet is headed by.
    heading: str
    # The line under that heading.
    strapline: str
    # What the footer says the sheet is, after the name of the house sending it.
    footer: str
    # What the line in the margin calls the whole sheet, the finished page, the ink that
    # runs past it, and the inks it is separated for.
    slug: tuple[str, str, str, str]
    # What each of the five panels is headed by.
    panels: tuple[str, str, str, str, str]
    # The line under the first panel.
    boxes_note: str
    # What each of the three boxes is for, after the size it measures.
    boxes: tuple[str, str, str]
    # The lines under the second panel.
    ink_stops: tuple[str, str, str]
    # The lines under the third panel.
    four_inks: tuple[str, str]
    # What each printing ink is called, in the order the swatches lay them.
    inks: tuple[str, str, str, str, str]
    # The lines under the fourth panel.
    marks: tuple[str, str, str]
    # What the last two pairs the page states are called.
    stated_for: str
    claim: str
    # What the trapping entry means, after the word the file writes.
    trapped: str
    # What stands before and after the size of the profile the file is stated for.
    profile_of: str
    bytes: str
    # What the claim says when the file makes none.
    no_claim: str
    # What the condition a profile off the command line describes is called.
    custom_info: str
    # The closing note of a file that claims the print standard.
    claimed: tuple[str, str, str, str, str]
    # The closing note of a file that claims nothing.
    unclaimed: tuple[str, str, str, str, str]


# The sheet in English.
ENGLISH = Words(
    title="Spring catalogue, cover",
    heading="Ready for the press",
    strapline=(
        "One sheet, handed over with nothing left for the printer to ask about."
    ),
    footer="one sheet, ready to run",
    slug=("sheet", "trim", "bleed", "CMYK"),
    panels=(
        "The three boxes this sheet states",
        "Where the ink stops",
        "Four inks, not three",
        "The marks stand outside the ink",
        "What the file says about itself",
    ),
    boxes_note=(
        "The paper is larger than the finished page; the ink runs to a box "
        "in between."
    ),
    boxes=(
        "the whole sheet, marks and all",
        "how far the ink runs",
        "where the blade falls",
    ),
    ink_stops=(
        "The band at the head of this page runs 9 points past the blade on "
        "three",
        "sides, and the bar at its foot does the same. A blade that falls a "
        "point",
        "wide still lands in ink, so the finished page carries no white edge.",
    ),
    four_inks=(
        "Every colour on this page is said in the four printing inks. A "
        "screen colour",
        "would have to be separated by somebody — and that somebody is the "
        "printer.",
    ),
    inks=("Cyan", "Magenta", "Yellow", "Black", "Registration"),
    marks=(
        "The corner marks are drawn past the bleed, so the blade takes them "
        "away",
        "with the rest of the paper. They are laid in all four inks at once: a",
        "mark that prints on every plate is a mark the press lines its plates "
        "by.",
    ),
    stated_for="Stated for",
    claim="Claim",
    trapped="said in the file, not left unknown",
    profile_of="a profile of",
    bytes="bytes",
    no_claim="none — the note below says why",
    custom_info="A press profile handed over on the command line",
    claimed=(
        "This file claims PDF/X-4, through the profile handed on the command line.",
        "The claim is written twice over: the output intent answers for it as",
        "GTS_PDFX, and the metadata names the part claimed. The library refuses the",
        "claim unless the file carries the press profile, a title, a word on",
        "trapping and a trim box on every page. All four are here.",
    ),
    unclaimed=(
        "This file makes no PDF/X-4 claim. That claim promises a press it can run",
        "the file without asking back, and the promise rests on the profile of the",
        "press the job was separated for. The library builds one profile, sRGB, and",
        "sRGB describes a screen: it is not allowed to stand in for a press. Hand a",
        "press profile on the command line and the same sheet claims PDF/X-4.",
    ),
)

# The sheet in French.
FRENCH = Words(
    title="Catalogue de printemps, couverture",
    heading="Prête pour la presse",
    strapline="Une feuille, remise sans rien laisser à demander à l'imprimeur.",
    footer="une feuille, prête à tourner",
    slug=("feuille", "coupe", "fond perdu", "CMJN"),
    panels=(
        "Les trois boîtes que cette feuille déclare",
        "Où l'encre s'arrête",
        "Quatre encres, pas trois",
        "Les repères se tiennent hors de l'encre",
        "Ce que le fichier dit de lui-même",
    ),
    boxes_note=(
        "Le papier est plus grand que la page finie ; l'encre court jusqu'à une "
        "boîte entre les deux."
    ),
    boxes=(
        "la feuille entière, repères compris",
        "jusqu'où court l'encre",
        "où tombe la lame",
    ),
    ink_stops=(
        "La bande en tête de page court 9 points au-delà de la lame sur trois "
        "côtés,",
        "et la barre au pied fait de même. Une lame qui tombe un point trop loin",
        "retombe encore dans l'encre : la page finie n'a aucun bord blanc.",
    ),
    four_inks=(
        "Chaque couleur de cette page est dite dans les quatre encres "
        "d'imprimerie. Une",
        "couleur d'écran devrait être séparée par quelqu'un — et ce quelqu'un est "
        "l'imprimeur.",
    ),
    inks=("Cyan", "Magenta", "Jaune", "Noir", "Repérage"),
    marks=(
        "Les repères d'angle sont tracés au-delà du fond perdu, donc la lame les",
        "emporte avec le reste du papier. Ils sont posés dans les quatre encres à "
        "la",
        "fois : un repère qui s'imprime sur chaque plaque est un repère qui cale "
        "la presse.",
    ),
    stated_for="Déclarée pour",
    claim="Revendication",
    trapped="dit dans le fichier, pas laissé inconnu",
    profile_of="un profil de",
    bytes="octets",
    no_claim="aucune — la note ci-dessous dit pourquoi",
    custom_info="Un profil de presse passé en ligne de commande",
    claimed=(
        "Ce fichier revendique PDF/X-4, par le profil passé en ligne de commande.",
        "La revendication est écrite deux fois : l'intention de sortie en répond sous",
        "le nom GTS_PDFX, et les métadonnées nomment la partie revendiquée. La",
        "bibliothèque refuse la revendication tant que le fichier ne porte pas le profil",
        "de presse, un titre, un mot sur le défonçage et une boîte de coupe par page.",
    ),
    unclaimed=(
        "Ce fichier ne revendique pas PDF/X-4. Cette revendication promet à une presse",
        "qu'elle peut tourner sans rien redemander, et elle tient au profil de la presse",
        "pour laquelle le travail a été séparé. La bibliothèque n'a qu'un profil, sRGB,",
        "qui décrit un écran : il ne peut pas tenir lieu de presse. Passez un profil de",
        "presse en ligne de commande et la même feuille revendique PDF/X-4.",
    ),
)

# 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 band():
    """The band across the head of the sheet, and the bar across its foot."""
    return hqf_pdf.Cmyk(0.85, 0.55, 0.0, 0.25)


def on_band():
    """The line under the title, on the band."""
    return hqf_pdf.Cmyk(0.22, 0.1, 0.0, 0.0)


def heading_ink():
    """A panel's heading."""
    return hqf_pdf.Cmyk(0.0, 0.0, 0.0, 1.0)


def body_ink():
    """A panel's body."""
    return hqf_pdf.Cmyk(0.0, 0.0, 0.0, 0.72)


def label_ink():
    """What a stated pair is called."""
    return hqf_pdf.Cmyk(0.0, 0.0, 0.0, 0.5)


def value_ink():
    """What it says."""
    return hqf_pdf.Cmyk(0.0, 0.0, 0.0, 0.85)


def registration():
    """All four inks at once, which prints on every plate."""
    return hqf_pdf.Cmyk(1.0, 1.0, 1.0, 1.0)


def inks():
    """The colour each swatch is laid in, in the order `Words.inks` names them."""
    return [
        hqf_pdf.Cmyk(1.0, 0.0, 0.0, 0.0),
        hqf_pdf.Cmyk(0.0, 1.0, 0.0, 0.0),
        hqf_pdf.Cmyk(0.0, 0.0, 1.0, 0.0),
        hqf_pdf.Cmyk(0.0, 0.0, 0.0, 1.0),
        registration(),
    ]


def trim():
    """Where the blade falls."""
    return hqf_pdf.Rect(TRIM[0], TRIM[1], TRIM[2], TRIM[3])


def bleed():
    """What the ink runs to."""
    return hqf_pdf.Rect(
        TRIM[0] - BLEED_OVER,
        TRIM[1] - BLEED_OVER,
        TRIM[2] + BLEED_OVER + BLEED_OVER,
        TRIM[3] + BLEED_OVER + BLEED_OVER,
    )


def flood(content, font, words):
    """Draw the flood at the head of the sheet and the bar at its foot.

    Both run to the ink's own box and off the blade on three sides.
    """
    ink = bleed()
    top = ink.y + ink.height

    content.save_state()
    content.set_fill(band())
    content.rect(ink.x, BAND_BOTTOM, ink.width, top - BAND_BOTTOM)
    content.fill()
    content.rect(ink.x, ink.y, ink.width, BAR_TOP - ink.y)
    content.fill()
    content.restore_state()

    content.save_state()
    content.set_fill(hqf_pdf.Cmyk.white())
    content.draw_text(font, HEADING_SIZE, LEFT, 620.0, words.heading)
    content.draw_text(font, 7.0, LEFT, 38.0, f"{HOUSE} — {words.footer}")
    content.set_fill(on_band())
    content.draw_text(font, STRAPLINE, LEFT, 600.0, words.strapline)
    content.restore_state()


def corner_marks(content):
    """Draw the four corner marks.

    Each pair of arms stands outside the ink and points at a corner of the
    finished page. They are laid in all four inks at once, so that each plate
    carries them.
    """
    left, bottom, width, height = TRIM
    right = left + width
    top = bottom + height
    near = BLEED_OVER
    far = BLEED_OVER + MARK_LENGTH

    content.save_state()
    content.set_stroke(registration())
    content.set_line_width(0.4)
    for x, from_x, to_x in [
        (left, left - near, left - far),
        (right, right + near, right + far),
    ]:
        for y, from_y, to_y in [
            (bottom, bottom - near, bottom - far),
            (top, top + near, top + far),
        ]:
            content.move_to(from_x, y)
            content.line_to(to_x, y)
            content.move_to(x, from_y)
            content.line_to(x, to_y)
    content.stroke()
    content.restore_state()


def slug_line(words):
    """What the line in the margin says: the job, then each box the paper takes."""
    sheet_word, trim_word, bleed_word, inks_word = words.slug
    return (
        f"{words.title} — {SHEET[0]:g} × {SHEET[1]:g} {sheet_word}"
        f" — {TRIM[2]:g} × {TRIM[3]:g} {trim_word}"
        f" — {BLEED_OVER:g} pt {bleed_word} — {inks_word}"
    )


def slug(content, font, words):
    """Draw the line in the margin naming the job.

    The blade takes it away with the rest of the paper outside the finished page.
    """
    content.save_state()
    content.set_fill(registration())
    content.draw_text(font, SLUG, LEFT, 14.0, slug_line(words))
    content.restore_state()


def panel(content, font, y, title, lines):
    """Draw one panel: its heading, and the lines of body under it."""
    content.save_state()
    content.set_fill(heading_ink())
    content.draw_text(font, PANEL, LEFT, y, title)
    content.set_fill(body_ink())
    line_y = y - 15.0
    for line in lines:
        content.draw_text(font, PANEL_BODY, LEFT, line_y, line)
        line_y -= 12.0
    content.restore_state()


def stated(content, font, top, pairs):
    """Draw a run of stated pairs, one to a line.

    What each is called on the left, and what it says beside it.
    """
    y = top
    for label, value in pairs:
        content.save_state()
        content.set_fill(label_ink())
        content.draw_text(font, PAIR, LEFT, y, label)
        content.set_fill(value_ink())
        content.draw_text(font, PAIR, LEFT + COLUMN, y, value)
        content.restore_state()
        y -= 14.0


def swatches(content, font, words):
    """Draw one swatch per printing ink, each at full strength.

    Under each stands the name of the ink and the four numbers that say it.
    """
    x = LEFT
    for name, mix, ink in zip(words.inks, MIXES, inks()):
        content.save_state()
        content.set_fill(ink)
        content.rect(x, SWATCH_Y, SWATCH, SWATCH_HEIGHT)
        content.fill()
        content.restore_state()

        content.save_state()
        content.set_fill(value_ink())
        content.draw_text(font, SWATCH_NAME, x, SWATCH_Y - 10.0, name)
        content.set_fill(label_ink())
        content.draw_text(font, 7.0, x, SWATCH_Y - 20.0, mix)
        content.restore_state()
        x += SWATCH + SWATCH_GAP


def closing(content, font, words, claims):
    """Draw the closing note: what the file claims, or why it claims nothing."""
    content.save_state()
    content.set_fill(body_ink())
    y = CLOSING_TOP
    for line in words.claimed if claims else words.unclaimed:
        content.draw_text(font, PANEL_BODY, LEFT, y, line)
        y -= CLOSING_STEP
    content.restore_state()


def stated_for():
    """The profile the document is stated for, and what it is called.

    Also whether the print standard is claimed on it: the file named on the
    command line, or the one the library builds.
    """
    if len(sys.argv) > 3:
        return Path(sys.argv[3]).read_bytes(), CUSTOM, None, True
    return hqf_pdf.srgb_icc_profile(), BUILT_IN[0], BUILT_IN[1], False


def box_sizes():
    """The size each of the three boxes measures, in the order the page states them."""
    ink = bleed()
    return [
        (SHEET[0], SHEET[1]),
        (ink.width, ink.height),
        (TRIM[2], TRIM[3]),
    ]


def sheet(font, words, identifier, size, claims):
    """The whole sheet, drawn once."""
    content = hqf_pdf.Content()
    flood(content, font, words)
    corner_marks(content)
    slug(content, font, words)

    panel(content, font, 518.0, words.panels[0], [words.boxes_note])
    stated(
        content,
        font,
        487.0,
        [
            (name, f"{width:g} × {height:g} pt — {says}")
            for name, (width, height), says in zip(BOXES, box_sizes(), words.boxes)
        ],
    )

    panel(content, font, 434.0, words.panels[1], words.ink_stops)

    panel(content, font, 371.0, words.panels[2], words.four_inks)
    swatches(content, font, words)

    panel(content, font, 248.0, words.panels[3], words.marks)

    panel(content, font, 185.0, words.panels[4], [])
    stated(
        content,
        font,
        169.0,
        [
            (ENTRIES[0], words.title),
            (ENTRIES[1], f"{TRAPPED} — {words.trapped}"),
            (
                words.stated_for,
                f"{identifier} — {words.profile_of} {size} {words.bytes}",
            ),
            (words.claim, "PDF/X-4" if claims else words.no_claim),
        ],
    )

    closing(content, font, words, claims)
    return content


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("print_ready.pdf", language)).stem)
    profile, identifier, info, claims = stated_for()

    described = hqf_pdf.icc_profile_space(profile)
    if described is None:
        raise SystemExit("the profile must describe a grey, RGB or CMYK space")
    intent = hqf_pdf.OutputIntent(
        hqf_pdf.IccBased(profile, described), identifier, info or words.custom_info
    )

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_metadata(
        hqf_pdf.Metadata(
            title=words.title,
            producer="hqf-pdf",
            created=MADE,
            trapped=hqf_pdf.Trapped.No,
        )
    )
    document.set_output_intent(intent)
    if claims:
        document.set_print_conformance(hqf_pdf.PdfX.X4)
        document.set_version(hqf_pdf.Version.V1_6)

    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, identifier, len(profile), claims))
    page.set_bleed_box(bleed())
    page.set_trim_box(trim())
    document.add_page(page)

    written = document.write(out)
    print(
        f"wrote {out}: {written} bytes, a press sheet stated for {identifier}, "
        f"{'claiming PDF/X-4' if claims else 'claiming nothing'}"
    )


if __name__ == "__main__":
    main()
