"""The device a document's colours are meant for, stated in the file.

The Python twin of the `write_output_intent` example in Rust. A page that fills
a rectangle with four numbers has said four numbers and nothing about what
colour they are. The output intent is the answer: the profile of the device the
job was prepared for, travelling inside the file, together with the name of that
device's condition. An archival claim makes stating one compulsory, and a job
separated for a press states the press's own profile rather than the screen the
library falls back to.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set the page is written in. What the file states is not language: the name of the
condition and the name of the profile travel as they are given.

Usage: python examples/write_output_intent.py [out.pdf] [font.ttf] [press.icc]
       HQF_PDF_LANG=fr python examples/write_output_intent.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 left edge of everything.
LEFT = 70.0

# How far in from LEFT the values of the intent's rows are set, which is the room the
# labels beside them have.
VALUE_IN = 100.0

# 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"

# The claim the file makes, which is a name the standard gives rather than anything a
# language says.
CLAIM = "PDF/A-3B"

# The tints the ramp runs through, all the profile's channels together.
TINTS = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
# The width of one cell of the ramp, the gap to the next, and its height.
RAMP_CELL = 38.0
RAMP_GAP = 3.5
RAMP_HEIGHT = 86.0
# The bottom edge of the ramp.
RAMP_Y = 440.0

# The size of one channel swatch, the gap to the next, and their bottom edge.
CHANNEL_CELL = 90.0
CHANNEL_GAP = 15.0
CHANNEL_Y = 230.0

# 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 name that
# profile is registered under.
BUILT_IN = ("sRGB", "sRGB IEC61966-2.1")


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

    # The title over the page, which is also the title the file states.
    title: str
    # The two lines under the title.
    intro: tuple[str, str]
    # The heading over the panel that lists what the file carries.
    intent_panel: str
    # The heading over the ramp, and the line under it.
    profile_panel: str
    profile_note: str
    # The heading over the channel swatches.
    channel_panel: str
    # What the four rows of the intent panel are called.
    rows: tuple[str, str, str, str]
    # What stands between the size of the profile and what it describes, so that the row
    # reads as one sentence in every language.
    profile_size: str
    # What follows the name of the claim on its row.
    claim_note: str
    # What a colour is made of, in words, in a grey, an RGB and a CMYK space.
    described: tuple[str, str, str]
    # The one channel of a grey space.
    gray_channels: tuple[str]
    # The three channels of an RGB space.
    rgb_channels: tuple[str, str, str]
    # The four channels of a CMYK space.
    cmyk_channels: tuple[str, str, str, str]
    # What the caller is told a profile of their own would do, under the swatches.
    closing: tuple[str, str]
    # What the condition is called when a profile is handed over on the command line,
    # which is prose rather than a registered name.
    custom_info: str

    def channels(self, space) -> tuple[str, ...]:
        """The channels a colour in `space` is made of, in the order their numbers
        are given."""
        return {
            hqf_pdf.DeviceSpace.Gray: self.gray_channels,
            hqf_pdf.DeviceSpace.Rgb: self.rgb_channels,
            hqf_pdf.DeviceSpace.Cmyk: self.cmyk_channels,
        }[space]

    def describes(self, space) -> str:
        """What a colour in `space` is made of, in words."""
        return {
            hqf_pdf.DeviceSpace.Gray: self.described[0],
            hqf_pdf.DeviceSpace.Rgb: self.described[1],
            hqf_pdf.DeviceSpace.Cmyk: self.described[2],
        }[space]


# The page in English.
ENGLISH = Words(
    title="What the numbers on this page mean",
    intro=(
        "A file states the device it was prepared for, and the profile of that "
        "device travels inside it.",
        "Nothing else in the file says what its colours are: the numbers alone "
        "say none.",
    ),
    intent_panel="The output intent this file carries",
    profile_panel="The same profile, painted through",
    profile_note=(
        "One profile does both jobs: it says what the file is for, and it is "
        "what these swatches are painted in. The file carries it once."
    ),
    channel_panel="One channel at a time, at full strength",
    rows=("Condition", "Description", "Profile", "Claim"),
    profile_size=" bytes, describing ",
    claim_note=", which makes stating one compulsory",
    described=("grey", "red, green and blue", "the four printing inks"),
    gray_channels=("Grey",),
    rgb_channels=("Red", "Green", "Blue"),
    cmyk_channels=("Cyan", "Magenta", "Yellow", "Black"),
    closing=(
        "Hand a press profile on the command line and the page is drawn in its "
        "inks instead, under its own",
        "condition. A file that paints in CMYK and states a screen is a file a "
        "validator refuses.",
    ),
    custom_info="A profile handed over on the command line",
)

# The page in French.
FRENCH = Words(
    title="Ce que veulent dire les nombres de cette page",
    intro=(
        "Un fichier dit l'appareil pour lequel il a été préparé, et le profil de "
        "cet appareil voyage dedans.",
        "Rien d'autre dans le fichier ne dit ce que sont ses couleurs : les "
        "nombres seuls n'en disent rien.",
    ),
    intent_panel="L'intention de sortie que porte ce fichier",
    profile_panel="Le même profil, utilisé pour peindre",
    profile_note=(
        "Un seul profil fait les deux : il dit à quoi sert le fichier, et c'est "
        "lui qui peint ces pastilles. Le fichier ne le porte qu'une fois."
    ),
    channel_panel="Une composante à la fois, à pleine force",
    rows=("Condition", "Description", "Profil", "Revendication"),
    profile_size=" octets, décrivant ",
    claim_note=", qui rend la mention obligatoire",
    described=(
        "le gris",
        "le rouge, le vert et le bleu",
        "les quatre encres d'imprimerie",
    ),
    gray_channels=("Gris",),
    rgb_channels=("Rouge", "Vert", "Bleu"),
    cmyk_channels=("Cyan", "Magenta", "Jaune", "Noir"),
    closing=(
        "Donnez un profil de presse en ligne de commande et la page est peinte "
        "dans ses encres, sous sa propre",
        "condition. Un fichier qui peint en CMJN en annonçant un écran est refusé "
        "par un validateur.",
    ),
    custom_info="Un profil donné en ligne de commande",
)

# 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 heading(content, font, words):
    """Draw the title and the two lines saying what the page is showing."""
    content.draw_text(font, 15.0, LEFT, 780.0, words.title)
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.35))
    content.draw_text(font, 10.0, LEFT, 762.0, words.intro[0])
    content.draw_text(font, 10.0, LEFT, 748.0, words.intro[1])
    content.restore_state()


def panel_heading(content, font, y, title, under):
    """Draw one panel's heading, with the line under it that a panel may have."""
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.2))
    content.draw_text(font, 11.0, LEFT, y, title)
    if under is not None:
        content.set_fill(hqf_pdf.Color.gray(0.45))
        content.draw_text(font, 8.0, LEFT, y - 16.0, under)
    content.restore_state()


