# SPDX-License-Identifier: Apache-2.0
"""Generate original A4 Foundation mathematics pages with selectable PDF text."""

from __future__ import annotations

import subprocess
from pathlib import Path
from xml.sax.saxutils import escape

HERE = Path(__file__).resolve().parent
INK = "#193746"
BLUE = "#eaf4f8"
GREEN = "#eef7ee"
PEACH = "#fff1e8"
DIGIT_SPLIT = 10
GIVEN_PATTERN_CARDS = 6


def rect(x: int, y: int, w: int, h: int, fill: str = "white", **attrs: str) -> str:
    """Draw an outlined region with optional countable source metadata."""
    other = " ".join(
        f'{key.replace("_", "-")}="{escape(value)}"' for key, value in attrs.items()
    )
    return (
        f'<rect x="{x}" y="{y}" width="{w}" height="{h}" '
        f'fill="{fill}" stroke="{INK}" stroke-width="2" {other}/>'
    )


def word(x: int, y: int, value: str, size: int = 19, bold: bool = False) -> str:
    """Draw selectable text."""
    return (
        f'<text x="{x}" y="{y}" fill="{INK}" font-family="DejaVu Sans" '
        f'font-size="{size}" font-weight="{700 if bold else 400}">'
        f"{escape(value)}</text>"
    )


def line(x1: int, y1: int, x2: int, y2: int, width: int = 3) -> str:
    """Draw a dark straight line."""
    return (
        f'<path d="M{x1} {y1} L{x2} {y2}" fill="none" '
        f'stroke="{INK}" stroke-width="{width}"/>'
    )


def circle(cx: int, cy: int, r: int, **attrs: str) -> str:
    """Draw an outlined circle source or familiar shape."""
    other = " ".join(
        f'{key.replace("_", "-")}="{escape(value)}"' for key, value in attrs.items()
    )
    return (
        f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="white" '
        f'stroke="{INK}" stroke-width="3" {other}/>'
    )


def triangle(x: int, y: int, side: int = 46) -> str:
    """Draw a three-edge familiar outline."""
    return (
        f'<path d="M{x} {y + side} L{x + side // 2} {y} '
        f'L{x + side} {y + side} Z" fill="white" '
        f'stroke="{INK}" stroke-width="3"/>'
    )


def icon(x: int, y: int, kind: str) -> str:
    """Draw an original simple pictogram and its word label."""
    if kind == "PINE":
        picture = triangle(x + 18, y + 12, 42) + line(x + 39, y + 54, x + 39, y + 68, 4)
    elif kind == "SHELL":
        picture = (
            f'<path d="M{x + 10} {y + 62} Q{x + 39} {y + 5} '
            f'{x + 68} {y + 62} Z" fill="white" '
            f'stroke="{INK}" stroke-width="3"/>'
        )
        picture += line(x + 39, y + 28, x + 39, y + 61, 2)
    else:
        raise ValueError(kind)
    return (
        f'<g data-icon-kind="{kind}">{picture}{word(x + 8, y + 94, kind, 14, True)}</g>'
    )


def page(title: str, desc: str, body: str) -> str:
    """Wrap a one-page physical A4 SVG with machine-readable metadata."""
    return (
        '<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" '
        'viewBox="0 0 794 1123" role="img">'
        f"<title>{escape(title)}</title><desc>{escape(desc)}</desc>"
        '<rect width="794" height="1123" fill="white"/>'
        f"{word(43, 64, title, 27, True)}"
        f"{word(43, 100, desc, 13)}"
        f"{body}{line(43, 1052, 751, 1052, 2)}"
        f"{word(43, 1080, 'SubjectNest original Foundation maths aid · CC BY 4.0', 11)}"
        "</svg>\n"
    )


