"""Draws a land plan whose layers decide for themselves when to be shown.

The Python twin of the `write_self_showing_layers` example in Rust. The plot and
the house it holds are the page's own ink and are always there. Three kinds of layer
sit on top and nobody has to touch reading software's panel for any of them: one wide label
stands over the plot while the plan is looked at whole, the fine figures come in once
the plan is enlarged twice over, and one layer per language carries the sentence under
the plan, so reading software set to French reads French.

The page is held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
drawn. The table of words is also what the language layers are built from: writing one
more set of words gives the document one more layer, without a line that draws being
touched. The distances are not words — a metre is a metre.

The terminal also says that a pair of magnifications none falls between, from the
crossover down to nothing, is refused.

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

from __future__ import annotations

import math
from dataclasses import dataclass
from pathlib import Path
from typing import Callable

import _language
import _licence
import _out

import hqf_pdf

# Where the page's text begins, in points from the left edge.
LEFT = 80.0

# The baseline of the first language's sentence under the plan.
CAPTION_TOP = 350.0

# How far each language's sentence stands under the one before it.
CAPTION_STEP = 15.0

# The plot: where its lower left corner sits on the page, and how big it is.
PLOT = (LEFT, 380.0, 435.0, 360.0)

# The house inside the plot, as an inset from the plot's lower left corner and a size of
# its own.
HOUSE = (60.0, 90.0, 200.0, 170.0)

# The magnification the wide label gives way to the fine figures at. The label is shown
# below it and the figures from it, so exactly one of the two stands at every
# magnification.
CROSSOVER = 2.0

# How many layers watch the magnification.
SCALED_COUNT = 2

# The distances the plan is measured at, each written inside the line it stands for.
# None of them is language: a metre is a metre.
PLOT_ALONG = "24.30"
PLOT_ACROSS = "17.60"
HOUSE_ALONG = "11.20"
HOUSE_ACROSS = "8.40"
SIDE_SETBACK = "3.35"
FOOT_SETBACK = "5.10"

# The two sides of the plot, written the way the wide label states them.
OVERALL = "24.30 m × 17.60 m"


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

    The name of the language and the sentence under the plan are the language layer's
    own: reading software's panel shows the one and the page shows the other, and both stay
    in their language whatever language the rest of the page is in. Everything else
    follows the page.
    """

    # The line at the head of the page.
    title: str
    # What stands under it, saying what the page is about.
    intro: str
    # What this language is called, which is the name its layer carries in reading
    # software's panel.
    tongue: str
    # The sentence this language's layer carries under the plan.
    caption: str
    # What the two layers that watch the magnification are called.
    scaled: tuple[str, str]
    # What the legend says each of them holds.
    holdings: tuple[str, str]
    # The wide label the plot carries while the plan is looked at whole.
    whole: str
    # What the legend says about the layers that carry the wording.
    wording: str
    # The lines under the legend saying how to see all of it.
    seeing: tuple[str, str]


# The page in English.
ENGLISH = Words(
    title="A plan that shows what can be read",
    intro=(
        "Every layer here decides for itself, by the magnification and by the "
        "language."
    ),
    tongue="English",
    caption="Plot 12, Mill Lane — sold with the barn and the well.",
    scaled=("Whole plot", "Fine figures"),
    holdings=(
        "stands over the plan while it is looked at whole",
        "comes in once the plan is enlarged twice over",
    ),
    whole="PLOT 12",
    wording="one layer a language, shown when reading software is set to it",
    seeing=(
        "Enlarge the plan past two hundred per cent: the figures on the walls come in "
        "as the",
        "wide label steps back. Set reading software to another language and the "
        "sentence",
        "follows.",
    ),
)

