"""Draws a month's activity report out of the shapes a page has no operator for.

The Python twin of the `write_shapes` example in Rust. PDF states a path with straight
segments and cubic curves and nothing else, so a circle is four curves and a slice of a
pie is an arc with two straight sides. Everything on this page is asked for by name — a
centre and a radius, two angles, three points — and the curves are worked out for it.

Both sets of words are held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set is drawn.

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

from __future__ import annotations

import math
from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf


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

    # The page's title.
    title: str
    # What the page is about.
    lead: str
    # The heading over the pie.
    pie: str
    # What the four slices of the pie are called.
    slices: tuple[str, str, str, str]
    # The heading over the ring gauge.
    gauge: str
    # What the ring gauge counts.
    gauge_label: str
    # The heading over the card.
    card: str
    # What the line inside the card shows.
    card_label: str
    # The heading over the two arcs.
    arcs: str
    # What the arc through three points is called.
    through: str
    # What the four arcs of one ellipse are called.
    four: str
    # What a curve costs, and what it does not.
    caveat: str


# The page in English.
ENGLISH = Words(
    title="The month, drawn in curves",
    lead=(
        "A page states a path with straight segments and cubic curves, and with "
        "nothing else: there is no circle operator and no arc operator. Every shape "
        "below was asked for by name — a centre and a radius, two angles, three "
        "points to pass through — and the curves were worked out for it. No piece "
        "bends further than a quarter turn, so a circle is four curves and the "
        "outline is out by a fortieth of a point on a circle an inch across."
    ),
    pie="Where the hours went",
    slices=("Drawing office", "Plates and proofs", "Delivery", "Storage"),
    gauge="How far into the year the work is",
    gauge_label="of the year's work is behind us",
    card="A card whose corners are eased off",
    card_label="Orders taken, week by week",
    arcs="Arcs told where to go, not how far to turn",
    through="One arc, through three points",
    four="Four arcs, between the same two points",
    caveat=(
        "A curve costs six numbers and a straight segment costs two, so a circle is "
        "five lines of a page rather than one. What it does not cost is a picture: "
        "the shapes here are drawn by the reader at whatever size the page is shown, "
        "and they stay sharp at any of them."
    ),
)

# The page in French.
FRENCH = Words(
    title="Le mois, dessiné en courbes",
    lead=(
        "Une page décrit un tracé avec des segments droits et des courbes cubiques, "
        "et avec rien d'autre : il n'y a ni opérateur de cercle ni opérateur d'arc. "
        "Chaque forme ci-dessous a été demandée par son nom — un centre et un rayon, "
        "deux angles, trois points par où passer — et les courbes ont été calculées "
        "pour elle. Aucun morceau ne tourne de plus d'un quart de tour : un cercle "
        "fait donc quatre courbes, et le contour s'écarte d'un quarantième de point "
        "sur un cercle de deux centimètres et demi."
    ),
    pie="Où sont passées les heures",
    slices=("Bureau d'études", "Plaques et épreuves", "Livraison", "Stockage"),
    gauge="Où en est l'année de travail",
    gauge_label="du travail de l'année est derrière nous",
    card="Une carte aux coins adoucis",
    card_label="Commandes prises, semaine par semaine",
    arcs="Des arcs à qui l'on dit où aller, pas de combien tourner",
    through="Un arc, par trois points",
    four="Quatre arcs, entre les deux mêmes points",
    caveat=(
        "Une courbe coûte six nombres et un segment droit en coûte deux : un cercle "
        "fait donc cinq lignes de page au lieu d'une. Ce qu'il ne coûte pas, c'est "
        "une image : les formes d'ici sont tracées par le lecteur à la taille où la "
        "page est affichée, et elles restent nettes à toutes."
    ),
)

# Every language the example is written in.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}

# The left edge of everything on the page.
LEFT = 72.0

# How wide a block of text, and the card, are.
WIDTH = 451.0

# The hours the four slices of the pie stand for.
HOURS = (64, 41, 27, 18)

# The orders taken in each of seven weeks, which the line in the card draws.
ORDERS = (26, 38, 31, 52, 44, 61, 57)

# The most orders the line in the card leaves room for.
CEILING = 70.0

# How far round the year the gauge has come.
DONE = 0.62


def palette() -> list[hqf_pdf.Rgb]:
    """The four colours the slices and the line are drawn in."""
    return [
        hqf_pdf.Rgb(0.16, 0.33, 0.62),
        hqf_pdf.Rgb(0.30, 0.57, 0.75),
        hqf_pdf.Rgb(0.55, 0.73, 0.82),
        hqf_pdf.Rgb(0.79, 0.86, 0.90),
    ]


def block(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    size: float,
    top: float,
    text: str,
) -> float:
    """Sets a block of words at `top`, and hands back the ordinate it ends at."""
    flow = hqf_pdf.TextFlow(handle, size)
    lines = flow.break_lines(text, WIDTH)
    # The binding opens the text object itself, so opening another here would write a
    # pair of operators its twin in Rust does not.
    flow.draw(content, lines, LEFT, top, WIDTH)
    return top - flow.height(lines)


def label(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    size: float,
    x: float,
    y: float,
    text: str,
    room: float,
) -> None:
    """Sets one line of words at `(x, y)`."""
    flow = hqf_pdf.TextFlow(handle, size)
    flow.draw(content, flow.break_lines(text, room), x, y, room)


def shares() -> list[float]:
    """What share of the hours each slice of the pie stands for."""
    total = 0
    for hours in HOURS:
        total += hours
    return [hours / total for hours in HOURS]


def pie(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    words: Words,
    cx: float,
    cy: float,
) -> None:
    """Draws the pie about `(cx, cy)`, and names each slice beside it.

    A slice is one arc with two straight sides: the arc writes where it begins, a
    segment runs back to the centre, and closing the subpath draws the other side.
    """
    radius = 68.0
    start = math.pi / 2.0
    for share, colour in zip(shares(), palette()):
        sweep = math.tau * share
        content.save_state()
        content.set_fill(colour)
        content.arc(cx, cy, radius, start, -sweep)
        content.line_to(cx, cy)
        content.close_path()
        content.fill()
        content.restore_state()
        start -= sweep

    # The names sit to the right of the pie, one under the other, each behind a swatch
    # of its own colour.
    y = cy + 46.0
    for colour, name in zip(palette(), words.slices):
        content.save_state()
        content.set_fill(colour)
        content.rounded_rect(cx + 96.0, y, 12.0, 12.0, 3.0)
        content.fill()
        content.restore_state()
        label(content, handle, 9.5, cx + 116.0, y + 3.0, name, 180.0)
        y -= 26.0


def gauge(content: hqf_pdf.Content, cx: float, cy: float) -> None:
    """Draws the ring gauge about `(cx, cy)`, the part that is done in the darkest
    colour of the palette.

    The pale track is one circle laid inside another and filled by the even-odd rule,
    which leaves the ring between them. The part that is done is an arc drawn with a
    thick pen over that ring.
    """
    content.save_state()
    content.set_fill(hqf_pdf.Rgb(0.90, 0.92, 0.95))
    content.circle(cx, cy, 46.0)
    content.circle(cx, cy, 30.0)
    content.fill_even_odd()
    content.restore_state()

    content.save_state()
    content.set_line_width(16.0)
    content.set_stroke(palette()[0])
    content.arc(cx, cy, 38.0, math.pi / 2.0, -(math.tau * DONE))
    content.stroke()
    content.restore_state()


def card(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    words: Words,
    top: float,
) -> float:
    """Draws the card and the line of orders inside it, and hands back the ordinate the
    card ends at."""
    height = 96.0
    bottom = top - height

    content.save_state()
    content.set_fill(hqf_pdf.Rgb(0.96, 0.97, 0.98))
    content.set_stroke(hqf_pdf.Rgb.gray(0.72))
    content.set_line_width(0.8)
    content.rounded_rect(LEFT, bottom, WIDTH, height, 14.0)
    content.fill_and_stroke()
    content.restore_state()

    label(content, handle, 9.0, LEFT + 18.0, top - 20.0, words.card_label, 300.0)

    # The seven weeks are spaced evenly across the card, and each order count is
    # measured up from the floor of the plot.
    floor = bottom + 18.0
    plot = 52.0
    step = (WIDTH - 36.0) / 6.0
    points = []
    x = LEFT + 18.0
    for orders in ORDERS:
        share = orders / CEILING
        points.append((x, floor + plot * share))
        x += step

    content.save_state()
    content.set_line_width(2.2)
    content.set_stroke(palette()[1])
    content.rounded_polyline(points, 14.0)
    content.stroke()
    content.restore_state()
    return bottom


def curves(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    words: Words,
    top: float,
) -> None:
    """Draws the arc through three points and the four arcs of one ellipse, and names
    both."""
    # The two figures bulge above and below the line they are drawn on, so both are
    # named over their own drawing rather than under it.
    base = top - 74.0

    label(content, handle, 8.5, LEFT, top - 12.0, words.through, 220.0)
    content.save_state()
    content.set_line_width(1.6)
    content.set_stroke(palette()[0])
    content.move_to(LEFT, base)
    content.arc_through(LEFT, base, LEFT + 100.0, base + 26.0, LEFT + 200.0, base)
    content.stroke()
    content.restore_state()

    # The same two ends, the same two radii, and the four arcs that answer them: the
    # long way round and the short, each walked either way.
    from_x = LEFT + 300.0
    to_x = LEFT + 400.0
    shapes = [
        hqf_pdf.EllipticalArc(),
        hqf_pdf.EllipticalArc(clockwise=True),
        hqf_pdf.EllipticalArc(large=True),
        hqf_pdf.EllipticalArc(large=True, clockwise=True),
    ]
    label(content, handle, 8.5, from_x, top - 12.0, words.four, 220.0)
    for shape, colour in zip(shapes, palette()):
        content.save_state()
        content.set_line_width(1.6)
        content.set_stroke(colour)
        content.move_to(from_x, base)
        content.elliptical_arc(from_x, base, to_x, base, 62.0, 18.0, shape)
        content.stroke()
        content.restore_state()


def build(words: Words, font: Path) -> bytes:
    """Draws the whole page."""
    doc = hqf_pdf.Document()
    doc.set_license(_licence.licensed())
    text = doc.add_font(hqf_pdf.Font.from_path(font))

    content = hqf_pdf.Content()
    top = 790.0
    top = block(content, text, 17.0, top, words.title) - 12.0
    top = block(content, text, 9.5, top, words.lead) - 22.0

    top = block(content, text, 11.0, top, words.pie) - 12.0
    pie(content, text, words, LEFT + 74.0, top - 74.0)
    top -= 152.0

    top = block(content, text, 11.0, top, words.gauge) - 12.0
    gauge(content, LEFT + 52.0, top - 50.0)
    label(content, text, 9.5, LEFT + 118.0, top - 46.0, words.gauge_label, 280.0)
    top -= 100.0

    top = block(content, text, 11.0, top, words.card) - 12.0
    top = card(content, text, words, top) - 24.0

    top = block(content, text, 11.0, top, words.arcs) - 12.0
    curves(content, text, words, top)
    top -= 118.0

    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    block(content, text, 8.5, top, words.caveat)

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    doc.add_page(page)
    return doc.to_bytes()


def main() -> None:
    """Writes the page."""
    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("shapes.pdf", language)).stem)

    data = build(words, _out.font_path())

    Path(out).parent.mkdir(parents=True, exist_ok=True)
    Path(out).write_bytes(data)
    print(f"wrote {out}: {len(data)} bytes")


if __name__ == "__main__":
    main()