def intent_panel(content, font, words, intent, size):
    """Draw the intent's parts, one to a line, as the file carries them."""
    described = words.describes(intent.profile.space)
    values = (
        intent.identifier,
        intent.info,
        f"{size}{words.profile_size}{described}",
        f"{CLAIM}{words.claim_note}",
    )

    y = 672.0
    for label, value in zip(words.rows, values):
        content.save_state()
        content.set_fill(hqf_pdf.Color.gray(0.45))
        content.draw_text(font, 9.0, LEFT, y, label)
        content.set_fill(hqf_pdf.Color.gray(0.15))
        content.draw_text(font, 9.0, LEFT + VALUE_IN, y, value)
        content.restore_state()
        y -= 18.0


def frame(content, x, y, width, height):
    """Frame a swatch, so that one painted white is a swatch rather than a hole."""
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.75))
    content.rect(x - 1.0, y - 1.0, width + 2.0, height + 2.0)
    content.fill()
    content.restore_state()


def ramp(content, font, space, width):
    """Draw every channel of the profile at the same tint, none of it to all."""
    x = LEFT
    for tint in TINTS:
        frame(content, x, RAMP_Y, RAMP_CELL, RAMP_HEIGHT)
        content.save_state()
        content.set_fill_space(space)
        content.set_fill_components([tint] * width)
        content.rect(x, RAMP_Y, RAMP_CELL, RAMP_HEIGHT)
        content.fill()
        content.restore_state()

        content.save_state()
        content.set_fill(hqf_pdf.Color.gray(0.45))
        content.draw_text(font, 7.0, x, RAMP_Y - 12.0, f"{tint * 100.0:.0f}%")
        content.restore_state()
        x += RAMP_CELL + RAMP_GAP


def channel_swatches(content, font, space, names):
    """Draw one swatch per channel, each at full strength with the others empty."""
    x = LEFT
    for channel, name in enumerate(names):
        components = [0.0] * len(names)
        components[channel] = 1.0

        frame(content, x, CHANNEL_Y, CHANNEL_CELL, CHANNEL_CELL)
        content.save_state()
        content.set_fill_space(space)
        content.set_fill_components(components)
        content.rect(x, CHANNEL_Y, CHANNEL_CELL, CHANNEL_CELL)
        content.fill()
        content.restore_state()

        content.save_state()
        content.set_fill(hqf_pdf.Color.gray(0.45))
        content.draw_text(font, 8.0, x, CHANNEL_Y - 12.0, name)
        content.restore_state()
        x += CHANNEL_CELL + CHANNEL_GAP


def closing(content, font, words):
    """Draw the closing note under the swatches."""
    content.save_state()
    content.set_fill(hqf_pdf.Color.gray(0.35))
    content.draw_text(font, 9.0, LEFT, 160.0, words.closing[0])
    content.draw_text(font, 9.0, LEFT, 146.0, words.closing[1])
    content.restore_state()


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

    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, words.custom_info)
    return hqf_pdf.srgb_icc_profile(), BUILT_IN


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

    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
    )
    names = words.channels(described)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_conformance(hqf_pdf.PdfA.A3B)
    document.set_metadata(
        hqf_pdf.Metadata(
            title=words.title,
            producer="hqf-pdf",
            created=MADE,
        )
    )
    document.set_output_intent(intent)

    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
    space = document.add_color_space(hqf_pdf.IccBased(profile, described))

    content = hqf_pdf.Content()
    heading(content, font, words)
    panel_heading(content, font, 700.0, words.intent_panel, None)
    intent_panel(content, font, words, intent, len(profile))

    panel_heading(
        content, font, 566.0, words.profile_panel, words.profile_note
    )
    ramp(content, font, space, len(names))

    panel_heading(content, font, 360.0, words.channel_panel, None)
    channel_swatches(content, font, space, names)
    closing(content, font, words)

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

    written = document.write(out)
    print(
        f"wrote {out}: {written} bytes, stated for {identifier} through a "
        f"profile of {len(profile)} bytes"
    )


if __name__ == "__main__":
    main()
