# Compile the lodge plan into sealed shells, circulation, furnishings, and landscape.
from __future__ import annotations

import json
import math
import random
from pathlib import Path
from functools import lru_cache
from .plan import Plan, Volume

STATE_FILE = Path('/work/generator/MCRender/resources/1.21.1-blocks.json')
STATES = json.loads(STATE_FILE.read_text())
STONE = ('stone_bricks',) * 8 + ('andesite', 'cobblestone', 'mossy_stone_bricks')
ROCK = ('stone',) * 9 + ('andesite', 'tuff', 'cobblestone')
PAVING = ('stone_bricks',) * 9 + ('andesite', 'mossy_stone_bricks', 'cobblestone')
ROOF = ('deepslate_tiles',) * 12 + ('deepslate_bricks', 'cobbled_deepslate')
PALETTES = {
    'honey': ('smooth_sandstone', 'stripped_spruce_log', 'spruce_planks', 'dark_oak_log', 'yellow_stained_glass', 'orange'),
    'ivory': ('calcite', 'stripped_dark_oak_log', 'spruce_planks', 'dark_oak_log', 'white_stained_glass', 'green'),
    'ochre': ('smooth_sandstone', 'stripped_oak_log', 'oak_planks', 'spruce_log', 'yellow_stained_glass', 'red'),
}
DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}


@lru_cache(None)
def blockstate(name, properties=()):
    """Validate and canonicalize a vanilla state with explicit default properties."""
    name = name.removeprefix('minecraft:')
    if name not in STATES:
        raise ValueError(f'not a Java 1.21.1 block: {name}')
    domains, defaults = STATES[name]
    values = dict(defaults)
    for key, value in properties:
        if key not in domains or str(value) not in domains[key]:
            raise ValueError(f'invalid {name}[{key}={value}]')
        values[key] = str(value)
    return 'minecraft:' + name + ('[' + ','.join(f'{k}={v}' for k, v in sorted(values.items())) + ']' if values else '')


def name_of(state):
    return (state or 'minecraft:air').split('[')[0].removeprefix('minecraft:')


