# Generate and verify a furnished Java 1.21.1 Root House with deterministic block and entity data.
from __future__ import annotations

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

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

from mcio.nbt.nbt import File as NBTFile
from mcio.nbt.tag import Compound, Int, Byte, String, Double, Float, IntArray, List as NBTList
from mcio.schematic import load_schematic # noqa: E402
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402

CONFIG = {
    "seed": 20260923,
    "size_xyz": (25, 23, 22), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Root House",
    "author": "generator",
    "description": "A hearth and sleeping loft sheltered beneath ancient dark oak roots. Front faces north (-Z).",
}

AIR = 'minecraft:air'
ROCK = ['minecraft:stone'] * 4 + ['minecraft:cobblestone'] * 3 + ['minecraft:mossy_cobblestone'] * 3 + ['minecraft:andesite']
HILL_TOP = ['minecraft:moss_block'] * 5 + ['minecraft:mossy_cobblestone'] * 3 + ['minecraft:stone', 'minecraft:rooted_dirt']
GROUND_BASE = ['minecraft:dirt'] * 5 + ['minecraft:rooted_dirt'] * 2 + ['minecraft:stone']
GROUND_TOP = ['minecraft:grass_block'] * 7 + ['minecraft:moss_block'] * 2 + ['minecraft:coarse_dirt']
PATH = ['minecraft:andesite'] * 3 + ['minecraft:cobblestone'] * 3 + ['minecraft:stone', 'minecraft:mossy_cobblestone']
FLOOR = ['minecraft:spruce_planks'] * 8 + ['minecraft:dark_oak_planks']
CARDINAL = [(1, 0), (-1, 0), (0, 1), (0, -1)]
MAIN_ROWS = {9: (7, 18), 10: (6, 18), 11: (6, 18), 12: (6, 18), 13: (6, 18), 14: (6, 18), 15: (6, 18), 16: (6, 18), 17: (7, 17), 18: (9, 16)}
LOFT_ROWS = {10: (10, 15), 11: (8, 17), 12: (8, 17), 13: (8, 17), 14: (8, 17), 15: (8, 18), 16: (8, 18), 17: (9, 16)}

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.block_entities: list[Compound] = []
        self.entities: list[Compound] = []

    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 the rooted hillside, two furnished floors, and planted forecourt.

        Returns:
            Build: Completed build ready for export.
        """
        self.terrain()
        self.roots()
        self.rooms()
        self.front()
        self.furnish()
        self.garden()
        self.orient_front()
        return self

    def terrain(self) -> None:
        """Raise an irregular stone hill above a rounded woodland ground patch."""
        for x in range(self.size_x):
            for z in range(self.size_z):
                if ((x - 12) / 13.0)**2 + ((z - 11) / 12.2)**2 > 1.25:
                    continue
                self.put(x, 0, z, self.rng.choice(GROUND_BASE))
                self.put(x, 1, z, self.rng.choice(GROUND_TOP))
                if z < 7:
                    continue
                radial = ((x - 12) / 11.0)**2 + ((z - 13.5) / 9.0)**2
                if radial >= 1:
                    continue
                height = 2 + int(12.5 * (1 - radial)**0.58)
                for y in range(2, height + 1):
                    palette = ROCK if y < height else HILL_TOP
                    self.put(x, y, z, self.rng.choice(palette))
        # An uneven flagstone walk disappears beneath the porch.
        for z in range(0, 9):
            for x in range(10, 14):
                if z < 3 and (x + z) % 5 == 0:
                    continue
                self.put(x, 1, z, self.rng.choice(PATH))
        self.texture((9, 2, 4, 15, 2, 8), PATH)
        for x in range(10, 14):
            self.put(x, 2, 3, 'minecraft:stone_brick_stairs', facing='south', half='bottom', shape='straight', waterlogged='false')

    def root(self, points: list[Position], radius: int = 1, front: bool = False) -> None:
        """Sweep overlapping squared timbers along an irregular root centerline.

        Args:
            points (list[Position]): XYZ nodes of the connected root.
            radius (int): Half width of the thick root.
            front (bool): Add plank scales to the forward face.
        """
        for a, b in zip(points, points[1:]):
            delta = tuple(b[i] - a[i] for i in range(3))
            steps = max(abs(d) for d in delta)
            axis = 'xyz'[max(range(3), key=lambda i: abs(delta[i]))]
            for step in range(steps + 1):
                p = tuple(round(a[i] + delta[i] * step / max(steps, 1)) for i in range(3))
                for dx in range(-radius, radius + 1):
                    for dy in range(-radius, radius + 1):
                        for dz in range(-radius, radius + 1):
                            if radius and abs(dx) + abs(dy) + abs(dz) > 2 * radius:
                                continue
                            x, y, z = p[0] + dx, p[1] + dy, p[2] + dz
                            if not self.inside(x, y, z):
                                continue
                            wood = 'minecraft:dark_oak_log'
                            if dx == 0 or (axis == 'x' and dy == 0):
                                wood = 'minecraft:stripped_dark_oak_log'
                            if front and dz == -radius and dy <= 0:
                                self.put(x, y, z, 'minecraft:dark_oak_planks')
                            else:
                                self.put(x, y, z, wood, axis=axis)

    def roots(self) -> None:
        """Spread thick roots down the hill and curl three broken tips skyward."""
        # Hollowing follows this step, so these timbers become the room envelope.
        for y in range(10, 18):
            for x in range(8, 17):
                for z in range(9, 17):
                    if ((x - 12) / (4.6 - max(0, y - 14) * 0.3))**2 + ((z - 12.5) / 4.2)**2 <= 1:
                        wood = 'minecraft:stripped_dark_oak_log' if x in (10, 13) else 'minecraft:dark_oak_log'
                        self.put(x, y, z, wood, axis='y')
        # Two sweeping front roots, each with a raised, hooked toe.
        self.root([(11, 16, 10), (9, 14, 9), (7, 12, 8), (6, 10, 7), (4, 8, 6), (3, 6, 5), (2, 4, 4)], front=True)
        self.root([(2, 4, 4), (1, 4, 3), (1, 5, 3)], radius=0, front=True)
        self.root([(14, 16, 10), (16, 14, 9), (17, 12, 8), (19, 10, 7), (21, 8, 6), (22, 6, 5), (22, 4, 4)], front=True)
        self.root([(22, 4, 4), (23, 4, 3), (23, 5, 3)], radius=0, front=True)
        # Front buttresses remain stout and rooted in the ground.
        self.root([(6, 10, 8), (5, 7, 7), (5, 2, 6), (4, 2, 4)], front=True)
        self.root([(18, 11, 8), (19, 7, 7), (20, 2, 6), (21, 2, 4)], front=True)
        # Side and rear ribs run down the slope, leaving mossy stone panels.
        for nodes in [
            [(9, 15, 13), (6, 13, 13), (4, 10, 12), (3, 6, 11), (2, 2, 10), (1, 3, 9)],
            [(15, 15, 13), (18, 13, 13), (20, 10, 13), (22, 6, 12), (23, 2, 11)],
            [(10, 15, 15), (8, 13, 17), (6, 9, 18), (5, 5, 19), (4, 2, 20)],
            [(14, 15, 15), (16, 13, 17), (18, 9, 18), (20, 5, 19), (21, 2, 20)],
            [(12, 16, 15), (12, 13, 18), (12, 9, 20), (13, 4, 21)],
        ]:
            self.root(nodes)
        # Asymmetrical hooked crowns; air between the prongs defines the silhouette.
        self.root([(10, 16, 12), (8, 18, 12), (8, 20, 12)], front=True)
        self.box((8, 21, 11, 10, 21, 13), 'minecraft:dark_oak_planks')
        self.box((9, 22, 11, 11, 22, 12), 'minecraft:dark_oak_slab', type='bottom', waterlogged='false')
        self.root([(14, 16, 13), (17, 18, 13), (18, 20, 13)], front=True)
        self.box((16, 21, 12, 18, 21, 14), 'minecraft:dark_oak_planks')
        self.box((15, 22, 12, 17, 22, 13), 'minecraft:dark_oak_slab', type='bottom', waterlogged='false')
        self.root([(7, 12, 13), (4, 14, 14), (4, 16, 14)], front=True)
        self.box((4, 17, 13, 6, 17, 15), 'minecraft:dark_oak_slab', type='bottom', waterlogged='false')
        # Stepped grain bands follow the fan, without turning it into a roof plane.
        for x, y, z, facing in [(3, 7, 4, 'east'), (4, 9, 5, 'east'), (6, 11, 6, 'east'), (8, 14, 7, 'east'), (17, 14, 7, 'west'), (19, 12, 6, 'west'), (21, 10, 5, 'west'), (22, 8, 4, 'west')]:
            self.put(x, y, z, 'minecraft:dark_oak_stairs', facing=facing, half='bottom', shape='straight', waterlogged='false')
            self.put(x, y - 1, z, 'minecraft:dark_oak_planks')

    def rooms(self) -> None:
        """Hollow polygonal rooms and a two-block-wide staircase through the hill."""
        # Ground floor, including a modest stone perimeter beneath the whole room.
        for z, (left, right) in MAIN_ROWS.items():
            for x in range(left - 1, right + 2):
                self.put(x, 2, z, self.rng.choice(FLOOR))
                if x in (left - 1, right + 1):
                    for y in range(3, 8):
                        if self.get(x, y, z) in (None, AIR):
                            self.put(x, y, z, self.rng.choice(ROCK))
            for x in range(left, right + 1):
                self.box((x, 3, z, x, 6, z), AIR)
                self.put(x, 7, z, 'minecraft:spruce_planks')
        # Stone surrounds the front and back ends of the carved room.
        self.texture((7, 3, 8, 18, 7, 8), ROCK)
        self.texture((8, 3, 19, 17, 7, 19), ROCK)
        # A taller vaulted loft fits inside the trunk and its broad shoulder.
        for z, (left, right) in LOFT_ROWS.items():
            for x in range(left, right + 1):
                self.put(x, 7, z, 'minecraft:spruce_planks')
                self.box((x, 8, z, x, 11, z), AIR)
                ceiling = 12
                if 9 <= x <= 16 and z <= 16:
                    self.put(x, 12, z, AIR)
                    ceiling = 13
                if 10 <= x <= 14 and z <= 15:
                    self.put(x, 13, z, AIR)
                    ceiling = 14
                if 11 <= x <= 13 and z <= 15:
                    self.put(x, 14, z, AIR)
                    ceiling = 15
                if self.get(x, ceiling, z) in (None, AIR):
                    self.put(x, ceiling, z, 'minecraft:dark_oak_planks')
        # Unbroken perimeter seals the loft even where the natural hill recedes.
        loft_cells = {(x, z) for z, (a, b) in LOFT_ROWS.items() for x in range(a, b + 1)}
        for x, z in sorted(loft_cells):
            for dx, dz in CARDINAL:
                if (x + dx, z + dz) in loft_cells:
                    continue
                for y in range(7, 12):
                    if self.get(x + dx, y, z + dz) in (None, AIR):
                        self.put(x + dx, y, z + dz, self.rng.choice(ROCK))
        # Five full stair rises, facing the direction of ascent.
        for z in range(11, 16):
            y = z - 8
            for x in (17, 18):
                self.box((x, 3, z, x, y, z), 'minecraft:dark_oak_planks')
                self.put(x, y, z, 'minecraft:spruce_stairs', facing='south', half='bottom', shape='straight', waterlogged='false')
                self.box((x, y + 1, z, x, max(y + 3, 6), z), AIR)
        self.box((17, 7, 16, 18, 7, 16), 'minecraft:spruce_planks')
        self.box((17, 8, 16, 18, 10, 16), AIR)
        # Thick roots emerge as interior posts and crossbeams.
        for x, z in [(7, 10), (6, 16), (17, 9), (17, 18)]:
            self.box((x, 3, z, x, 6, z), 'minecraft:stripped_dark_oak_log', axis='y')
        self.box((7, 6, 10, 16, 6, 10), 'minecraft:dark_oak_log', axis='x')
        self.box((7, 6, 16, 16, 6, 16), 'minecraft:dark_oak_log', axis='x')
        for z in (11, 16):
            self.box((8, 8, z, 8, 11, z), 'minecraft:stripped_dark_oak_log', axis='y')
            self.box((16, 11, z, 16, 12, z), 'minecraft:dark_oak_log', axis='y')
            self.box((9, 12, z, 10, 12, z), 'minecraft:dark_oak_log', axis='x')
            self.box((14, 12, z, 16, 12, z), 'minecraft:dark_oak_log', axis='x')
        # Sturdy stairwell balustrade, kept beside the two-wide passage.
        for z in range(12, 16):
            self.put(16, 8, z, 'minecraft:dark_oak_fence', north='true', south='true', east='false', west='false', waterlogged='false')
            self.put(16, 7, z, 'minecraft:dark_oak_log', axis='z')
        # Two small daylight windows, framed in stripped timber.
        for x in range(10, 15):
            for y in range(3, 7):
                self.put(x, y, 19, 'minecraft:stripped_dark_oak_log', axis='z')
        self.box((11, 4, 19, 13, 5, 19), 'minecraft:light_gray_stained_glass')
        self.box((11, 4, 20, 13, 5, 21), AIR)
        for z in range(12, 16):
            for y in range(3, 7):
                self.put(5, y, z, 'minecraft:stripped_dark_oak_log', axis='x')
        self.box((5, 4, 13, 5, 5, 14), 'minecraft:light_gray_stained_glass')
        self.box((6, 4, 13, 6, 5, 14), AIR)
        self.box((1, 4, 13, 4, 5, 14), AIR)

    def front(self) -> None:
        """Set a recessed double doorway, small porch, and round trunk window."""
        # The open cave vestibule is outside the closed door envelope.
        self.box((10, 3, 6, 14, 5, 7), AIR)
        self.box((11, 6, 7, 13, 6, 7), 'minecraft:dark_oak_log', axis='x')
        for x in (10, 13):
            self.box((x, 3, 8, x, 5, 8), 'minecraft:stripped_dark_oak_log', axis='y')
        self.box((10, 6, 8, 13, 6, 8), 'minecraft:dark_oak_log', axis='x')
        for x, hinge in ((11, 'left'), (12, 'right')):
            for y, half in ((3, 'lower'), (4, 'upper')):
                self.put(x, y, 8, 'minecraft:spruce_door', facing='north', hinge=hinge, half=half, open='false', powered='false')
            self.put(x, 5, 8, 'minecraft:brown_stained_glass')
        for x in (9, 15):
            self.put(x, 2, 5, 'minecraft:dark_oak_log', axis='y')
            for y in range(3, 6):
                self.put(x, y, 5, 'minecraft:spruce_fence', north='false', south='false', east='false', west='false', waterlogged='false')
        self.box((9, 6, 5, 15, 6, 7), 'minecraft:spruce_planks')
        self.box((9, 6, 4, 15, 6, 4), 'minecraft:spruce_slab', type='top', waterlogged='false')
        for x in range(9, 16):
            self.put(x, 7, 4, 'minecraft:spruce_trapdoor', facing='north', half='bottom', open='false', powered='false', waterlogged='false')
        self.put(12, 5, 5, 'minecraft:lantern', hanging='true', waterlogged='false')
        # Five-block octagonal frame with a cross-shaped glazed aperture.
        for dy in range(-2, 3):
            for dx in range(-2, 3):
                if abs(dx) + abs(dy) > 3:
                    continue
                x, y = 12 + dx, 13 + dy
                if abs(dx) + abs(dy) <= 1:
                    self.put(x, y, 9, 'minecraft:light_gray_stained_glass')
                    self.put(x, y, 10, AIR)
                else:
                    self.put(x, y, 9, 'minecraft:stripped_spruce_log', axis='z')
        for x, y, facing, half in [(10, 12, 'east', 'bottom'), (10, 14, 'east', 'top'), (14, 12, 'west', 'bottom'), (14, 14, 'west', 'top')]:
            self.put(x, y, 8, 'minecraft:dark_oak_stairs', facing=facing, half=half, shape='straight', waterlogged='false')
        self.box((11, 10, 8, 13, 10, 8), 'minecraft:dark_oak_slab', type='top', waterlogged='false')

    def container(self, position: Position, block: str, facing: str, items: list[tuple[str, int]]) -> None:
        """Place a usable storage block and record its Java 1.21 item inventory.

        Args:
            position (Position): XYZ location.
            block (str): Namespaced container block.
            facing (str): Front direction.
            items (list[tuple[str, int]]): Item names and counts, one stack per slot.
        """
        self.put(*position, block, facing=facing)
        x, y, z = position
        self.block_entities.append(Compound({
            'id': String(block),
            'x': Int(x),
            'y': Int(y),
            'z': Int(z),
            'Items': NBTList[Compound]([Compound({
                'Slot': Byte(slot),
                'id': String('minecraft:' + item),
                'count': Int(count)
            }) for slot, (item, count) in enumerate(items)]),
        }))

    def furnish(self) -> None:
        """Fit a kitchen, hearth, workbench, dining corner, and sleeping loft."""
        # A stone hearth embedded in the rear wall, with a closed stone flue.
        self.box((8, 2, 16, 10, 2, 18), 'minecraft:stone_bricks')
        for x in (8, 10):
            self.box((x, 3, 17, x, 4, 17), 'minecraft:cobblestone')
        self.put(9, 3, 17, 'minecraft:campfire', facing='north', lit='true', signal_fire='false', waterlogged='false')
        self.box((8, 5, 17, 10, 5, 17), 'minecraft:stone_bricks')
        self.box((9, 4, 18, 9, 14, 18), 'minecraft:cobblestone')
        self.put(9, 15, 18, 'minecraft:campfire', facing='north', lit='true', signal_fire='false', waterlogged='false')
        for dx, dz, facing in [(0, -1, 'south'), (0, 1, 'north'), (-1, 0, 'east'), (1, 0, 'west')]:
            self.put(9 + dx, 14, 18 + dz, 'minecraft:cobblestone')
            self.put(9 + dx, 15, 18 + dz, 'minecraft:spruce_trapdoor', facing=facing, half='bottom', open='true', powered='false', waterlogged='false')
        self.put(9, 16, 18, 'minecraft:stone_brick_slab', type='top', waterlogged='false')
        self.put(6, 3, 15, 'minecraft:smoker', facing='east', lit='false')
        self.put(6, 3, 14, 'minecraft:furnace', facing='east', lit='false')
        self.container((7, 3, 17), 'minecraft:barrel', 'north', [('bread', 12), ('baked_potato', 16), ('sweet_berries', 20)])
        self.put(7, 4, 17, 'minecraft:flower_pot')
        self.put(6, 4, 15, 'minecraft:stone_pressure_plate', powered='false')
        # Workbench and shelves built into the left stone recess.
        self.put(6, 3, 11, 'minecraft:crafting_table')
        self.put(6, 3, 12, 'minecraft:loom', facing='east')
        self.box((6, 3, 10, 6, 4, 10), 'minecraft:bookshelf')
        self.put(6, 5, 10, 'minecraft:lantern', hanging='false', waterlogged='false')
        self.container((6, 3, 9), 'minecraft:barrel', 'east', [('iron_axe', 1), ('iron_pickaxe', 1), ('oak_sapling', 8)])
        # Low table and two facing chairs keep the central passage generous.
        self.box((11, 3, 12, 12, 3, 12), 'minecraft:spruce_slab', type='top', waterlogged='false')
        self.put(10, 3, 12, 'minecraft:spruce_stairs', facing='east', half='bottom', shape='straight', waterlogged='false')
        self.put(13, 3, 12, 'minecraft:spruce_stairs', facing='west', half='bottom', shape='straight', waterlogged='false')
        self.put(11, 4, 12, 'minecraft:candle', candles='2', lit='true', waterlogged='false')
        for x in range(11, 15):
            for z in range(14, 17):
                self.put(x, 3, z, 'minecraft:brown_carpet' if z in (14, 16) else 'minecraft:green_carpet')
        for x in (14, 15):
            self.container((x, 3, 18), 'minecraft:barrel', 'north', [('dark_oak_log', 32), ('coal', 24), ('wheat', 16)])
        self.container((16, 3, 18), 'minecraft:chest', 'north', [('leather_boots', 1), ('torch', 32), ('iron_ingot', 8)])
        self.put(14, 4, 18, 'minecraft:lantern', hanging='false', waterlogged='false')
        self.put(12, 5, 16, 'minecraft:lantern', hanging='true', waterlogged='false')
        self.put(15, 5, 10, 'minecraft:lantern', hanging='true', waterlogged='false')
        self.put(16, 6, 14, 'minecraft:dark_oak_log', axis='x')
        self.put(16, 5, 14, 'minecraft:lantern', hanging='true', waterlogged='false')
        # Upstairs: paired moss-green beds, writing nook, and personal storage.
        for x in (9, 10):
            self.put(x, 8, 14, 'minecraft:green_bed', facing='south', part='foot', occupied='false')
            self.put(x, 8, 15, 'minecraft:green_bed', facing='south', part='head', occupied='false')
        self.container((11, 8, 15), 'minecraft:barrel', 'up', [('book', 4), ('apple', 6)])
        self.put(11, 9, 15, 'minecraft:lantern', hanging='false', waterlogged='false')
        self.box((9, 8, 17, 10, 9, 17), 'minecraft:bookshelf')
        self.container((14, 8, 17), 'minecraft:chest', 'north', [('white_wool', 8), ('map', 1), ('compass', 1)])
        self.put(15, 8, 17, 'minecraft:barrel', facing='north', open='false')
        self.put(15, 9, 17, 'minecraft:potted_fern')
        self.box((11, 8, 10, 13, 8, 10), 'minecraft:spruce_slab', type='top', waterlogged='false')
        self.put(11, 9, 10, 'minecraft:lantern', hanging='false', waterlogged='false')
        self.put(13, 9, 10, 'minecraft:potted_red_mushroom')
        self.put(12, 8, 11, 'minecraft:spruce_stairs', facing='north', half='bottom', shape='straight', waterlogged='false')
        for x in (12, 13, 14):
            for z in (13, 14, 15):
                self.put(x, 8, z, 'minecraft:green_carpet')
        # A wall-mounted landing light rests on its bracket, above walking height.
        self.put(17, 11, 16, 'minecraft:dark_oak_planks')
        self.put(17, 10, 16, 'minecraft:lantern', hanging='true', waterlogged='false')

    def garden(self) -> None:
        """Plant the roots and arrange small outdoor household props."""
        # A leaf crown grows from the smaller left branch.
        for x in range(2, 8):
            for y in range(11, 16):
                for z in range(10, 16):
                    if ((x - 4.7) / 3)**2 + ((y - 12.7) / 2.4)**2 + ((z - 12.5) / 3.1)**2 < 1:
                        if self.get(x, y, z) in (None, AIR):
                            self.put(x, y, z, 'minecraft:oak_leaves', persistent='true', distance='1', waterlogged='false')
        # Tiny soil pockets make the berries and ferns physically supported.
        for x, z, plant in [(7, 8, 'sweet_berry_bush'), (8, 8, 'sweet_berry_bush'), (7, 10, 'sweet_berry_bush'), (6, 9, 'fern'), (8, 12, 'sweet_berry_bush'), (16, 15, 'fern'), (19, 16, 'fern'), (4, 17, 'fern')]:
            ys = [y for y in range(2, 20) if self.get(x, y, z) not in (None, AIR) and 'leaves' not in self.get(x, y, z)]
            if not ys:
                continue
            y = max(ys)
            self.put(x, y, z, 'minecraft:moss_block')
            self.put(x, y + 1, z, 'minecraft:' + plant, **({'age': '3'} if plant == 'sweet_berry_bush' else {}))
        # Vines cling to existing timber faces on the left shoulder.
        for x in (4, 5, 6, 7):
            for y in range(9, 16):
                for z in range(6, 15):
                    support = self.get(x, y, z + 1) or AIR
                    if ('log' in support or 'planks' in support or 'leaves' in support) and self.get(x, y, z) in (None, AIR):
                        self.put(x, y, z, 'minecraft:vine', south='true', north='false', west='false', east='false', up='false')
                        break
        # Barrel, a water pail, composter, and three cut-log seats.
        self.container((18, 2, 3), 'minecraft:barrel', 'up', [('bucket', 1), ('bone_meal', 12), ('sweet_berries', 16)])
        self.put(19, 2, 3, 'minecraft:water_cauldron', level='3')
        # A real bucket in an item frame hangs on the barrel above the wash pail.
        self.entities.append(Compound({
            'id': String('minecraft:item_frame'),
            'Pos': NBTList[Double]([Double(18.5), Double(2.5), Double(2.96875)]),
            'Motion': NBTList[Double]([Double(0), Double(0), Double(0)]),
            'Rotation': NBTList[Float]([Float(180), Float(0)]),
            'TileX': Int(18),
            'TileY': Int(2),
            'TileZ': Int(2),
            'Facing': Byte(2),
            'ItemRotation': Byte(0),
            'Fixed': Byte(1),
            'Invisible': Byte(0),
            'Invulnerable': Byte(1),
            'UUID': IntArray([self.rng.randint(-2147483648, 2147483647) for _ in range(4)]),
            'Item': Compound({
                'id': String('minecraft:bucket'),
                'count': Int(1)
            }),
        }))
        self.put(6, 2, 3, 'minecraft:composter', level='7')
        for x, z in [(7, 3), (16, 3), (8, 5)]:
            self.put(x, 2, z, 'minecraft:dark_oak_log', axis='y')
        self.put(7, 3, 3, 'minecraft:spruce_slab', type='bottom', waterlogged='false')
        self.put(16, 3, 3, 'minecraft:spruce_slab', type='bottom', waterlogged='false')
        for x, z in [(3, 2), (5, 3), (7, 1), (9, 1), (15, 1), (17, 2), (21, 2), (22, 7), (2, 13), (4, 19), (18, 20), (22, 18), (7, 20)]:
            if self.get(x, 2, z) in (None, AIR):
                self.put(x, 1, z, 'minecraft:grass_block')
                self.put(x, 2, z, self.rng.choice(['minecraft:fern', 'minecraft:short_grass']))
        for x, z in [(3, 16), (21, 15)]:
            if self.get(x, 2, z) in (None, AIR) and self.get(x, 3, z) in (None, AIR):
                self.put(x, 1, z, 'minecraft:grass_block')
                self.put(x, 2, z, 'minecraft:large_fern', half='lower')
                self.put(x, 3, z, 'minecraft:large_fern', half='upper')
        for x, y, z in [(7, 2, 6), (17, 2, 6), (21, 2, 8), (3, 2, 7)]:
            if self.get(x, y, z) in (None, AIR):
                self.put(x, y - 1, z, 'minecraft:moss_block')
                self.put(x, y, z, 'minecraft:azalea')

    def orient_front(self) -> None:
        """Reflect the plan so the leaf crown appears left of the north-facing door."""
        placed = list(self.placed.items())
        self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
        self.placed = {}
        for (x, y, z), state in placed:
            name, _, suffix = state.partition('[')
            props = dict(item.split('=') for item in suffix.rstrip(']').split(',') if item)
            if props.get('facing') in ('east', 'west'):
                props['facing'] = {'east': 'west', 'west': 'east'}[props['facing']]
            if 'east' in props and 'west' in props:
                props['east'], props['west'] = props['west'], props['east']
            if 'hinge' in props:
                props['hinge'] = {'left': 'right', 'right': 'left'}[props['hinge']]
            if props.get('shape', '').endswith(('_left', '_right')):
                part, side = props['shape'].rsplit('_', 1)
                props['shape'] = part + '_' + {'left': 'right', 'right': 'left'}[side]
            self.put(self.size_x - 1 - x, y, z, name, **props)
        for entity in self.block_entities:
            entity['x'] = Int(self.size_x - 1 - int(entity['x']))
        for entity in self.entities:
            entity['TileX'] = Int(self.size_x - 1 - int(entity['TileX']))
            entity['Pos'][0] = Double(self.size_x - float(entity['Pos'][0]))

    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)
        root = schematic.write_to_nbt()
        region = next(iter(root['Regions'].values()))
        region['TileEntities'] = NBTList[Compound](self.block_entities)
        region['Entities'] = NBTList[Compound](self.entities)
        stream = io.BytesIO()
        NBTFile(root).write(stream)
        path.write_bytes(gzip.compress(stream.getvalue(), 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()
