#!/usr/pkg/bin/python3.14
# priforgepng

"""Forge PNG image from raw computation."""

from array import array
from fractions import Fraction

import argparse
import re
import sys

import png


def gen_glr(x):
    """Gradient Left to Right"""
    return x


def gen_grl(x):
    """Gradient Right to Left"""
    return 1 - x


def gen_gtb(x, y):
    """Gradient Top to Bottom"""
    return y


def gen_gbt(x, y):
    """Gradient Bottom to Top"""
    return 1.0 - y


def gen_rtl(x, y):
    """Radial gradient, centred at Top-Left"""
    return max(1 - (float(x) ** 2 + float(y) ** 2) ** 0.5, 0.0)


def gen_rctr(x, y):
    """Radial gradient, centred at Centre"""
    return gen_rtl(float(x) - 0.5, float(y) - 0.5)


def gen_rtr(x, y):
    """Radial gradient, centred at Top-Right"""
    return gen_rtl(1.0 - float(x), y)


def gen_rbl(x, y):
    """Radial gradient, centred at Bottom-Left"""
    return gen_rtl(x, 1.0 - float(y))


def gen_rbr(x, y):
    """Radial gradient, centred at Bottom-Right"""
    return gen_rtl(1.0 - float(x), 1.0 - float(y))


def stripe(x, n):
    return int(x * n) & 1


def gen_vs2(x):
    """2 Vertical Stripes"""
    return stripe(x, 2)


def gen_vs4(x):
    """4 Vertical Stripes"""
    return stripe(x, 4)


def gen_vs10(x):
    """10 Vertical Stripes"""
    return stripe(x, 10)


def gen_hs2(x, y):
    """2 Horizontal Stripes"""
    return stripe(float(y), 2)


def gen_hs4(x, y):
    """4 Horizontal Stripes"""
    return stripe(float(y), 4)


def gen_hs10(x, y):
    """10 Horizontal Stripes"""
    return stripe(float(y), 10)


def gen_slr(x, y):
    """10 diagonal stripes, rising from Left to Right"""
    return stripe(x + y, 10)


def gen_srl(x, y):
    """10 diagonal stripes, rising from Right to Left"""
    return stripe(1 + x - y, 10)


def checker(x, y, n):
    return stripe(x, n) ^ stripe(y, n)


def gen_ck8(x, y):
    """8 by 8 checkerboard"""
    return checker(x, y, 8)


def gen_ck15(x, y):
    """15 by 15 checkerboard"""
    return checker(x, y, 15)


def gen_zero(x):
    """All zero (black)"""
    return 0


def gen_one(x):
    """All one (white)"""
    return 1


def yield_fun_rows(size, bitdepth, pattern):
    """
    Create a single channel (monochrome) test pattern.
    Yield each row in turn.
    """

    width, height = size

    maxval = 2 ** bitdepth - 1
    if maxval > 255:
        typecode = "H"
    else:
        typecode = "B"
    pfun = pattern_function(pattern)

    # The coordinates are an integer + 0.5,
    # effectively sampling each pixel at its centre.
    # This is morally better, and produces all 256 sample values
    # in a 256-pixel wide gradient.

    # We make a list of x coordinates here and re-use it,
    # because Fraction instances are slow to allocate.
    xs = [Fraction(x, 2 * width) for x in range(1, 2 * width, 2)]

    # The general case is a function in x and y,
    # but if the function only takes an x argument,
    # it's handled in a special case that is a lot faster.
    if n_args(pfun) == 2:
        for y in range(height):
            a = array(typecode)
            fy = Fraction(Fraction(y + 0.5), height)
            for fx in xs:
                a.append(int(round(maxval * pfun(fx, fy))))
            yield a
        return

    # For functions in x only, it's a _lot_ faster
    # to generate a single row and repeatedly yield it
    a = array(typecode)
    for fx in xs:
        a.append(int(round(maxval * pfun(x=fx))))
    for y in range(height):
        yield a
    return


def generate(args):
    """
    Create a PNG test image and write the file to stdout.

    `args` should be an argparse Namespace instance or similar.
    """

    size = args.size
    bitdepth = args.depth

    out = png.binary_stdout()

    for pattern in args.pattern:
        rows = yield_fun_rows(size, bitdepth, pattern)
        writer = png.Writer(
            size[0], size[1], bitdepth=bitdepth, greyscale=True, alpha=False
        )
        writer.write(out, rows)


def n_args(fun):
    """Number of arguments in fun's argument list."""
    return fun.__code__.co_argcount


def pattern_function(pattern):
    """From `pattern`, a string,
    return the function for that pattern.
    """

    lpat = pattern.lower()
    for name, fun in globals().items():
        parts = name.split("_")
        if parts[0] != "gen":
            continue
        if parts[1] == lpat:
            return fun


def patterns():
    """
    List the patterns.
    """

    for name, fun in globals().items():
        parts = name.split("_")
        if parts[0] == "gen":
            yield parts[1], fun.__doc__


def dimensions(s):
    """
    Typecheck the --size option, which should be
    one or two comma separated numbers.
    Example: "64,40".
    """

    tupl = re.findall(r"\d+", s)
    if len(tupl) not in (1, 2):
        raise ValueError("%r should be width or width,height" % s)
    if len(tupl) == 1:
        tupl *= 2
    assert len(tupl) == 2
    return list(map(int, tupl))


def main(argv=None):
    if argv is None:
        argv = sys.argv
    parser = argparse.ArgumentParser(description="Forge greyscale PNG patterns")

    parser.add_argument(
        "-l", "--list", action="store_true", help="print list of patterns and exit"
    )
    parser.add_argument(
        "-d", "--depth", default=8, type=int, metavar="N", help="N bits per pixel"
    )
    parser.add_argument(
        "-s",
        "--size",
        default=[256, 256],
        type=dimensions,
        metavar="w[,h]",
        help="width and height of the image in pixels",
    )
    parser.add_argument("pattern", nargs="*", help="name of pattern")

    args = parser.parse_args(argv[1:])

    if args.list:
        for name, doc in sorted(patterns()):
            print(name, doc, sep="\t")
        return

    if not args.pattern:
        parser.error("--list or pattern is required")
    return generate(args)


if __name__ == "__main__":
    main()
