"""Lay the seven lids out on the inside of the cupboard door, with their clips, and
work out the backboard and the bars that carry the clips.

Writes diagrams/door-layout.svg (both versions side by side) and cad/layout.scad
(positions for the OpenSCAD assembly). Run from the project folder:
    python3 diagrams/door_layout.py

Door coordinates, mm: x from the hinge edge, y down from the top, seen from inside.
"""
import itertools, math, time

L = {"L7": 215, "L3": 195, "L2": 192, "L6": 190, "L1": 173, "L4": 150, "L5": 150}

DOOR = (495, 715)
FRAME = 70
# Carcass opening C1 464 x C2 684, centred across; the base panel (18) sits at the
# door's bottom edge, so the cupboard floor is 18 up from it and the opening top 13 down.
X0, X1, Y0, Y1 = 15.5, 479.5, 13, 697
EDGE = 4                                       # keep everything this far inside the opening
HINGES = [(0, 75, 45, 125), (0, 75, 590, 670)]  # Titus plates, full width of the hinge stile
# The shelf: 20 thick, C4 328 from the floor (top or underside unknown, so allow both),
# only 55 behind the door there. A lid stands ~85 off the door at the handle, so only
# its outer ring (RING mm in from the rim: rim, glass and clip, under 40) may cross it.
SHELF = (715 - 18 - 348 - 5, 715 - 18 - 308 + 5)
RING = 35
GAP = 8                                        # rim to rim
HW_GAP = 3                                     # clip to anything

# Clip heads, relative to the rim point they hold (must match cad/lid-clips.scad)
HOOK_W, HOOK_UP, HOOK_DOWN = 30, 14, 6         # above the top of the rim / overlapping it
CRADLE_W, CRADLE_UP, CRADLE_DOWN = 56, 7, 5    # lip over the rim / base below it

# Bars (cad/lid-clips.scad): ends screwed into the stiles
BAR_H, BAR_X = 24, (20, 475)   # the bar heights in layout.scad were chosen with 24; bars are now 30 (cad/lid-clips.scad), still clear of the hinges
STEM_MAX = 45                                  # furthest a clip head sits from its bar


def heads(x, y, r):
    """Rectangles (x0, x1, y0, y1) of a lid's hook and cradle."""
    top, bot = y - r, y + r
    return [(x - HOOK_W / 2, x + HOOK_W / 2, top - HOOK_UP, top + HOOK_DOWN),
            (x - CRADLE_W / 2, x + CRADLE_W / 2, bot - CRADLE_UP, bot + CRADLE_DOWN)]


def rect_circle(rc, x, y, r):
    a, b, c, d = rc
    return math.hypot(x - min(max(x, a), b), y - min(max(y, c), d)) < r


def rect_rect(p, q, g):
    return p[0] < q[1] + g and q[0] < p[1] + g and p[2] < q[3] + g and q[2] < p[3] + g


def fits(x, y, r, placed):
    if y - (r - RING) < SHELF[1] and y + (r - RING) > SHELF[0]:
        return False
    if any(rect_circle(h, x, y, r + 3) for h in HINGES):
        return False
    hs = heads(x, y, r)
    for h in hs:
        if h[0] < X0 + EDGE or h[1] > X1 - EDGE or h[2] < Y0 + EDGE or h[3] > Y1 - EDGE:
            return False
        if any(rect_rect(h, k, 3) for k in HINGES):
            return False
    for _, px, py, pr in placed:
        if math.hypot(x - px, y - py) < r + pr + GAP:
            return False
        for q in heads(px, py, pr):
            if rect_circle(q, x, y, r + HW_GAP) or any(rect_rect(h, q, HW_GAP) for h in hs):
                return False
        if any(rect_circle(h, px, py, pr + HW_GAP) for h in hs):
            return False
    return True


def pack(order, step=4):
    placed = []
    for n in order:
        r = L[n] / 2
        spot = None
        for y in range(int(Y0 + r), int(Y1 - r) + 1, step):
            for x in range(int(X1 - r), int(X0 + r) - 1, -step):
                if fits(x, y, r, placed):
                    spot = (n, x, y, r)
                    break
            if spot:
                break
        if not spot:
            return None
        placed.append(spot)
    return placed


def bar_ok(yc):
    band = (yc - BAR_H / 2, yc + BAR_H / 2)
    if band[0] < Y0 + EDGE or band[1] > Y1 - EDGE:
        return False
    return not any(band[0] < h[3] + 3 and h[2] - 3 < band[1] for h in HINGES)


def choose_bars(layout):
    """Fewest bar heights so every clip head is within STEM_MAX of one (greedy cover)."""
    targets = sorted([(y - r - HOOK_UP / 2, n, "hook") for n, x, y, r in layout] +
                     [(y + r + CRADLE_DOWN / 2, n, "cradle") for n, x, y, r in layout])
    bars, i = [], 0
    while i < len(targets):
        t = targets[i][0]
        yc = next((c for c in range(int(t + STEM_MAX), int(t - STEM_MAX) - 1, -1) if bar_ok(c)), None)
        assert yc is not None, f"no bar can reach {targets[i]}"
        bars.append(yc)
        while i < len(targets) and abs(targets[i][0] - yc) <= STEM_MAX:
            i += 1
    return bars


def backboard():
    """Board outline: the opening less EDGE, notched round the hinges."""
    x0, x1, y0, y1 = 20, 475, 17, 693
    (_, hx, a0, a1), (_, _, b0, b1) = HINGES
    hx += 5
    return [(x0, a1 + 5), (hx, a1 + 5), (hx, y0), (x1, y0), (x1, y1),
            (hx, y1), (hx, b0 - 5), (x0, b0 - 5)]