def rail() -> str:
    """Make a large complete numeral rail from zero through twenty."""
    body = word(
        43, 151, "Count a source once, then find its matching numeral.", 17, True
    )
    for number in range(21):
        x = 43 + (number % 7) * 101
        y = 194 + (number // 7) * 218
        body += rect(
            x, y, 91, 168, BLUE if number % 2 == 0 else GREEN, data_number=str(number)
        )
        body += word(
            x + (22 if number < DIGIT_SPLIT else 13), y + 94, str(number), 42, True
        )
    body += rect(43, 889, 708, 112, PEACH)
    body += word(60, 931, "Empty frame = 0 objects. Which numeral fits? ____", 16)
    body += word(
        60, 968, "Just before: ____    My numeral: ____    Just after: ____", 15
    )
    return page(
        "Zero to twenty · numeral rail",
        "An empty source can show zero; cards are numerals, not objects to count.",
        body,
    )


def part_whole() -> str:
    """Make two labelled parts and a full-source whole frame."""
    body = word(
        43, 151, "Each source item belongs to one part, then one whole.", 17, True
    )
    body += rect(43, 185, 345, 360, BLUE)
    body += rect(406, 185, 345, 360, GREEN)
    body += word(62, 230, "PART A", 23, True)
    body += word(425, 230, "PART B", 23, True)
    body += word(62, 508, "Count: ____", 17)
    body += word(425, 508, "Count: ____", 17)
    body += rect(43, 577, 708, 280, PEACH)
    body += word(62, 620, "WHOLE: both parts, every source item once", 19, True)
    body += word(62, 816, "Part A ____ and Part B ____ make Whole ____", 17)
    body += rect(43, 887, 708, 113)
    body += word(62, 932, "For grouping: ____ in each group; ____ groups.", 17)
    return page(
        "Two parts · one whole · visible groups",
        "Large frames for broad cards or child-directed adult placement.",
        body,
    )


def place_mat() -> str:
    """Make a fixed-landmark gallery without perspective ambiguity."""
    body = word(43, 150, "Keep named landmarks fixed; place model shapes.", 17, True)
    body += rect(43, 181, 708, 704)
    body += rect(80, 257, 195, 160, BLUE)
    body += rect(519, 257, 195, 160, BLUE)
    body += word(101, 340, "SHELF A", 21, True)
    body += word(540, 340, "SHELF B", 21, True)
    body += rect(288, 467, 219, 132, GREEN)
    body += word(348, 545, "BOARD", 23, True)
    body += rect(311, 268, 170, 131, PEACH)
    body += word(333, 321, "PLACE A", 16)
    body += rect(314, 635, 164, 135, PEACH)
    body += word(336, 685, "PLACE B", 16)
    body += word(76, 833, "Trace: curved boundary / straight edges / corners", 16)
    body += rect(43, 916, 708, 88)
    body += word(60, 954, "Where? Name the fixed landmark, not 'over there'.", 15)
    return page(
        "Shape and place · fixed gallery markers",
        "Shelves and board stay still; shape positions are chosen by the child.",
        body,
    )


def data_mat() -> str:
    """Make two-column sorting and evidence/source boundary spaces."""
    body = word(43, 151, "GIVEN QUESTION: Which icon is more in THIS source?", 17, True)
    body += rect(43, 182, 708, 86, PEACH)
    body += word(60, 218, "SOURCE: supplied paper pictures only", 18, True)
    body += word(60, 249, "One picture = one placement or count mark.", 14)
    for x, label, fill in ((43, "ICON A", BLUE), (407, "ICON B", GREEN)):
        body += rect(x, 302, 344, 548, fill)
        body += word(x + 20, 347, label, 22, True)
        body += word(x + 20, 814, "Count: ____", 17)
    body += rect(43, 878, 708, 125)
    body += word(60, 921, "Counted ____ of ____ supplied pictures.", 17)
    body += word(60, 966, "More in this source: ____ because ____", 16)
    return page(
        "Given picture question · counted evidence",
        "Two named sort zones and an explicit supplied-source boundary.",
        body,
    )


def explain_mat() -> str:
    """Make a four-step mathematical explanation without pretend feedback."""
    body = word(43, 150, "A paper visitor is a model, not a real listener.", 17, True)
    for y, label, fill, hint in (
        (182, "1  SOURCE", BLUE, "Which exact objects or pictures did I use?"),
        (392, "2  MODEL / MOVE", GREEN, "What did I move, match, draw or trace?"),
        (602, "3  CHECK", PEACH, "Did each source item count once?"),
        (812, "4  EXPLAIN", BLUE, "What can I say only about this source?"),
    ):
        body += rect(43, y, 708, 187, fill)
        body += word(62, y + 42, label, 21, True)
        body += word(62, y + 80, hint, 16)
    return page(
        "Show · check · explain a source",
        "Four labelled steps for a child-controlled mathematical explanation.",
        body,
    )


def pattern_strip() -> str:
    """Make an exact practice unit and three blank continuation spaces."""
    body = word(43, 153, "Box ONE WHOLE unit, then copy all its places.", 18, True)
    kinds = ["CIRCLE", "SQUARE", "TRIANGLE"] * 2 + ["?", "?", "?"]
    for index, kind in enumerate(kinds):
        x = 43 + (index % 3) * 239
        y = 200 + (index // 3) * 252
        body += rect(
            x,
            y,
            224,
            213,
            BLUE if index < GIVEN_PATTERN_CARDS else PEACH,
            data_pattern_kind=kind,
        )
        if kind == "CIRCLE":
            body += circle(x + 112, y + 88, 43)
        elif kind == "SQUARE":
            body += rect(x + 69, y + 45, 86, 86)
        elif kind == "TRIANGLE":
            body += triangle(x + 67, y + 40, 91)
        else:
            body += word(x + 99, y + 110, "?", 38, True)
        body += word(x + 28, y + 188, kind, 17, True)
    body += word(43, 1003, "Shape reason: curve / straight sides / corners?", 16)
    return page(
        "A full repeating unit · shape reason",
        "Two original three-shape units then three child-filled spaces.",
        body,
    )


def length_time() -> str:
    """Make comparable same-page lengths and ordered day-part cards."""
    body = word(
        43, 156, "Align the LEFT start marks; compare right endpoints.", 17, True
    )
    body += rect(43, 189, 708, 430, BLUE)
    body += word(68, 249, "M", 23, True)
    body += word(68, 405, "N", 23, True)
    body += (
        f'<g data-strip="M" data-length-units="180">{line(160, 279, 340, 279, 12)}</g>'
    )
    body += (
        f'<g data-strip="N" data-length-units="250">{line(160, 435, 410, 435, 12)}</g>'
    )
    body += line(160, 257, 160, 457, 3)
    body += word(68, 553, "Same page / same scale: which extends farther?", 16)
    body += word(43, 685, "Order these named parts of a fictional day:", 18, True)
    for index, name in enumerate(("MORNING", "LUNCHTIME", "AFTERNOON", "NIGHT")):
        x = 43 + (index % 2) * 362
        y = 726 + (index // 2) * 132
        body += rect(
            x, y, 346, 108, GREEN if index % 2 == 0 else PEACH, data_day_part=name
        )
        body += word(x + 20, y + 66, name, 23, True)
    body += word(43, 1020, "Labels show order, not measured elapsed time.", 14)
    return page(
        "Direct printed length · day-part order",
        "M and N are same-scale drawn strips; four labelled time cards.",
        body,
    )


def check_a() -> str:
    """Make new number, shape/place and picture sources with no answer total."""
    body = word(
        43, 144, "1  Count each round source mark. Does claim 17 match?", 17, True
    )
    body += rect(43, 166, 708, 193, BLUE)
    for index in range(16):
        x = 88 + (index % 8) * 83
        y = 235 + (index // 8) * 74
        body += circle(x, y, 17, data_source="NUMBER")
    body += word(43, 409, "2  Name the rounded shape and its place.", 17, True)
    body += rect(43, 430, 708, 214, GREEN)
    body += circle(396, 484, 33, data_shape="circle")
    body += rect(266, 552, 260, 58, PEACH, data_landmark="BOARD")
    body += word(350, 589, "BOARD", 21, True)
    body += word(
        43, 689, "3  Given: which picture icon is more on THIS sheet?", 17, True
    )
    kinds = [
        "PINE",
        "SHELL",
        "PINE",
        "PINE",
        "SHELL",
        "PINE",
        "PINE",
        "SHELL",
        "PINE",
        "SHELL",
        "PINE",
        "PINE",
        "SHELL",
        "PINE",
    ]
    for index, kind in enumerate(kinds):
        x = 43 + (index % 7) * 101
        y = 707 + (index // 7) * 154
        body += rect(
            x, y, 91, 143, PEACH if index % 2 == 0 else BLUE, data_card_kind=kind
        )
        body += icon(x + 6, y + 8, kind)
    body += word(43, 1032, "A paper source only; no real-world count.", 13)
    return page(
        "Check A · explain three fresh sources",
        "Number marks, a circle/BOARD relation and mixed picture icons.",
        body,
    )


def check_b() -> str:
    """Make new part, add, pattern and direct-length sources on one page."""
    body = word(43, 143, "1  PART A and PART B: show one WHOLE.", 17, True)
    body += rect(43, 160, 708, 172, BLUE)
    body += word(62, 194, "PART A", 15, True)
    body += word(391, 194, "PART B", 15, True)
    for index in range(2):
        body += rect(65 + index * 64, 217, 51, 51, "white", data_part="A")
    for index in range(6):
        body += rect(
            390 + (index % 3) * 66,
            209 + (index // 3) * 66,
            51,
            51,
            "white",
            data_part="B",
        )
    body += word(43, 374, "2  START + ADD: test display claim 9.", 17, True)
    body += rect(43, 393, 708, 178, GREEN)
    body += word(62, 429, "START", 15, True)
    body += word(433, 429, "ADD", 15, True)
    for index in range(7):
        x = 69 + (index % 4) * 73
        y = 469 + (index // 4) * 62
        body += circle(x, y, 18, data_add_group="START")
    for index in range(3):
        body += circle(454 + index * 79, 478, 18, data_add_group="ADD")
    body += word(43, 613, "3  Continue the whole repeating part.", 17, True)
    kinds = ["SQUARE", "CIRCLE", "CIRCLE"] * 2 + ["?", "?", "?"]
    for index, kind in enumerate(kinds):
        x = 43 + index * 78
        body += rect(
            x,
            635,
            70,
            112,
            PEACH if index >= GIVEN_PATTERN_CARDS else BLUE,
            data_pattern_kind=kind,
        )
        if kind == "SQUARE":
            body += rect(x + 18, 660, 34, 34)
        elif kind == "CIRCLE":
            body += circle(x + 35, 677, 18)
        else:
            body += word(x + 26, 692, "?", 29, True)
        body += word(x + 28, 731, str(index + 1), 12)
    body += word(43, 800, "4  Align LEFT starts; compare right endpoints.", 17, True)
    body += rect(43, 818, 708, 184, GREEN)
    body += word(63, 865, "A", 20, True)
    body += word(63, 958, "B", 20, True)
    body += (
        f'<g data-strip="A" data-length-units="145">{line(145, 858, 290, 858, 12)}</g>'
    )
    body += (
        f'<g data-strip="B" data-length-units="230">{line(145, 949, 375, 949, 12)}</g>'
    )
    body += line(145, 833, 145, 974, 3)
    return page(
        "Check B · four fresh source models",
        "Visible parts, add-to cards, two full pattern units and equal-scale lengths.",
        body,
    )


PAGES = {
    "zero-to-twenty-rail": rail,
    "part-whole-mat": part_whole,
    "shape-and-place-mat": place_mat,
    "given-data-and-evidence-mat": data_mat,
    "show-check-explain-mat": explain_mat,
    "repeat-and-reason-strip": pattern_strip,
    "direct-length-and-day-parts": length_time,
    "check-a-explanation-source": check_a,
    "check-b-transfer-source": check_b,
}


def main() -> None:
    """Write SVG and render a matching PDF with installed librsvg."""
    for stem, maker in PAGES.items():
        svg_path = HERE / f"{stem}.svg"
        svg_path.write_text(maker(), encoding="utf-8")
        subprocess.run(
            [
                "rsvg-convert",
                "-f",
                "pdf",
                "-o",
                str(HERE / f"{stem}.pdf"),
                str(svg_path),
            ],
            check=True,
        )
        print(f"WROTE {stem}.svg/.pdf")


if __name__ == "__main__":
    main()
