# Generate a furnished six-level storybook house from the supplied exterior reference.
# Bounding box: X=0..46, Y=0..58, Z=0..40 (47 x 59 x 41 blocks).
# Reference reading: six warm timber/plaster levels, 3:1 slate roof, projecting dormers,
# low entrance wing, stepped masonry gardens, tall sparse conifers, amber glazing.
# The unseen rear repeats the frame; a backed ladder connects all six inhabited floors.
# Everything that has to be right for the harness is already right here: bounds checked placement,
# one seeded random source, and a byte reproducible export.
from __future__ import annotations

import argparse
import gzip
import random
import sys
from pathlib import Path
from typing import Dict, Iterable, Optional, Tuple

sys.path.insert(0, "/work/generator/MCIO")

from mcio.schematic import load_schematic # noqa: E402
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402

CONFIG = {
    "seed": 20260923,
    "size_xyz": (47, 59, 41), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "The Lanternkeeper's House",
    "author": "generator",
    "description": "Six furnished storeys under a steep slate roof, with dormers and terraced conifer garden",
}

FLOORS = (6, 13, 20, 27, 34, 41)
CENTER_X = 25
ROOF_PEAK = 51
ROOF_PITCH = 3
STONE = ('minecraft:stone_bricks',) * 5 + ('minecraft:cobblestone',) * 3 + ('minecraft:mossy_cobblestone',) * 2
PAVING = ('minecraft:stone_bricks',) * 5 + ('minecraft:andesite',) * 2 + ('minecraft:mossy_stone_bricks',)
EARTH = ('minecraft:dirt',) * 4 + ('minecraft:coarse_dirt',) * 2 + ('minecraft:rooted_dirt',)
SOIL = ('minecraft:grass_block',) * 6 + ('minecraft:podzol',) * 2 + ('minecraft:coarse_dirt',)
PLASTER = ('minecraft:smooth_sandstone',) * 8 + ('minecraft:sandstone',) * 2 + ('minecraft:white_terracotta',)
WOOD = ('minecraft:spruce_planks',) * 8 + ('minecraft:oak_planks',) * 2 + ('minecraft:dark_oak_planks',)
ROOF = ('minecraft:deepslate_tiles',) * 7 + ('minecraft:deepslate_bricks',) * 3 + ('minecraft:cobbled_deepslate',)
VERGE = ('minecraft:cobbled_deepslate',) * 4 + ('minecraft:deepslate_bricks',) * 3 + ('minecraft:polished_deepslate',)
CHIMNEY = ('minecraft:stone_bricks',) * 6 + ('minecraft:cracked_stone_bricks',) * 2 + ('minecraft:cobblestone',)
LEAVES = ('minecraft:spruce_leaves',) * 5 + ('minecraft:oak_leaves',)

Position = Tuple[int, int, int]


