"""Pours one long passage into three columns, flowing from the foot of each to
the head of the next.

The Python twin of the `write_columns` example in Rust. The passage is broken
once to the column width, then set column by column: each column fits as many of
the lines as its height allows, draws that slice with `Lines.sub`, and the next
column takes up where it stopped. The passage carries a form feed the flow reads
as a box break: the second column ends on the line before it, room to spare, and
the paragraph after it opens the third. A thin rule outlines each column so the
text can be seen reaching its foot and carrying on. A line of its own runs up the
left margin, the block turned a quarter about its corner.

Under each column a caption says how many lines it took and why it took no more, how
much of the text they carry and how wide the widest of them runs. A second passage is
poured the same way by a flow that lets a column take seven lines at most and keeps
three lines of a paragraph together either side of a cut, so that a column may stop at
its limit or hold back lines it had room for; a marker planted in the text says which
column and line the word after it went into.

The passage is held in `Words`, once per language, and `HQF_PDF_LANG` picks which one
is poured. The measure, the gap and the height are not language: they are the same
three columns whoever reads the page.

The terminal says after which lines of the first passage its box breaks fall, read off
each line.

Usage: python examples/write_columns.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_columns.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 line that runs up the left margin.
    margin: str
    # The passage poured through the columns, a form feed sending its second paragraph
    # to the third.
    passage: str
    # The passage poured through the second row of columns, a `MARKER` planted before
    # one of its words.
    second: str
    # How many lines a column took and why it took no more; `{taken}` and `{why}` stand
    # for them.
    took: str
    # Why a column took no more lines, one reason per kind, in the order `REASONS` lists
    # them.
    reasons: tuple[str, str, str, str, str]
    # How much of the text a column carries and how wide its widest line runs;
    # `{characters}` and `{widest}` stand for them.
    carries: str
    # How many lines a column that held some back had room for; `{room}` stands for
    # them.
    room: str
    # Where the marked word went; `{column}` and `{line}` stand for them.
    marked: str


# The page in English.
ENGLISH = Words(
    margin="Poured into three columns",
    passage=(
        "A flow too long for one column is set column by column. Each column takes as "
        "many of the broken lines as its height allows, draws them, and hands the rest "
        "to the column beside it, which takes up exactly where the last one stopped. "
        "The words are broken once, to the width one column has, so every column is "
        "set to the same measure.\x0cA column break written in the text ends the "
        "second column early, and this paragraph opens the third. This is what sets a "
        "newspaper's text, a report's two columns, or the narrow measure a wide page "
        "reads best in."
    ),
    second=(
        "This flow lets a column take seven lines at most, and keeps at least three "
        "lines of a paragraph on either side of a cut. A column may therefore stop "
        "short of its foot: at its limit of lines, or where a cut would strand fewer "
        "than three lines of a paragraph, which then waits for the next column whole. "
        "The caption under each column says which it was.\nA §marker planted in the "
        "text draws nothing and takes no room; it says which column and which line the "
        "word after it went into, as the line under the columns reports."
    ),
    took="{taken} lines: {why}.",
    reasons=(
        "the text ends here",
        "the column is full",
        "its limit of lines",
        "a column break",
        "lines held back",
    ),
    carries="{characters} characters, widest line {widest} pt",
    room="It had room for {room} lines.",
    marked="The marker: column {column}, line {line}.",
)

# The page in French.
FRENCH = Words(
    margin="Réparti sur trois colonnes",
    passage=(
        "Un texte trop long pour une seule colonne se compose colonne par colonne. "
        "Chaque colonne prend autant de lignes coupées que sa hauteur le permet, les "
        "place, et passe le reste à sa voisine, qui reprend exactement là où la "
        "précédente s’est arrêtée. Les mots ne sont coupés qu’une fois, à la largeur "
        "d’une colonne : toutes sont donc composées à la même justification.\x0cUn "
        "saut de colonne écrit dans le texte termine la deuxième colonne plus tôt, et "
        "ce paragraphe ouvre la troisième. C’est ainsi que se composent le texte d’un "
        "journal, les deux colonnes d’un rapport ou la colonne étroite d’une page "
        "large."
    ),
    second=(
        "Ce texte limite une colonne à sept lignes et garde au moins trois lignes "
        "d’un paragraphe de chaque côté d’une coupure. Une colonne peut donc s’arrêter "
        "avant le bas : à sa limite de lignes, ou là où une coupure isolerait moins "
        "de trois lignes d’un paragraphe, qui passe alors entier à la colonne suivante. "
        "Sa légende dit pourquoi.\nUn §repère placé dans le texte ne dessine rien et "
        "ne prend pas de place ; il dit dans quelle colonne et à quelle ligne est allé "
        "le mot qui le suit, comme l’indique la ligne sous les colonnes."
    ),
    took="{taken} lignes : {why}.",
    reasons=(
        "fin du texte",
        "colonne pleine",
        "limite de lignes",
        "saut de colonne",
        "lignes reportées",
    ),
    carries="{characters} caractères, lignes d’au plus {widest} pt",
    room="Elle avait la place pour {room} lignes.",
    marked="Le repère : colonne {column}, ligne {line}.",
)

# The page in Dutch.
DUTCH = Words(
    margin="Gegoten in drie kolommen",
    passage=(
        "Een tekst die te lang is voor één kolom wordt kolom voor kolom gezet. Elke "
        "kolom neemt zoveel regels als haar hoogte toelaat, tekent ze en geeft de "
        "rest door aan de kolom ernaast, die precies verdergaat waar de vorige "
        "stopte. De woorden worden één keer afgebroken, op de breedte van één kolom, "
        "dus elke kolom heeft dezelfde zetbreedte.\fEen kolomeinde in de tekst "
        "beëindigt de tweede kolom vroeger, en deze alinea opent de derde. Zo worden "
        "een krant, een rapport in twee kolommen of een smalle zetbreedte gezet."
    ),
    second=(
        "Hier neemt een kolom hoogstens zeven regels en houdt ze minstens drie regels "
        "van een alinea aan elke kant van een breuk. Een kolom kan dus boven haar "
        "voet stoppen: bij haar limiet, of waar een breuk minder dan drie regels zou "
        "achterlaten. De regel onder elke kolom zegt waarom.\nEen §-markering in de "
        "tekst tekent niets en neemt geen ruimte in; ze zegt in welke kolom en regel "
        "het volgende woord terechtkwam."
    ),
    took="{taken} regels: {why}.",
    reasons=(
        "de tekst eindigt hier",
        "de kolom is vol",
        "haar limiet aan regels",
        "een kolomeinde",
        "regels vastgehouden",
    ),
    carries="{characters} tekens, breedste regel {widest} pt",
    room="Er was plaats voor {room} regels.",
    marked="De markering: kolom {column}, regel {line}.",
)

# The page in Portuguese.
PORTUGUESE = Words(
    margin="Vertido em três colunas",
    passage=(
        "Um texto longo demais para uma coluna compõe-se coluna a coluna. Cada uma "
        "leva tantas linhas quantas a altura permite, desenha-as e passa o resto à do "
        "lado, que retoma exatamente onde a outra parou. As palavras quebram-se uma "
        "vez, à largura de uma coluna, por isso todas têm a mesma medida.\fUma quebra "
        "de coluna escrita no texto acaba mais cedo a segunda coluna, e este "
        "parágrafo abre a terceira. Assim se compõe um jornal, um relatório a duas "
        "colunas ou uma medida estreita."
    ),
    second=(
        "Aqui uma coluna leva no máximo sete linhas e deixa pelo menos três linhas de "
        "um parágrafo de cada lado de um corte. Uma coluna pode por isso acabar antes "
        "do pé: no limite de linhas, ou onde um corte deixaria menos de três linhas. "
        "A linha sob cada coluna diz porquê.\nUma §marca no texto não desenha nada "
        "nem ocupa espaço; diz em que coluna e linha foi a palavra seguinte."
    ),
    took="{taken} linhas: {why}.",
    reasons=(
        "o texto acaba aqui",
        "a coluna está cheia",
        "o seu limite de linhas",
        "uma quebra de coluna",
        "linhas retidas",
    ),
    carries="{characters} caracteres, linha mais larga {widest} pt",
    room="Tinha lugar para {room} linhas.",
    marked="A marca: coluna {column}, linha {line}.",
)

# The page in Italian.
ITALIAN = Words(
    margin="Versato in tre colonne",
    passage=(
        "Un testo troppo lungo per una colonna si compone colonna per colonna. Ognuna "
        "prende quante righe consente la sua altezza, le disegna e passa il resto a "
        "quella accanto, che riprende esattamente dove l’altra si è fermata. Le "
        "parole si spezzano una volta, alla larghezza di una colonna, così tutte "
        "hanno la stessa giustezza.\fUn salto di colonna scritto nel testo chiude "
        "prima la seconda colonna, e questo paragrafo apre la terza. Così si compone "
        "un giornale, un rapporto a due colonne o una giustezza stretta."
    ),
    second=(
        "Qui una colonna prende al massimo sette righe e lascia almeno tre righe di "
        "un paragrafo da ogni lato di un taglio. Una colonna può quindi finire prima "
        "del piede: al limite di righe, o dove un taglio lascerebbe meno di tre "
        "righe. La riga sotto ogni colonna dice perché.\nUn §segno nel testo non "
        "disegna nulla e non occupa spazio; dice in quale colonna e riga è finita la "
        "parola seguente."
    ),
    took="{taken} righe: {why}.",
    reasons=(
        "il testo finisce qui",
        "la colonna è piena",
        "il suo limite di righe",
        "un salto di colonna",
        "righe trattenute",
    ),
    carries="{characters} caratteri, riga più larga {widest} pt",
    room="Aveva posto per {room} righe.",
    marked="Il segno: colonna {column}, riga {line}.",
)

# The page in Spanish.
SPANISH = Words(
    margin="Vertido en tres columnas",
    passage=(
        "Un texto demasiado largo para una columna se compone columna a columna. Cada "
        "una toma tantas líneas como permite su altura, las dibuja y pasa el resto a "
        "la de al lado, que sigue justo donde paró la anterior. El texto se corta en "
        "líneas una sola vez, al ancho de una columna, así que todas tienen la misma "
        "medida.\fUn salto de columna escrito en el texto termina antes la segunda "
        "columna, y este párrafo abre la tercera. Así se compone un periódico, un "
        "informe a dos columnas o una medida estrecha."
    ),
    second=(
        "Aquí una columna lleva siete líneas como mucho y deja al menos tres líneas "
        "de un párrafo a cada lado de un corte. Por eso una columna puede acabar "
        "antes de su pie: en su límite de líneas, o donde un corte dejaría menos de "
        "tres líneas. La línea bajo cada columna dice por qué.\nUna §marca en el "
        "texto no dibuja nada ni ocupa sitio; dice en qué columna y línea cayó la "
        "palabra siguiente."
    ),
    took="{taken} líneas: {why}.",
    reasons=(
        "el texto acaba aquí",
        "la columna está llena",
        "su límite de líneas",
        "un salto de columna",
        "líneas retenidas",
    ),
    carries="{characters} caracteres, línea más ancha {widest} pt",
    room="Tenía sitio para {room} líneas.",
    marked="La marca: columna {column}, línea {line}.",
)

# The page in German.
GERMAN = Words(
    margin="In drei Spalten gegossen",
    passage=(
        "Ein Text, der für eine Spalte zu lang ist, wird Spalte für Spalte gesetzt. "
        "Jede Spalte nimmt so viele Zeilen, wie ihre Höhe erlaubt, und gibt den Rest "
        "an die nächste weiter, die genau dort weitermacht. Die Wörter werden einmal "
        "auf die Breite einer Spalte umbrochen, also haben alle Spalten dieselbe "
        "Satzbreite.\fEin Spaltenumbruch im Text beendet die zweite Spalte früher, "
        "und dieser Absatz beginnt die dritte. So setzt man eine Zeitung, einen "
        "zweispaltigen Bericht oder eine schmale Satzbreite."
    ),
    second=(
        "Hier nimmt eine Spalte höchstens sieben Zeilen und lässt mindestens drei "
        "Zeilen eines Absatzes auf jeder Seite eines Umbruchs. Eine Spalte kann also "
        "vor ihrem Fuß enden: an ihrer Grenze oder wo ein Umbruch weniger als drei "
        "Zeilen zurückließe. Die Zeile unter jeder Spalte nennt den Grund.\nEine "
        "§Marke im Text zeichnet nichts und nimmt keinen Platz ein; sie sagt, in "
        "welche Spalte und Zeile das nächste Wort kam."
    ),
    took="{taken} Zeilen: {why}.",
    reasons=(
        "der Text endet hier",
        "die Spalte ist voll",
        "ihre Zeilengrenze",
        "ein Spaltenumbruch",
        "zurückgehaltene Zeilen",
    ),
    carries="{characters} Zeichen, breiteste Zeile {widest} pt",
    room="Sie hatte Platz für {room} Zeilen.",
    marked="Die Marke: Spalte {column}, Zeile {line}.",
)

# 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 the first column.
LEFT = 70.0
# The top edge every column shares.
TOP = 720.0
# The width of one column.
COLUMN = 145.0
# The gap between one column and the next.
GAP = 25.0
# How far down from TOP a column reaches.
HEIGHT = 130.0
# How many columns the passage is poured through.
COLUMNS = 3
# How far from the page's left edge the line up the margin is turned about.
MARGIN_X = 44.0
# The top edge every column of the second row shares.
SECOND_TOP = 500.0
# The size the captions under the columns are set at, in points.
CAPTION = 7.0
# The character the second passage plants before the word it marks.
MARKER = "\u00a7"

# The reasons a column stops taking lines, in the order `Words.reasons` names them.
REASONS = (
    hqf_pdf.BoxStop.TextEnd,
    hqf_pdf.BoxStop.Full,
    hqf_pdf.BoxStop.LineLimit,
    hqf_pdf.BoxStop.BoxBreak,
    hqf_pdf.BoxStop.HeldBack,
)


def caption(flow, lines, placed, text, words) -> list[str]:
    """What the column `placed` took of `lines`, laid out from `text` by `flow`, said
    in a line or two for the caption under it."""
    said = [
        words.took.replace("{taken}", str(placed.taken)).replace(
            "{why}", words.reasons[REASONS.index(placed.stop)]
        )
    ]
    first, end = placed.lines
    widest = max(
        (lines.natural_width(index) for index in range(first, end)), default=0.0
    )
    start, stop = placed.source
    carried = text[start:stop]
    said.append(
        words.carries.replace("{characters}", str(len(carried))).replace(
            "{widest}", f"{widest:.0f}"
        )
    )
    if placed.stop == hqf_pdf.BoxStop.HeldBack:
        room = flow.fit_regardless(lines, HEIGHT, first)
        said.append(words.room.replace("{room}", str(room)))
    return said


def set_caption(content, handle, x, foot, lines) -> None:
    """Set the caption `lines` under a column whose left edge is `x` and whose foot is
    `foot`."""
    baseline = foot - 12.0
    for line in lines:
        content.draw_text(handle, CAPTION, x, baseline, line)
        baseline -= 10.0


def pour(content, handle, flow, text, top, words) -> list[tuple[int, int]]:
    """Pour `text`, broken by `flow` to the column's measure, through a row of columns
    whose top edge is `top`, a caption under each; hand back where each marker the text
    carries went, as a column and a line of it, counted from one."""
    lines = flow.break_lines(text, COLUMN)
    start = 0
    x = LEFT
    marked = []
    for column in range(COLUMNS):
        placed = flow.fit_box(lines, HEIGHT, start)
        flow.draw(content, lines.sub(start, placed.next), x, top, COLUMN)

        content.save_state()
        content.set_stroke(hqf_pdf.Rgb.gray(0.8))
        content.set_line_width(0.5)
        content.rect(x, top - HEIGHT, COLUMN, HEIGHT)
        content.stroke()
        content.restore_state()

        said = caption(flow, lines, placed, text, words)
        set_caption(content, handle, x, top - HEIGHT, said)

        first, end = placed.lines
        for mark in placed.marks:
            line = next(
                (
                    index - first
                    for index in range(first, end)
                    if mark in lines.marks(index)
                ),
                0,
            )
            marked.append((column + 1, line + 1))

        start = placed.next
        x += COLUMN + GAP
    return marked


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

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

    # A paragraph cut between two columns leaves at least two lines at the foot of the
    # first and carries at least two to the top of the next.
    flow = hqf_pdf.TextFlow(
        handle,
        10.0,
        leading=14.0,
        align=hqf_pdf.Align.Justify,
        orphans=2,
        widows=2,
        box_break="\f",
    )
    content = hqf_pdf.Content()
    pour(content, handle, flow, words.passage, TOP, words)

    limited = hqf_pdf.TextFlow(
        handle,
        10.0,
        leading=14.0,
        align=hqf_pdf.Align.Justify,
        max_lines=7,
        orphans=3,
        widows=3,
        marker=MARKER,
    )
    marked = pour(content, handle, limited, words.second, SECOND_TOP, words)
    for column, line in marked:
        said = words.marked.replace("{column}", str(column)).replace(
            "{line}", str(line)
        )
        set_caption(content, handle, LEFT, SECOND_TOP - HEIGHT - 40.0, [said])

    # Up the left margin, reading from the foot of the columns: the block is laid out
    # upright, the height of a column wide, and turned a quarter.
    note = hqf_pdf.TextFlow(handle, 9.0, turned=math.pi / 2)
    margin = note.break_lines(words.margin, HEIGHT)
    note.draw_with_turn(content, margin, MARGIN_X, TOP - HEIGHT, HEIGHT)

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

    written = document.write(out)
    print(f"wrote {out}: {written} bytes")
    broken = flow.break_lines(words.passage, COLUMN)
    box_ends = [index + 1 for index in range(len(broken)) if broken.ends_box(index)]
    print(f"the first passage's box breaks fall after lines {box_ends}")


if __name__ == "__main__":
    main()
