# Generate Willowbank Cottage with furnished rooms, terrain, waterfront and a usable boat.
# Run with /opt/venv/bin/python generate.py from any writable directory.
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.nbt.nbt import File as NBTFile
from mcio.nbt.tag import Compound, String, Double, Float, Byte, Int, 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": (32, 24, 26), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Willowbank Cottage",
    "author": "generator",
    "description": "A furnished sandstone-roof cottage above a river, with a sunken workshop, brick stair and timber jetty.",
}

PLASTER = ["minecraft:smooth_sandstone"] * 8 + ["minecraft:sandstone"] * 2 + ["minecraft:cut_sandstone"]
STONE = ["minecraft:stone_bricks"] * 5 + ["minecraft:andesite"] * 2 + ["minecraft:mossy_stone_bricks"]
FLOOR = ["minecraft:spruce_planks"] * 7 + ["minecraft:oak_planks"]
BRICK = ["minecraft:bricks"] * 7 + ["minecraft:granite", "minecraft:polished_granite"]
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 stair(self, x: int, y: int, z: int, block: str, facing: str, half: str = "bottom") -> None:
        """Place a stair with explicit geometry and dry state."""
        self.put(x, y, z, block, facing=facing, half=half, shape="straight", waterlogged="false")

    def slab(self, x: int, y: int, z: int, block: str, kind: str = "bottom") -> None:
        """Place a slab with explicit height."""
        self.put(x, y, z, block, type=kind, waterlogged="false")

    def door(self, x: int, y: int, z: int, facing: str = "north", hinge: str = "left") -> None:
        """Place matching closed door halves on an existing floor."""
        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 lantern(self, x: int, y: int, z: int, hanging: bool = False) -> None:
        """Place a lantern and remember its lighting location."""
        self.put(x, y, z, "minecraft:lantern", hanging=str(hanging).lower(), waterlogged="false")

    def terrain(self) -> None:
        """Sculpt the rising riverbank, with exposed stone below the turf."""
        for x in range(self.size_x):
            for z in range(self.size_z):
                if z <= 3:
                    self.put(x, 0, z, self.rng.choice(["minecraft:gravel", "minecraft:clay", "minecraft:sand"]))
                    self.put(x, 1, z, "minecraft:water", level="0")
                    continue
                edge = 1 if x < 3 or x > 29 else 0
                h = 2 if z == 4 else 3 + max(0, z - 10) // 3
                h = min(8, h + (edge if z > 10 else 0))
                self.texture((x, 0, z, x, max(0, h - 2), z), ["minecraft:stone"] * 12 + ["minecraft:andesite", "minecraft:tuff"])
                self.texture((x, max(1, h - 1), z, x, h - 1, z), ["minecraft:dirt"] * 4 + ["minecraft:coarse_dirt"])
                self.put(x, h, z, "minecraft:grass_block", snowy="false")
                self.ground[(x, z)] = h
        # Compact jetty, with posts driven to the riverbed.
        for x in range(11, 16):
            for z in range(0, 5):
                self.slab(x, 2, z, "minecraft:spruce_slab" if (x + z) % 4 else "minecraft:oak_slab", "top")
        for x, z in ((11, 0), (15, 0), (11, 3), (15, 3)):
            self.box((x, 0, z, x, 2, z), "minecraft:stripped_spruce_log", axis="y")
        self.put(15, 3, 0, "minecraft:spruce_fence", north="false", south="false", east="false", west="false", waterlogged="false")
        self.lantern(15, 4, 0)
        # A small block-built skiff keeps the boat silhouette visible in previews.
        for x in range(5, 9):
            for keel_z in range(3):
                self.slab(x, 1, keel_z, "minecraft:dark_oak_slab", "top")
            for z in (0, 2):
                self.put(x, 2, z, "minecraft:spruce_trapdoor", facing="south" if z == 0 else "north", half="bottom", open="true", powered="false", waterlogged="false")
        for x in (5, 8):
            self.stair(x, 2, 1, "minecraft:spruce_stairs", "west" if x == 5 else "east")
        self.slab(7, 2, 1, "minecraft:spruce_slab")

    def structure(self) -> None:
        """Frame two floors and the forward gabled bay around clear room volumes."""
        for bounds in ((4, 3, 12, 27, 4, 22), (9, 3, 9, 16, 4, 12)):
            self.texture(bounds, STONE)
        # The two joined shells are cleared as a union after wall construction.
        for bounds in ((4, 5, 12, 27, 13, 22), (9, 5, 9, 16, 13, 12)):
            self.texture(bounds, PLASTER)
        self.box((5, 5, 13, 26, 13, 21), "minecraft:air")
        self.box((10, 5, 10, 15, 13, 13), "minecraft:air")
        # Warm boards on both living surfaces, interrupted later by the stairwell.
        for y in (4, 9):
            self.texture((5, y, 13, 26, y, 21), FLOOR)
            self.texture((10, y, 10, 15, y, 13), FLOOR)
        for x, z in ((4, 12), (9, 12), (16, 12), (22, 12), (27, 12), (4, 22), (10, 22), (16, 22), (22, 22), (27, 22), (9, 9), (16, 9)):
            self.box((x, 4, z, x, 13, z), "minecraft:stripped_spruce_log", axis="y")
            self.put(x, 9, z, "minecraft:stripped_oak_log", axis="z")
        for y in (9, 13):
            for z in (12, 22):
                self.box((4, y, z, 27, y, z), "minecraft:stripped_spruce_log", axis="x")
            for x in (4, 27):
                self.box((x, y, 12, x, y, 22), "minecraft:stripped_spruce_log", axis="z")
            self.box((9, y, 9, 16, y, 9), "minecraft:stripped_spruce_log", axis="x")
        # Reopen the join where the bay meets the main house.
        self.box((10, 5, 12, 15, 8, 12), "minecraft:air")
        self.box((10, 10, 12, 15, 13, 12), "minecraft:air")
        self.texture((10, 9, 12, 15, 9, 12), FLOOR)
        # Ceiling provides an unbroken weather seal, independent of stair models.
        self.box((4, 14, 12, 27, 14, 22), "minecraft:spruce_planks")
        # Lower workshop and store connected through a broad, two-high arch.
        self.box((16, 5, 13, 16, 8, 21), "minecraft:stripped_spruce_log", axis="y")
        self.box((16, 5, 17, 16, 7, 18), "minecraft:air")
        # Floor-to-floor stair: two lanes, five risers, full overhead clearance.
        self.box((25, 5, 14, 26, 13, 18), "minecraft:air")
        for z in range(14, 19):
            y = z - 9
            for x in (25, 26):
                self.box((x, 4, z, x, y - 1, z), "minecraft:spruce_planks")
                self.stair(x, y, z, "minecraft:spruce_stairs", "south")
        for z in range(14, 18):
            self.put(24, 10, z, "minecraft:spruce_fence", north=str(z > 14).lower(), south=str(z < 17).lower(), west="false", east="false", waterlogged="false")
        self.door(19, 10, 12, hinge="left")
        self.door(20, 10, 12, hinge="right")
        self.door(12, 5, 9)
        self.put(12, 4, 8, "minecraft:stone_bricks")
        self.stair(12, 3, 7, "minecraft:stone_brick_stairs", "south")
        # Glass blocks make the small cellar lights weather-tight.
        for a, b, y0, y1, z in ((6, 7, 6, 6, 12), (24, 25, 6, 6, 12), (6, 7, 10, 12, 12), (24, 25, 10, 12, 12), (12, 13, 10, 15, 9), (6, 8, 10, 12, 22), (18, 20, 10, 12, 22)):
            for x in range(a, b + 1):
                for y in range(y0, y1 + 1):
                    self.put(x, y, z, "minecraft:glass")
            for x in (a - 1, b + 1):
                if not (z == 9 and x in (11, 14)):
                    self.box((x, y0, z, x, y1, z), "minecraft:stripped_oak_log", axis="y")
        for x in (4, 27):
            self.box((x, 10, 16, x, 12, 18), "minecraft:glass")
            for z in (15, 19):
                self.box((x, 10, z, x, 12, z), "minecraft:stripped_oak_log", axis="y")
        # Beam ends and deep eaves echo the reference's dark horizontal line.
        for z in (11, 23):
            self.box((3, 13, z, 28, 13, z), "minecraft:dark_oak_log", axis="x")
        for x in (3, 28):
            self.box((x, 13, 11, x, 13, 23), "minecraft:dark_oak_log", axis="z")
        for x in (5, 8, 18, 21, 23, 26):
            self.stair(x, 12, 11, "minecraft:dark_oak_stairs", "south", "top")
        # Dark shutters flank the tall wing windows without covering the glass.
        for x in (5, 8, 23, 26):
            for y in (10, 11, 12):
                self.put(x, y, 11, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false")

    def roof(self) -> None:
        """Join the broad main roof with the forward cross-gable."""
        surfaces = {}
        for x in range(3, 29):
            for z in range(10, 25):
                y = 14 + min(z - 10, 24 - z)
                surfaces[(x, z)] = (y, "south" if z <= 17 else "north", x in (3, 28) or z in (10, 24), "main")
        for x in range(8, 18):
            for z in range(8, 18):
                y = 14 + min(x - 8, 17 - x)
                old = surfaces.get((x, z))
                if old is None or y >= old[0]:
                    surfaces[(x, z)] = (y, "east" if x <= 12 else "west", z == 8 or x in (8, 17), "gable")
        # Plaster triangles on the two ends and the front of the projecting bay.
        for x in (4, 27):
            for z in range(12, 23):
                roof_y = surfaces[(x, z)][0]
                if roof_y > 15:
                    self.texture((x, 15, z, x, roof_y - 1, z), PLASTER)
        for x in range(9, 17):
            cap = 14 + min(x - 8, 17 - x)
            self.texture((x, 14, 9, x, cap - 1, 9), PLASTER)
        for x in (9, 16):
            for z in (10, 11):
                self.box((x, 14, z, x, surfaces[(x, z)][0] - 1, z), "minecraft:smooth_sandstone")
        for (x, z), (y, facing, dark, roof_type) in surfaces.items():
            self.put(x, y - 1, z, "minecraft:dark_oak_planks" if dark else "minecraft:smooth_sandstone")
            self.stair(x, y, z, "minecraft:dark_oak_stairs" if dark else "minecraft:smooth_sandstone_stairs", facing)
            if roof_type == "main" and z == 17:
                self.put(x, y, z, "minecraft:dark_oak_log", axis="x")
                self.slab(x, y + 1, z, "minecraft:dark_oak_slab")
            if roof_type == "gable" and x in (12, 13):
                self.slab(x, y + 1, z, "minecraft:dark_oak_slab")
        # Smooth sandstone ridge-side caps break up the pale field subtly.
        for x in (5, 6, 24, 25):
            self.slab(x, 20, 16, "minecraft:smooth_sandstone_slab", "double")
        self.box((12, 10, 9, 13, 15, 9), "minecraft:glass")
        for x in (11, 14):
            self.box((x, 10, 9, x, 15, 9), "minecraft:stripped_oak_log", axis="y")
        # Chimney is backed by solid masonry and capped against the sky.
        self.texture((6, 10, 19, 7, 22, 20), BRICK)
        self.box((6, 23, 19, 7, 23, 20), "minecraft:brick_slab", type="bottom", waterlogged="false")
        # Decorative attic vents are backed by plaster, so remain sealed.
        for x, face in ((3, "west"), (28, "east")):
            self.put(x, 17, 17, "minecraft:dark_oak_trapdoor", facing=face, half="bottom", open="true", powered="false", waterlogged="false")

    def exterior(self) -> None:
        """Add the brick stair, slab flower shelves and planted riverbank."""
        for z in range(5, 12):
            y = z - 2
            for x in range(18, 22):
                self.texture((x, 2, z, x, y - 1, z), BRICK)
                self.stair(x, y, z, "minecraft:brick_stairs", "south")
            for x in (17, 22):
                self.texture((x, 2, z, x, y - 1, z), STONE)
                self.slab(x, y, z, "minecraft:polished_granite_slab")
        for x in (18, 21):
            self.slab(x, 9, 11, "minecraft:brick_slab", "top")
        # A mixed path forks toward the cellar and climbs from the jetty.
        path_cells = set()
        for z in range(4, 7):
            for x in range(12 + (z - 4), 20):
                path_cells.add((x, z))
        path_cells.update((x, z) for z in (6, 7) for x in range(11, 16))
        for x, z in sorted(path_cells):
            if 17 <= x <= 22 and z >= 5:
                continue
            y = self.ground[(x, z)]
            self.put(x, y, z, self.rng.choice(["minecraft:dirt_path"] * 4 + ["minecraft:coarse_dirt", "minecraft:gravel", "minecraft:andesite"]))
        # Small stairs make the river-to-bank elevation change walkable.
        for x in range(12, 16):
            self.stair(x, 3, 5, "minecraft:stone_brick_stairs", "south")
        for a, b in ((4, 8), (23, 27)):
            for x in range(a, b + 1):
                self.slab(x, 8, 10, "minecraft:smooth_sandstone_slab", "top")
                self.put(x, 8, 11, "minecraft:rooted_dirt")
                self.put(x, 9, 11, self.rng.choice(["minecraft:oxeye_daisy", "minecraft:cornflower", "minecraft:azure_bluet"]))
                self.put(x, 8, 9, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false")
            for x in (a, b):
                self.stair(x, 7, 11, "minecraft:spruce_stairs", "south", "top")
        # Door lamps stand on supported bracket blocks beside the openings.
        for x in (18, 21):
            self.put(x, 11, 11, "minecraft:dark_oak_fence", north="false", south="true", east="false", west="false", waterlogged="false")
            self.lantern(x, 12, 11)
        self.put(15, 7, 8, "minecraft:spruce_fence", north="false", south="true", east="false", west="false", waterlogged="false")
        self.lantern(15, 8, 8)
        # Deliberate flower beds, clear of doors and stair approaches.
        for x, z in [(x, 10) for x in range(4, 9)] + [(x, 10) for x in range(24, 29)]:
            self.put(x, 3, z, "minecraft:rooted_dirt")
            self.put(x, 4, z, "minecraft:sweet_berry_bush", age="3")
        for x, z in ((3, 10), (8, 9), (9, 7), (16, 8), (23, 9), (28, 11)):
            y = self.ground.get((x, z), 3)
            self.put(x, y + 1, z, "minecraft:azalea")
            if x in (3, 28):
                self.put(x, y + 2, z, "minecraft:oak_leaves", persistent="true", distance="1", waterlogged="false")
        # Boundary planting gives the slope a natural, uneven edge.
        for (x, z), y in sorted(self.ground.items()):
            if self.get(x, y, z) != "minecraft:grass_block[snowy=false]" or self.get(x, y + 1, z) not in (None, "minecraft:air"):
                continue
            if (4 <= x <= 28 and z >= 8) or (10 <= x <= 22 and z <= 8):
                continue
            roll = self.rng.random()
            if roll < 0.15 and y + 2 < self.size_y and self.get(x, y + 2, z) is None:
                self.put(x, y + 1, z, "minecraft:tall_grass", half="lower")
                self.put(x, y + 2, z, "minecraft:tall_grass", half="upper")
            elif roll < 0.45:
                self.put(x, y + 1, z, "minecraft:short_grass")
            elif roll < 0.52:
                self.put(x, y + 1, z, "minecraft:oxeye_daisy")
        # One small birch off the left rear corner frames the cottage.
        self.box((1, 8, 21, 1, 13, 21), "minecraft:birch_log", axis="y")
        for x in range(0, 3):
            for z in range(19, 24):
                for y in range(12, 16):
                    if abs(x - 1) + abs(z - 21) + max(0, y - 13) <= 3 and self.get(x, y, z) in (None, "minecraft:air"):
                        self.put(x, y, z, "minecraft:birch_leaves", persistent="true", distance="1", waterlogged="false")

    def interiors(self) -> None:
        """Furnish living, dining, sleeping and cellar corners around open routes."""
        # Hearth in the rear left, backed by the chimney stack.
        self.texture((5, 9, 18, 8, 9, 21), STONE)
        for x in (5, 8):
            self.box((x, 10, 20, x, 12, 20), "minecraft:bricks")
        self.put(6, 10, 20, "minecraft:campfire", facing="north", lit="true", signal_fire="false", waterlogged="false")
        self.put(7, 10, 20, "minecraft:campfire", facing="north", lit="true", signal_fire="false", waterlogged="false")
        self.box((5, 12, 20, 8, 12, 20), "minecraft:bricks")
        for x in range(5, 9):
            self.slab(x, 12, 19, "minecraft:dark_oak_slab", "top")
        self.put(5, 13, 20, "minecraft:potted_fern")
        self.put(8, 13, 20, "minecraft:candle", candles="3", lit="true", waterlogged="false")
        for x in range(7, 10):
            self.stair(x, 10, 15, "minecraft:spruce_stairs", "south")
        for x in (6, 10):
            self.put(x, 10, 15, "minecraft:spruce_planks")
        self.put(6, 11, 15, "minecraft:potted_azalea_bush")
        self.box((7, 10, 16, 10, 10, 18), "minecraft:green_carpet")
        for x in (8, 9):
            self.slab(x, 10, 17, "minecraft:dark_oak_slab", "top")
        self.put(8, 11, 17, "minecraft:candle", candles="2", lit="true", waterlogged="false")
        for z in (13, 14):
            self.put(5, 10, z, "minecraft:bookshelf")
        self.lantern(5, 11, 14)
        # Reading and sleeping nook inside the projecting bay.
        self.put(10, 10, 10, "minecraft:cyan_bed", facing="south", part="foot", occupied="false")
        self.put(10, 10, 11, "minecraft:cyan_bed", facing="south", part="head", occupied="false")
        self.put(15, 10, 10, "minecraft:barrel", facing="north", open="false")
        self.lantern(15, 11, 10)
        self.put(12, 10, 11, "minecraft:light_gray_carpet")
        self.put(13, 10, 11, "minecraft:light_gray_carpet")
        # Kitchen at the back, well away from the stairwell along the east wall.
        for x in range(17, 23):
            self.put(x, 10, 21, "minecraft:barrel", facing="north", open="false")
        self.put(18, 10, 21, "minecraft:smoker", facing="north", lit="false")
        self.put(19, 10, 21, "minecraft:water_cauldron", level="3")
        self.put(20, 10, 21, "minecraft:crafting_table")
        self.put(17, 11, 21, "minecraft:potted_red_tulip")
        self.lantern(22, 11, 21)
        # Solid slab table on legs, with chairs facing inward.
        for x in (18, 20):
            self.put(x, 10, 17, "minecraft:dark_oak_fence", north="false", south="false", east="false", west="false", waterlogged="false")
        for x in range(18, 21):
            self.slab(x, 11, 17, "minecraft:spruce_slab", "top")
        self.put(19, 12, 17, "minecraft:candle", candles="3", lit="true", waterlogged="false")
        for x in (18, 20):
            self.stair(x, 10, 16, "minecraft:dark_oak_stairs", "south")
            self.stair(x, 10, 18, "minecraft:dark_oak_stairs", "north")
        self.box((18, 10, 13, 21, 10, 14), "minecraft:brown_carpet")
        # Beam-mounted lamps leave the entrance and circulation routes clear.
        for x, z in ((13, 17), (21, 15), (24, 20)):
            self.lantern(x, 13, z, hanging=True)
        self.box((11, 13, 17, 15, 13, 17), "minecraft:stripped_spruce_log", axis="x")
        self.lantern(13, 12, 17, hanging=True)
        # Workbench row, storage, and a clear landing in the cellar.
        for x in range(5, 10):
            self.put(x, 5, 21, "minecraft:barrel", facing="north", open="false")
        self.put(6, 5, 21, "minecraft:crafting_table")
        self.put(7, 5, 21, "minecraft:smithing_table")
        self.put(8, 5, 21, "minecraft:stonecutter", facing="north")
        self.lantern(5, 6, 21)
        self.put(9, 6, 21, "minecraft:potted_cactus")
        for x in (6, 8):
            self.put(x, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false")
        self.put(5, 5, 17, "minecraft:barrel", facing="east", open="false")
        self.lantern(5, 6, 17)
        self.put(14, 5, 10, "minecraft:barrel", facing="north", open="false")
        self.lantern(14, 6, 10)
        self.box((11, 5, 15, 13, 5, 17), "minecraft:brown_carpet")
        for x in (18, 19, 21, 22):
            self.put(x, 5, 21, "minecraft:barrel", facing="north", open="false")
            self.put(x, 6, 21, "minecraft:barrel", facing="north", open="false")
        self.put(18, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false")
        self.put(21, 5, 14, "minecraft:chest", facing="south", type="single", waterlogged="false")
        self.put(23, 5, 21, "minecraft:crafting_table")
        self.lantern(23, 6, 21)
        self.put(23, 5, 13, "minecraft:barrel", facing="north", open="false")
        self.lantern(23, 6, 13)
        self.box((18, 8, 17, 23, 8, 17), "minecraft:stripped_spruce_log", axis="x")
        self.lantern(20, 7, 17, hanging=True)
        self.lantern(12, 8, 19, hanging=True)
        self.box((10, 8, 16, 14, 8, 16), "minecraft:stripped_spruce_log", axis="x")
        self.lantern(11, 7, 16, hanging=True)

    def build(self) -> "Build":
        """Build the complete cottage, hillside, waterfront and furnishings."""
        self.ground = {}
        self.terrain()
        self.structure()
        self.roof()
        self.exterior()
        self.interiors()
        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)
        nbt = schematic.write_to_nbt()
        # Actual usable Java 1.21.1 boat, kept beside the visible timber skiff.
        boat = Compound({
            "id": String("minecraft:boat"),
            "Type": String("spruce"),
            "Pos": NBTList[Double]([Double(3.0), Double(1.55), Double(1.8)]),
            "Motion": NBTList[Double]([Double(0), Double(0), Double(0)]),
            "Rotation": NBTList[Float]([Float(90), Float(0)]),
            "UUID": IntArray([145995982, 77563907, 642301188, 23812227]),
            "OnGround": Byte(0),
            "Invulnerable": Byte(0),
        })
        nbt["Regions"][CONFIG["name"]]["Entities"] = NBTList[Compound]([boat])
        NBTFile(nbt).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()
