#!/usr/bin/env python3
"""Derive src/cells/table.rs from Unicode's own character names.

The Box Drawing and Block Elements blocks are named systematically, so the 160
recipes are already written down: BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY says
which arms a glyph has and how heavy each one is, and LEFT THREE EIGHTHS BLOCK
says which fraction of the cell is filled. Typing that out by hand would be 160
chances to transpose two arms in a way nothing catches until a border looks
wrong in one corner.

So the table is derived rather than authored, from the `unicodedata` module in
the Python standard library, and the result is committed. Re-run it when the
shape vocabulary changes:

    python3 scripts/derive-cell-table.py > src/cells/table.rs

Every name must parse. The script fails rather than emitting a partial table,
because a name it did not understand is a glyph that would silently come out
blank.
"""

import sys
import unicodedata as ud

WEIGHT = {"LIGHT": "L", "HEAVY": "H", "DOUBLE": "D", "SINGLE": "L"}
DIRS = {
    "UP": ["up"],
    "DOWN": ["down"],
    "LEFT": ["left"],
    "RIGHT": ["right"],
    "HORIZONTAL": ["left", "right"],
    "VERTICAL": ["up", "down"],
}
ARMS = ["left", "right", "up", "down"]
FRACTION = {
    "ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4,
    "FIVE": 5, "SIX": 6, "SEVEN": 7, "EIGHT": 8,
}


def die(msg):
    print(f"derive-cell-table: {msg}", file=sys.stderr)
    sys.exit(1)


def parse_box(cp):
    """(kind, dash, arms) for a Box Drawing codepoint."""
    name = ud.name(chr(cp))
    body = name.replace("BOX DRAWINGS ", "")

    if "DIAGONAL" in body:
        if "UPPER RIGHT TO LOWER LEFT" in body:
            return ("diagonal", "Rising", {})
        if "UPPER LEFT TO LOWER RIGHT" in body:
            return ("diagonal", "Falling", {})
        if "CROSS" in body:
            return ("diagonal", "Cross", {})
        die(f"U+{cp:04X} {name}: unrecognised diagonal")

    kind = "stems"
    if "ARC " in body:
        kind = "arc"
        body = body.replace("ARC ", "")

    dash = "None"
    for spelling, variant in [
        ("QUADRUPLE DASH", "Quadruple"),
        ("TRIPLE DASH", "Triple"),
        ("DOUBLE DASH", "Double"),
    ]:
        if spelling in body:
            dash = variant
            body = body.replace(spelling + " ", "")

    arms = {}
    for clause in body.split(" AND "):
        tokens = clause.split()
        weights = [t for t in tokens if t in WEIGHT]
        directions = [t for t in tokens if t in DIRS]
        leftover = [t for t in tokens if t not in WEIGHT and t not in DIRS]
        if leftover or not directions:
            die(f"U+{cp:04X} {name}: cannot read clause {clause!r}")
        weight = WEIGHT[weights[0]] if weights else None
        for direction in directions:
            for arm in DIRS[direction]:
                arms[arm] = weight

    # A leading weight applies to every clause that named none of its own:
    # "LIGHT DOWN AND RIGHT" weighs both arms, "DOWN LIGHT AND RIGHT HEAVY"
    # weighs them separately.
    stated = [w for w in arms.values() if w]
    if not stated:
        die(f"U+{cp:04X} {name}: no weight anywhere")
    for arm, weight in arms.items():
        if weight is None:
            arms[arm] = stated[0]
    return (kind, dash, arms)