class Build:
    """Hold a block volume and write it out as a Litematica schematic."""

    def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None:
        """Create an empty volume with one seeded random source.

        Args:
            size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z.
            seed (int): Seed for every random decision in this build.
        """
        self.size_x, self.size_y, self.size_z = size_xyz
        self.rng = random.Random(seed)
        self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
        self.placed: Dict[Position, str] = {}

    def inside(self, x: int, y: int, z: int) -> bool:
        """Report whether a coordinate is inside the volume.

        Args:
            x (int): Width coordinate.
            y (int): Height coordinate.
            z (int): Depth coordinate.

        Returns:
            bool: True when every axis is within range.
        """
        return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z

    def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None:
        """Place one block, in XYZ order, with an immediate bounds check.

        Args:
            x (int): Width coordinate.
            y (int): Height coordinate.
            z (int): Depth coordinate.
            block (str | Material): Namespaced block name, a full blockstate string, or a Material.
            **properties: Blockstate properties such as facing or half.

        Raises:
            AssertionError: When the coordinate is outside the volume, naming the coordinate.
        """
        assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}"
        material = self.material(block, **properties) if isinstance(block, str) else block
        self.canvas.block((y, z, x), material)
        self.placed[(x, y, z)] = material.blockstate

    def material(self, name: str, **properties: str) -> Material:
        """Build a Material from a block name and its properties.

        Args:
            name (str): Namespaced block name, or a full blockstate string.
            **properties: Blockstate properties.

        Returns:
            Material: Material ready for placement.
        """
        if "[" in name:
            return parse_blockstate(name)
        return self.canvas.material(name, **dict(sorted(properties.items())))

    def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None:
        """Fill an inclusive box.

        Args:
            bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends.
            block (str | Material): Block to place.
            **properties: Blockstate properties.
        """
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(min(x0, x1), max(x0, x1) + 1):
            for y in range(min(y0, y1), max(y0, y1) + 1):
                for z in range(min(z0, z1), max(z0, z1) + 1):
                    self.put(x, y, z, block, **properties)

    def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None:
        """Fill only the faces of a box, leaving the inside empty.

        Args:
            bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive.
            block (str | Material): Block to place.
            **properties: Blockstate properties.
        """
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(min(x0, x1), max(x0, x1) + 1):
            for y in range(min(y0, y1), max(y0, y1) + 1):
                for z in range(min(z0, z1), max(z0, z1) + 1):
                    on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1)
                    if on_face:
                        self.put(x, y, z, block, **properties)

    def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None:
        """Fill a box with a weighted mix of related blocks, so the surface is not flat.

        Args:
            bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive.
            palette (Iterable[str]): Block names. Repeat a name to make it more common.
        """
        choices = list(palette)
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(min(x0, x1), max(x0, x1) + 1):
            for y in range(min(y0, y1), max(y0, y1) + 1):
                for z in range(min(z0, z1), max(z0, z1) + 1):
                    self.put(x, y, z, self.rng.choice(choices))

    def get(self, x: int, y: int, z: int) -> Optional[str]:
        """Read back what was placed at a coordinate.

        Args:
            x (int): Width coordinate.
            y (int): Height coordinate.
            z (int): Depth coordinate.

        Returns:
            Optional[str]: Blockstate string, or None when the cell is empty.
        """
        return self.placed.get((x, y, z))

    def build(self) -> "Build":
        """Build a six-storey timber house, steep tiled roof and planted terraces."""
        self.rooms = []
        self.windows = []
        self.doors = []
        self.beds = []
        self.stair_runs = []
        self.lights = []
        self.decorations = []
        self.indoor = set()
        self.terrain()
        self.main_house()
        self.roof()
        self.entrance_wing()
        self.dormer(20, 30, 36, 22, 26)
        self.dormer(34, 27, 32, 23, 27)
        self.facade()
        self.furnish()
        self.garden()
        self.circulation()
        return self

    def mix(self, x: int, y: int, z: int, family: tuple) -> None:
        """Place one reproducibly varied material from a family."""
        self.put(x, y, z, self.rng.choice(family))

    def air(self, bounds: tuple) -> None:
        """Clear a box and register its cells as enclosed interior volume."""
        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.put(x, y, z, 'minecraft:air')
                    self.indoor.add((x, y, z))

    def roof_height(self, x: int) -> int:
        """Return the underside of the three-to-one main roof pitch."""
        return ROOF_PEAK - ROOF_PITCH * abs(x - CENTER_X)

    def terrain(self) -> None:
        """Make an irregular soil island, retaining terraces and continuous steps."""
        self.ground = {}
        for x in range(2, 45):
            for z in range(2, 39):
                d = ((x - 23) / 22)**2 + ((z - 21) / 19)**2
                if d > 1 + self.rng.uniform(-0.055, 0.055):
                    continue
                h = 1 if d > 0.82 else 2
                if z > 17 and d < 0.8:
                    h = 3
                for y in range(h):
                    self.mix(x, y, z, EARTH if y else STONE)
                self.mix(x, h, z, SOIL)
                self.ground[x, z] = h
        # Foundations rise out of the hillside, with no hidden hollow plinth.
        self.texture((17, 0, 16, 33, 5, 30), STONE)
        self.texture((10, 0, 10, 24, 5, 17), STONE)
        self.texture((9, 0, 7, 25, 5, 10), STONE)
        self.texture((25, 0, 10, 36, 4, 20), STONE)
        self.texture((9, 6, 7, 25, 6, 10), PAVING)
        self.texture((25, 5, 10, 36, 5, 20), PAVING)
        self.texture((12, 0, 0, 34, 0, 2), STONE)
        self.texture((12, 1, 0, 34, 1, 2), PAVING)
        # West flight meets the entrance porch at floor six.
        run = []
        for i in range(5):
            z, y = 2 + i, 2 + i
            for x in range(13, 18):
                self.texture((x, 0, z, x, y - 1, z), STONE)
                self.put(x, y, z, 'minecraft:stone_brick_stairs', facing='south')
            run.append((15, y, z))
        self.stair_runs.append(run)
        # Second stair reaches a landing and one last tread into the porch.
        run = []
        for i in range(4):
            z, y = 3 + i, 2 + i
            for x in range(28, 33):
                self.texture((x, 0, z, x, y - 1, z), STONE)
                self.put(x, y, z, 'minecraft:stone_brick_stairs', facing='south')
            run.append((30, y, z))
        self.texture((27, 0, 7, 34, 4, 10), STONE)
        self.texture((27, 5, 7, 34, 5, 10), PAVING)
        self.stair_runs.append(run)
        self.texture((26, 0, 8, 26, 4, 10), STONE)
        self.box((26, 5, 8, 26, 5, 10), 'minecraft:stone_brick_stairs', facing='west')
        # Low garden walls step down with the stairs, leaving both routes open.
        for x in (11, 19, 26, 34):
            for z in range(3, 8):
                h = min(z, 6)
                self.texture((x, 0, z, x, h, z), STONE)
                self.put(x, h + 1, z, 'minecraft:stone_brick_slab', type='bottom')
        for z in range(11, 21):
            self.put(36, 6, z, 'minecraft:cobblestone_wall')
        # Mixed paving at the toe of the two flights.
        for x in range(10, 37):
            for z in range(1, 4):
                if (x, z) in self.ground:
                    self.put(x, 1, z, self.rng.choice(PAVING))
                    self.ground[x, z] = 1

    def main_house(self) -> None:
        """Raise the timber frame and plaster gables around six walkable floors."""
        for x in range(17, 34):
            top = self.roof_height(x)
            for z in range(16, 31):
                for y in range(6, top):
                    edge = z in (16, 30) or x in (17, 33)
                    if edge:
                        self.mix(x, y, z, PLASTER)
                    else:
                        self.indoor.add((x, y, z))
        for floor in FLOORS:
            for x in range(17, 34):
                if self.roof_height(x) <= floor:
                    continue
                for z in range(16, 31):
                    self.mix(x, floor, z, WOOD)
            for z in (16, 30):
                for x in range(17, 34):
                    if self.roof_height(x) > floor:
                        self.put(x, floor, z, 'minecraft:dark_oak_log', axis='x')
            for x in (17, 33):
                if floor < self.roof_height(x):
                    self.box((x, floor, 16, x, floor, 30), 'minecraft:dark_oak_log', axis='z')
        # Exposed studs support the infill rather than being painted on it.
        for z in (16, 30):
            for x in (17, 21, 29, 33):
                for y in range(7, self.roof_height(x)):
                    if y not in FLOORS:
                        self.put(x, y, z, 'minecraft:stripped_spruce_log', axis='y')
        for x in (17, 33):
            for z in (16, 21, 26, 30):
                self.box((x, 7, z, x, 26, z), 'minecraft:stripped_spruce_log', axis='y')
        # Smaller upper studs follow the narrowing triangular facade.
        for z in (16, 30):
            for x in (23, 27):
                self.box((x, 34, z, x, self.roof_height(x) - 1, z), 'minecraft:stripped_spruce_log', axis='y')
        # Each joist is visible along the warm ceilings.
        for floor in FLOORS[1:]:
            for z in (19, 25):
                for x in range(18, 33):
                    if self.roof_height(x) > floor:
                        self.put(x, floor, z, 'minecraft:stripped_spruce_log', axis='x')

    def roof(self) -> None:
        """Lay thick textured slate slopes with stepped verges and brass finials."""
        for x in range(15, 36):
            h = self.roof_height(x)
            for z in range(14, 33):
                for y in range(h, h + ROOF_PITCH + 1):
                    self.mix(x, y, z, ROOF)
                if x != CENTER_X:
                    self.put(x, h + ROOF_PITCH + 1, z, 'minecraft:deepslate_tile_stairs', facing='east' if x < CENTER_X else 'west')
            # The dark irregular verge stands proud of the plaster gable.
            for z in (14, 32):
                for y in range(h, h + ROOF_PITCH + 1):
                    self.mix(x, y, z, VERGE)
                if x % 2:
                    self.put(x, h - 1, z, 'minecraft:polished_blackstone_brick_slab', type='top')
        # Broken horizontal bands, just as on an old heavily shingled roof.
        for x in (16, 19, 22, 28, 31, 34):
            h = self.roof_height(x) + 3
            for z in range(15, 32):
                if (z + x) % 5:
                    self.put(x, h, z, 'minecraft:polished_deepslate')
        self.box((25, 55, 14, 25, 55, 32), 'minecraft:deepslate_tile_slab', type='bottom')
        for z in (15, 23, 31):
            self.put(25, 55, z, 'minecraft:polished_blackstone_bricks')
            self.put(25, 56, z, 'minecraft:oak_fence')
            self.put(25, 57, z, 'minecraft:oak_fence')
            self.put(25, 58, z, 'minecraft:lightning_rod', facing='up')
        # A masonry chimney grows from a hearth and emerges beside the ridge.
        self.texture((28, 7, 27, 29, 48, 28), CHIMNEY)
        self.box((27, 49, 26, 30, 49, 29), 'minecraft:stone_bricks')
        self.box((28, 50, 27, 29, 50, 28), 'minecraft:stone_brick_wall')
        self.box((28, 51, 27, 29, 51, 28), 'minecraft:stone_brick_slab', type='bottom')

    def window(self, axis: str, plane: int, lo: int, hi: int, bottom: int, top: int) -> None:
        """Glaze an opening and frame it with timber lintel and sill."""
        cells, clearance, exterior = [], [], []
        for a in range(lo, hi + 1):
            for y in range(bottom, top + 1):
                p = (a, y, plane) if axis == 'z' else (plane, y, a)
                self.put(*p, 'minecraft:yellow_stained_glass')
                cells.append(p)
                # Main front/rear and east-facing dormers have inward daylight space.
                side = 1 if axis == 'z' and plane < 25 else -1
                q = (a, y, plane + side) if axis == 'z' else (plane - 1, y, a)
                self.put(*q, 'minecraft:air')
                clearance.append(q)
                self.indoor.add(q)
                outside = (a, y, plane - side) if axis == 'z' else (plane + 1, y, a)
                self.put(*outside, 'minecraft:air')
                exterior.append(outside)
            for y in (bottom - 1, top + 1):
                p = (a, y, plane) if axis == 'z' else (plane, y, a)
                self.put(*p, 'minecraft:spruce_planks')
        for a in (lo - 1, hi + 1):
            for y in range(bottom - 1, top + 2):
                p = (a, y, plane) if axis == 'z' else (plane, y, a)
                self.put(*p, 'minecraft:stripped_spruce_log', axis='y')
        self.windows.append({'cells': cells, 'clearance': clearance, 'exterior': exterior})

    def door(self, x: int, y: int, z: int, facing: str = 'north', hinge: str = 'left') -> None:
        """Install both halves of a complete wooden door."""
        for half, dy in (('lower', 0), ('upper', 1)):
            self.put(x, y + dy, z, 'minecraft:spruce_door', half=half, facing=facing, hinge=hinge, open='false')
        self.doors.append((x, y, z))

    def entrance_wing(self) -> None:
        """Build the low cross-gabled entrance wing and its sheltered porch."""
        self.texture((10, 6, 11, 24, 6, 17), WOOD)
        self.air((11, 7, 12, 23, 12, 17))
        for x in range(10, 25):
            for z in range(11, 18):
                h = 18 - abs(z - 14)
                if (x in (10, 24) or z in (11, 17)) and not (x >= 17 and z >= 16):
                    self.texture((x, 7, z, x, h - 1, z), PLASTER)
        for x in (10, 16, 24):
            self.box((x, 7, 11, x, 13, 11), 'minecraft:stripped_spruce_log', axis='y')
        self.box((10, 12, 11, 24, 12, 11), 'minecraft:dark_oak_log', axis='x')
        # A vaulted porch, whose roof shares the long cross ridge.
        for x in range(9, 26):
            for z in range(7, 19):
                if x >= 17 and z >= 16:
                    continue
                h = 18 - abs(z - 14)
                self.mix(x, h, z, ROOF)
                self.mix(x, h + 1, z, ROOF)
                if z != 14:
                    self.put(x, h + 2, z, 'minecraft:deepslate_tile_stairs', facing='south' if z < 14 else 'north')
                else:
                    self.put(x, h + 2, z, 'minecraft:deepslate_tile_slab', type='bottom')
        for x in (10, 18, 24):
            self.box((x, 7, 8, x, 10, 8), 'minecraft:stripped_spruce_log', axis='y')
            self.put(x, 11, 8, 'minecraft:dark_oak_planks')
        self.box((10, 11, 7, 24, 11, 7), 'minecraft:dark_oak_log', axis='x')
        self.door(17, 7, 11)
        self.window('z', 11, 12, 14, 8, 10)
        self.window('z', 11, 20, 22, 8, 10)
        self.window('x', 24, 13, 15, 8, 10)
        # Large internal arch links the vestibule to the tower kitchen.
        self.air((20, 7, 16, 23, 10, 17))
        # A small front dormer over the entrance, reached from the sitting room.
        self.texture((15, 13, 10, 20, 13, 17), WOOD)
        for x in range(15, 21):
            h = 22 - abs(x - 17)
            for z in range(10, 18):
                if x in (15, 20) or z == 10:
                    self.texture((x, 14, z, x, h - 1, z), PLASTER)
                else:
                    self.air((x, 14, z, x, h - 1, z))
                self.mix(x, h, z, ROOF)
                self.mix(x, h + 1, z, ROOF)
                self.put(x, h + 2, z, 'minecraft:deepslate_tile_stairs', facing='east' if x <= 17 else 'west')
        self.texture((16, 14, 17, 16, 20, 17), PLASTER)
        self.window('z', 10, 17, 18, 15, 17)
        self.air((18, 14, 16, 19, 16, 17))
        self.rooms.append({'name': 'Entrance hall', 'floor': 6, 'point': (17, 7, 13), 'furniture': [(11, 7, 15), (13, 7, 16)]})
        self.rooms.append({'name': 'Dormer reading nook', 'floor': 13, 'point': (18, 14, 14), 'furniture': [(16, 14, 14)]})

    def dormer(self, floor: int, start: int, face: int, z0: int, z1: int) -> None:
        """Open a timber dormer through the east slope with a sealed gabled cap."""
        center = (z0 + z1) // 2
        peak = floor + 9
        self.texture((start, floor, z0, face, floor, z1), WOOD)
        for z in range(z0, z1 + 1):
            h = peak - abs(z - center)
            self.air((start, floor + 1, z, face - 1, h - 1, z))
            if z in (z0, z1):
                self.texture((start, floor + 1, z, face, h - 1, z), PLASTER)
            self.texture((face, floor + 1, z, face, h - 1, z), PLASTER)
        for z in range(z0 - 1, z1 + 2):
            h = peak - abs(z - center)
            for x in range(start, face + 2):
                self.mix(x, h, z, ROOF)
                self.mix(x, h + 1, z, ROOF)
                self.put(x, h + 2, z, 'minecraft:deepslate_tile_stairs', facing='south' if z <= center else 'north')
        for z in (z0, z1):
            self.box((face, floor, z, face, floor + 6, z), 'minecraft:stripped_spruce_log', axis='y')
        self.window('x', face, z0 + 1, z1 - 1, floor + 2, floor + 4)
        for z in range(z0, z1 + 1):
            self.put(face + 1, floor, z, 'minecraft:dark_oak_slab', type='top')
        # Brackets bear on the existing roof or on the tower's east wall.
        for z in (z0, z1):
            for x in range(start, face + 1):
                self.put(x, floor - 1, z, 'minecraft:stripped_spruce_log', axis='x')
        self.lamp(face - 1, floor + 5, center, hanging=True)
        self.put(face - 1, floor + 6, center, 'minecraft:dark_oak_planks')
        self.rooms.append({'name': 'East bay ' + str(floor), 'floor': floor, 'point': (face - 2, floor + 1, center), 'furniture': []})

    def lamp(self, x: int, y: int, z: int, hanging: bool = False) -> None:
        """Place and record a lantern for support and room-lighting verification."""
        self.put(x, y, z, 'minecraft:lantern', hanging=str(hanging).lower())
        self.lights.append((x, y, z))
        self.decorations.append(((x, y, z), (x, y + (1 if hanging else -1), z)))

    def flower_box(self, x0: int, x1: int, y: int, z: int) -> None:
        """Plant a timber-backed window box, below rather than across its glass."""
        for x in range(x0, x1 + 1):
            self.put(x, y, z, 'minecraft:spruce_planks')
            self.put(x, y + 1, z, 'minecraft:flowering_azalea_leaves', persistent='true')
            self.put(x, y, z - 1, 'minecraft:spruce_trapdoor', facing='north', half='bottom', open='true')

    def facade(self) -> None:
        """Add amber glazing, shutters, flower boxes and warm exterior lanterns."""
        for floor in (13, 20, 27):
            self.window('z', 16, 24, 26, floor + 2, floor + 4)
            for x in (23, 27):
                for y in range(floor + 2, floor + 5):
                    self.put(x, y, 15, 'minecraft:spruce_trapdoor', facing='north', open='true', half='bottom')
            self.flower_box(24, 26, floor, 15)
            self.put(22, floor + 3, 15, 'minecraft:dark_oak_fence', south='true')
            self.lamp(22, floor + 2, 15, hanging=True)
        self.window('z', 16, 24, 26, 36, 38)
        self.flower_box(24, 26, 34, 15)
        self.window('z', 16, 25, 25, 43, 45)
        # Smaller asymmetric side lights keep the lower facade from feeling gridded.
        self.window('z', 16, 30, 31, 15, 17)
        self.window('z', 16, 19, 19, 22, 24)
        self.window('x', 33, 18, 19, 8, 10)
        self.window('x', 33, 23, 24, 15, 17)
        for floor in (6, 13, 20, 27, 34):
            lo, hi = (20, 22) if floor < 34 else (26, 27)
            self.window('z', 30, lo, hi, floor + 2, floor + 4)
        # Corbels below the three projecting timber belt lines.
        for y in (13, 20, 27):
            for x in (17, 21, 29, 33):
                if self.roof_height(x) > y and not (x == 17 and y == 20):
                    self.put(x, y - 1, 15, 'minecraft:spruce_stairs', facing='south', half='top')
                    self.put(x, y - 2, 15, 'minecraft:stripped_spruce_log', axis='y')
            for z in (17, 21, 29):
                if y < 27:
                    self.put(34, y - 1, z, 'minecraft:spruce_stairs', facing='west', half='top')
                    self.put(34, y - 2, z, 'minecraft:stripped_spruce_log', axis='y')
        # Porch lamps hang from the crossbeam.
        for x in (11, 19, 23):
            self.put(x, 10, 8, 'minecraft:dark_oak_planks')
            self.lamp(x, 9, 8, hanging=True)
        self.put(32, 12, 15, 'minecraft:dark_oak_fence', south='true')
        self.lamp(32, 11, 15, hanging=True)
        # Ground-floor entry to the garden terrace.
        self.door(33, 7, 20, facing='east')
        self.box((34, 6, 20, 35, 6, 20), 'minecraft:stone_brick_stairs', facing='west')
        self.box((34, 5, 20, 35, 5, 20), 'minecraft:stone_bricks')

    def chair(self, x: int, y: int, z: int, facing: str) -> None:
        """Set a stair-block seat on a floor."""
        self.put(x, y, z, 'minecraft:spruce_stairs', facing=facing)

    def pot(self, x: int, y: int, z: int, plant: str) -> None:
        """Set a supported potted plant."""
        self.put(x, y, z, 'minecraft:potted_' + plant)
        self.decorations.append(((x, y, z), (x, y - 1, z)))

    def room_light(self, floor: int, x: int, z: int) -> None:
        """Hang a lantern within three blocks of the occupied floor."""
        self.put(x, floor + 4, z, 'minecraft:dark_oak_planks')
        self.put(x, floor + 5, z, 'minecraft:chain', axis='y')
        # The chain runs to an actual ceiling, including in vaulted roof rooms.
        y = floor + 6
        while y < self.size_y and self.get(x, y, z) in (None, 'minecraft:air'):
            self.put(x, y, z, 'minecraft:chain', axis='y')
            y += 1
        self.lamp(x, floor + 3, z, hanging=True)

    def furnish(self) -> None:
        """Furnish the hall and every storey around the open rear ladder route."""
        # Vestibule: boots, coats, parcels and a bench.
        self.box((11, 7, 14, 11, 7, 16), 'minecraft:barrel', facing='up')
        self.chair(13, 7, 16, 'south')
        self.chair(14, 7, 16, 'south')
        self.pot(11, 8, 14, 'fern')
        self.room_light(6, 15, 13)
        self.chair(16, 14, 14, 'west')
        self.put(16, 14, 12, 'minecraft:bookshelf')
        self.lamp(16, 15, 12)
        # Ground floor: a full kitchen, hearth and six-seat refectory table.
        f = 6
        for z, block in ((22, 'smoker'), (23, 'furnace'), (24, 'crafting_table'), (25, 'barrel')):
            self.put(18, f + 1, z, 'minecraft:' + block, facing='east' if block in ('smoker', 'furnace', 'barrel') else 'north') if block != 'crafting_table' else self.put(18, f + 1, z, 'minecraft:' + block)
        self.put(18, f + 1, 26, 'minecraft:water_cauldron', level='3')
        self.box((19, 7, 27, 22, 8, 27), 'minecraft:barrel', facing='north')
        self.put(27, 7, 27, 'minecraft:campfire', lit='true', facing='north')
        self.put(27, 6, 27, 'minecraft:bricks')
        self.put(27, 8, 27, 'minecraft:iron_bars')
        self.put(27, 9, 27, 'minecraft:stone_bricks')
        self.put(28, 7, 26, 'minecraft:stone_bricks')
        self.box((24, 7, 21, 27, 7, 21), 'minecraft:dark_oak_planks')
        for x in (24, 26, 27):
            self.chair(x, 7, 20, 'north')
            self.chair(x, 7, 22, 'south')
        self.pot(24, 8, 21, 'dandelion')
        self.room_light(f, 25, 24)
        self.room_light(f, 20, 19)
        self.rooms.append({'name': 'Kitchen and dining room', 'floor': f, 'point': (25, 7, 25), 'furniture': [(18, 7, 22), (18, 7, 26), (24, 7, 21)]})
        # Sitting room, with bookcases, sofa, low table and writing desk.
        f = 13
        self.box((18, 14, 22, 18, 15, 25), 'minecraft:bookshelf')
        for z in range(20, 24):
            self.chair(31, 14, z, 'east')
        self.box((28, 14, 21, 29, 14, 22), 'minecraft:spruce_slab', type='bottom')
        self.box((23, 14, 21, 26, 14, 24), 'minecraft:green_carpet')
        self.box((20, 14, 26, 22, 14, 26), 'minecraft:spruce_planks')
        self.put(21, 15, 26, 'minecraft:lectern', facing='south')
        self.chair(21, 14, 27, 'south')
        self.pot(20, 15, 26, 'blue_orchid')
        self.put(31, 14, 26, 'minecraft:chest', facing='west', type='single')
        self.room_light(f, 24, 23)
        self.room_light(f, 30, 19)
        self.rooms.append({'name': 'Sitting room', 'floor': f, 'point': (25, 14, 26), 'furniture': [(18, 14, 22), (31, 14, 22), (21, 14, 26)]})
        # Library: stacks, map desk and a chair in the lower east bay.
        f = 20
        self.box((19, 21, 20, 19, 23, 25), 'minecraft:bookshelf')
        self.box((20, 21, 27, 22, 23, 27), 'minecraft:bookshelf')
        self.box((23, 21, 21, 26, 21, 21), 'minecraft:spruce_planks')
        self.put(24, 22, 21, 'minecraft:cartography_table')
        self.put(26, 22, 21, 'minecraft:lectern', facing='south')
        self.chair(24, 21, 22, 'south')
        self.chair(35, 21, 24, 'east')
        self.put(33, 21, 22, 'minecraft:bookshelf')
        self.put(30, 21, 18, 'minecraft:chest', facing='west', type='single')
        self.room_light(f, 25, 24)
        self.room_light(f, 21, 19)
        self.rooms.append({'name': 'Library and map room', 'floor': f, 'point': (25, 21, 26), 'furniture': [(19, 21, 20), (24, 22, 21), (26, 22, 21)]})
        # Alchemy workshop: counters, water, brewing, reagent stores and workbench.
        f = 27
        self.box((21, 28, 20, 21, 28, 25), 'minecraft:spruce_planks')
        self.put(21, 29, 21, 'minecraft:brewing_stand', has_bottle_0='true', has_bottle_1='true', has_bottle_2='true')
        self.pot(21, 29, 23, 'red_mushroom')
        self.put(21, 29, 25, 'minecraft:flower_pot')
        self.decorations.append(((21, 29, 25), (21, 28, 25)))
        self.put(22, 28, 26, 'minecraft:water_cauldron', level='3')
        self.put(29, 28, 21, 'minecraft:crafting_table')
        self.box((29, 28, 23, 30, 29, 24), 'minecraft:barrel', facing='west')
        self.put(24, 28, 21, 'minecraft:enchanting_table')
        self.chair(24, 28, 23, 'south')
        self.room_light(f, 25, 24)
        self.rooms.append({'name': 'Alchemy workshop', 'floor': f, 'point': (25, 28, 26), 'furniture': [(21, 29, 21), (22, 28, 26), (29, 28, 21)]})
        # Bedroom beneath the upper roof, with a double bed and dormer wash nook.
        f = 34
        for x in (24, 25):
            self.put(x, 35, 21, 'minecraft:red_bed', part='foot', facing='north', occupied='false')
            self.put(x, 35, 20, 'minecraft:red_bed', part='head', facing='north', occupied='false')
            self.beds.append((x, 35, 21))
        self.put(23, 35, 20, 'minecraft:barrel', facing='up')
        self.lamp(23, 36, 20)
        self.box((21, 35, 25, 21, 36, 26), 'minecraft:barrel', facing='east')
        self.put(30, 35, 24, 'minecraft:water_cauldron', level='3')
        self.put(30, 35, 26, 'minecraft:barrel', facing='up')
        self.pot(30, 36, 26, 'white_tulip')
        self.box((24, 35, 23, 26, 35, 25), 'minecraft:red_carpet')
        self.room_light(f, 25, 26)
        self.rooms.append({'name': 'Bedroom', 'floor': f, 'point': (25, 35, 27), 'furniture': [(24, 35, 21), (21, 35, 25), (30, 35, 24)]})
        # Observatory loft: star charts, optical bench, seat and expedition chest.
        f = 41
        self.box((24, 42, 19, 26, 42, 19), 'minecraft:spruce_planks')
        self.put(24, 43, 19, 'minecraft:cartography_table')
        self.put(26, 43, 19, 'minecraft:amethyst_cluster', facing='up')
        self.decorations.append(((26, 43, 19), (26, 42, 19)))
        self.chair(25, 42, 21, 'south')
        self.put(23, 42, 25, 'minecraft:chest', facing='east', type='single')
        self.put(23, 42, 24, 'minecraft:bookshelf')
        self.lamp(23, 43, 24)
        self.room_light(f, 25, 25)
        self.rooms.append({'name': 'Observatory loft', 'floor': f, 'point': (25, 42, 27), 'furniture': [(24, 43, 19), (23, 42, 25), (23, 42, 24)]})

    def tree(self, x: int, z: int, height: int, radius: int) -> None:
        """Grow a narrow, irregular spruce with connected drooping branch tiers."""
        base = self.ground.get((x, z), 2)
        self.box((x, base + 1, z, x, base + height, z), 'minecraft:spruce_log', axis='y')
        if height > 26:
            self.box((x + 1, base + 1, z, x + 1, base + height - 8, z), 'minecraft:spruce_log', axis='y')
        for rel in range(5, height, 4):
            reach = max(1, round(radius * (1 - rel / height)))
            y = base + rel
            for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                length = max(1, reach + self.rng.choice((-1, 0, 1)))
                for d in range(1, length + 1):
                    xx, zz = x + dx * d, z + dz * d
                    yy = y - (1 if d == length else 0)
                    if self.get(xx, yy, zz) in (None, 'minecraft:air'):
                        self.put(xx, yy, zz, 'minecraft:spruce_log', axis='x' if dx else 'z')
                    for ax, az, ay in ((0, 0, 1), (1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, -1)):
                        p = (xx + ax, yy + ay, zz + az)
                        if self.inside(*p) and self.get(*p) in (None, 'minecraft:air'):
                            self.put(*p, self.rng.choice(LEAVES), persistent='true')
            self.put(x, y + 1, z, 'minecraft:spruce_leaves', persistent='true')
        for y in range(base + height - 3, base + height + 2):
            for dx, dz in ((0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)):
                if y > base + height - 1 and dx + dz != 0:
                    continue
                if self.get(x + dx, y, z + dz) is None:
                    self.put(x + dx, y, z + dz, 'minecraft:spruce_leaves', persistent='true')

    def garden(self) -> None:
        """Surround the terraces with conifers, ferns, rocks, ivy and workaday props."""
        self.tree(7, 26, 39, 7)
        self.tree(5, 16, 25, 5)
        self.tree(40, 28, 29, 6)
        self.tree(36, 34, 21, 4)
        # Dense, low planting uses the terrain map and stays clear of built paths.
        for (x, z), h in list(self.ground.items()):
            if self.get(x, h + 1, z) is not None:
                continue
            if 10 <= x <= 36 and z <= 11:
                continue
            if 16 <= x <= 35 and 15 <= z <= 31:
                continue
            r = self.rng.random()
            if r < 0.16:
                self.put(x, h + 1, z, self.rng.choice(('minecraft:azalea_leaves', 'minecraft:oak_leaves', 'minecraft:flowering_azalea_leaves')), persistent='true')
            elif r < 0.23:
                self.put(x, h + 1, z, self.rng.choice(STONE))
            elif r < 0.43:
                self.put(x, h + 1, z, self.rng.choice(('minecraft:fern', 'minecraft:short_grass', 'minecraft:dandelion', 'minecraft:oxeye_daisy', 'minecraft:brown_mushroom')))
                self.decorations.append(((x, h + 1, z), (x, h, z)))
        # Ivy is attached to the visible timber and leaves the glazing clear.
        for x in (17, 21, 29):
            for y in range(7, 31):
                if self.rng.random() < 0.72 and self.get(x, y, 15) in (None, 'minecraft:air') and self.get(x, y, 16) not in (None, 'minecraft:air'):
                    self.put(x, y, 15, 'minecraft:vine', south='true')
                    self.decorations.append(((x, y, 15), (x, y, 16)))
        for z in (17, 26, 29):
            for y in range(8, 26):
                if self.rng.random() < 0.65 and self.get(34, y, z) in (None, 'minecraft:air'):
                    self.put(34, y, z, 'minecraft:vine', west='true')
                    self.decorations.append(((34, y, z), (33, y, z)))
        # Retaining-wall planting at the front matches the garden's high density.
        for x, z in ((9, 4), (20, 4), (23, 5), (35, 5), (38, 12), (8, 10)):
            h = self.ground.get((x, z), 2)
            self.texture((x, 0, z, x + 1, h + 1, z + 1), STONE)
            self.box((x, h + 2, z, x + 1, h + 2, z + 1), 'minecraft:flowering_azalea_leaves', persistent='true')
        for x, z, base in ((11, 3, 4), (34, 7, 7), (35, 18, 6), (9, 10, 7)):
            self.put(x, base, z, 'minecraft:stone_bricks')
            self.box((x, base + 1, z, x, base + 3, z), 'minecraft:spruce_fence')
            self.lamp(x, base + 4, z)
        self.box((34, 6, 14, 34, 7, 15), 'minecraft:barrel', facing='east')
        self.put(35, 6, 13, 'minecraft:composter', level='7')
        self.put(12, 7, 9, 'minecraft:barrel', facing='up')
        self.pot(12, 8, 9, 'fern')
        for z in range(23, 27):
            self.put(15, 4, z, 'minecraft:oak_log', axis='z')
            self.put(15, 5, z, 'minecraft:spruce_slab', type='bottom')
        # Stone edging and flowers at the very foot of the island.
        for x in range(6, 43, 3):
            z = 4 if x < 12 or x > 35 else 1
            if (x, z) in self.ground and self.get(x, 2, z) is None:
                self.put(x, 2, z, 'minecraft:mossy_cobblestone_slab', type='bottom')

        # Plant the wedge between the flights densely, as in the reference foreground.
        for x in range(20, 25):
            for z in range(2, 7):
                h = min(4, z)
                self.texture((x, 0, z, x, h - 1, z), STONE)
                self.put(x, h, z, 'minecraft:moss_block')
                plant = self.rng.choice(('minecraft:azalea_leaves', 'minecraft:oak_leaves', 'minecraft:fern', 'minecraft:oxeye_daisy'))
                if plant.endswith('_leaves'):
                    self.put(x, h + 1, z, plant, persistent='true')
                else:
                    self.put(x, h + 1, z, plant)
                    self.decorations.append(((x, h + 1, z), (x, h, z)))

    def circulation(self) -> None:
        """Open a clear ladder shaft and landings connecting every occupied level."""
        for y in range(7, 44):
            self.put(25, y, 29, 'minecraft:ladder', facing='north')
            # A continuous solid backing is independent of rear-window geometry.
            self.put(25, y, 30, 'minecraft:dark_oak_log', axis='y')
        for floor in FLOORS:
            # Two blocks of headroom on each landing, with a route into the room.
            self.air((24, floor + 1, 28, 26, floor + 2, 28))
            self.put(25, floor, 28, 'minecraft:spruce_planks')
        # Solid flashing at intersecting roof pitches supports every tile stair.
        for (x, y, z), block in list(self.placed.items()):
            if '_stairs' in block and '_stairs' in (self.get(x, y - 1, z) or ''):
                self.put(x, y - 1, z, 'minecraft:deepslate_tiles')
        self.entrance = (17, 7, 10)

    def export(self, path: Path) -> Path:
        """Write the schematic so that two runs produce identical bytes.

        Args:
            path (Path): Destination .litematic path.

        Returns:
            Path: The path written.
        """
        schematic = self.canvas.to_litematica(
            name=CONFIG["name"],
            author=CONFIG["author"],
            description=CONFIG["description"],
            minecraft_data_version=CONFIG["data_version"],
        )
        # The metadata otherwise records the current wall clock, which changes the bytes on every run.
        schematic.metadata.time_created = 0
        schematic.metadata.time_modified = 0
        path.parent.mkdir(parents=True, exist_ok=True)
        schematic.save(path)
        # Use the normal compressed Litematica container with a fixed gzip timestamp.
        path.write_bytes(gzip.compress(path.read_bytes(), mtime=0))
        return path

    def verify(self, path: Path) -> Dict[str, object]:
        """Reload the written file and confirm it matches what was placed.

        Args:
            path (Path): Schematic to reload.

        Returns:
            Dict[str, object]: Size, block count and palette size of the reloaded file.

        Raises:
            AssertionError: When a reloaded cell differs from what was placed.
        """
        loaded = load_schematic(path)
        size_y, size_z, size_x = loaded.size_yzx
        assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload"
        ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx)
        for (x, y, z), state in self.placed.items():
            assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload"
        return {
            "size_xyz": [size_x, size_y, size_z],
            "placed_blocks": len(self.placed),
            "palette_states": len(loaded.palette),
        }


def main() -> None:
    """Generate the build, export it, and verify the exported file."""
    parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.")
    parser.add_argument("--output", type=Path, default=Path(CONFIG["output"]))
    parser.add_argument("--seed", type=int, default=CONFIG["seed"])
    args = parser.parse_args()

    build = Build(seed=args.seed).build()
    path = build.export(args.output)
    report = build.verify(path)
    print(f"wrote {path}: {report}")


if __name__ == "__main__":
    main()