class Scene:
    """Sparse local-coordinate scene with shared spatial reservations and evidence."""

    def __init__(self, plan: Plan):
        self.plan = plan
        self.rng = random.Random(plan.seed)
        self.blocks = {}
        self.reserved = set()
        self.walks = set()
        self.windows = []
        self.doors = []
        self.beds = []
        self.stairs = []
        self.ladders = []
        self.rooms = []
        self.lights = []
        self.attachments = []
        self.enclosed = []
        self.ground = {}
        self.furniture = []
        self.plaster, self.timber, self.floor, self.beam, self.glass, self.color = PALETTES[plan.parameters['palette']]

    def p(self, x, y, z, name, **props):
        self.blocks[x, y, z] = blockstate(name, tuple(sorted(props.items())))

    def get(self, x, y, z):
        return name_of(self.blocks.get((x, y, z)))

    def box(self, bounds, name, **props):
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(x0, x1 + 1):
            for y in range(y0, y1 + 1):
                for z in range(z0, z1 + 1):
                    self.p(x, y, z, name, **props)

    def mix(self, bounds, palette):
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(x0, x1 + 1):
            for y in range(y0, y1 + 1):
                for z in range(z0, z1 + 1):
                    self.p(x, y, z, self.rng.choice(palette))

    def clear(self, bounds):
        self.box(bounds, 'air')

    def reserve(self, point, headroom=2):
        x, y, z = point
        self.walks.add(point)
        for dy in range(headroom):
            self.reserved.add((x, y + dy, z))

    def lamp(self, x, y, z, hanging=False):
        self.p(x, y, z, 'lantern', hanging=str(hanging).lower())
        self.lights.append((x, y, z))
        self.attachments.append(((x, y, z), (x, y + (1 if hanging else -1), z)))

    def door(self, x, y, z, facing):
        for half, dy in (('lower', 0), ('upper', 1)):
            self.p(x, y + dy, z, 'spruce_door', half=half, facing=facing)
        if self.get(x, y + 2, z) == 'air':
            self.p(x, y + 2, z, self.glass)
        self.doors.append((x, y, z))
        dx, dz = DIRECTIONS[facing]
        for side in (-1, 1):
            self.reserve((x + side * dx, y, z + side * dz))

    def stair(self, x, y, z, facing, material='spruce', walking=False):
        self.p(x, y, z, material + '_stairs', facing=facing)
        if walking:
            self.stairs.append((x, y, z, facing))
            self.reserve((x, y + 1, z), 3)

    def occupied_elsewhere(self, volume, x, y, z):
        return any(v is not volume and v.interior(x, y, z) for v in self.plan.volumes)

    def roof_covered(self, volume, x, y, z):
        return any(v is not volume and v.contains(x, z) and v.base <= y < v.roof(x, z) for v in self.plan.volumes)

    def route_reservations(self):
        """Reserve main axes, portals, furniture access, stairs, and ladder landings."""
        p = self.plan
        for v in p.volumes:
            if v.kind == 'dormer':
                for vv in range(v.v0 + 1, v.v1):
                    self.reserve(v.xyz(v.uc, v.base + 1, vv), 3)
                continue
            for floor in v.levels:
                for u in range(v.uc - 1, v.uc + 2):
                    for vv in range(v.v0 + 1, v.v1):
                        x, y, z = v.xyz(u, floor + 1, vv)
                        if y + 1 < v.roof(x, z):
                            self.reserve((x, y, z))
                # A crossing axis keeps side-wing interfaces connected.
                vv = (v.v0 + v.v1) // 2
                for u in range(v.u0 + 1, v.u1):
                    self.reserve(v.xyz(u, floor + 1, vv))
            if v.name == 'main':
                for floor in v.levels:
                    for u in range(v.u0 + 2, v.u1 - 1):
                        for vv in (v.v0 + 1, v.v0 + 2, v.v1 - 2, v.v1 - 1):
                            self.reserve(v.xyz(u, floor + 1, vv))
                sign = -1 if p.parameters['stair_side'] == 'left' else 1
                us = (v.uc + sign * 2, v.uc + sign * 3)
                for floor in v.levels:
                    for vv in range(v.v1 - 9, v.v1):
                        self.reserve(v.xyz(v.uc + sign * 5, floor + 1, vv))
                for floor in v.levels[:-1]:
                    for i in range(6):
                        vv = v.v1 - 8 + i
                        for u in us:
                            x, y, z = v.xyz(u, floor + i + 1, vv)
                            for yy in range(floor + 1, floor + 10):
                                self.reserved.add((x, yy, z))
                            self.reserve((x, y + 1, z), 3)
                    for u in range(min(us) - 1, max(us) + 2):
                        for vv in (v.v1 - 9, v.v1 - 2, v.v1 - 1):
                            self.reserve(v.xyz(u, floor + 1, vv))
                            self.reserve(v.xyz(u, floor + 7, vv))
            elif len(v.levels) > 1:
                x, _, z = v.xyz(v.uc, 0, v.v1 - 1)
                for y in range(v.base + 1, v.levels[-1] + 3):
                    self.reserved.add((x, y, z))
        px0, pz0, px1, _ = p.porch
        post_xs = {px0, px1}
        if px1 - px0 > 9:
            post_xs.update((p.entrance[0] - 3, p.entrance[0] + 3))
        for x in post_xs:
            for z in (pz0 - 1, pz0):
                for y in range(p.volumes[0].base + 1, p.volumes[0].base + 5):
                    self.reserved.add((x, y, z))
        for v in p.volumes:
            if v.kind == 'stone':
                f = v.levels[-1]
                zz = (v.z0 + v.z1) // 2
                for x in range(v.x1 - 1, v.x1 + 3):
                    for z in range(zz - 2, zz + 3):
                        self.reserve((x, f + 1, z), 3)
        for link in p.links:
            x, z, f = link['x'], link['z'], link['floor']
            for dx in range(-2, 3):
                for dz in range(-2, 3):
                    self.reserve((x + dx, f + 1, z + dz), 3)
        ex, ey, ez = p.entrance
        for x in range(ex - 1, ex + 2):
            for z in range(ez - 4, ez + 4):
                self.reserve((x, ey, z), 3)
        if p.parameters['balcony']:
            for x in range(ex - 3, ex + 4):
                for z in range(-3, 4):
                    self.reserve((x, ey + 6, z), 3)

    def terrain(self):
        """Grow solid crag columns and an irregular garden island around the plan."""
        plan = self.plan
        base = plan.volumes[0].base
        ox, _, oz = plan.origin
        xmin, xmax = 2 - ox, plan.size_xyz[0] - ox - 3
        zmin, zmax = 2 - oz, plan.size_xyz[2] - oz - 3
        masses = [v for v in plan.volumes if v.kind != 'dormer']
        cx = (xmin + xmax) / 2
        cz = (zmin + zmax) / 2
        for x in range(xmin, xmax + 1):
            for z in range(zmin, zmax + 1):
                corner = max(0, xmin + 4 - x, x - xmax + 4) + max(0, zmin + 4 - z, z - zmax + 4)
                if corner > 4 + self.rng.randrange(2):
                    continue
                dist = min(max(v.x0 - x, 0, x - v.x1) + max(v.z0 - z, 0, z - v.z1) for v in masses)
                h = 1
                if dist < 6 and z >= plan.terrace_front:
                    h = max(1, base - 1 - max(0, dist - 2) * (3 if base > 10 else 1))
                if dist <= 2:
                    h = base
                if plan.porch[0] - 2 <= x <= plan.porch[2] + 2 and plan.terrace_front <= z < 1:
                    h = base
                if x in (xmin, xmax) or z in (zmin, zmax):
                    h = 1
                seam = self.rng.choice(('stone', 'stone', 'andesite', 'tuff'))
                for y in range(h):
                    self.p(x, y, z, self.rng.choice((seam,) * 15 + ROCK) if base > 10 or y < h - 2 else 'dirt')
                self.p(x, h, z, self.rng.choice(('grass_block',) * 6 + ('moss_block', 'coarse_dirt')))
                self.ground[x, z] = h
        # Foundations, walks, retaining edges: all connected to terrain at y=0.
        for v in masses:
            self.mix((v.x0, 0, v.z0, v.x1, base - 1, v.z1), STONE)
        for x in range(plan.porch[0] - 1, plan.porch[2] + 2):
            for z in range(plan.terrace_front, 0):
                self.mix((x, 0, z, x, base, z), STONE)
                self.p(x, base, z, self.rng.choice(PAVING))
                self.ground[x, z] = base
        for x, z in list(self.ground):
            h = self.ground[x, z]
            if h == base and not any(v.contains(x, z) for v in masses):
                self.p(x, h, z, self.rng.choice(PAVING))
                edge = any(self.ground.get((x + dx, z + dz), -1) < base - 1 for dx, dz in DIRECTIONS.values())
                if edge and abs(x - plan.entrance[0]) > 3:
                    self.p(x, h + 1, z, 'cobblestone_wall')
                    if (x + z) % 6 == 0:
                        self.p(x, h + 1, z, 'stone_bricks')
                        self.p(x, h + 2, z, 'stone_brick_slab')
        for patch in plan.approach:
            x, z, y = patch['x'], patch['z'], patch['y']
            for xx in range(x - 2, x + 3):
                self.mix((xx, 0, z, xx, y - 1, z), STONE)
                self.clear((xx, y, z, xx, max(base + 2, y + 3), z))
                if patch['kind'] == 'step':
                    self.stair(xx, y, z, 'south', 'stone_brick', True)
                else:
                    self.p(xx, y, z, self.rng.choice(PAVING))
                    self.reserve((xx, y + 1, z), 3)
                self.ground[xx, z] = y
            if z < plan.terrace_front:
                for xx in (x - 3, x + 3):
                    self.mix((xx, 0, z, xx, y, z), STONE)
                    self.p(xx, y + 1, z, 'stone_brick_slab')
                    if (z - plan.approach[0]['z']) % 5 == 0:
                        self.p(xx, y + 1, z, 'stone_bricks')
                        self.lamp(xx, y + 2, z)

    def shells(self):
        """Union room envelopes before placing roof planes and real timber frames."""
        for v in self.plan.volumes:
            for f in v.levels:
                self.box((v.x0, f, v.z0, v.x1, f, v.z1), self.floor if v.kind != 'glass' else 'polished_andesite')
            for x in range(v.x0, v.x1 + 1):
                for z in range(v.z0, v.z1 + 1):
                    if x not in (v.x0, v.x1) and z not in (v.z0, v.z1):
                        continue
                    u, vv = v.uv(x, z)
                    top = v.roof(x, z)
                    for y in range(v.base + 1, top):
                        if self.occupied_elsewhere(v, x, y, z):
                            continue
                        frame = x in (v.x0, v.x1) and z in (v.z0, v.z1)
                        frame |= ((x - v.x0) % 4 == 0 and z in (v.z0, v.z1)) or ((z - v.z0) % 4 == 0 and x in (v.x0, v.x1))
                        belt = y in v.levels or y == v.levels[-1] + (1 if v.attic else 4)
                        name = self.timber if frame else self.beam if belt else 'glass' if v.kind == 'glass' else self.rng.choice(STONE) if v.kind == 'stone' else self.plaster
                        self.p(x, y, z, name, **({'axis': 'y' if frame else 'x' if z in (v.z0, v.z1) else 'z'} if name.endswith('log') else {}))
                    if v.kind not in ('glass', 'stone', 'timber'):
                        # Timber follows both gable verges below the slate.
                        if vv in (v.v0, v.v1) and not self.occupied_elsewhere(v, x, top - 1, z):
                            self.p(x, top - 1, z, self.timber, axis=v.axis)
            for f in v.levels:
                for x in range(v.x0, v.x1 + 1):
                    for z in (v.z0, v.z1):
                        if not self.occupied_elsewhere(v, x, f, z):
                            self.p(x, f, z, self.beam, axis='x')
                for z in range(v.z0, v.z1 + 1):
                    for x in (v.x0, v.x1):
                        if not self.occupied_elsewhere(v, x, f, z):
                            self.p(x, f, z, self.beam, axis='z')
        for v in self.plan.volumes:
            overhang = 1 if v.kind == 'glass' else 2
            for x in range(v.x0 - overhang, v.x1 + overhang + 1):
                for z in range(v.z0 - overhang, v.z1 + overhang + 1):
                    h = v.roof(x, z)
                    u, vv = v.uv(x, z)
                    toward = 'east' if u < v.uc else 'west'
                    if v.axis == 'x':
                        toward = 'south' if u < v.uc else 'north'
                    if v.kind in ('stone', 'timber', 'glass'):
                        ds = [(x - v.x0, 'east'), (v.x1 - x, 'west'), (z - v.z0, 'south'), (v.z1 - z, 'north')]
                        toward = min(ds)[1]
                    dx, dz = DIRECTIONS[toward]
                    low = min(h, v.roof(x - dx, z - dz))
                    for y in range(low, h + 1):
                        if not self.roof_covered(v, x, y, z):
                            self.p(x, y, z, 'glass' if v.kind == 'glass' else self.rng.choice(ROOF))
                    if v.kind != 'glass' and not self.roof_covered(v, x, h + 1, z):
                        if u == v.uc and v.kind not in ('stone', 'timber'):
                            self.p(x, h + 1, z, 'deepslate_tile_slab')
                        else:
                            edge = x in (v.x0 - overhang, v.x1 + overhang) or z in (v.z0 - overhang, v.z1 + overhang)
                            self.stair(x, h + 1, z, toward, 'cobbled_deepslate' if edge else 'deepslate_tile')
            # Carved ridge ends, anchored into the roof cap.
            if v.kind not in ('glass', 'stone', 'timber'):
                for vv in (v.v0 - 2, v.v1 + 2):
                    x, _, z = v.xyz(v.uc, 0, vv)
                    h = v.roof(x, z)
                    if not self.roof_covered(v, x, h + 2, z):
                        self.p(x, h + 1, z, self.timber, axis=v.axis)
                        self.p(x, h + 2, z, 'oak_fence')
                        if v.name == 'main' and self.plan.parameters['profile'] != 'chalet':
                            self.p(x, h + 3, z, 'lightning_rod')

    def opening(self, x, f, z, face, width=2):
        """Glaze an exterior bay only when it has clear indoor and outdoor space."""
        dx, dz = DIRECTIONS[face]
        cells = [(x + (i if dz else 0), y, z + (i if dx else 0)) for i in range(width) for y in (f + 2, f + 3)]
        inside = [(a - dx, b, c - dz) for a, b, c in cells]
        outside = [(a + dx, b, c + dz) for a, b, c in cells]
        if any(c in self.reserved for c in cells):
            return
        if any(any(v.interior(*q) for v in self.plan.volumes) for q in outside):
            return
        if any(self.get(*q) not in ('air',) for q in inside + outside):
            return
        if any(self.get(*q) in ('air', 'glass') for q in cells):
            return
        for q in cells:
            self.p(*q, self.glass)
        self.windows.append({'cells': cells, 'inside': inside, 'outside': outside})
        self.reserved.update(inside + outside + cells)
        for i in range(width):
            xx, zz = x + (i if dz else 0), z + (i if dx else 0)
            self.p(xx + dx, f + 1, zz + dz, 'spruce_trapdoor', facing=face, half='top')
            self.attachments.append(((xx + dx, f + 1, zz + dz), (xx, f + 1, zz)))
        # Compact shutters anchor to wall blocks, never cover the glazing.
        for i in (-1, width):
            xx, zz = x + (i if dz else 0), z + (i if dx else 0)
            for y in (f + 2, f + 3):
                q = (xx + dx, y, zz + dz)
                if self.get(xx, y, zz) != 'air' and self.get(*q) == 'air' and q not in self.reserved:
                    self.p(*q, 'spruce_trapdoor', facing=face, open='true')
                    self.attachments.append((q, (xx, y, zz)))

    def gable_details(self):
        """Frame vaulted attic gables with tall paired lights and collar beams."""
        v = self.plan.volumes[0]
        f = v.levels[-1]
        for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')):
            dx, dz = DIRECTIONS[face]
            peak = v.roof(*v.xyz(v.uc, 0, vv)[::2])
            for y in (f + 6, f + 12):
                for u in range(v.u0 + 2, v.u1 - 1):
                    x, _, z = v.xyz(u, 0, vv)
                    if y < v.roof(x, z) - 2 and not self.occupied_elsewhere(v, x, y, z):
                        self.p(x, y, z, self.beam, axis='x' if v.axis == 'z' else 'z')
            for u in (v.uc - 1, v.uc + 1):
                x, _, z = v.xyz(u, 0, vv)
                top = min(f + 9, v.roof(x, z) - 3)
                cells = [(x, y, z) for y in range(f + 3, top + 1)]
                inside = [(x - dx, y, z - dz) for _, y, _ in cells]
                outside = [(x + dx, y, z + dz) for _, y, _ in cells]
                if cells and all(q not in self.reserved and self.get(*q) == 'air' for q in inside + outside) and not any(any(other.interior(*q) for other in self.plan.volumes) for q in outside):
                    for q in cells:
                        self.p(*q, self.glass)
                    self.windows.append({'cells': cells, 'inside': inside, 'outside': outside})
                    self.reserved.update(cells + inside + outside)
            x, _, z = v.xyz(v.uc, 0, vv)
            for y in range(f + 2, peak):
                if not self.occupied_elsewhere(v, x, y, z):
                    self.p(x, y, z, self.timber, axis='y')
        # Corbels and small low lamps make the timber frame read in relief.
        for v in self.plan.volumes:
            if v.kind in ('glass', 'dormer'):
                continue
            for f in v.levels[1:]:
                for x in range(v.x0, v.x1 + 1, 4):
                    for z, face in ((v.z0 - 1, 'south'), (v.z1 + 1, 'north')):
                        dx, dz = DIRECTIONS[face]
                        if self.get(x + dx, f - 1, z + dz) != 'air':
                            self.exterior_p(x, f - 1, z, 'spruce_stairs', facing=face, half='top')
            for x in (v.x0, v.x1):
                z = v.z0 - 1
                y = v.base + 3
                if all(self.get(x, yy, z) == 'air' and (x, yy, z) not in self.reserved for yy in (y, y + 1)) and not any(other.contains(x, z) for other in self.plan.volumes):
                    self.p(x, y + 1, z, self.beam, axis='z')
                    self.lamp(x, y, z, True)

    def glazing(self):
        for v in self.plan.volumes:
            if v.kind == 'glass':
                continue
            for f in v.levels:
                for face, z in (('north', v.z0), ('south', v.z1)):
                    for x in range(v.x0 + 2, v.x1 - 1, 4):
                        self.opening(x, f, z, face)
                for face, x in (('west', v.x0), ('east', v.x1)):
                    for z in range(v.z0 + 2, v.z1 - 1, 4):
                        self.opening(x, f, z, face)
            if v.attic:
                for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')):
                    x, f, z = v.xyz(v.uc - 1, v.levels[-1], vv)
                    self.opening(x, f + 1, z, face, 3)

    def openings(self):
        for link in self.plan.links:
            x, z, f = link['x'], link['z'], link['floor']
            if link['axis'] == 'x':
                self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1))
                self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor)
            else:
                self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1))
                self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor)
        x, y, z = self.plan.entrance
        self.clear((x, y, z - 1, x, y + 2, z + 1))
        self.door(x, y, z, 'north')
        for xx in (x - 1, x + 1):
            self.box((xx, y, z - 1, xx, y + 2, z - 1), self.timber, axis='y')
        self.box((x - 1, y + 3, z - 1, x + 1, y + 3, z - 1), self.beam, axis='x')

    def circulation(self):
        """Cut stacked stairs and supported ladders after all intersecting roofs."""
        for v in self.plan.volumes:
            if v.name == 'main':
                sign = -1 if self.plan.parameters['stair_side'] == 'left' else 1
                for f in v.levels[:-1]:
                    for i in range(6):
                        for u in (v.uc + 2 * sign, v.uc + 3 * sign):
                            x, y, z = v.xyz(u, f + i + 1, v.v1 - 8 + i)
                            self.clear((x, y + 1, z, x, y + 3, z))
                            self.p(x, y - 1, z, self.floor)
                            self.stair(x, y, z, 'south' if v.axis == 'z' else 'east', walking=True)
                    for u in (v.uc + 2 * sign, v.uc + 3 * sign):
                        for vv in (v.v1 - 2, v.v1 - 1):
                            x, _, z = v.xyz(u, 0, vv)
                            self.p(x, f + 6, z, self.floor)
                            self.clear((x, f + 7, z, x, f + 9, z))
                # Outer guard on each stair opening, leaving return aisles clear.
                for f in v.levels[1:]:
                    for vv in range(v.v1 - 7, v.v1 - 2):
                        u = v.uc + 4 * sign
                        x, y, z = v.xyz(u, f + 1, vv)
                        if self.get(x, f, z) != 'air' and (x, y, z) not in self.reserved and y + 2 < v.roof(x, z):
                            self.p(x, y, z, 'spruce_fence')
            elif len(v.levels) > 1:
                x, _, z = v.xyz(v.uc, 0, v.v1 - 1)
                bx, _, bz = v.xyz(v.uc, 0, v.v1)
                self.box((bx, v.base + 1, bz, bx, v.levels[-1] + 3, bz), self.beam, axis='y')
                for y in range(v.base + 1, v.levels[-1] + 3):
                    self.p(x, y, z, 'ladder', facing='north' if v.axis == 'z' else 'west')
                    self.ladders.append((x, y, z))
                for f in v.levels:
                    u = v.uc - 1
                    xx, yy, zz = v.xyz(u, f + 1, v.v1 - 1)
                    self.clear((xx, yy, zz, xx, yy + 1, zz))
                    self.reserve((xx, yy, zz))

    def room(self, v, f, purpose, bounds, target):
        self.rooms.append({'volume': v.name, 'floor': f, 'purpose': purpose, 'bounds': bounds, 'target': target, 'furniture': [], 'lights': []})
        self.reserve(target)
        return self.rooms[-1]

    def furnish_cell(self, room, x, y, z, name, **props):
        if (x, y, z) in self.reserved or self.get(x, y, z) != 'air' or self.get(x, y - 1, z) == 'air':
            return False
        self.p(x, y, z, name, **props)
        room['furniture'].append((x, y, z))
        self.furniture.append((x, y, z))
        return True

    def bed(self, room, x, y, z, facing):
        dx, dz = DIRECTIONS[facing]
        pts = [(x, y, z), (x + dx, y, z + dz)]
        if any(q in self.reserved or self.get(*q) != 'air' or not self.get(q[0], y - 1, q[2]) or self.get(q[0], y - 1, q[2]) == 'air' or self.get(q[0], y + 1, q[2]) != 'air' for q in pts):
            return False
        for q, part in zip(pts, ('foot', 'head')):
            self.p(*q, self.color + '_bed', facing=facing, part=part)
            room['furniture'].append(q)
        self.beds.append(pts[0])
        for xx, yy, zz in pts:
            for a, b in DIRECTIONS.values():
                if all(self.get(xx + a, yy + dy, zz + b) == 'air' for dy in (0, 1)):
                    self.reserve((xx + a, yy, zz + b))
        return True

    def interiors(self):
        """Allocate functions to actual rooms, with layouts responding to the plan."""
        for v in self.plan.volumes:
            if v.kind == 'dormer':
                # Its floor continues the furnished attic, with a useful window bench.
                continue
            for level, f in enumerate(v.levels):
                layout = self.plan.parameters['layout']
                vc = (v.v0 + v.v1) // 2 + (2 if layout == 'suites' else 0)
                segments = [(v.v0 + 1, v.v1 - 1)]
                if v.name == 'main' and level < v.floors and layout != 'open':
                    segments = [(v.v0 + 1, vc - 1), (vc + 1, v.v1 - 1)]
                    for u in range(v.u0 + 1, v.u1):
                        for y in range(f + 1, f + 6):
                            x, _, z = v.xyz(u, y, vc)
                            if abs(u - v.uc) <= 1 or (x, y, z) in self.reserved:
                                continue
                            self.p(x, y, z, self.timber if u % 4 == 0 else self.plaster, **({'axis': 'y'} if u % 4 == 0 else {}))
                    # A framed three-wide arch leaves the central hall continuous.
                    for u in range(v.uc - 1, v.uc + 2):
                        x, _, z = v.xyz(u, 0, vc)
                        self.p(x, f + 4, z, self.beam, axis='x' if v.axis == 'z' else 'z')
                    if layout == 'suites' and level > 0:
                        for u in (v.uc - 1, v.uc + 1):
                            for yy in range(f + 1, f + 6):
                                self.p(*v.xyz(u, yy, vc), self.timber, axis='y')
                        self.door(*v.xyz(v.uc, f + 1, vc), 'south' if v.axis == 'z' else 'east')
                for index, (va, vb) in enumerate(segments):
                    if v.kind == 'glass':
                        purpose = 'winter garden and herbal workroom'
                    elif v.name == 'main':
                        purpose = ('hearth kitchen and dining', 'parlour and pantry')[index] if level == 0 else 'bedchamber and study' if level == len(v.levels) - 1 else ('guest suite' if layout == 'suites' else 'library and workroom')
                    elif v.name == 'tower':
                        purpose = ('pantry', 'map room', 'watchkeeper bedroom', 'archive', 'lookout study')[min(level, 4)]
                    else:
                        purpose = 'guest bedroom' if level else self.rng.choice(('craft workshop', 'dining parlour', 'library'))
                    ua, ub = v.u0 + 1, v.u1 - 1
                    target = v.xyz(v.uc, f + 1, (va + vb) // 2)
                    x0, _, z0 = v.xyz(ua, 0, va)
                    x1, _, z1 = v.xyz(ub, 0, vb)
                    room = self.room(v, f, purpose, (x0, f + 1, z0, x1, f + 4, z1), target)
                    # Furnishing banks on both sides of the protected longitudinal hall.
                    spots = []
                    for u in (ua + 1, ub - 1):
                        for vv in range(va + 1, vb, 2):
                            x, y, z = v.xyz(u, f + 1, vv)
                            if y + 3 < v.roof(x, z) and all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)):
                                spots.append((x, y, z))
                    # The minimum room width always admits perimeter furniture, even at ladders.
                    for x in range(x0, x1 + 1):
                        for z in range(z0, z1 + 1):
                            y = f + 1
                            if all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)) and self.get(x, f, z) != 'air':
                                spots.append((x, y, z))
                    # Bound furniture density and retain walkable gaps, including low attics.
                    spaced = []
                    for q in spots:
                        if all(abs(q[0] - r[0]) + abs(q[2] - r[2]) >= 3 for r in spaced):
                            spaced.append(q)
                    spots = spaced[:max(4, min(12, ((ub - ua + 1) * (vb - va + 1)) // 16))]
                    if not spots:
                        raise AssertionError(f'no furnishing positions in {v.name} {f}')
                    # Every room has a low supported lamp and a second task light when large.
                    for lamp_index in sorted(set((0, len(spots) - 1))):
                        x, y, z = spots[lamp_index]
                        self.p(x, y, z, 'barrel', facing='up')
                        room['furniture'].append((x, y, z))
                        self.lamp(x, y + 1, z)
                        room['lights'].append((x, y + 1, z))
                    candidates = spots[1:-1] or spots[1:]
                    wants_bed = 'bed' in purpose or 'suite' in purpose
                    if wants_bed:
                        found = False
                        for x, y, z in candidates:
                            if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'):
                                found = True
                                break
                        if not found:
                            for u in range(ua, ub + 1):
                                for vv in range(va, vb):
                                    x, y, z = v.xyz(u, f + 1, vv)
                                    if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'):
                                        found = True
                                        break
                                if found:
                                    break
                        assert found, f'bed has no valid location in {v.name} {f}'
                    furnishings = ('smoker', 'crafting_table', 'barrel', 'furnace', 'bookshelf') if 'kitchen' in purpose else ('composter', 'moss_block', 'barrel', 'crafting_table') if v.kind == 'glass' else ('bookshelf', 'cartography_table', 'barrel', 'loom', 'chest')
                    for i, (x, y, z) in enumerate(candidates):
                        name = furnishings[i % len(furnishings)]
                        props = {'facing': 'south'} if name in ('smoker', 'furnace', 'chest', 'loom', 'barrel') else {}
                        if name in ('smoker', 'furnace'):
                            props['lit'] = 'true'
                        if self.furnish_cell(room, x, y, z, name, **props):
                            if i % 2 == 0 and name not in ('composter', 'chest') and (x, y + 1, z) not in self.reserved:
                                if name == 'bookshelf' and self.get(x, y + 2, z) == 'air':
                                    self.p(x, y + 1, z, 'bookshelf')
                                else:
                                    self.p(x, y + 1, z, 'potted_fern' if v.kind != 'glass' else self.rng.choice(('potted_azalea_bush', 'potted_poppy', 'potted_dandelion')))
                                    self.attachments.append(((x, y + 1, z), (x, y, z)))
                    # A table and facing seat fill a front-side bay without covering the hall.
                    for offset in (() if wants_bed else (-3, 3)):
                        x, y, z = v.xyz(v.uc + offset, f + 1, va + 1)
                        if self.furnish_cell(room, x, y, z, self.floor):
                            if (x, y + 1, z) not in self.reserved and self.get(x, y + 1, z) == 'air':
                                self.p(x, y + 1, z, 'potted_spruce_sapling')
                                self.attachments.append(((x, y + 1, z), (x, y, z)))
                            sx, sy, sz = v.xyz(v.uc + offset, f + 1, va + 2)
                            self.furnish_cell(room, sx, sy, sz, 'spruce_stairs', facing='north' if v.axis == 'z' else 'west')
                    # Wool inlays provide colour without reducing headroom.
                    for u in range(v.uc - 1, v.uc + 2):
                        for vv in range(va + 2, min(vb, va + 5)):
                            x, _, z = v.xyz(u, 0, vv)
                            if self.get(x, f, z).endswith('planks'):
                                self.p(x, f, z, self.color + '_wool')
                    self.enclosed.append(target)

    def dormer_nooks(self):
        """Furnish and verify the accessible reading bays extending the attic."""
        main = self.plan.volumes[0]
        for v in self.plan.volumes:
            if v.kind != 'dormer':
                continue
            exterior_v = v.v0 + 1 if v.v0 < main.uc else v.v1 - 1
            target = v.xyz(v.uc, v.base + 1, exterior_v)
            room = self.room(v, v.base, 'dormer reading nook', (v.x0 + 1, v.base + 1, v.z0 + 1, v.x1 - 1, v.base + 4, v.z1 - 1), target)
            # The parent attic remains open behind the bay. Its own small lamp is near the floor.
            for vv in range(v.v0 + 1, v.v1):
                for u in (v.uc - 1, v.uc + 1):
                    x, y, z = v.xyz(u, v.base + 1, vv)
                    if all(self.get(x, y + dy, z) == 'air' and (x, y + dy, z) not in self.reserved for dy in (0, 1)):
                        self.p(x, y, z, 'bookshelf')
                        self.lamp(x, y + 1, z)
                        room['furniture'].append((x, y, z))
                        room['lights'].append((x, y + 1, z))
                        break
                if room['lights']:
                    break
            assert room['lights'], f'no task-light position for {v.name}'
            self.enclosed.append(target)

    def chimney(self):
        """Run a masonry chimney from a supported hearth through the roof shoulder."""
        v = self.plan.volumes[0]
        x, z, f = v.x0 + 1, v.z1 - 2, v.base
        h = max(v.roof(x, z), v.roof(x + 1, z)) + 5
        self.mix((x, f + 1, z, x + 1, h, z + 1), STONE)
        for yy in (h - 3, h):
            self.box((x - 1, yy, z - 1, x + 2, yy, z + 2), 'stone_bricks')
        self.p(x, h + 1, z, 'campfire', lit='true')
        for xx, zz in ((x - 1, z - 1), (x + 2, z - 1), (x - 1, z + 2), (x + 2, z + 2)):
            self.p(xx, h + 1, zz, 'stone_brick_wall')
        self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_bricks')
        # A contained hearth opens to the room, with a proper masonry hood.
        for xx in (x, x + 1):
            self.p(xx, f + 1, z, 'campfire', lit='true')
            self.p(xx, f + 2, z, 'air')
            self.p(xx, f + 1, z - 1, 'iron_bars', east='true', west='true')
            self.p(xx, f + 2, z - 1, 'iron_bars', east='true', west='true')
        self.box((x, f + 3, z - 1, x + 1, f + 3, z - 1), 'stone_bricks')

    def exterior_p(self, x, y, z, name, **props):
        if any(v.contains(x, z) and v.base < y <= v.roof(x, z) + 1 for v in self.plan.volumes):
            return
        if (x, y, z) in self.reserved or self.get(x, y, z) != 'air':
            return
        self.p(x, y, z, name, **props)

    def porches(self):
        """Build post-supported canopies, optional balcony, and an open side gallery."""
        p = self.plan
        f = p.volumes[0].base
        ex = p.entrance[0]
        x0, z0, x1, z1 = p.porch
        self.box((x0, f, z0, x1, f, z1), self.floor)
        for x in range(x0 - 1, x1 + 2):
            for z in range(z0 - 1, 0):
                balcony = p.parameters['balcony'] and ex - 3 <= x <= ex + 3 and -3 <= z <= -1
                if not balcony:
                    h = f + 5 + (z - z0) // 2
                    self.exterior_p(x, h, z, self.rng.choice(ROOF))
                    self.exterior_p(x, h + 1, z, 'deepslate_tile_slab')
        posts = {x0, x1}
        if x1 - x0 > 9:
            posts.update((ex - 3, ex + 3))
        for x in sorted(posts):
            if abs(x - ex) <= 1 or any(v.contains(x, z0) for v in p.volumes):
                continue
            self.mix((x, 0, z0, x, f, z0), STONE)
            self.box((x, f + 1, z0, x, f + 4, z0), self.timber, axis='y')
            self.p(x, f + 4, z0 - 1, self.floor)
            self.lamp(x, f + 3, z0 - 1, True)
            for dx in (-1, 1):
                if x0 <= x + dx <= x1:
                    self.p(x + dx, f + 4, z0, 'spruce_stairs', facing='east' if dx < 0 else 'west', half='top')
        for x in range(x0, x1 + 1):
            self.exterior_p(x, f + 4, z0, self.beam, axis='x')
        for x in range(x0 + 1, x1):
            if abs(x - ex) > 2:
                self.p(x, f + 1, z0, 'spruce_fence')
        if p.parameters['balcony']:
            self.box((ex - 3, f + 6, -3, ex + 3, f + 6, -1), self.floor)
            for x in range(ex - 3, ex + 4):
                self.p(x, f + 7, -3, 'spruce_fence')
            for x in (ex - 3, ex + 3):
                self.box((x, f + 1, -3, x, f + 6, -3), self.timber, axis='y')
                for z in (-2, -1):
                    self.p(x, f + 7, z, 'spruce_fence')
            self.clear((ex, f + 7, -2, ex, f + 9, 1))
            self.door(ex, f + 7, 0, 'north')
            self.rooms.append({'volume': 'balcony', 'floor': f + 6, 'purpose': 'open balcony', 'bounds': (ex - 2, f + 7, -2, ex + 2, f + 9, -1), 'target': (ex, f + 7, -2), 'furniture': [], 'lights': []})
            self.p(ex - 2, f + 7, -1, 'barrel', facing='up')
            self.lamp(ex - 2, f + 8, -1)
            self.rooms[-1]['furniture'].append((ex - 2, f + 7, -1))
            self.rooms[-1]['lights'].append((ex - 2, f + 8, -1))
        if p.parameters['porch'] == 'wrap':
            main = p.volumes[0]
            # Choose a free facade interface at planning time.
            side = p.parameters['wrap_side']
            xa, xb = (-4, -1) if side == 'left' else (main.x1 + 1, main.x1 + 4)
            for x in range(xa, xb + 1):
                for z in range(-3, main.z1 + 1):
                    self.mix((x, 0, z, x, f - 1, z), STONE)
                    self.p(x, f, z, self.floor)
                    h = f + 5 + (x - xa) // 2 if side == 'left' else f + 5 + (xb - x) // 2
                    self.exterior_p(x, h, z, self.rng.choice(ROOF))
                    self.exterior_p(x, h + 1, z, 'deepslate_tile_slab')
            outer = xa if side == 'left' else xb
            for z in range(-2, main.z1 + 1, 5):
                self.box((outer, f + 1, z, outer, f + 4, z), self.timber, axis='y')
                self.p(outer, f + 5, z, self.beam, axis='z')
                self.lamp(outer, f + 3, z + 1, True)
                self.p(outer, f + 4, z + 1, self.floor)
            for z in range(0, main.z1):
                if self.get(outer, f + 1, z) == 'air':
                    self.p(outer, f + 1, z, 'spruce_fence')
        # Stone tower lookout: a glazed room with an east balcony and railed ledges.
        for v in p.volumes:
            if v.kind != 'stone':
                continue
            top = v.levels[-1]
            for x in range(v.x0 - 1, v.x1 + 2):
                for z in range(v.z0 - 1, v.z1 + 2):
                    if v.contains(x, z) or any(other is not v and other.contains(x, z, 2) and top <= other.roof(x, z) + 2 for other in p.volumes):
                        continue
                    self.p(x, top, z, self.floor)
                    self.p(x, top + 1, z, 'oak_fence')
                    if x in (v.x0 - 1, v.x1 + 1) and z in (v.z0, v.z1):
                        self.p(x, top - 1, z, 'spruce_stairs', facing='east' if x < v.x0 else 'west', half='top')
            # East gallery has a two-wide deck so the rail does not block the door.
            zz = (v.z0 + v.z1) // 2
            for z in range(zz - 2, zz + 3):
                self.p(v.x1 + 2, top, z, self.floor)
                self.p(v.x1 + 2, top + 1, z, 'oak_fence')
                self.p(v.x1 + 1, top + 1, z, 'air')
            self.door(v.x1, top + 1, zz, 'east')
            self.reserve((v.x1 - 1, top + 1, zz))

    def tree(self, x, z, height):
        base = self.ground[x, z]
        self.box((x, base + 1, z, x, base + height - 2, z), 'spruce_log', axis='y')
        for rise in range(4, height, 3):
            radius = max(1, min(3, (height - rise + 3) // 5))
            for dx, dz in DIRECTIONS.values():
                for i in range(1, radius):
                    xx, yy, zz = x + dx * i, base + rise, z + dz * i
                    if self.get(xx, yy, zz) == 'air':
                        self.p(xx, yy, zz, 'spruce_log', axis='x' if dx else 'z')
            for dy in (0, 1):
                r = max(1, radius - dy)
                for dx in range(-r, r + 1):
                    for dz in range(-r, r + 1):
                        if abs(dx) + abs(dz) > r + 1 or (abs(dx) == abs(dz) == r and r > 1):
                            continue
                        xx, yy, zz = x + dx, base + rise + dy, z + dz
                        if self.get(xx, yy, zz) == 'air' and (xx, yy, zz) not in self.reserved:
                            self.p(xx, yy, zz, 'spruce_leaves', persistent='true')
        for yy in range(base + height - 1, base + height + 2):
            self.p(x, yy, z, 'spruce_leaves', persistent='true')

    def landscape(self):
        """Plant grounded conifers and garden borders while preserving every opening."""
        for x, z, height in self.plan.tree_sites:
            self.tree(x, z, height)
        for (x, z), y in sorted(self.ground.items()):
            if self.get(x, y, z) not in ('grass_block', 'moss_block', 'coarse_dirt') or self.get(x, y + 1, z) != 'air' or (x, y + 1, z) in self.reserved:
                continue
            n = self.rng.random()
            if n < .14:
                self.p(x, y + 1, z, self.rng.choice(('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet')))
                self.attachments.append(((x, y + 1, z), (x, y, z)))
            elif n < .20:
                self.p(x, y + 1, z, self.rng.choice(('azalea_leaves', 'flowering_azalea_leaves')), persistent='true')
            elif n < .23:
                self.p(x, y + 1, z, 'mossy_cobblestone')
        # Vertical ivy grows only against an actual solid corner post.
        for v in self.plan.volumes:
            if v.kind in ('dormer', 'glass'):
                continue
            for x, z, face in ((v.x0, v.z0 - 1, 'south'), (v.x1 + 1, v.z1, 'west')):
                dx, dz = DIRECTIONS[face]
                for y in range(v.base + 1, min(v.levels[-1] + 4, v.base + 13)):
                    if self.get(x, y, z) == 'air' and (x, y, z) not in self.reserved and self.get(x + dx, y, z + dz).endswith('log'):
                        self.p(x, y, z, 'vine', **{face: 'true'})
                        self.attachments.append(((x, y, z), (x + dx, y, z + dz)))
        # Entrance heraldry has physical textile blocks, visible without block entity NBT.
        if self.plan.parameters['floors'] >= 2:
            v = self.plan.volumes[0]
            for x in (v.x0 + 1, v.x1 - 1):
                for y in range(v.base + 8, v.base + 12):
                    z = -1
                    if (x, y, z) not in self.reserved and self.get(x, y, z) == 'air' and self.get(x, y, 0) != 'air':
                        self.p(x, y, z, self.color + '_wool')
                self.p(x, v.base + 12, -1, self.beam, axis='z')

    def connections(self):
        """Resolve fence arms from final neighboring geometry."""
        for (x, y, z), state in list(self.blocks.items()):
            name = name_of(state)
            if name.endswith('_fence'):
                props = {}
                for face, (dx, dz) in DIRECTIONS.items():
                    neighbor = self.get(x + dx, y, z + dz)
                    props[face] = 'true' if neighbor.endswith(('_fence', '_log', '_planks')) or neighbor in ('stone_bricks', 'cobblestone') else 'false'
                self.p(x, y, z, name, **props)

    def build(self):
        self.route_reservations()
        v = self.plan.volumes[0]
        for x in range(v.x0, v.x0 + 4):
            for z in range(v.z1 - 3, v.z1 + 2):
                for y in range(v.base + 1, self.plan.size_xyz[1]):
                    self.reserved.add((x, y, z))
        self.terrain()
        self.shells()
        self.openings()
        self.circulation()
        self.chimney()
        self.porches()
        self.gable_details()
        self.glazing()
        self.interiors()
        self.dormer_nooks()
        self.landscape()
        self.connections()
        return self
