"""Lays every image the tests ship with on one page, each captioned with what the
file itself declares.

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

Nothing in the captions is written by hand: each line is read back out of the image
after it was parsed, so the page is a statement of what the library understood rather
than of what the example was told. A file that declares nothing says so, which is the
point of showing it next to the ones that do.

Usage: python examples/write_image_gallery.py [out.pdf] [font.ttf]
"""

from __future__ import annotations

from pathlib import Path

import _licence
import _out

import hqf_pdf

# Every image committed for the tests.
IMAGES = Path(__file__).resolve().parents[2] / "hqf-pdf" / "tests" / "images"

# The page, and the grid laid over it.
MARGIN = 30.0
COLUMNS = 4
ROWS = 5

# The tallest and widest a thumbnail is drawn.
THUMBNAIL = (110.0, 92.0)

# The caption under each thumbnail.
CAPTION_SIZE = 5.6
CAPTION_LEADING = 7.4

# Ink.
TEXT = hqf_pdf.Rgb(0.15, 0.16, 0.2)
FADED = hqf_pdf.Rgb(0.42, 0.44, 0.5)
TILE = hqf_pdf.Rgb(0.95, 0.96, 0.97)

# The way up an ``Exif`` header names by its number, in words.
WAYS_UP = {
    2: "mirrored left to right",
    3: "upside down",
    4: "mirrored top to bottom",
    5: "mirrored, turned left",
    6: "turned right",
    7: "mirrored, turned right",
    8: "turned left",
}


def colour_of(image: hqf_pdf.Image) -> str:
    """What one pixel of the image stands for, in the fewest words that still say it."""
    palette = image.color_space.palette
    if palette is not None:
        return f"a table of {len(palette) // 3} colours"
    return {1: "grey", 3: "red, green, blue", 4: "four inks"}.get(
        image.color_space.components, "colours of some other kind"
    )


def facts(image: hqf_pdf.Image) -> list[str]:
    """What the file says about itself, one statement at a time.

    A file that says nothing beyond its shape says so: the point of the page is that
    both kinds sit side by side.
    """
    bits = image.bits_per_component
    facts = [
        f"{image.width} by {image.height} pixels",
        colour_of(image),
        f"{bits} bit" if bits == 1 else f"{bits} bits",
    ]

    stated = []
    resolution = image.resolution
    if resolution is not None:
        stated.append(f"{resolution.x:.0f} by {resolution.y:.0f} dots to the inch")
    orientation = image.orientation
    if orientation is not None:
        stated.append(f"seen {WAYS_UP.get(orientation.tag, 'the way it is stored')}")
    if image.has_alpha:
        stated.append("opacity per pixel")
    if image.color_key is not None:
        stated.append("one colour left out")
    if image.icc_profile is not None:
        stated.append("read through sRGB")

    facts.extend(stated if stated else ["declaring nothing else of itself"])
    return facts


def packed(font: hqf_pdf.FontHandle, width: float, statements: list[str]) -> list[str]:
    """The statements laid end to end, broken into lines that fit ``width``.

    Nothing is dropped: a caption that will not fit on one line takes as many as it
    needs, which is what keeps the page a full account of what was read.
    """
    lines: list[str] = []
    for statement in statements:
        joined = f"{lines[-1]}, {statement}" if lines else ""
        if lines and font.measure(joined, CAPTION_SIZE) <= width:
            lines[-1] = joined
        else:
            lines.append(statement)
    return lines


def text(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    at: tuple[float, float],
    colour: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text, left-aligned, in a colour of its own."""
    content.set_fill(colour)
    content.draw_text(font, size, at[0], at[1], s)


def text_fitted(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    size: float,
    at: tuple[float, float],
    width: float,
    colour: hqf_pdf.Rgb,
    s: str,
) -> None:
    """Draws a line of text, shortened until it fits the width it is given."""
    line = s
    while font.measure(line, size) > width and len(line) > 1:
        line = line[:-2] + "…"
    text(content, font, size, at, colour, line)


def tile(
    content: hqf_pdf.Content,
    font: hqf_pdf.FontHandle,
    handle: hqf_pdf.ImageHandle,
    lines: list[str],
    at: tuple[float, float],
    cell: tuple[float, float],
) -> None:
    """Draws one tile: the picture inside its box, and what the file says under it."""
    x, y = at
    cell_width, cell_height = cell
    caption_height = CAPTION_LEADING * len(lines)
    box_height = cell_height - caption_height - 6.0

    content.set_fill(TILE)
    content.rect(x, y + caption_height + 6.0, cell_width, box_height)
    content.fill()

    # The picture keeps its proportions inside the box, and sits in the middle of it.
    width, height = handle.fit_within(min(THUMBNAIL[0], cell_width - 8.0), THUMBNAIL[1])
    content.draw_image(
        handle,
        x + (cell_width - width) / 2.0,
        y + caption_height + 6.0 + (box_height - height) / 2.0,
        width,
        height,
    )

    for index, line in enumerate(lines):
        colour = TEXT if index == 0 else FADED
        text_fitted(
            content,
            font,
            CAPTION_SIZE,
            (x, y + caption_height - CAPTION_LEADING * (index + 1) + 2.0),
            cell_width,
            colour,
            line,
        )


def main() -> None:
    out = _out.output_path("image_gallery")

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", "hqf-pdf image gallery")
    font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    page = hqf_pdf.Page.a4()
    cell_width = (page.width - 2.0 * MARGIN) / COLUMNS - 6.0

    tiles = []
    for path in sorted(path for path in IMAGES.iterdir() if path.is_file()):
        image = hqf_pdf.Image.from_path(path)
        lines = [path.name, *packed(font, cell_width, facts(image))]
        tiles.append((document.add_image(image), lines))

    content = hqf_pdf.Content()

    text(
        content,
        font,
        13.0,
        (MARGIN, page.height - MARGIN - 10.0),
        TEXT,
        "Every picture the tests ship with, and what each file says about itself",
    )

    top = page.height - MARGIN - 34.0
    cell_height = (top - MARGIN) / ROWS

    for index, (handle, lines) in enumerate(tiles):
        column, row = index % COLUMNS, index // COLUMNS
        if row >= ROWS:
            print(
                f"{len(tiles)} pictures do not fit on one page; the rest are left off"
            )
            break

        tile(
            content,
            font,
            handle,
            lines,
            (MARGIN + (cell_width + 6.0) * column, top - cell_height * (row + 1)),
            (cell_width, cell_height - 6.0),
        )

    page.set_content(content)
    document.add_page(page)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes, {len(tiles)} pictures")


if __name__ == "__main__":
    main()