# The page in French.
FRENCH = Words(
    title="Un plan qui montre ce qui se lit",
    intro=("Ici, chaque calque décide seul de son affichage, selon l’agrandissement et"
           " la langue."),
    tongue="Français",
    caption="Parcelle 12, chemin du Moulin — vendue avec la grange et le puits.",
    scaled=("Parcelle entière", "Cotes fines"),
    holdings=(
        "s’affiche sur le plan tant qu’on le voit en entier",
        "apparaissent dès que le plan est agrandi deux fois",
    ),
    whole="PARCELLE 12",
    wording="un calque par langue, affiché quand le logiciel de lecture y est réglé",
    seeing=(
        (("Agrandissez le plan au-delà de deux cents pour cent : les cotes "
          "apparaissent et")),
        ("le grand titre s’efface. Réglez le logiciel de lecture sur une autre "
         "langue : la phrase"),
        "suit.",
    ),
)

# The page in Dutch.
DUTCH = Words(
    title="Een plattegrond die toont wat leesbaar is",
    intro=(
        "Elke laag beslist hier zelf, naar de vergroting en naar de taal."
    ),
    tongue="Nederlands",
    caption="Perceel 12, Mill Lane — verkocht met de schuur en de put.",
    scaled=("Heel perceel", "Fijne maten"),
    holdings=(
        "staat over de plattegrond zolang die geheel in beeld is",
        "komt tevoorschijn zodra de plattegrond twee keer vergroot is",
    ),
    whole="PERCEEL 12",
    wording="één laag per taal, getoond als de leessoftware op die taal staat",
    seeing=(
        "Vergroot de plattegrond voorbij 200 %: de maten op de muren komen "
        "tevoorschijn",
        "terwijl het brede label wijkt. Zet de leessoftware op een andere taal en de "
        "zin",
        "volgt.",
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    title="Uma planta que mostra o que se pode ler",
    intro=(
        "Cada camada decide aqui por si, pela ampliação e pela língua."
    ),
    tongue="Português",
    caption="Lote 12, Mill Lane — vendido com o celeiro e o poço.",
    scaled=("Lote inteiro", "Cotas finas"),
    holdings=(
        "fica sobre a planta enquanto é vista inteira",
        "aparece quando a planta é ampliada para o dobro",
    ),
    whole="LOTE 12",
    wording="uma camada por língua, visível quando o programa de leitura está nessa língua",
    seeing=(
        "Amplie a planta para lá dos 200 %: as cotas nas paredes aparecem enquanto",
        "a etiqueta larga se retira. Ponha o programa de leitura noutra língua e a "
        "frase",
        "acompanha.",
    ),
)

# The page in Italian.
ITALIAN = Words(
    title="Una pianta che mostra ciò che si può leggere",
    intro=(
        "Ogni livello decide qui da sé, secondo l’ingrandimento e secondo la lingua."
    ),
    tongue="Italiano",
    caption="Lotto 12, Mill Lane — venduto con il fienile e il pozzo.",
    scaled=("Lotto intero", "Quote fini"),
    holdings=(
        "sta sopra la pianta mentre la si guarda intera",
        "compare quando la pianta è ingrandita due volte",
    ),
    whole="LOTTO 12",
    wording="un livello per lingua, visibile quando il programma di lettura è impostato su di essa",
    seeing=(
        "Ingrandite la pianta oltre il duecento per cento: le quote sui muri "
        "compaiono mentre",
        "l’etichetta larga si ritira. Impostate il programma di lettura su un’altra "
        "lingua e la frase",
        "segue.",
    ),
)

# The page in Spanish.
SPANISH = Words(
    title="Un plano que muestra lo que se puede leer",
    intro=(
        "Cada capa decide aquí por sí misma, según el aumento y según el idioma."
    ),
    tongue="Español",
    caption="Parcela 12, Mill Lane — se vende con el granero y el pozo.",
    scaled=("Parcela entera", "Cotas finas"),
    holdings=(
        "está sobre el plano mientras se mira entero",
        "aparece cuando el plano se amplía al doble",
    ),
    whole="PARCELA 12",
    wording="una capa por idioma, visible cuando el programa de lectura está en ese idioma",
    seeing=(
        "Amplíe el plano por encima del 200 %: las cotas de los muros aparecen cuando",
        "la etiqueta ancha se retira. Ponga el programa de lectura en otro idioma y "
        "la frase",
        "le sigue.",
    ),
)

# The page in German.
GERMAN = Words(
    title="Ein Plan, der zeigt, was sich lesen lässt",
    intro=(
        "Jede Ebene hier entscheidet selbst, nach Vergrößerung und nach Sprache."
    ),
    tongue="Deutsch",
    caption="Flurstück 12, Mill Lane — mit Scheune und Brunnen verkauft.",
    scaled=("Ganzes Flurstück", "Feine Maße"),
    holdings=(
        "steht über dem Plan, solange er als Ganzes betrachtet wird",
        "erscheint, sobald der Plan auf das Doppelte vergrößert ist",
    ),
    whole="FLURSTÜCK 12",
    wording="eine Ebene je Sprache, gezeigt, wenn die Lesesoftware darauf eingestellt ist",
    seeing=(
        "Den Plan über 200 Prozent vergrößern: Die Maße an den Wänden erscheinen, und "
        "die",
        "breite Beschriftung tritt zurück. Die Lesesoftware auf eine andere Sprache "
        "stellen,",
        "und der Satz folgt.",
    ),
)


# Every language the example is written in. A language is added by writing its own set
# of words and naming it here, and the document gains its layer.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}


def house() -> tuple[float, float, float, float]:
    """The house, as a place and a size on the page."""
    x, y, _, _ = PLOT
    inset_x, inset_y, width, height = HOUSE
    return (x + inset_x, y + inset_y, width, height)


def ground(content: hqf_pdf.Content) -> None:
    """Draws the ink the plan always carries: the plot's boundary and the house
    standing in it."""
    x, y, width, height = PLOT
    hx, hy, hw, hh = house()
    content.save_state()
    content.set_stroke(hqf_pdf.Rgb.gray(0.2))
    content.set_line_width(1.6)
    content.rect(x, y, width, height)
    content.stroke()
    content.set_fill(hqf_pdf.Rgb(0.9, 0.91, 0.94))
    content.set_line_width(1.2)
    content.rect(hx, hy, hw, hh)
    content.fill()
    content.rect(hx, hy, hw, hh)
    content.stroke()
    content.restore_state()


def wide_label(
    content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
    """Draws what the plot is called and how big it is, in type that reads from across
    a desk."""
    x, y, width, height = PLOT
    middle = x + width / 2.0
    content.save_state()
    content.set_fill(hqf_pdf.Rgb(0.35, 0.42, 0.55))
    name = font.measure(words.whole, 26.0)
    content.draw_text(
        font, 26.0, middle - name / 2.0, y + height - 54.0, words.whole
    )
    sizes = font.measure(OVERALL, 12.0)
    content.draw_text(font, 12.0, middle - sizes / 2.0, y + height - 76.0, OVERALL)
    content.restore_state()


def fine_figures(content: hqf_pdf.Content, font: hqf_pdf.FontHandle) -> None:
    """Draws the distance each wall and each setback stands for, in type too small to
    read until the plan is enlarged."""
    x, y, width, height = PLOT
    hx, hy, hw, hh = house()
    figures = [
        (x + width / 2.0, y + 5.0, PLOT_ALONG),
        (x + 14.0, y + height / 2.0, PLOT_ACROSS),
        (hx + hw / 2.0, hy + 5.0, HOUSE_ALONG),
        (hx + 14.0, hy + hh / 2.0, HOUSE_ACROSS),
        ((x + hx) / 2.0, hy + hh / 2.0, SIDE_SETBACK),
        (hx + hw / 2.0, (y + hy) / 2.0, FOOT_SETBACK),
    ]
    content.save_state()
    content.set_fill(hqf_pdf.Rgb(0.2, 0.45, 0.35))
    for at, up, figure in figures:
        content.draw_text(
            font, 4.5, at - font.measure(figure, 4.5) / 2.0, up, figure
        )
    content.restore_state()


def legend(content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words) -> None:
    """Draws the legend under the plan: what each layer holds and when it stands."""
    # The legend starts under the last language's sentence, however many languages there
    # are.
    baseline = CAPTION_TOP - len(WORDS) * CAPTION_STEP - 10.0
    for name, what in zip(words.scaled, words.holdings):
        content.draw_text(font, 10.0, LEFT, baseline, name)
        content.save_state()
        content.set_fill(hqf_pdf.Rgb.gray(0.35))
        content.draw_text(font, 10.0, LEFT + 110.0, baseline, what)
        content.restore_state()
        baseline -= 18.0

    tongues = " · ".join(said.tongue for said in WORDS.values())
    content.draw_text(font, 10.0, LEFT, baseline, tongues)
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 10.0, LEFT, baseline - 14.0, words.wording)
    for rank, line in enumerate(words.seeing):
        content.draw_text(font, 10.0, LEFT, baseline - 46.0 - rank * 14.0, line)
    content.restore_state()


def on_layer(
    content: hqf_pdf.Content,
    layer: hqf_pdf.LayerHandle,
    body: Callable[[hqf_pdf.Content], None],
) -> None:
    """Draws `body` inside `layer`, so reading software shows and hides all of it at once."""
    content.begin_layer(layer)
    body(content)
    content.end_marked()


def build(language: str, words: Words) -> hqf_pdf.Document:
    """The document the example writes, in `words`."""
    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    wide = document.add_layer(hqf_pdf.Layer(words.scaled[0]).zoom(0.0, CROSSOVER))
    fine = document.add_layer(
        hqf_pdf.Layer(words.scaled[1]).zoom(CROSSOVER, math.inf)
    )
    # One layer a language, built from the same table the page is written from. The one
    # the page itself is in is the one reading software falls back on when is set to a
    # language no layer states.
    spoken = []
    for named, said in WORDS.items():
        layer = hqf_pdf.Layer(said.tongue)
        if named == language:
            layer = layer.preferred_language(named)
        else:
            layer = layer.language(named)
        spoken.append((document.add_layer(layer), said))

    content = hqf_pdf.Content()
    content.draw_text(font, 15.0, LEFT, 790.0, words.title)
    content.save_state()
    content.set_fill(hqf_pdf.Rgb.gray(0.35))
    content.draw_text(font, 10.0, LEFT, 770.0, words.intro)
    content.restore_state()

    ground(content)
    on_layer(content, wide, lambda target: wide_label(target, font, words))
    on_layer(content, fine, lambda target: fine_figures(target, font))
    # Every language's sentence stands on a line of its own, so reading software that
    # states no language of its own reads them one under the other rather than one over
    # the other.
    for rank, (layer, said) in enumerate(spoken):

        def sentence(
            target: hqf_pdf.Content, rank: int = rank, said: Words = said
        ) -> None:
            target.draw_text(font, 10.0, LEFT, CAPTION_TOP - rank * CAPTION_STEP, said.caption)

        on_layer(content, layer, sentence)
    legend(content, font, words)

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


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

    document = build(language, words)
    written = document.write(out)
    print(f"wrote {out}: {written} bytes, {SCALED_COUNT + len(WORDS)} layers")
    try:
        hqf_pdf.Document().add_layer(hqf_pdf.Layer("refused").zoom(CROSSOVER, 0.0))
        refused = False
    except hqf_pdf.PdfError:
        refused = True
    print(
        f"a layer shown from {CROSSOVER:.1f} and below 0.0 is refused: "
        f"{'yes' if refused else 'no'}"
    )


if __name__ == "__main__":
    main()
