# Generate a furnished woodland cottage and verify its deterministic schematic.
# Bounding box: X=0..48, Y=0..42, Z=0..48 (49 x 43 x 49 blocks).
# Two occupied levels, steep east-west slate ridge, west gable and south dormer.
# Stone footing and chimney, spruce/oak frame, cream plaster, low open porch.
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": (49, 43, 49), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Emberpine Cottage",
    "author": "generator",
    "description": "Furnished two-storey timber cottage, slate roof, chimney, dormer, hearth porch and conifer garden.",
}

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] = {}
        self.rooms = {}
        self.windows = []
        self.stair_runs = []
        self.attachments = []
        self.lights = []

    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 block_name(self, x: int, y: int, z: int) -> str:
        """Return the unqualified block name at a position."""
        return (self.get(x, y, z) or "minecraft:air").split("[")[0].split(":")[1]

    def timber(self, bounds: tuple, axis: str = "y") -> None:
        """Place a warm structural timber beam."""
        self.box(bounds, "minecraft:stripped_spruce_log", axis=axis)

    def lamp(self, x: int, y: int, z: int, ceiling: int | None = None) -> None:
        """Hang a lantern from a beam, or stand it on a solid surface."""
        if ceiling is not None:
            for cy in range(y + 1, ceiling):
                self.put(x, cy, z, "minecraft:chain", axis="y")
            self.attachments.append(((x, y, z), (x, ceiling, z), "hanging"))
        else:
            self.attachments.append(((x, y, z), (x, y - 1, z), "standing"))
        self.put(x, y, z, "minecraft:lantern", hanging=str(ceiling is not None).lower())
        self.lights.append((x, y, z))

    def door(self, x: int, y: int, z: int, facing: str) -> None:
        """Place a complete, closed spruce door."""
        for dy, half in enumerate(("lower", "upper")):
            self.put(x, y + dy, z, "minecraft:spruce_door", facing=facing, half=half, hinge="left", open="false", powered="false")

    def window(self, axis: str, fixed: int, first: int, last: int, low: int, high: int) -> None:
        """Glaze an opening and record its outward daylight clearance."""
        for along in range(first, last + 1):
            for y in range(low, high + 1):
                x, z = (fixed, along) if axis == "x" else (along, fixed)
                self.put(x, y, z, "minecraft:light_gray_stained_glass_pane", north=str(axis == "x").lower(), south=str(axis == "x").lower(), east=str(axis == "z").lower(), west=str(axis == "z").lower())
                self.windows.append((x, y, z, axis))

    def pot(self, x: int, y: int, z: int, plant: str) -> None:
        """Place a supported flowerpot."""
        self.put(x, y, z, f"minecraft:potted_{plant}")
        self.attachments.append(((x, y, z), (x, y - 1, z), "standing"))

    def bed(self, x: int, y: int, z: int, color: str) -> None:
        """Place a north-facing bed with head and foot."""
        self.put(x, y, z, f"minecraft:{color}_bed", part="foot", facing="north", occupied="false")
        self.put(x, y, z - 1, f"minecraft:{color}_bed", part="head", facing="north", occupied="false")

    def stair_run(self, positions: list, facing: str, material: str) -> None:
        """Build a walking stair with masonry or timber under every tread."""
        for x, y, z in positions:
            self.box((x, 0 if material == "stone_brick" else 5, z, x, y - 1, z), "minecraft:stone_bricks" if material == "stone_brick" else "minecraft:spruce_planks")
            self.put(x, y, z, f"minecraft:{material}_stairs", facing=facing, half="bottom", shape="straight")
        self.stair_runs.append((positions, facing))

    def terrain(self) -> None:
        """Make an irregular mossy island and a raised, retained stone terrace."""
        ground = ["grass_block"] * 6 + ["moss_block"] * 4 + ["coarse_dirt", "rooted_dirt", "mossy_cobblestone", "cobblestone"]
        for x in range(2, 47):
            for z in range(3, 47):
                # Chamfered, irregular edge, kept separate from the level terrace.
                corner = max(0, 8 - x) + max(0, x - 40) + max(0, 8 - z) + max(0, z - 40)
                if corner > 8 + self.rng.randrange(3) or (z >= 44 and not 18 <= x <= 29):
                    continue
                top = 1 if z >= 43 or x <= 3 or x >= 45 else 2
                self.box((x, 0, z, x, top - 1, z), "minecraft:dirt")
                self.put(x, top, z, "minecraft:" + self.rng.choice(ground))
        stone = ["minecraft:stone_bricks"] * 5 + ["minecraft:andesite"] * 3 + ["minecraft:cobblestone"] * 2 + ["minecraft:mossy_stone_bricks", "minecraft:mossy_cobblestone"] * 2
        self.stone_palette = stone
        self.texture((5, 2, 10, 40, 4, 40), stone)
        self.texture((5, 5, 20, 11, 5, 38), stone)
        self.texture((11, 5, 31, 39, 5, 40), stone)
        # A low broken retaining edge leaves the broad centre stair unobstructed.
        for x in list(range(6, 19)) + list(range(28, 41)):
            self.put(x, 5, 40, self.rng.choice(stone))
            if x % 4 == 0:
                self.put(x, 6, 40, "minecraft:stone_brick_wall", east="low", west="low", up="true")
        for z in range(13, 39):
            if z not in range(23, 29):
                self.put(5, 5, z, self.rng.choice(stone))
        for x in range(20, 27):
            self.stair_run([(x, 2 + step, 44 - step) for step in range(4)], "north", "stone_brick")
            self.box((x, 0, 45, x, 1, 46), "minecraft:cobblestone")
        self.texture((19, 5, 40, 27, 5, 40), stone)
        # Low corners and irregular buttresses at the terrace foot.
        for x, z in ((4, 20), (4, 30), (8, 41), (15, 41), (30, 41), (40, 37), (41, 19)):
            self.texture((x, 2, z, x + 1, 3, z + 1), stone)
            self.put(x, 4, z, "minecraft:mossy_stone_brick_slab", type="bottom")

    def house(self) -> None:
        """Build both sealed storeys, the frame, glazed openings and vestibule."""
        plaster = ["minecraft:smooth_sandstone"] * 12 + ["minecraft:sandstone"] * 2 + ["minecraft:calcite"]
        floor = ["minecraft:spruce_planks"] * 25 + ["minecraft:oak_planks", "minecraft:dark_oak_planks"]
        self.texture((12, 3, 12, 34, 4, 30), self.stone_palette)
        self.texture((12, 5, 12, 34, 5, 30), floor)
        self.texture((12, 12, 12, 34, 12, 30), floor)
        for x in (12, 34):
            for z in range(12, 31):
                self.texture((x, 6, z, x, 26 - abs(z - 21), z), plaster)
        for z in (12, 30):
            self.texture((13, 6, z, 33, 17, z), plaster)
        # Vertical posts, exposed ground and floor girders.
        for x in (12, 18, 24, 29, 34):
            for z in (12, 30):
                self.timber((x, 6, z, x, 18, z))
        for z in (12, 16, 21, 26, 30):
            for x in (12, 34):
                self.timber((x, 6, z, x, 26 - abs(z - 21), z))
        for y in (6, 12):
            for z in (12, 30):
                self.timber((12, y, z, 34, y, z), "x")
            for x in (12, 34):
                self.timber((x, y, 12, x, y, 30), "z")
        # Interior ground-floor ceiling joists and an upstairs partition.
        for x in (16, 23, 30):
            self.box((x, 11, 13, x, 11, 29), "minecraft:spruce_log", axis="z")
        self.texture((23, 6, 13, 23, 10, 29), plaster)
        self.box((23, 6, 21, 23, 9, 23), "minecraft:air")
        for z in range(13, 30):
            self.texture((23, 13, z, 23, 26 - abs(z - 21), z), plaster)
        self.box((23, 13, 21, 23, 15, 23), "minecraft:air")
        self.timber((23, 16, 20, 23, 16, 24), "z")
        # Cream west gable framed with fine, sloped spruce trim outside the slate.
        self.timber((11, 14, 18, 11, 14, 25), "z")
        self.timber((11, 21, 17, 11, 21, 25), "z")
        self.timber((11, 19, 21, 11, 26, 21))
        for z in range(13, 30):
            if z not in range(18, 25):
                self.put(11, 26 - abs(z - 21), z, "minecraft:spruce_log", axis="z")
        self.window("x", 12, 19, 20, 15, 18)
        self.window("x", 12, 22, 23, 15, 18)
        self.window("x", 12, 27, 28, 8, 10)
        self.window("x", 34, 20, 22, 15, 18)
        self.window("x", 34, 25, 27, 8, 10)
        self.window("x", 34, 14, 16, 8, 10)
        for first, last in ((14, 16), (20, 22), (26, 28), (31, 32)):
            self.window("z", 12, first, last, 8, 10)
        self.window("z", 30, 14, 16, 8, 10)
        self.window("z", 30, 27, 28, 8, 10)
        self.window("z", 30, 31, 32, 8, 10)
        # Broad west sill and two side shutters, below and beside the glazing.
        self.box((11, 14, 19, 11, 14, 23), "minecraft:spruce_slab", type="top")
        for z in (18, 24):
            for y in range(15, 19):
                self.put(11, y, z, "minecraft:spruce_trapdoor", facing="west", half="bottom", open="true")
        self.door(25, 6, 30, "south")
        # Small front entrance bay under its own lower lean-to roof.
        self.texture((6, 3, 21, 11, 4, 30), self.stone_palette)
        self.texture((6, 5, 21, 11, 5, 30), floor)
        self.texture((6, 6, 21, 6, 10, 30), plaster)
        for z in (21, 30):
            self.texture((6, 6, z, 11, 12, z), plaster)
        for x, z in ((6, 21), (6, 30), (11, 21), (11, 30)):
            self.timber((x, 6, z, x, 11, z))
        self.timber((6, 10, 21, 6, 10, 30), "z")
        self.door(6, 6, 25, "west")
        self.door(12, 6, 25, "west")
        self.window("x", 6, 22, 23, 7, 9)
        self.window("x", 6, 27, 28, 7, 9)
        self.window("z", 30, 8, 9, 7, 9)
        self.rooms = {
            "entrance vestibule": {
                "feet": (8, 6, 25),
                "floor": 5
            },
            "hearth and dining room": {
                "feet": (19, 6, 24),
                "floor": 5
            },
            "kitchen and stair hall": {
                "feet": (26, 6, 22),
                "floor": 5
            },
            "upstairs bedroom": {
                "feet": (20, 13, 22),
                "floor": 12
            },
            "upstairs study": {
                "feet": (28, 13, 22),
                "floor": 12
            },
            "dormer alcove": {
                "feet": (26, 13, 30),
                "floor": 12
            },
        }

    def roofs(self) -> None:
        """Lay a steep sealed slate roof, projecting dormer and low porch roof."""
        tile = ["minecraft:deepslate_tiles"] * 10 + ["minecraft:deepslate_bricks"] * 2 + ["minecraft:cobbled_deepslate"]
        for z in range(10, 33):
            y = 27 - abs(z - 21)
            for x in range(10, 37):
                self.put(x, y, z, self.rng.choice(tile))
                if z != 21:
                    stairs = "deepslate_tile_stairs" if self.rng.random() < 0.8 else "deepslate_brick_stairs"
                    self.put(x, y + 1, z, "minecraft:" + stairs, facing="south" if z < 21 else "north", half="bottom", shape="straight")
                else:
                    self.put(x, y + 1, z, "minecraft:deepslate_tile_slab", type="bottom")
            # Dark full-block bargeboards make a thick, crisp gable edge.
            for x in (10, 36):
                self.put(x, y, z, "minecraft:polished_deepslate")
        for x in (9, 37):
            self.put(x, 27, 21, "minecraft:spruce_log", axis="x")
            self.put(x, 28, 21, "minecraft:oak_slab", type="bottom")
        # Dormer cuts through the south slope; its interior is part of the study.
        self.box((24, 12, 30, 28, 12, 32), "minecraft:spruce_planks")
        self.box((25, 13, 26, 27, 24, 31), "minecraft:air")
        plaster = ["minecraft:smooth_sandstone"] * 7 + ["minecraft:sandstone"]
        for x in (24, 28):
            self.texture((x, 13, 26, x, 22, 32), plaster)
            self.timber((x, 13, 32, x, 22, 32))
        for x in range(24, 29):
            self.texture((x, 13, 32, x, 24 - abs(x - 26), 32), plaster)
            self.texture((x, 22, 26, x, 24 - abs(x - 26), 26), plaster)
        for x in (24, 28):
            self.timber((x, 17, 32, x, 22, 32))
        self.timber((24, 17, 32, 28, 17, 32), "x")
        self.timber((24, 22, 32, 28, 22, 32), "x")
        self.window("z", 32, 25, 27, 18, 21)
        for x in range(23, 30):
            y = 25 - abs(x - 26)
            for z in range(26, 34):
                self.put(x, y, z, self.rng.choice(tile))
                if x != 26:
                    self.put(x, y + 1, z, "minecraft:deepslate_tile_stairs", facing="east" if x < 26 else "west", half="bottom", shape="straight")
                else:
                    self.put(x, y + 1, z, "minecraft:deepslate_tile_slab", type="bottom")
        # The base of each roof stair is a full slate block; no loose shingles.
        for x in range(4, 13):
            y = 11 + (x - 4) // 3
            for z in range(20, 32):
                self.put(x, y, z, self.rng.choice(tile))
                self.put(x, y + 1, z, "minecraft:deepslate_tile_slab", type="bottom")
        # Fill the small triangular lean-to ends, closing the entrance bay.
        for x in range(6, 12):
            for z in (21, 30):
                for y in range(11, 11 + (x - 4) // 3):
                    self.put(x, y, z, "minecraft:smooth_sandstone")
        for z in range(28, 41):
            y = 16 - (z - 28) // 3
            for x in range(16, 41):
                if 24 <= x <= 28 and z <= 32:
                    continue
                # Preserve the higher main roof at the attachment.
                if self.block_name(x, y, z) == "air":
                    self.put(x, y, z, self.rng.choice(tile))
                if self.block_name(x, y + 1, z) == "air":
                    self.put(x, y + 1, z, "minecraft:deepslate_tile_slab", type="bottom")
        # Exposed porch rafters and a continuous fascia.
        self.box((16, 11, 40, 40, 11, 40), "minecraft:spruce_log", axis="x")
        for x in (17, 26, 38):
            self.box((x, 12, 31, x, 12, 39), "minecraft:spruce_log", axis="z")
        self.box((17, 12, 38, 38, 12, 38), "minecraft:spruce_log", axis="x")

    def chimney(self) -> None:
        """Make the tall weathered chimney, vented crown and sheltered fire."""
        palette = ["minecraft:stone_bricks"] * 5 + ["minecraft:andesite"] * 3 + ["minecraft:cobblestone", "minecraft:cracked_stone_bricks"]
        self.texture((8, 3, 14, 11, 32, 17), palette)
        self.texture((9, 5, 13, 12, 10, 18), palette)
        for y in (12, 24, 31):
            self.texture((7, y, 13, 12, y, 18), palette)
        for x in (8, 11):
            for z in (14, 17):
                self.box((x, 33, z, x, 34, z), "minecraft:stone_brick_wall", up="true")
        self.box((7, 35, 13, 12, 35, 18), "minecraft:stone_brick_slab", type="bottom")
        self.put(9, 33, 15, "minecraft:campfire", lit="true", signal_fire="false", facing="north")
        for x, y, z in ((8, 16, 13), (10, 20, 13), (7, 27, 15), (7, 19, 16), (10, 29, 18)):
            self.put(x, y, z, "minecraft:stone_button", face="wall", facing="west" if x == 7 else "north" if z == 13 else "south")
        # Heavy masonry fire surround in the west living room.
        self.texture((13, 6, 14, 15, 9, 18), palette)
        self.box((15, 6, 15, 15, 7, 17), "minecraft:air")
        for z in (15, 16, 17):
            self.put(15, 6, z, "minecraft:campfire", facing="east", lit="true")
            self.lights.append((15, 6, z))
        self.box((13, 10, 14, 15, 10, 18), "minecraft:stone_brick_slab", type="bottom")

    def circulation(self) -> None:
        """Cut the stairwell and build a two-block-wide supported staircase."""
        self.box((30, 12, 18, 33, 12, 22), "minecraft:air")
        # Remove the crossing ground-floor joist above the upper treads.
        self.box((30, 11, 18, 33, 11, 21), "minecraft:air")
        for x in (31, 32):
            self.stair_run([(x, 6 + i, 24 - i) for i in range(7)], "north", "spruce")
            for i in range(7):
                y, z = 6 + i, 24 - i
                self.box((x, y + 1, z, x, y + 3, z), "minecraft:air")
        # Stairwell perimeter rail has solid floor below it.
        for z in range(18, 23):
            self.put(30, 12, z, "minecraft:spruce_planks")
            self.put(30, 13, z, "minecraft:spruce_fence", north="true", south="true")
            self.put(33, 12, z, "minecraft:spruce_planks")
            self.put(33, 13, z, "minecraft:spruce_fence", north="true", south="true")
        self.put(30, 13, 17, "minecraft:spruce_fence", south="true", east="true")
        self.timber((30, 6, 24, 30, 10, 24))

    def furnishings(self) -> None:
        """Furnish each level with working, dining, cooking and sleeping fittings."""
        # Entrance cloak bench and tidy storage, clear of the door axis.
        for z in (22, 23):
            self.put(10, 6, z, "minecraft:spruce_stairs", facing="east", half="bottom", shape="straight")
        self.put(10, 6, 28, "minecraft:barrel", facing="up")
        self.pot(10, 7, 28, "fern")
        self.lamp(9, 8, 26, ceiling=12)
        # Hearth room: a fireplace, broad table, chairs, sideboard and bookcase.
        self.box((13, 6, 20, 13, 8, 22), "minecraft:bookshelf")
        self.box((14, 6, 28, 16, 6, 29), "minecraft:spruce_planks")
        self.put(14, 7, 29, "minecraft:barrel", facing="south")
        self.pot(16, 7, 29, "dandelion")
        for z in (20, 21, 22):
            self.put(18, 6, z, "minecraft:spruce_fence", north="true", south="true")
            self.put(18, 7, z, "minecraft:spruce_pressure_plate")
            self.put(20, 6, z, "minecraft:spruce_stairs", facing="east", half="bottom", shape="straight")
        self.box((16, 6, 24, 21, 6, 26), "minecraft:orange_carpet")
        # Keep the central entrance-to-arch walking strip uncarpeted for navigation.
        self.box((17, 6, 24, 22, 6, 24), "minecraft:air")
        self.put(21, 6, 14, "minecraft:chest", facing="south", type="single")
        self.lamp(19, 9, 18, ceiling=12)
        self.lamp(20, 9, 27, ceiling=12)
        # Kitchen on the back wall, windows remain completely clear above counters.
        for x in range(25, 30):
            self.put(x, 6, 13, "minecraft:spruce_planks")
        self.put(25, 6, 13, "minecraft:smoker", facing="south", lit="true")
        self.put(26, 6, 13, "minecraft:furnace", facing="south", lit="true")
        self.put(27, 6, 13, "minecraft:water_cauldron", level="3")
        self.put(28, 6, 13, "minecraft:crafting_table")
        self.put(29, 6, 13, "minecraft:barrel", facing="south")
        self.pot(29, 7, 13, "red_mushroom")
        self.box((24, 6, 15, 24, 7, 18), "minecraft:barrel", facing="east")
        self.put(27, 6, 18, "minecraft:oak_planks")
        self.put(28, 6, 18, "minecraft:oak_planks")
        self.put(28, 7, 18, "minecraft:stone_pressure_plate")
        self.put(33, 6, 28, "minecraft:composter", level="5")
        self.put(29, 6, 28, "minecraft:chest", facing="west", type="single")
        self.lamp(27, 9, 16, ceiling=12)
        self.lamp(28, 9, 25, ceiling=12)
        # Bedroom under the tall gable: paired beds, wardrobe, reading nook.
        self.bed(16, 13, 18, "orange")
        self.bed(17, 13, 18, "brown")
        self.box((15, 13, 16, 18, 14, 16), "minecraft:spruce_planks")
        for x in (15, 18):
            self.put(x, 13, 17, "minecraft:barrel", facing="up")
            self.lamp(x, 14, 17)
        self.box((20, 13, 14, 21, 15, 14), "minecraft:barrel", facing="south")
        self.put(14, 13, 25, "minecraft:spruce_stairs", facing="west", half="bottom", shape="straight")
        self.box((13, 13, 27, 13, 15, 28), "minecraft:bookshelf")
        self.put(15, 13, 28, "minecraft:chest", facing="north", type="single")
        self.box((16, 13, 20, 18, 13, 23), "minecraft:brown_carpet")
        self.put(20, 13, 27, "minecraft:barrel", facing="up")
        self.lamp(20, 14, 27)
        self.box((13, 13, 19, 13, 13, 23), "minecraft:spruce_planks")
        self.lamp(13, 14, 19)
        self.lamp(13, 14, 23)
        # Study, textile work table, shelves and dormer writing nook.
        self.box((25, 13, 14, 28, 14, 14), "minecraft:bookshelf")
        self.put(25, 13, 16, "minecraft:loom", facing="south")
        self.put(27, 13, 16, "minecraft:cartography_table")
        self.put(28, 13, 16, "minecraft:barrel", facing="south")
        self.put(28, 14, 16, "minecraft:lantern", hanging="false")
        self.lights.append((28, 14, 16))
        self.box((32, 13, 26, 33, 13, 27), "minecraft:spruce_planks")
        self.put(32, 14, 27, "minecraft:lectern", facing="west", has_book="false")
        self.put(31, 13, 27, "minecraft:spruce_stairs", facing="west", half="bottom", shape="straight")
        self.lamp(33, 14, 26)
        self.put(25, 13, 31, "minecraft:barrel", facing="up")
        self.put(27, 13, 31, "minecraft:spruce_planks")
        self.pot(27, 14, 31, "azure_bluet")
        self.lamp(25, 14, 31)
        self.lamp(26, 19, 30, ceiling=25)
        self.put(26, 13, 28, "minecraft:spruce_stairs", facing="north", half="bottom", shape="straight")

    def porch(self) -> None:
        """Build the open work porch, glowing outdoor hearth and stacked supplies."""
        self.texture((16, 5, 31, 39, 5, 39), ["minecraft:spruce_planks"] * 20 + ["minecraft:oak_planks"])
        for x in (17, 26, 38):
            self.texture((x, 5, 38, x, 6, 38), self.stone_palette)
            self.timber((x, 7, 38, x, 12, 38))
            self.put(x, 10, 37, "minecraft:oak_fence", south="true", north="true")
            self.put(x, 11, 37, "minecraft:oak_planks")
            self.lamp(x, 9, 36, ceiling=12)
        for z in (32, 35):
            self.timber((38, 6, z, 38, 13, z))
        # Open front and side bays; rails only behind the working furniture.
        self.box((38, 7, 33, 38, 7, 37), "minecraft:spruce_fence", north="true", south="true")
        self.box((30, 7, 38, 36, 7, 38), "minecraft:spruce_fence", east="true", west="true")
        for x in (30, 36):
            self.box((x, 5, 38, x, 6, 38), "minecraft:spruce_log", axis="y")
        # Stone hearth, recessed fires, masonry hood attached to the house wall.
        self.texture((18, 5, 31, 22, 5, 34), self.stone_palette)
        for x in (18, 22):
            self.texture((x, 6, 31, x, 9, 33), self.stone_palette)
        self.texture((18, 10, 31, 22, 10, 33), self.stone_palette)
        self.texture((19, 11, 31, 21, 12, 32), self.stone_palette)
        for x in (19, 20, 21):
            self.put(x, 6, 31, "minecraft:magma_block")
            self.put(x, 7, 31, "minecraft:shroomlight")
            self.put(x, 6, 32, "minecraft:campfire", lit="true", facing="south")
            self.lights.append((x, 6, 32))
        self.box((18, 6, 34, 22, 6, 34), "minecraft:stone_brick_slab", type="bottom")
        self.put(23, 6, 33, "minecraft:cauldron")
        self.put(24, 6, 34, "minecraft:anvil", facing="north")
        # Carpenter's workbench and provisions in the far porch bay.
        self.put(34, 6, 32, "minecraft:crafting_table")
        self.box((35, 6, 32, 36, 6, 32), "minecraft:barrel", facing="south")
        self.put(36, 7, 32, "minecraft:grindstone", face="floor", facing="south")
        self.put(32, 6, 34, "minecraft:oak_stairs", facing="south", half="bottom", shape="straight")
        for x, z in ((29, 32), (29, 33), (35, 36), (36, 36)):
            self.put(x, 6, z, "minecraft:barrel", facing="up")
        self.put(35, 7, 36, "minecraft:barrel", facing="south")
        self.pot(36, 7, 36, "fern")
        self.box((32, 6, 37, 34, 6, 37), "minecraft:spruce_log", axis="z")
        self.put(33, 7, 37, "minecraft:spruce_log", axis="z")
        # Entrance lamps and low stone bollards along the stair.
        self.lamp(5, 8, 24, ceiling=11)
        self.lamp(5, 8, 29, ceiling=11)
        for x in (18, 28):
            self.texture((x, 2, 42, x, 4, 42), self.stone_palette)
            self.lamp(x, 5, 42)
        self.texture((8, 5, 35, 8, 6, 35), self.stone_palette)
        self.lamp(8, 7, 35)

    def pine(self, x: int, z: int, height: int) -> None:
        """Grow an irregular conifer with visible boughs and attached persistent needles."""
        base = max(y for y in range(5) if self.block_name(x, y, z) != "air")
        self.put(x, base, z, "minecraft:rooted_dirt")
        self.box((x, base + 1, z, x, base + height, z), "minecraft:spruce_log", axis="y")
        for level in range(5, height, 4):
            radius = 3 if level < height * 0.55 else 2 if level < height - 4 else 1
            cy = base + level
            for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                for distance in range(1, radius):
                    self.put(x + dx * distance, cy, z + dz * distance, "minecraft:spruce_log", axis="x" if dx else "z")
            for dy, rad in ((-1, radius), (0, max(1, radius - 1)), (1, max(1, radius - 2))):
                for dx in range(-rad, rad + 1):
                    for dz in range(-rad, rad + 1):
                        if abs(dx) + abs(dz) > rad:
                            continue
                        if self.block_name(x + dx, cy + dy, z + dz) == "air":
                            self.put(x + dx, cy + dy, z + dz, "minecraft:spruce_leaves", persistent="true", distance="1")
        self.put(x, base + height + 1, z, "minecraft:spruce_leaves", persistent="true", distance="1")
        for cy in range(base + height - 5, base + height + 1):
            for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                if cy < base + height - 1:
                    self.put(x + dx, cy, z + dz, "minecraft:spruce_leaves", persistent="true", distance="1")
            if cy >= base + height - 1:
                self.put(x, cy, z, "minecraft:spruce_leaves", persistent="true", distance="1")

    def landscaping(self) -> None:
        """Add conifers, low planting, ivy, window boxes and outdoor supplies."""
        for x, z, height in ((42, 10, 36), (7, 8, 23), (43, 29, 24), (3, 19, 18)):
            self.pine(x, z, height)
        # Window box below the dormer, backed by the sill.
        self.box((25, 16, 33, 27, 16, 33), "minecraft:spruce_planks")
        for x in range(25, 28):
            self.put(x, 17, 33, "minecraft:flowering_azalea_leaves", persistent="true", distance="1")
        self.box((25, 16, 34, 27, 16, 34), "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true")
        # Creeping vines attach to west and south wall surfaces only.
        for z in (18, 29):
            for y in range(7, 14 if z == 29 else 20):
                if self.block_name(11, y, z) == "air" and self.block_name(12, y, z) not in ("air", "light_gray_stained_glass_pane"):
                    self.put(11, y, z, "minecraft:vine", east="true")
                    self.attachments.append(((11, y, z), (12, y, z), "wall"))
        for x in (13, 33):
            for y in range(7, 13):
                if self.block_name(x, y, 31) == "air":
                    self.put(x, y, 31, "minecraft:vine", north="true")
                    self.attachments.append(((x, y, 31), (x, y, 30), "wall"))
        # Intentionally irregular grass, ferns and flowers surround the terrace.
        plants = ["fern"] * 6 + ["short_grass"] * 5 + ["dandelion", "poppy", "azure_bluet", "brown_mushroom"]
        for x in range(3, 47):
            for z in range(4, 47):
                if 5 <= x <= 40 and 10 <= z <= 40:
                    continue
                if 18 <= x <= 29 and z >= 40:
                    continue
                for y in (2, 1):
                    if self.block_name(x, y, z) in ("grass_block", "moss_block", "coarse_dirt", "rooted_dirt") and self.block_name(x, y + 1, z) == "air":
                        if self.rng.random() < 0.40:
                            self.put(x, y + 1, z, "minecraft:" + self.rng.choice(plants))
                            self.attachments.append(((x, y + 1, z), (x, y, z), "standing"))
                        break
        for x, z in ((7, 39), (13, 39), (37, 40), (40, 34), (5, 20)):
            if self.block_name(x, 5, z) == "air":
                self.put(x, 5, z, "minecraft:moss_block")
            self.put(x, 6, z, "minecraft:azalea")
            self.attachments.append(((x, 6, z), (x, 5, z), "standing"))
        for x, z in ((14, 33), (15, 34), (11, 35)):
            self.put(x, 6, z, "minecraft:barrel", facing="up")
        self.pot(14, 7, 33, "spruce_sapling")
        for x, z in ((37, 29), (39, 26)):
            self.box((x, 5, z, x, 6, z), "minecraft:hay_block", axis="y")
        self.box((35, 5, 16, 35, 6, 20), "minecraft:spruce_log", axis="x")
        self.box((36, 5, 17, 36, 5, 19), "minecraft:spruce_log", axis="x")
        # Low planter crates, rough terrace rubble and small roof moss patches.
        for x, z in ((7, 32), (10, 32), (39, 34), (39, 37)):
            self.put(x, 6, z, "minecraft:spruce_planks")
            self.put(x, 7, z, "minecraft:azalea_leaves", persistent="true", distance="1")
            self.put(x, 6, z + 1, "minecraft:spruce_trapdoor", facing="south", open="true", half="bottom")
        for x, z in ((6, 41), (12, 41), (33, 41), (38, 41), (41, 32)):
            self.texture((x, 1, z, x + 1, 2, z), self.stone_palette)
            self.put(x, 3, z, "minecraft:mossy_cobblestone_slab", type="bottom")
        for x, z in ((14, 24), (20, 29), (31, 25), (32, 19), (17, 14), (29, 16)):
            self.put(x, 28 - abs(z - 21), z, "minecraft:moss_carpet")

    def build(self) -> "Build":
        """Reconstruct the reference and furnish the unseen rooms consistently."""
        self.terrain()
        self.house()
        self.roofs()
        self.chimney()
        self.circulation()
        self.furnishings()
        self.porch()
        self.landscaping()
        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)
        # Litematica's normal on-disk format is gzip-compressed NBT.
        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()
