"""Lays a day's meals out in a table whose dish column is narrower than some of the
dishes named in it, three times: at the top as the names come, then cut to the column,
then in rows of a height of their own that set a dish too wide smaller.

The Python twin of the `write_cell_overflow` example in Rust.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set
the page is set in. A dish is not only a word here: two of them are single compounds
wider than the column they are put in, which is the whole of what the two tables differ
over, so every language has to name its meals with compounds of its own.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# When a meal is taken, and what it comes to in kilocalories. What is on the plate is a
# word, and stands in `Words` beside them.
MEALS = [
    ("07:30", 420),
    ("10:00", 210),
    ("12:30", 540),
    ("16:00", 180),
    ("19:30", 610),
    ("21:00", 0),
]

# What the energies are counted in, which is the same wherever the page is read.
UNIT = "kcal"


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

    # The heading over each of the three tables.
    headings: tuple[str, str, str]
    # What the time column and the dish column are called. The third heading is the unit
    # the energies are counted in.
    columns: tuple[str, str]
    # What is on the plate at each meal of `MEALS`. A compound name is one word to a
    # line breaker, however many words it reads as, and two of these are compounds wider
    # than the column they are put in.
    dishes: tuple[str, str, str, str, str, str]
    # What the boxed-off row at the foot of the table is called.
    total: str
    # The notes under each table.
    notes: tuple[str, str, str]


# The page in English.
ENGLISH = Words(
    headings=(
        "Overflow: a dish wider than its column runs past the edge, over the "
        "kcal beside it.",
        "Break: the same dish is cut after its last glyph that fits, and "
        "carries on the next line.",
        "Shrink: each row keeps its 17 points, and a dish too wide is set "
        "smaller until it fits.",
    ),
    columns=("Time", "Dish"),
    dishes=(
        "Wholemeal-sourdough-with-almond-butter",
        "Greek yoghurt, walnuts",
        "Cauliflower-and-broccoli-gratin-with-hazelnuts",
        "Apple, oat biscuits",
        "Pan-fried-mackerel-with-buckwheat",
        "Camomile infusion",
    ),
    total="Total",
    notes=(
        "Drink 1.5–2 litres of water over the day, between the meals rather "
        "than with them.",
        "Swap any dish for another of ≈ the same energy.",
        "Weigh the portions dry, not cooked.",
    ),
)

# The page in French.
FRENCH = Words(
    headings=(
        ("Débordement : un plat plus large que sa colonne en dépasse le bord et "
         "empiète sur la colonne des kcal."),
        ("Coupure : le même plat s’interrompt après le dernier caractère qui tient et "
         "continue à la ligne suivante."),
        ("Réduction : chaque ligne garde ses 17 points de haut, et un plat trop large "
         "passe à un corps plus petit."),
    ),
    columns=("Heure", "Plat"),
    dishes=(
        "Pain-complet-au-levain-et-purée-d’amande",
        "Yaourt grec, noix",
        "Gratin-de-chou-fleur-et-brocolis-aux-noisettes",
        "Pomme, biscuits à l’avoine",
        "Maquereau-poêlé-au-sarrasin",
        "Infusion de camomille",
    ),
    total="Total",
    notes=(
        ("Buvez de 1,5 à 2 litres d’eau au cours de la journée, entre les repas plutôt"
         " que pendant."),
        "Remplacez un plat par un autre d’environ autant de kcal.",
        "Pesez les portions crues, pas cuites.",
    ),
)

# The page in Dutch.
DUTCH = Words(
    headings=(
        "Overlopen: een gerecht breder dan zijn kolom loopt over de rand, over de "
        "kcal ernaast.",
        "Afbreken: hetzelfde gerecht wordt na zijn laatste passende glyph afgebroken "
        "en loopt door.",
        "Verkleinen: elke rij houdt zijn 17 punten, en een te breed gerecht krimpt "
        "tot het past.",
    ),
    columns=("Tijd", "Gerecht"),
    dishes=(
        "Volkoren-zuurdesembrood-met-amandelboter",
        "Griekse yoghurt, walnoten",
        "Bloemkool-broccoligratin-met-hazelnoten",
        "Appel, havermoutkoekjes",
        "Gebakken-makreel-met-boekweit",
        "Kamillethee",
    ),
    total="Totaal",
    notes=(
        "Drink 1,5 tot 2 liter water per dag, tussen de maaltijden en niet erbij.",
        "Ruil een gerecht voor een ander met ongeveer dezelfde energie.",
        "Weeg de porties rauw, niet gekookt.",
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    headings=(
        "Transbordar: um prato mais largo que a coluna sai da margem, por cima das "
        "kcal ao lado.",
        "Quebrar: o mesmo prato é cortado após o último glifo que cabe e continua na "
        "linha seguinte.",
        "Reduzir: cada linha mantém os seus 17 pontos, e um prato largo demais "
        "encolhe até caber.",
    ),
    columns=("Hora", "Prato"),
    dishes=(
        "Pão-integral-de-massa-mãe-com-manteiga-de-amêndoa",
        "Iogurte grego, nozes",
        "Gratinado-de-couve-flor-e-brócolos-com-avelãs",
        "Maçã, bolachas de aveia",
        "Cavala-na-frigideira-com-trigo-sarraceno",
        "Infusão de camomila",
    ),
    total="Total",
    notes=(
        "Beba 1,5 a 2 litros de água por dia, entre as refeições e não durante.",
        "Troque um prato por outro com ≈ a mesma energia.",
        "Pese as porções cruas, não cozinhadas.",
    ),
)

# The page in Italian.
ITALIAN = Words(
    headings=(
        "Sborda: un piatto più largo della colonna esce dal bordo, sopra le kcal "
        "accanto.",
        "Spezza: lo stesso piatto è tagliato dopo l’ultimo glifo che ci sta, e "
        "continua sotto.",
        "Riduci: ogni riga tiene i suoi 17 punti, e un piatto troppo largo si "
        "rimpicciolisce fino a starci.",
    ),
    columns=("Ora", "Piatto"),
    dishes=(
        "Pane-integrale-a-lievito-madre-con-burro-di-mandorle",
        "Yogurt greco, noci",
        "Gratin-di-cavolfiore-e-broccoli-con-nocciole",
        "Mela, biscotti d’avena",
        "Sgombro-in-padella-con-grano-saraceno",
        "Infuso di camomilla",
    ),
    total="Totale",
    notes=(
        "Bere da 1,5 a 2 litri d’acqua al giorno, tra i pasti piuttosto che durante.",
        "Sostituire un piatto con un altro di energia ≈ uguale.",
        "Pesare le porzioni da crude, non da cotte.",
    ),
)

# The page in Spanish.
SPANISH = Words(
    headings=(
        "Desbordar: un plato más ancho que su columna sale del borde, sobre las kcal "
        "de al lado.",
        "Cortar: el mismo plato se corta tras el último glifo que cabe y sigue en la "
        "línea siguiente.",
        "Reducir: cada fila guarda sus 17 puntos, y un plato demasiado ancho se "
        "reduce hasta caber.",
    ),
    columns=("Hora", "Plato"),
    dishes=(
        "Pan-integral-de-masa-madre-con-crema-de-almendra",
        "Yogur griego, nueces",
        "Gratinado-de-coliflor-y-brócoli-con-avellanas",
        "Manzana, galletas de avena",
        "Caballa-a-la-sartén-con-trigo-sarraceno",
        "Infusión de manzanilla",
    ),
    total="Total",
    notes=(
        "Beba de 1,5 a 2 litros de agua al día, entre las comidas y no durante ellas.",
        "Cambie un plato por otro de ≈ la misma energía.",
        "Pese las raciones en crudo, no cocinadas.",
    ),
)

# The page in German.
GERMAN = Words(
    headings=(
        "Überlauf: Ein Gericht, breiter als seine Spalte, läuft über den Rand, über "
        "die kcal daneben.",
        "Umbruch: Dasselbe Gericht wird nach seinem letzten passenden Glyphen "
        "getrennt und läuft weiter.",
        "Verkleinern: Jede Zeile behält ihre 17 Punkt, ein zu breites Gericht wird "
        "kleiner gesetzt.",
    ),
    columns=("Uhrzeit", "Gericht"),
    dishes=(
        "Vollkorn-Sauerteigbrot-mit-Mandelmus",
        "Griechischer Joghurt, Walnüsse",
        "Blumenkohl-Brokkoli-Gratin-mit-Haselnüssen",
        "Apfel, Haferkekse",
        "Gebratene-Makrele-mit-Buchweizen",
        "Kamillentee",
    ),
    total="Summe",
    notes=(
        "Über den Tag 1,5–2 Liter Wasser trinken, lieber zwischen den Mahlzeiten als "
        "dazu.",
        "Jedes Gericht lässt sich gegen ein anderes mit ≈ derselben Energie tauschen.",
        "Die Portionen roh wiegen, nicht gekocht.",
    ),
)

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

# The left edge of both tables, the width the plan is fitted across -- a card, not a
# page -- and the two columns that do not take what is left.
X = 80.0
TABLE_WIDTH = 250.0
TIME_WIDTH = 44.0
ENERGY_WIDTH = 46.0
# How far a note is set from the bullet that introduces it.
BULLET_INDENT = 10.0
# The size everything in the table is set at.
BODY = 8.5
# The height of a meal's row held to a height of its own -- one line at `BODY` and its
# padding fit in it, two do not -- and the smallest size a dish is set down to there.
MEAL_HEIGHT = 17.0
SMALLEST = 5.0


def plan(
    font: hqf_pdf.FontHandle, words: Words, overlong: hqf_pdf.Overlong, held: bool
) -> hqf_pdf.Table:
    """The plan, laid out with the given answer to a dish too wide for its column, its
    meals' rows given a height of their own when `held` says so."""
    # The dish takes whatever the time and the energy leave it.
    columns = hqf_pdf.Columns(
        [
            hqf_pdf.ColumnWidth.points(TIME_WIDTH),
            hqf_pdf.ColumnWidth.fraction(1.0),
            hqf_pdf.ColumnWidth.points(ENERGY_WIDTH),
        ],
        TABLE_WIDTH,
    )

    table = hqf_pdf.Table(columns)
    table.header(1)
    table.rule(hqf_pdf.Rule.frame(), hqf_pdf.Stroke(0.8))
    hairline = hqf_pdf.Stroke(0.25, color=hqf_pdf.Rgb.gray(0.75))
    table.rule(hqf_pdf.Rule.horizontal_other(), hairline)
    table.rule(hqf_pdf.Rule.vertical_other(), hairline)
    table.rule(hqf_pdf.Rule.horizontal(1), hqf_pdf.Stroke(0.8))

    pad = hqf_pdf.Padding.symmetric(4.0, 3.0)
    shade = hqf_pdf.Rgb.gray(0.88)

    def heading(label: str, align: hqf_pdf.Align = hqf_pdf.Align.Left) -> hqf_pdf.Cell:
        return hqf_pdf.Cell(
            font,
            BODY,
            label,
            padding=pad,
            fill=shade,
            align=align,
            valign=hqf_pdf.VAlign.Middle,
        )

    table.push(
        hqf_pdf.Row(
            cells=[
                heading(words.columns[0]),
                heading(words.columns[1]),
                heading(UNIT, hqf_pdf.Align.Right),
            ],
            min_height=18.0,
        )
    )

    for (time, energy), dish in zip(MEALS, words.dishes):
        table.push(
            hqf_pdf.Row(
                cells=[
                    hqf_pdf.Cell(font, BODY, time, padding=pad),
                    hqf_pdf.Cell(font, BODY, dish, padding=pad, overlong=overlong),
                    hqf_pdf.Cell(
                        font,
                        BODY,
                        str(energy),
                        padding=pad,
                        align=hqf_pdf.Align.Right,
                    ),
                ],
                fixed_height=(MEAL_HEIGHT, SMALLEST) if held else None,
            )
        )

    # The day's energy, boxed off the grid so that it reads as a total rather than as
    # one more meal. The label carries the box's left edge and the figure its right, so
    # the two cells trace one box between them.
    rule = hqf_pdf.Stroke(0.8)
    box_margin = hqf_pdf.Margin.symmetric(0.0, 2.0)
    total = sum(energy for _, energy in MEALS)
    table.push(
        hqf_pdf.Row(
            cells=[
                hqf_pdf.Cell(
                    font,
                    BODY,
                    words.total,
                    span=2,
                    align=hqf_pdf.Align.Right,
                    padding=pad,
                    margin=box_margin,
                    border=hqf_pdf.Border(top=rule, bottom=rule, left=rule),
                ),
                hqf_pdf.Cell(
                    font,
                    BODY,
                    str(total),
                    align=hqf_pdf.Align.Right,
                    padding=pad,
                    margin=box_margin,
                    border=hqf_pdf.Border(top=rule, bottom=rule, right=rule),
                ),
            ],
            min_height=16.0,
        )
    )

    return table