def parse_block(cp):
    """(kind, payload) for a Block Elements codepoint."""
    name = ud.name(chr(cp))
    if name == "FULL BLOCK":
        return ("fill", [(0, 0, 8, 8)])
    if name.endswith("SHADE"):
        return ("shade", name.split()[0].capitalize())
    if name.startswith("QUADRANT"):
        quads = {
            "UPPER LEFT": (0, 4, 4, 8),
            "UPPER RIGHT": (4, 4, 8, 8),
            "LOWER LEFT": (0, 0, 4, 4),
            "LOWER RIGHT": (4, 0, 8, 4),
        }
        rects = []
        for part in name[len("QUADRANT "):].split(" AND "):
            if part not in quads:
                die(f"U+{cp:04X} {name}: unknown quadrant {part!r}")
            rects.append(quads[part])
        return ("fill", rects)

    tokens = name.split()
    side, count, unit = tokens[0], tokens[1], tokens[2]
    if unit.startswith("QUARTER"):
        eighths = FRACTION[count] * 2
    elif unit.startswith("EIGHTH"):
        eighths = FRACTION[count]
    elif count == "HALF":
        eighths = 4
        unit = "HALF"
    else:
        die(f"U+{cp:04X} {name}: unknown fraction {count} {unit}")
    if count == "HALF":
        eighths = 4

    if side == "UPPER":
        return ("fill", [(0, 8 - eighths, 8, 8)])
    if side == "LOWER":
        return ("fill", [(0, 0, 8, eighths)])
    if side == "LEFT":
        return ("fill", [(0, 0, eighths, 8)])
    if side == "RIGHT":
        return ("fill", [(8 - eighths, 0, 8, 8)])
    die(f"U+{cp:04X} {name}: unknown side {side!r}")


def main():
    stems, arcs, diagonals, fills, shades = [], [], [], [], []

    for cp in range(0x2500, 0x2580):
        kind, payload, arms = parse_box(cp)
        if kind == "diagonal":
            diagonals.append((cp, payload))
            continue
        row = (cp, [arms.get(a, "N") for a in ARMS], payload)
        (arcs if kind == "arc" else stems).append(row)

    for cp in range(0x2580, 0x25A0):
        kind, payload = parse_block(cp)
        (shades if kind == "shade" else fills).append((cp, payload))

    counts = (len(stems), len(arcs), len(diagonals), len(fills), len(shades))
    if sum(counts) != 160:
        die(f"expected 160 codepoints, derived {sum(counts)}")

    out = []
    w = out.append
    w("// Generated by scripts/derive-cell-table.py from Unicode character names.")
    w("// Do not edit by hand: re-run the script instead. See its header for why")
    w("// the table is derived rather than authored.")
    w("//")
    w(f"// {counts[0]} stems, {counts[1]} arcs, {counts[2]} diagonals,"
      f" {counts[3]} fills, {counts[4]} shades.")
    w("")
    w("use super::Arm::{D, H, L, N};")
    w("use super::{Dash, Diagonal, Shade};")
    w("")
    w("/// Arms in the order left, right, up, down.")
    w(f"pub(super) const STEMS: [(u32, [super::Arm; 4], Dash); {counts[0]}] = [")
    for cp, arms, dash in stems:
        w(f"    (0x{cp:04X}, [{', '.join(arms)}], Dash::{dash}),")
    w("];")
    w("")
    w("/// The rounded corners. Same arms as a stem, drawn as a quarter turn.")
    w(f"pub(super) const ARCS: [(u32, [super::Arm; 4]); {counts[1]}] = [")
    for cp, arms, _ in arcs:
        w(f"    (0x{cp:04X}, [{', '.join(arms)}]),")
    w("];")
    w("")
    w(f"pub(super) const DIAGONALS: [(u32, Diagonal); {counts[2]}] = [")
    for cp, kind in diagonals:
        w(f"    (0x{cp:04X}, Diagonal::{kind}),")
    w("];")
    w("")
    w("/// Filled rectangles in eighths of the cell: x0, y0, x1, y1, with y up.")
    w(f"pub(super) const FILLS: [(u32, &[(u8, u8, u8, u8)]); {counts[3]}] = [")
    for cp, rects in fills:
        body = ", ".join(f"({a}, {b}, {c}, {d})" for a, b, c, d in rects)
        w(f"    (0x{cp:04X}, &[{body}]),")
    w("];")
    w("")
    w(f"pub(super) const SHADES: [(u32, Shade); {counts[4]}] = [")
    for cp, level in shades:
        w(f"    (0x{cp:04X}, Shade::{level}),")
    w("];")
    print("\n".join(out))


if __name__ == "__main__":
    main()
