# Home Assistant waste collection: which bin goes out tomorrow.
#
# Key pushed by Home Assistant (POST /api/device/<id>/data):
#   waste_tomorrow   the bin(s) collected tomorrow, e.g. "GFT", "PAPIER", "REST",
#                    "PMD", or several like "GFT, PMD". Empty (or "none"/"geen")
#                    means no pickup tomorrow.
#
# Example payload:
#   {"waste_tomorrow": "GFT"}
#
# The app reads it as config.get("ha_waste_tomorrow"). Dutch and English names
# both work (PAPIER/PAPER, PLASTIC/PMD, ...); unknown names are shown as they are.

load("render.star", "render")

WHITE = "#f8f8f0"
DIM = "#70707a"
QUIET = "#8a8a94"
WHEEL = "#5a5a64"

# Display name -> bin colour.
COLORS = {
    "GFT": "#5ee08a",
    "PAPER": "#58b4f0",
    "REST": "#a8b0bc",
    "PMD": "#ff7a1a",
    "GLASS": "#3dd6c6",
}

# Names Home Assistant integrations use (Afvalwijzer, Mijn AfvalWijzer, ...).
ALIASES = {
    "GFT": "GFT",
    "GROEN": "GFT",
    "GREEN": "GFT",
    "BIO": "GFT",
    "ORGANIC": "GFT",
    "PAPIER": "PAPER",
    "PAPER": "PAPER",
    "KARTON": "PAPER",
    "OPK": "PAPER",
    "REST": "REST",
    "RESTAFVAL": "REST",
    "GRIJS": "REST",
    "GREY": "REST",
    "PMD": "PMD",
    "PLASTIC": "PMD",
    "PBD": "PMD",
    "GLAS": "GLASS",
    "GLASS": "GLASS",
}

NO_PICKUP = ["", "NONE", "GEEN", "NO", "NIETS", "-", "--", "FALSE", "OFF"]
NO_DATA = ["UNKNOWN", "UNAVAILABLE"]

# Tall thin 9x15 pixel font for the bin name (same as the bin_day app).
SIDES = ["#.......#"]
LEFT = ["#........"]
RIGHT = ["........#"]
GLYPHS = {
    "P": ["########."] + SIDES * 6 + ["########."] + LEFT * 7,
    "A": [".#######."] + SIDES * 6 + ["#########"] + SIDES * 7,
    "E": ["#########"] + LEFT * 6 + ["#######.."] + LEFT * 6 + ["#########"],
    "R": ["########."] + SIDES * 6 + ["########.", "#...#....", "#....#...", "#.....#..", "#......#."] + SIDES * 3,
    "S": [".#######.", "#.......#"] + LEFT * 5 + [".#######."] + RIGHT * 5 + ["#.......#", ".#######."],
    "T": ["#########"] + ["....#...."] * 14,
    "G": [".#######.", "#.......#"] + LEFT * 6 + ["#...#####"] + SIDES * 5 + [".#######."],
    "F": ["#########"] + LEFT * 6 + ["#######.."] + LEFT * 7,
    "M": ["#.......#", "##.....##", "#.#...#.#", "#..#.#..#", "#...#...#"] + SIDES * 10,
    "D": ["#######..", "#......#."] + SIDES * 11 + ["#......#.", "#######.."],
    "L": LEFT * 14 + ["#########"],
}
GLYPH_W = 10  # 9 + 1 after bolding
GLYPH_GAP = 2

# Wheelie bin: "#" in the bin colour, "o" = wheels.
BIN_ICON = [
    "....###....",
    "###########",
    "...........",
    ".#########.",
    ".#.#.#.#.#.",
    ".#.#.#.#.#.",
    ".#.#.#.#.#.",
    ".#.#.#.#.#.",
    ".#########.",
    "..oo...oo..",
    "..oo...oo..",
]

def pixels(rows, x, y, color, ch = "#"):
    # One box per horizontal run of lit pixels.
    out = []
    for r, line in enumerate(rows):
        start = -1
        for c in range(len(line) + 1):
            on = c < len(line) and line[c] == ch
            if on and start < 0:
                start = c
            elif not on and start >= 0:
                out.append(render.Padding(pad = (x + start, y + r, 0, 0), child = render.Box(width = c - start, height = 1, color = color)))
                start = -1
    return out

def bold(rows):
    # Thicken every stroke to 2 px: a pixel is lit when it or its left/upper neighbour is.
    h = len(rows)
    w = len(rows[0])
    out = []
    for r in range(h + 1):
        line = ""
        for c in range(w + 1):
            on = False
            for dr, dc in [(0, 0), (1, 0), (0, 1), (1, 1)]:
                rr = r - dr
                cc = c - dc
                if rr >= 0 and rr < h and cc >= 0 and cc < w and rows[rr][cc] == "#":
                    on = True
            line += "#" if on else "."
        out.append(line)
    return out

def at(x, y, child):
    return render.Padding(pad = (x, y, 0, 0), child = child)

def bins(raw):
    # "GFT, PMD" / "Papier en karton" -> [("GFT", colour), ("PMD", colour)]
    s = " " + raw.upper() + " "
    for sep in [" EN ", " AND ", "+", "&", "/", ";"]:
        s = s.replace(sep, ",")
    out = []
    seen = []
    for part in s.split(","):
        part = part.strip()
        if part == "":
            continue
        name = ALIASES.get(part, part[:20])
        if name in seen:
            continue
        seen.append(name)
        out.append((name, COLORS.get(name, WHITE)))
    return out

def big_name(hits):
    name, color = hits[0]
    width = len(name) * GLYPH_W + (len(name) - 1) * GLYPH_GAP
    if len(hits) == 1 and width <= 64 and all([ch in GLYPHS for ch in name.elems()]):
        x = (64 - width) // 2
        out = []
        for i, ch in enumerate(name.elems()):
            out.extend(pixels(bold(GLYPHS[ch]), x + i * (GLYPH_W + GLYPH_GAP), 15, color))
        return out

    # Several bins or an unknown name: smaller font, scrolling when it does not fit.
    pieces = []
    for i, h in enumerate(hits):
        if i > 0:
            pieces.append(render.Text("+", font = "6x13", color = DIM))
        pieces.append(render.Text(h[0], font = "6x13", color = h[1]))
    row = render.Row(children = pieces)
    if len("+".join([h[0] for h in hits])) > 10:
        row = render.Marquee(width = 64, child = row)
    return [at(0, 16, render.Box(width = 64, height = 13, child = row))]

def screen(icon_color, label_color, big):
    # Full-size first child pins the Stack to the 64x32 canvas so positions are absolute.
    children = [render.Box(width = 64, height = 32)]
    children.extend(pixels(BIN_ICON, 1, 2, icon_color))
    children.extend(pixels(BIN_ICON, 1, 2, WHEEL, "o"))
    children.append(at(14, 2, render.Text("TOMORROW", font = "6x13", color = label_color)))
    children.extend(big)
    return render.Root(child = render.Box(width = 64, height = 32, color = "#000000", child = render.Stack(children = children)))

def quiet(text):
    return [at(0, 16, render.Box(width = 64, height = 13, child = render.Text(text, font = "6x13", color = QUIET)))]

def main(config):
    raw = config.get("ha_waste_tomorrow", "GFT").strip()

    if raw.upper() in NO_DATA:
        return screen(WHEEL, DIM, quiet("NO DATA"))

    hits = [] if raw.upper() in NO_PICKUP else bins(raw)
    if not hits:
        return screen(WHEEL, DIM, quiet("NO PICKUP"))

    return screen(hits[0][1], WHITE, big_name(hits))