def search(names, budget=120):
    """First packing found over orderings of names, or None after budget seconds."""
    t0 = time.time()
    for order in itertools.permutations(sorted(names, key=lambda n: -L[n])):
        found = pack(order)
        if found or time.time() - t0 > budget:
            return found


layout = search(L)
if not layout:
    print("all seven don't fit with their clips; trying six")
    for drop in sorted(L, key=lambda n: L[n]):
        layout = search([n for n in L if n != drop], 60)
        if layout:
            print("left off the door:", drop)
            break
assert layout, "nothing fits"
bars = choose_bars(layout)
board = backboard()
for n, x, y, r in layout:
    print(f"{n}: Ø{2*r:.0f} centre ({x}, {y})")
print("bars at y =", bars)

# which bar each clip hangs off, and how far its head sits from it
clips = []
for n, x, y, r in layout:
    for kind, ty in (("hook", y - r), ("cradle", y + r)):
        mid = ty - HOOK_UP / 2 if kind == "hook" else ty + CRADLE_DOWN / 2
        yc = min(bars, key=lambda b: abs(b - mid))
        clips.append((n, kind, x, ty, yc, ty - yc))
for c in clips:
    print("  {} {:6} x {:5.1f} rim y {:6.1f}  bar {:4d}  stem {:+6.1f}".format(*c))
assert all(abs(c[5]) <= STEM_MAX + HOOK_UP for c in clips)

# ---- cad/layout.scad ----
with open("cad/layout.scad", "w") as f:
    f.write("// Generated by diagrams/door_layout.py; do not edit by hand.\n")
    f.write("// Door coordinates: x from the hinge edge, y down from the top.\n")
    f.write("lids = [" + ", ".join(f'["{n}", {x}, {y}, {2*r}]' for n, x, y, r in layout) + "];\n")
    f.write("bars = " + str(bars) + ";\n")
    f.write("bar_x = " + str(list(BAR_X)) + ";\n")
    f.write("clips = [" + ", ".join(f'["{n}", "{k}", {x}, {ty}, {yc}]' for n, k, x, ty, yc, _ in clips) + "];\n")
    f.write("board = " + str([list(p) for p in board]) + ";\n")

# ---- SVG: backboard on the left, bars on the right ----
W, H = DOOR
s = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -50 {2*W+80} {H+130}" font-family="Helvetica" font-size="16">',
     f'<rect x="-20" y="-50" width="{2*W+80}" height="{H+130}" fill="white"/>']
for ox, title in ((0, "A · Backboard"), (W + 40, "B · Printed bars")):
    g = [f'<g transform="translate({ox},0)">',
         f'<text x="0" y="-20" font-size="22" font-weight="bold">{title}</text>',
         f'<rect x="0" y="0" width="{W}" height="{H}" fill="#c9cfc6" stroke="#555" stroke-width="2"/>',
         f'<rect x="{FRAME}" y="{FRAME}" width="{W-2*FRAME}" height="{H-2*FRAME}" fill="#dde2da" stroke="#555"/>',
         f'<rect x="{X0}" y="{Y0}" width="{X1-X0}" height="{Y1-Y0}" fill="none" stroke="#888" stroke-dasharray="6 4"/>',
         f'<rect x="{X0}" y="{SHELF[0]}" width="{X1-X0}" height="{SHELF[1]-SHELF[0]}" fill="#888" fill-opacity=".35"/>']
    for a, b, c, d in HINGES:
        g.append(f'<rect x="{a}" y="{c}" width="{b-a}" height="{d-c}" fill="#e06" fill-opacity=".4"/>')
    if ox == 0:
        g.append('<polygon points="' + " ".join(f"{x},{y}" for x, y in board) +
                 '" fill="#b9c0c6" stroke="#556" stroke-width="2" fill-opacity=".9"/>')
    else:
        for yc in bars:
            g.append(f'<rect x="{BAR_X[0]}" y="{yc-BAR_H/2}" width="{BAR_X[1]-BAR_X[0]}" height="{BAR_H}" '
                     'fill="#5a6b78" fill-opacity=".85"/>')
    for n, x, y, r in layout:
        g.append(f'<circle cx="{x}" cy="{y}" r="{r}" fill="#fff" fill-opacity=".5" stroke="#246" stroke-width="3"/>')
        g.append(f'<text x="{x}" y="{y}" text-anchor="middle" dy="7" font-size="22">{n}</text>')
        for a, b, c, d in heads(x, y, r):
            g.append(f'<rect x="{a}" y="{c}" width="{b-a}" height="{d-c}" fill="#e88a1a"/>')
    if ox:
        for n, k, x, ty, yc, dy in clips:
            hy = ty - HOOK_UP / 2 if k == "hook" else ty + CRADLE_DOWN / 2
            if abs(hy - yc) > BAR_H / 2:
                g.append(f'<line x1="{x}" y1="{yc}" x2="{x}" y2="{hy}" stroke="#e88a1a" stroke-width="5"/>')
    g.append("</g>")
    s += g
s.append(f'<text x="0" y="{H+30}" font-size="15">495 × 715 door seen from inside, hinges (pink) on the left. Dashed: cupboard opening. '
         'Grey band: the shelf behind (55 clear).</text>')
s.append(f'<text x="0" y="{H+52}" font-size="15">Orange: clips (hook above each lid, cradle below). A: clips stuck/screwed to a {board[3][0]-board[0][0]} wide '
         f'aluminium board. B: {len(bars)} printed bars, stile to stile, clips on stems.</text>')
s.append("</svg>")
open("diagrams/door-layout.svg", "w").write("\n".join(s))
