# Generate and verify a furnished stilted medieval manor for Minecraft Java 1.21.1.
from __future__ import annotations

import argparse
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": (33, 39, 31), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "The Brackenwatch Manor",
    "author": "generator",
    "description": "Stilted medieval manor with workshop, great room, two bedchambers, and ridge lookout. Java 1.21.1.",
}

# Restrained weighted palettes keep masonry and plaster readable at this scale.
STONE = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 3 + ['minecraft:andesite']
BASE = STONE + ['minecraft:mossy_stone_bricks'] * 3 + ['minecraft:mossy_cobblestone'] * 2
PLASTER = ['minecraft:smooth_sandstone'] * 10 + ['minecraft:cut_sandstone'] * 2 + ['minecraft:sandstone']
FLOOR = ['minecraft:spruce_planks'] * 9 + ['minecraft:dark_oak_planks']
ROOF_SOLID = ['minecraft:stone_bricks'] * 6 + ['minecraft:cobblestone'] * 2 + ['minecraft:andesite']

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 timber(self, bounds, axis='y', dark=False):
        """Place structural timber with its grain along the member."""
        self.box(bounds, 'minecraft:dark_oak_log' if dark else 'minecraft:stripped_spruce_log', axis=axis)

    def stairs(self, x, y, z, facing, block='spruce', half='bottom'):
        """Place a straight stair with explicit stable state."""
        self.put(x, y, z, f'minecraft:{block}_stairs', facing=facing, half=half, shape='straight', waterlogged='false')

    def slab(self, x, y, z, block='spruce', kind='bottom'):
        """Place a slab with an explicit half."""
        self.put(x, y, z, f'minecraft:{block}_slab', type=kind, waterlogged='false')

    def door(self, x, y, z, facing='north', hinge='left'):
        """Place both matching halves of an operable door."""
        for half, dy in [('lower', 0), ('upper', 1)]:
            self.put(x, y + dy, z, 'minecraft:spruce_door', facing=facing, hinge=hinge, half=half, open='false', powered='false')

    def trap(self, x, y, z, facing, half='bottom'):
        """Attach a vertical spruce shutter or planter facing."""
        self.put(x, y, z, 'minecraft:spruce_trapdoor', facing=facing, half=half, open='true', powered='false', waterlogged='false')

    def lantern(self, x, y, z, hanging=False):
        """Place a standing or suspended lantern."""
        self.put(x, y, z, 'minecraft:lantern', hanging=str(hanging).lower(), waterlogged='false')

    def candle(self, x, y, z, count=3):
        """Place a lit candle cluster on furniture."""
        self.put(x, y, z, 'minecraft:candle', candles=str(count), lit='true', waterlogged='false')

    def bed(self, x, y, z, facing, color):
        """Place a complete bed, specifying its foot coordinate."""
        dx, dz = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}[facing]
        for part, px, pz in [('foot', x, z), ('head', x + dx, z + dz)]:
            self.put(px, y, pz, f'minecraft:{color}_bed', part=part, facing=facing, occupied='false')

    def planter(self, axis, fixed, lo, hi, y, outward):
        """Build a soil trough with attached wooden fascias and flowers."""
        flowers = ['cornflower', 'poppy', 'azure_bluet', 'oxeye_daisy']
        for n in range(lo, hi + 1):
            x, z = (n, fixed) if axis == 'x' else (fixed, n)
            self.put(x, y, z, 'minecraft:dirt')
            self.put(x, y + 1, z, 'minecraft:' + flowers[(n - lo) % len(flowers)])
            dx, dz = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}[outward]
            self.trap(x + dx, y, z + dz, outward)
            self.stairs(x, y - 1, z, {'north': 'south', 'south': 'north', 'west': 'east', 'east': 'west'}[outward], half='top')
        for n, direction in [(lo - 1, 'west' if axis == 'x' else 'north'), (hi + 1, 'east' if axis == 'x' else 'south')]:
            x, z = (n, fixed) if axis == 'x' else (fixed, n)
            self.trap(x, y, z, direction)

    def terrain(self):
        """Lay an irregular garden island, approach, and side paths."""
        for x in range(self.size_x):
            for z in range(self.size_z):
                corner = min(x, self.size_x - 1 - x) + min(z, self.size_z - 1 - z)
                if corner < 2:
                    continue
                self.put(x, 0, z, 'minecraft:dirt')
                self.put(x, 1, z, 'minecraft:grass_block')
        for z in range(0, 10):
            center = 18 + (1 if z > 4 else 0)
            for x in range(center - 3, center + 4):
                self.put(x, 1, z, self.rng.choice(['minecraft:andesite'] * 3 + ['minecraft:gravel'] * 3 + ['minecraft:cobblestone', 'minecraft:coarse_dirt', 'minecraft:stone_bricks']))
        for x in range(4, 29):
            for z in [5, 25, 26]:
                if self.rng.random() < .8:
                    self.put(x, 1, z, self.rng.choice(['minecraft:coarse_dirt', 'minecraft:gravel', 'minecraft:mossy_cobblestone']))
        # Exterior approach rises one block to the stone floor.
        for x in range(18, 22):
            self.stairs(x, 2, 8, 'south', 'stone_brick')
        self.box((18, 2, 9, 21, 2, 10), 'minecraft:stone_bricks')

    def stone_base(self):
        """Build the recessed stone shell and its shuttered front window."""
        self.texture((8, 2, 9, 24, 2, 22), STONE)
        for y in range(3, 9):
            for x in range(8, 25):
                for z in [9, 22]:
                    self.put(x, y, z, self.rng.choice(BASE if y == 3 else STONE))
            for z in range(10, 22):
                for x in [8, 24]:
                    self.put(x, y, z, self.rng.choice(BASE if y == 3 else STONE))
        for x in [8, 24]:
            for z in [9, 22]:
                self.box((x, 3, z, x, 8, z), 'minecraft:stone_bricks')
        for x, hinge in [(19, 'left'), (20, 'right')]:
            self.door(x, 3, 9, hinge=hinge)
        self.timber((18, 3, 8, 18, 5, 8))
        self.timber((21, 3, 8, 21, 5, 8))
        self.timber((18, 5, 8, 21, 5, 8), 'x')
        self.stairs(18, 6, 8, 'east')
        self.stairs(21, 6, 8, 'west')
        for x in [19, 20]:
            self.slab(x, 6, 8)
        self.box((12, 4, 9, 15, 6, 9), 'minecraft:glass')
        self.timber((11, 4, 8, 11, 6, 8))
        self.timber((16, 4, 8, 16, 6, 8))
        self.timber((11, 7, 8, 16, 7, 8), 'x')
        for x in [12, 15]:
            for y in [4, 5, 6]:
                self.trap(x, y, 8, 'north')
        self.planter('x', 8, 12, 15, 3, 'north')
        # A small barred workshop window and a rear storage window.
        self.box((24, 5, 17, 24, 6, 19), 'minecraft:glass')
        self.box((13, 5, 22, 16, 6, 22), 'minecraft:glass')
        self.timber((8, 8, 9, 24, 8, 9), 'x', True)
        self.timber((8, 8, 22, 24, 8, 22), 'x', True)

    def postwork(self):
        """Carry the overhang on substantial posts, beams, and knee braces."""
        posts = [(5, 6), (27, 6), (5, 24), (27, 24), (12, 6), (23, 6), (12, 24), (23, 24), (5, 15), (27, 15)]
        for x, z in posts:
            self.put(x, 2, z, 'minecraft:stone_bricks')
            self.timber((x, 3, z, x, 8, z))
            for dx, dz, face in [(1, 0, 'west'), (-1, 0, 'east'), (0, 1, 'north'), (0, -1, 'south')]:
                for distance, y in [(1, 7), (2, 8)]:
                    nx, nz = x + dx * distance, z + dz * distance
                    if 5 <= nx <= 27 and 6 <= nz <= 24:
                        self.stairs(nx, y, nz, face, half='top')
                nx, nz = x + dx, z + dz
                if 5 <= nx <= 27 and 6 <= nz <= 24:
                    self.put(nx, 8, nz, 'minecraft:spruce_planks')
            # Cut log collars make each capital read as a joinery joint.
            self.put(x, 7, z, 'minecraft:spruce_log', axis='y')
        for x in [5, 27]:
            for z in [6, 24]:
                ix = x + (1 if x == 5 else -1)
                iz = z + (1 if z == 6 else -1)
                self.timber((ix, 2, z, ix, 8, z))
                self.timber((x, 2, iz, x, 8, iz))
        for x, z in posts:
            for dx, dz, face in [(1, 0, 'west'), (-1, 0, 'east'), (0, 1, 'north'), (0, -1, 'south')]:
                nx, nz = x + 2 * dx, z + 2 * dz
                if 5 <= nx <= 27 and 6 <= nz <= 24 and not (8 <= nx <= 24 and 9 <= nz <= 22):
                    self.stairs(nx, 7, nz, face, half='top')
        for z in [6, 15, 24]:
            self.timber((5, 8, z, 27, 8, z), 'x', True)
        for x in [5, 12, 19, 27]:
            self.timber((x, 8, 6, x, 8, 24), 'z', True)
        self.texture((5, 9, 6, 27, 9, 24), FLOOR)
        for z in [6, 24]:
            self.timber((5, 9, z, 27, 9, z), 'x')
        for x in [5, 27]:
            self.timber((x, 9, 6, x, 9, 24), 'z')
        # A sheltered bench and cooper's barrels beneath the portico.
        for x in range(13, 17):
            self.stairs(x, 2, 7, 'south')
        for x in [12, 17]:
            self.trap(x, 2, 7, 'west' if x == 12 else 'east')
        for y in [2, 3]:
            self.put(7, y, 11, 'minecraft:barrel', facing='up', open='false')
        self.put(7, 2, 12, 'minecraft:barrel', facing='north', open='false')
        for x, z in [(7, 7), (25, 7), (7, 23), (25, 23)]:
            self.put(x, 8, z, 'minecraft:spruce_planks')
            self.lantern(x, 7, z, True)

    def timber_floor(self):
        """Wrap the main living floor in plaster, exposed timber, and tall glazing."""
        for z in [6, 24]:
            self.texture((5, 10, z, 27, 15, z), PLASTER)
            for x in [5, 12, 19, 27]:
                self.timber((x, 10, z, x, 15, z))
            for y in [10, 15]:
                self.timber((5, y, z, 27, y, z), 'x', y == 15)
            for a, b in [(8, 10), (14, 16), (21, 23)]:
                self.box((a, 11, z, b, 14, z), 'minecraft:light_gray_stained_glass')
                out = z - 1 if z == 6 else z + 1
                facing = 'north' if z == 6 else 'south'
                for x in [a - 1, b + 1]:
                    self.timber((x, 11, z, x, 14, z))
                    for y in [11, 12, 13]:
                        self.trap(x, y, out, facing)
                self.planter('x', out, a, b, 10, facing)
                for x in range(a - 1, b + 2):
                    self.stairs(x, 15, out, 'south' if z == 6 else 'north', half='top')
        for x in [5, 27]:
            self.texture((x, 10, 7, x, 15, 23), PLASTER)
            for z in [6, 13, 20, 24]:
                self.timber((x, 10, z, x, 15, z))
            for y in [10, 15]:
                self.timber((x, y, 6, x, y, 24), 'z', y == 15)
            for a, b in [(8, 10), (16, 18), (21, 22)]:
                self.box((x, 11, a, x, 14, b), 'minecraft:light_gray_stained_glass')
                out = x - 1 if x == 5 else x + 1
                facing = 'west' if x == 5 else 'east'
                for z in [a - 1, b + 1]:
                    self.timber((x, 11, z, x, 14, z))
                self.planter('z', out, a, b, 10, facing)
        self.texture((5, 16, 6, 27, 16, 24), FLOOR)
        for x in [5, 12, 19, 27]:
            self.timber((x, 15, 7, x, 15, 23), 'z', True)
        for z in [6, 24]:
            self.timber((5, 16, z, 27, 16, z), 'x')
        for x in [5, 27]:
            self.timber((x, 16, 6, x, 16, 24), 'z')

    def roof(self):
        """Raise a steep, backed stair roof and richly framed end gables."""
        for z in range(4, 27):
            top = 17 + min(z - 4, 26 - z)
            facing = 'south' if z <= 15 else 'north'
            for x in range(3, 30):
                verge = x in [3, 29]
                self.put(x, top - 1, z, 'minecraft:spruce_planks' if verge else self.rng.choice(ROOF_SOLID))
                if z == 15:
                    self.put(x, top, z, 'minecraft:dark_oak_log', axis='x')
                    self.slab(x, top + 1, z, 'stone_brick')
                else:
                    material = 'spruce' if verge else self.rng.choice(['stone_brick'] * 12 + ['cobblestone'] * 4 + ['mossy_stone_brick'])
                    self.stairs(x, top, z, facing, material)
                if x in [4, 28] and z != 15:
                    self.stairs(x, top, z, facing, 'stone_brick')
        for x in [5, 27]:
            for z in range(6, 25):
                ceiling = 16 + min(z - 4, 26 - z)
                for y in range(17, ceiling + 1):
                    self.put(x, y, z, self.rng.choice(PLASTER))
                # Diagonal wooden verge and a central mast.
                self.put(x, ceiling, z, 'minecraft:stripped_spruce_log', axis='z')
            self.timber((x, 17, 15, x, 26, 15))
            for y, low, high in [(18, 7, 23), (22, 11, 19)]:
                self.timber((x, y, low, x, y, high), 'z')
            for z in [11, 19]:
                self.timber((x, 17, z, x, 21, z))
            for z in [12, 13, 17, 18]:
                self.box((x, 19, z, x, 21, z), 'minecraft:light_gray_stained_glass')
            self.box((x, 23, 14, x, 24, 14), 'minecraft:glass')
            self.box((x, 23, 16, x, 24, 16), 'minecraft:glass')
            out = x - 1 if x == 5 else x + 1
            for z in range(10, 21):
                self.slab(out, 18, z, 'spruce', 'top')
            for z in [11, 15, 19]:
                self.stairs(out, 17, z, 'east' if x == 5 else 'west', half='top')
            self.timber((out, 25, 15, out, 28, 15))
            self.put(out, 28, 15, 'minecraft:spruce_log', axis='x')
        for center in [10, 22]:
            self.dormer(center)

    def dormer(self, center):
        """Project a glazed little gable from the front roof into a bedroom."""
        a, b = center - 2, center + 2
        self.box((a + 1, 18, 8, b - 1, 23, 11), 'minecraft:air')
        for x in [a, b]:
            self.texture((x, 18, 7, x, 22, 11), PLASTER)
            self.timber((x, 18, 7, x, 22, 7))
        self.texture((a, 18, 7, b, 22, 7), PLASTER)
        for x in [a, b]:
            self.timber((x, 18, 7, x, 22, 7))
        self.timber((a, 18, 7, b, 18, 7), 'x')
        self.box((a + 1, 19, 7, b - 1, 21, 7), 'minecraft:glass')
        self.timber((a, 22, 7, b, 22, 7), 'x')
        self.put(center, 23, 7, 'minecraft:cut_sandstone')
        for x in range(a - 1, b + 2):
            top = 25 - abs(x - center)
            for z in range(6, 12):
                if top < 17 + min(z - 4, 26 - z):
                    continue
                self.put(x, top - 1, z, self.rng.choice(ROOF_SOLID))
                if x == center:
                    self.slab(x, top, z, 'stone_brick')
                else:
                    self.stairs(x, top, z, 'east' if x < center else 'west', 'stone_brick' if z != 6 else 'spruce')
        for x in range(a, b + 1):
            self.stairs(x, 17, 6, 'south', half='top')
        self.timber((center, 24, 6, center, 25, 6))

    def cupola(self):
        """Crown the ridge with a compact glazed lookout and pointed finial."""
        self.box((15, 27, 13, 19, 31, 17), 'minecraft:air')
        self.texture((14, 27, 12, 20, 27, 18), FLOOR)
        for x in range(15, 20):
            for z in range(13, 18):
                if x in [15, 19] or z in [13, 17]:
                    self.timber((x, 25, z, x, 26, z))
        for x in range(15, 20):
            for z in range(13, 18):
                if x in [15, 19] or z in [13, 17]:
                    for y in range(28, 31):
                        self.put(x, y, z, 'minecraft:glass')
                    self.put(x, 31, z, 'minecraft:dark_oak_log', axis='x' if z in [13, 17] else 'z')
        for x in [15, 17, 19]:
            for z in [13, 17]:
                self.timber((x, 28, z, x, 31, z))
        for x in [15, 19]:
            self.timber((x, 28, 15, x, 31, 15))
        for y, radius in [(32, 3), (33, 2), (34, 2), (35, 1), (36, 0)]:
            for x in range(17 - radius, 18 + radius):
                for z in range(15 - radius, 16 + radius):
                    edge = x in [17 - radius, 17 + radius] or z in [15 - radius, 15 + radius]
                    self.put(x, y, z, self.rng.choice(ROOF_SOLID))
                    if edge and radius and y != 33:
                        face = 'east' if x == 17 - radius else 'west' if x == 17 + radius else 'south' if z == 15 - radius else 'north'
                        self.stairs(x, y, z, face, 'stone_brick')
        self.put(17, 37, 15, 'minecraft:cobblestone_wall', up='true', north='none', south='none', east='none', west='none', waterlogged='false')
        self.put(17, 38, 15, 'minecraft:iron_bars', north='false', south='false', east='false', west='false', waterlogged='false')
        for x in [14, 20]:
            for z in [13, 17]:
                self.stairs(x, 26, z, 'east' if x == 14 else 'west', half='top')

    def chimney(self):
        """Run a masonry flue from the living hearth to twin clay chimney pots."""
        self.texture((23, 10, 12, 25, 30, 13), ['minecraft:stone_bricks'] * 4 + ['minecraft:cobblestone', 'minecraft:andesite'])
        for x in [23, 24, 25]:
            for z in [12, 13]:
                self.slab(x, 31, z, 'stone_brick', 'top')
        for x, z in [(23, 12), (25, 12)]:
            self.put(x, 32, z, 'minecraft:brick_wall', up='true', north='none', south='none', east='none', west='none', waterlogged='false')
            self.put(x, 33, z, 'minecraft:flower_pot')
        # The west-facing firebox is recessed behind the stone hearth lip.
        for z in [12, 13]:
            self.put(22, 10, z, 'minecraft:campfire', facing='west', lit='true', signal_fire='false', waterlogged='false')
        for z in [11, 14]:
            self.box((22, 10, z, 25, 12, z), 'minecraft:stone_bricks')
        self.box((22, 13, 11, 25, 13, 14), 'minecraft:dark_oak_planks')
        self.lantern(22, 14, 11)

    def furnishings(self):
        """Equip the workshop, great room, two bedchambers, and lookout."""
        # Ground storage. A clear central aisle joins both staircases.
        for x in [13, 14, 17, 18, 21]:
            self.put(x, 3, 21, 'minecraft:barrel', facing='north', open='false')
            self.put(x, 4, 21, 'minecraft:barrel', facing='north', open='false')
        for x in [14, 17, 20]:
            self.put(x, 3, 11, 'minecraft:chest', facing='south', type='single', waterlogged='false')
        for x, block in [(20, 'crafting_table'), (21, 'smithing_table'), (22, 'furnace')]:
            self.put(x, 3, 20, 'minecraft:' + block, **({'facing': 'north', 'lit': 'true'} if block == 'furnace' else {}))
        self.put(22, 3, 18, 'minecraft:anvil', facing='north')
        self.put(23, 3, 15, 'minecraft:stonecutter', facing='west')
        self.put(23, 3, 14, 'minecraft:crafting_table')
        for x, z in [(10, 11), (17, 17), (21, 16), (15, 20), (19, 11)]:
            self.put(x, 8, z, 'minecraft:dark_oak_planks')
            self.lantern(x, 7, z, True)
        self.put(21, 4, 20, 'minecraft:potted_fern')
        # Dining table, chairs, patterned carpet, and fireside settle.
        for x in range(13, 19):
            for z in range(17, 21):
                self.put(x, 10, z, 'minecraft:' + ('red_carpet' if x in [13, 18] or z in [17, 20] else 'orange_carpet'))
        for x in range(14, 19):
            if x in [14, 18]:
                self.stairs(x, 10, 13, 'east' if x == 14 else 'west', 'dark_oak', 'top')
            else:
                self.slab(x, 10, 13, 'dark_oak', 'top')
            if x in [14, 16, 18]:
                self.stairs(x, 10, 11, 'north')
                self.stairs(x, 10, 15, 'south')
        self.candle(15, 11, 13, 3)
        self.put(17, 11, 13, 'minecraft:potted_red_tulip')
        for x in range(14, 19):
            self.stairs(x, 10, 22, 'south')
        for x in [13, 19]:
            self.put(x, 10, 22, 'minecraft:dark_oak_planks')
            self.candle(x, 11, 22, 2)
        self.box((20, 10, 23, 24, 10, 23), 'minecraft:bookshelf')
        self.box((19, 10, 21, 19, 12, 22), 'minecraft:bookshelf')
        for x in [9, 10, 11]:
            self.put(x, 10, 23, 'minecraft:barrel', facing='north', open='false')
        self.put(9, 11, 23, 'minecraft:potted_fern')
        self.lantern(11, 11, 23)
        for x, z in [(9, 9), (17, 9), (21, 19), (11, 20)]:
            self.put(x, 15, z, 'minecraft:dark_oak_planks')
            self.put(x, 14, z, 'minecraft:chain', axis='y', waterlogged='false')
            self.lantern(x, 13, z, True)
        # A roof-height partition gives the attic two actual bedrooms.
        for z in range(6, 25):
            height = min(25, 16 + min(z - 4, 26 - z))
            self.texture((16, 17, z, 16, height, z), PLASTER)
            if z % 4 == 2:
                self.timber((16, 17, z, 16, height, z))
        for z, hinge in [(14, 'left'), (15, 'right')]:
            self.door(16, 17, z, 'east', hinge)
        self.bed(10, 17, 20, 'north', 'red')
        self.bed(12, 17, 20, 'north', 'red')
        self.bed(22, 17, 10, 'south', 'blue')
        self.bed(24, 17, 10, 'south', 'blue')
        for x, z in [(11, 19), (23, 11), (13, 11), (20, 21)]:
            self.put(x, 17, z, 'minecraft:barrel', facing='up', open='false')
            self.lantern(x, 18, z)
        self.box((6, 17, 14, 6, 19, 16), 'minecraft:bookshelf')
        self.box((26, 17, 14, 26, 19, 16), 'minecraft:bookshelf')
        for x, z in [(10, 10), (20, 20)]:
            self.put(x, 17, z, 'minecraft:chest', facing='south', type='single', waterlogged='false')
        for x0, z0, color in [(10, 13, 'red'), (20, 15, 'blue')]:
            self.box((x0, 17, z0, x0 + 3, 17, z0 + 2), 'minecraft:' + color + '_carpet')
        self.put(26, 17, 20, 'minecraft:barrel', facing='west', open='false')
        self.candle(26, 18, 20)
        self.put(7, 17, 10, 'minecraft:barrel', facing='up', open='false')
        self.candle(7, 18, 10)
        # Lookout writing shelf and seating leave the ladder landing free.
        self.put(16, 28, 16, 'minecraft:bookshelf')
        self.candle(16, 29, 16, 4)
        self.stairs(16, 28, 14, 'west')
        self.put(18, 31, 14, 'minecraft:spruce_planks')
        self.lantern(18, 30, 14, True)

    def circulation(self):
        """Cut generous two-wide stairs and a continuous supported lookout ladder."""
        for x0, z0, bottom, top in [(10, 13, 2, 9), (7, 12, 9, 16)]:
            for index in range(top - bottom):
                z, y = z0 + index, bottom + index + 1
                for x in [x0, x0 + 1]:
                    self.box((x, bottom + 1, z, x, y, z), 'minecraft:spruce_planks')
                    self.box((x, y + 1, z, x, y + 3, z), 'minecraft:air')
                    self.stairs(x, y, z, 'south')
            for x in [x0, x0 + 1]:
                self.box((x, top + 1, z0 + top - bottom, x, top + 2, z0 + top - bottom), 'minecraft:air')
        # Guard the long open edges of both stairwells.
        for x, y, lo, hi in [(9, 10, 17, 19), (12, 10, 17, 19), (9, 17, 16, 18)]:
            for z in range(lo, hi + 1):
                self.put(x, y, z, 'minecraft:spruce_fence', north=str(z > lo).lower(), south=str(z < hi).lower(), east='false', west='false', waterlogged='false')
        self.timber((18, 17, 17, 18, 29, 17))
        for y in range(17, 30):
            self.put(18, y, 16, 'minecraft:ladder', facing='north', waterlogged='false')
        # A landing next to the top rung is open and two blocks tall.
        self.box((17, 28, 16, 17, 30, 16), 'minecraft:air')

    def spruce_tree(self, x, z, height, radius):
        """Grow a narrow, tiered spruce with persistent, connected foliage."""
        self.box((x, 2, z, x, height, z), 'minecraft:spruce_log', axis='y')
        for y in range(5, height + 2, 3):
            r = max(1, min(radius, (height + 2 - y) // 3))
            for dx in range(-r, r + 1):
                for dz in range(-r, r + 1):
                    if abs(dx) + abs(dz) > r + 1:
                        continue
                    px, pz = x + dx, z + dz
                    if self.inside(px, y, pz) and not (dx == 0 and dz == 0) and self.get(px, y, pz) in [None, 'minecraft:air']:
                        self.put(px, y, pz, 'minecraft:spruce_leaves', persistent='true', distance=str(min(7, abs(dx) + abs(dz))))
            for dx, dz in [(0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)]:
                if self.inside(x + dx, y + 1, z + dz) and self.get(x + dx, y + 1, z + dz) in [None, 'minecraft:air']:
                    self.put(x + dx, y + 1, z + dz, 'minecraft:spruce_leaves', persistent='true', distance='1')
        self.put(x, height + 1, z, 'minecraft:spruce_leaves', persistent='true', distance='1')
        self.put(x, height + 2, z, 'minecraft:spruce_leaves', persistent='true', distance='2')

    def garden(self):
        """Finish the base with side spruces, ferns, shrubs, and flowers."""
        for x, z, height, radius in [(2, 16, 23, 2), (30, 22, 18, 2), (2, 26, 13, 2), (30, 6, 11, 2)]:
            self.spruce_tree(x, z, height, radius)
        for x in range(1, 32):
            for z in range(2, 30):
                if self.get(x, 1, z) != 'minecraft:grass_block' or self.get(x, 2, z) is not None:
                    continue
                if 7 <= x <= 25 and 8 <= z <= 23:
                    continue
                roll = self.rng.random()
                if roll < .16:
                    self.put(x, 2, z, 'minecraft:' + self.rng.choice(['short_grass'] * 4 + ['fern'] * 3 + ['dandelion', 'poppy']))
                elif roll < .18 and (x < 4 or x > 28):
                    self.put(x, 2, z, 'minecraft:azalea')
        for x, z in [(7, 4), (10, 4), (25, 27), (9, 26)]:
            self.put(x, 2, z, 'minecraft:flowering_azalea')

    def build(self) -> "Build":
        """Build the manor in structural order, then furnish and landscape it."""
        self.terrain()
        self.stone_base()
        self.postwork()
        self.timber_floor()
        self.roof()
        self.cupola()
        self.chimney()
        self.furnishings()
        self.circulation()
        self.garden()
        return self

    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)
        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()