def draw_notes(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words, top: float
) -> None:
    """Draws the notes under the plan as a bullet list, the first baseline one leading
    below `top`.

    The bullet sits at the left edge and the note is flowed beside it, so a note long
    enough to run on lines up under itself rather than under its bullet.
    """
    flow = hqf_pdf.TextFlow(font, 7.5, leading=10.0)
    bullet = flow.break_lines("•", BULLET_INDENT)
    width = TABLE_WIDTH - BULLET_INDENT

    y = top
    for note in words.notes:
        lines = flow.break_lines(note, width)
        flow.draw(content, bullet, X, y, BULLET_INDENT)
        flow.draw(content, lines, X + BULLET_INDENT, y, width)
        y -= flow.height(lines)


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

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    content = hqf_pdf.Content()

    for (top, overlong, held), label in zip(
        (
            (790.0, hqf_pdf.Overlong.Overflow, False),
            (540.0, hqf_pdf.Overlong.Break, False),
            (290.0, hqf_pdf.Overlong.Break, True),
        ),
        words.headings,
    ):
        heading = hqf_pdf.TextFlow(font, 8.0)
        lines = heading.break_lines(label, 430.0)
        heading.draw(content, lines, X, top, 430.0)

        table_top = top - 30.0
        placed = plan(font, words, overlong, held).fit(X, table_top, 220.0, 0)
        placed.draw(content)

        # The notes follow the table, which is taller when it cuts.
        draw_notes(content, font, words, table_top - placed.height - 12.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()
