# Generate and verify a furnished Java 1.21.1 Mediterranean villa from a seeded block plan.
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, Int, Byte, Float, Double, String, 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": (35, 27, 31), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Villa_al_Mare",
    "author": "generator",
    "description": "Furnished Mediterranean villa, two floors, two pergolas and a garden pool. Front faces south (+Z).",
}

PLASTER = ["white_concrete"] * 7 + ["smooth_quartz"] * 3 + ["calcite"]
FLOOR = ["oak_planks"] * 10 + ["birch_planks"] + ["bamboo_planks"] * 2
DIRECTIONS = {"north": (0, -1), "south": (0, 1), "west": (-1, 0), "east": (1, 0)}

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

    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 p(self, x: int, y: int, z: int, name: str, **properties: str) -> None:
        """Place a vanilla block using its short name.

        Args:
            x, y, z: Block coordinates.
            name: Vanilla block name.
            **properties: Blockstate properties.
        """
        self.put(x, y, z, f"minecraft:{name}", **properties)

    def fill(self, bounds: tuple, name: str, **properties: str) -> None:
        """Fill a named architectural volume.

        Args:
            bounds: Inclusive XYZ bounds.
            name: Vanilla block name.
            **properties: Blockstate properties.
        """
        self.box(bounds, f"minecraft:{name}", **properties)

    def mix(self, bounds: tuple, names: list) -> None:
        """Texture a surface with the seeded palette.

        Args:
            bounds: Inclusive XYZ bounds.
            names: Weighted vanilla material names.
        """
        self.texture(bounds, [f"minecraft:{name}" for name in names])

    def door(self, x: int, y: int, z: int, facing: str, hinge: str = "left", wood: str = "warped") -> None:
        """Install a complete closed door.

        Args:
            x, y, z: Lower-half coordinates.
            facing: Door direction.
            hinge: Hinge side.
            wood: Wood species.
        """
        for half, dy in (("lower", 0), ("upper", 1)):
            self.p(x, y + dy, z, f"{wood}_door", half=half, facing=facing, hinge=hinge, open="false", powered="false")

    def window(self, x: int, y: int, z: int, facing: str, width: int = 2) -> None:
        """Install recessed glazing, white lintels and turquoise timber shutters.

        Args:
            x, y, z: First glass block.
            facing: Exterior wall normal.
            width: Glazed width.
        """
        dx, dz = (1, 0) if facing in ("north", "south") else (0, 1)
        nx, nz = DIRECTIONS[facing]
        for i in range(-1, width + 1):
            px, pz = x + dx * i, z + dz * i
            self.p(px, y - 1, pz, "smooth_quartz")
            self.p(px, y + 3, pz, "quartz_bricks")
            if i in (-1, width):
                for yy in range(y, y + 3):
                    self.p(px, yy, pz, "stripped_warped_stem", axis="y")
                    self.p(px + nx, yy, pz + nz, "warped_trapdoor", facing=facing, half="bottom", open="true")
            else:
                for yy in range(y, y + 3):
                    self.p(px, yy, pz, "light_blue_stained_glass")
                self.p(px + nx, y - 1, pz + nz, "smooth_quartz_slab", type="top")

    def planter(self, x: int, y: int, z: int, flower: str = "potted_azalea_bush") -> None:
        """Place a terracotta planter with supported greenery.

        Args:
            x, y, z: Container coordinates.
            flower: Potted plant state.
        """
        self.p(x, y, z, self.rng.choice(["terracotta", "terracotta", "red_terracotta"]))
        self.p(x, y + 1, z, flower)

    def leaf(self, x: int, y: int, z: int, flower: bool = False) -> None:
        """Place persistent leaves.

        Args:
            x, y, z: Leaf coordinates.
            flower: Use flowering azalea foliage.
        """
        self.p(x, y, z, "flowering_azalea_leaves" if flower else "jungle_leaves", persistent="true", distance="1")

    def lamp(self, x: int, y: int, z: int) -> None:
        """Suspend a lantern one chain block below a structural ceiling.

        Args:
            x, y, z: Lantern coordinates.
        """
        self.p(x, y + 1, z, "chain", axis="y")
        self.p(x, y, z, "lantern", hanging="true")

    def inventory(self, x: int, y: int, z: int, kind: str, items: list) -> None:
        """Give a placed container a small useful inventory.

        Args:
            x, y, z: Container coordinates.
            kind: Block entity identifier.
            items: Item identifiers with stack counts, ordered by slot.
        """
        self.block_entities.append(Compound({
            "id": String(f"minecraft:{kind}"),
            "x": Int(x),
            "y": Int(y),
            "z": Int(z),
            "Items": NBTList[Compound]([Compound({
                "Slot": Byte(slot),
                "id": String(f"minecraft:{name}"),
                "count": Int(count)
            }) for slot, (name, count) in enumerate(items)]),
        }))

    def frame(self, x: int, y: int, z: int, facing: str, item: str) -> None:
        """Attach a deterministic item-frame entity to the block behind it.

        Args:
            x, y, z: Air cell containing the frame.
            facing: Outward direction.
            item: Displayed vanilla item.
        """
        dx, dz = DIRECTIONS[facing]
        self.entities.append(Compound({
            "id": String("minecraft:item_frame"),
            "TileX": Int(x),
            "TileY": Int(y),
            "TileZ": Int(z),
            "Facing": Byte({
                "south": 3,
                "north": 2,
                "west": 4,
                "east": 5
            }[facing]),
            "Pos": NBTList[Double]([x + .5 - dx * .46875, y + .5, z + .5 - dz * .46875]),
            "Motion": NBTList[Double]([0, 0, 0]),
            "Rotation": NBTList[Float]([{
                "south": 0,
                "west": 90,
                "north": 180,
                "east": 270
            }[facing], 0]),
            "UUID": IntArray([self.rng.randrange(-(2**31), 2**31) for _ in range(4)]),
            "Item": Compound({
                "id": String(f"minecraft:{item}"),
                "count": Int(1)
            }),
            "ItemRotation": Byte(0),
            "ItemDropChance": Float(1),
            "Fixed": Byte(0),
            "Invisible": Byte(0),
        }))

    def roof(self, x0: int, x1: int, z0: int, z1: int, base: int) -> None:
        """Build a sealed shallow tiled gable using vanilla terracotta and tile-shaped blocks.

        Args:
            x0, x1, z0, z1: Roof footprint including eaves.
            base: Lowest tile course height.
        """
        # Vanilla has no terracotta stairs; brick and red sandstone supply the half-block tile profile.
        for z in range(z0, z1 + 1):
            inset = min(z - z0, z1 - z)
            y = base + inset // 2
            facing = "south" if z <= (z0 + z1) / 2 else "north"
            for x in range(x0, x1 + 1):
                for yy in range(base, y):
                    gable = x in (x0, x0 + 1, x1 - 1, x1)
                    self.p(x, yy, z, self.rng.choice(PLASTER) if gable else self.rng.choice(["terracotta", "orange_terracotta", "red_terracotta"]))
                material = self.rng.choice(["brick"] * 9 + ["red_sandstone"])
                if inset % 2:
                    self.p(x, y, z, material + "_stairs", facing=facing, half="bottom", shape="straight")
                else:
                    self.p(x, y, z, material + "_slab", type="bottom")
        middle = (z0 + z1) // 2
        ridge_y = base + min(middle - z0, z1 - middle) // 2
        for x in range(x0, x1 + 1):
            self.p(x, ridge_y, middle, "terracotta")
            self.p(x, ridge_y + 1, middle, "brick_slab", type="bottom")
        for x in range(x0, x1 + 1, 2):
            self.p(x, base - 1, z0, "spruce_slab", type="top")
            self.p(x, base - 1, z1, "spruce_slab", type="top")

    def pergola(self, x0: int, x1: int, z0: int, z1: int, floor: int, beam: int) -> None:
        """Build an open timber pergola with slatted rafters and a climbing edge.

        Args:
            x0, x1, z0, z1: Frame footprint.
            floor: Deck block height.
            beam: Beam height.
        """
        for x in (x0, x1):
            for z in (z0, z1):
                self.p(x, floor + 1, z, "smooth_quartz")
                self.fill((x, floor + 2, z, x, beam - 1, z), "stripped_oak_log", axis="y")
            self.fill((x, beam, z0, x, beam, z1), "stripped_spruce_log", axis="z")
        for z in range(z0, z1 + 1, 2):
            self.fill((x0 + 1, beam, z, x1 - 1, beam, z), "oak_slab", type="bottom")
        for x in range(x0 + 1, x1, 3):
            self.leaf(x, beam, z0, flower=True)
            self.leaf(x, beam + 1, z0)
        for y in range(floor + 2, beam):
            self.p(x0 - 1, y, z0, "vine", east="true")
        self.lamp(x1 - 2, beam - 2, z1 - ((z1 - z0) % 2))

    def palm(self, x: int, z: int, crown: int) -> None:
        """Grow a slender palm with stepped fronds attached to its trunk.

        Args:
            x, z: Garden trunk coordinates.
            crown: Crown height.
        """
        self.fill((x, 2, z, x, crown, z), "jungle_log", axis="y")
        for yy in range(3, crown, 3):
            self.p(x, yy, z, "stripped_jungle_log", axis="y")
        self.leaf(x, crown + 1, z)
        self.leaf(x, crown + 2, z)
        for dx, dz in DIRECTIONS.values():
            for distance in range(1, 4):
                yy = crown + (1 if distance < 3 else 0)
                self.leaf(x + dx * distance, yy, z + dz * distance)
                if distance == 2:
                    self.leaf(x + dx * distance, crown, z + dz * distance)
                if distance == 3:
                    self.leaf(x + dx * distance, yy - 1, z + dz * distance)
        for dx, dz in ((-1, -1), (1, -1), (-1, 1), (1, 1)):
            self.leaf(x + dx, crown, z)
            self.leaf(x + 2 * dx, crown, z + dz)
            self.leaf(x + dx, crown, z + dz)
            self.leaf(x + 2 * dx, crown, z + 2 * dz)
            self.leaf(x + 2 * dx, crown - 1, z + 2 * dz)
        for dx in (-1, 1):
            self.p(x + dx, crown - 1, z, "cocoa", facing="east" if dx < 0 else "west", age="2")

    def grounds(self) -> None:
        """Build the raised tiled terrace, square pool, entry stairs and planted garden."""
        for x in range(self.size_x):
            for z in range(self.size_z):
                if (x < 2 or x > 32) and (z < 2 or z > 28):
                    continue
                self.p(x, 0, z, "dirt")
                self.p(x, 1, z, self.rng.choice(["grass_block"] * 12 + ["moss_block", "coarse_dirt"]))
        self.mix((5, 2, 3, 32, 3, 26), ["diorite"] * 6 + ["calcite", "andesite"])
        self.fill((5, 3, 3, 32, 3, 3), "light_blue_terracotta")
        self.fill((32, 3, 3, 32, 3, 26), "light_blue_terracotta")
        self.fill((5, 3, 26, 32, 3, 26), "light_blue_terracotta")
        for x in range(5, 33):
            for z in range(3, 27):
                motif = (x % 4 in (0, 1)) and (z % 4 in (0, 1))
                self.p(x, 4, z, self.rng.choice(["terracotta", "orange_terracotta", "smooth_red_sandstone"]) if motif else self.rng.choice(["smooth_sandstone"] * 4 + ["cut_sandstone", "sandstone"]))
        # Pool coping and contained water are flush with the terrace surface.
        self.mix((22, 1, 21, 30, 3, 29), ["diorite"] * 5 + ["calcite", "andesite"])
        self.fill((22, 4, 21, 30, 4, 29), "smooth_quartz")
        self.fill((23, 2, 22, 29, 2, 28), "prismarine_bricks")
        for x in range(23, 30):
            for z in range(22, 29):
                if (x + z) % 3 == 0:
                    self.p(x, 2, z, "light_blue_terracotta")
        self.fill((23, 3, 22, 29, 4, 28), "water", level="0")
        for x, z in ((23, 22), (29, 22), (23, 28), (29, 28)):
            self.p(x, 2, z, "sea_lantern")
        for x in (24, 25):
            self.p(x, 3, 22, "quartz_stairs", facing="north", half="bottom", shape="straight", waterlogged="true")
        for z in range(27, 30):
            yy = 31 - z
            self.fill((16, 2, z, 20, yy, z), "terracotta")
            for x in range(16, 21):
                self.p(x, yy, z, "brick_stairs", facing="north", half="bottom", shape="straight")
            for x in (15, 21):
                self.fill((x, 2, z, x, yy, z), "diorite")
                self.p(x, yy + 1, z, "smooth_quartz")
        # Low balustrades preserve the main garden approach and pool access.
        for x in range(5, 16):
            self.p(x, 5, 26, "oak_fence", east="true", west="true")
        for z in range(18, 27):
            self.p(5, 5, z, "oak_fence", north="true", south="true")
        for z in range(16, 27):
            self.p(32, 5, z, "oak_fence", north="true", south="true")
        for x, z in ((5, 18), (5, 22), (5, 26), (10, 26), (15, 26), (32, 16), (32, 21), (32, 26)):
            self.fill((x, 5, z, x, 6, z), "smooth_quartz")
            self.p(x, 7, z, "brick_slab", type="bottom")
        for x, z in ((6, 25), (14, 25), (31, 25), (31, 20), (6, 17), (23, 17)):
            self.planter(x, 5, z, self.rng.choice(["potted_azalea_bush", "potted_flowering_azalea_bush"]))
        # Lavender and white flowers sit in edged soil beds outside the terrace.
        for x in range(5, 14):
            self.p(x, 2, 29, "smooth_sandstone_slab", type="bottom")
            for z in (27, 28):
                self.p(x, 1, z, "rooted_dirt")
                self.p(x, 2, z, "allium" if (x + z) % 2 else "azure_bluet")
        for z in range(5, 28, 3):
            self.leaf(33, 2, z)
            self.p(34, 2, z, "short_grass")
        for x in (23, 26, 29):
            self.leaf(x, 2, 30)
        self.palm(3, 8, 21)
        self.palm(3, 21, 17)
        self.p(16, 6, 25, "lantern", hanging="false")
        self.p(16, 5, 25, "sandstone_wall")

    def structure(self) -> None:
        """Build both storeys, the enchanting wing and a continuous internal staircase."""
        for y0, y1 in ((5, 9), (11, 15)):
            for bounds in ((7, y0, 4, 24, y1, 4), (7, y0, 16, 24, y1, 16), (7, y0, 5, 7, y1, 15), (24, y0, 5, 24, y1, 15)):
                self.mix(bounds, PLASTER)
        self.mix((8, 4, 5, 23, 4, 15), FLOOR)
        self.fill((7, 10, 4, 24, 10, 16), "smooth_quartz")
        self.mix((8, 10, 5, 23, 10, 15), FLOOR)
        self.fill((7, 16, 4, 24, 16, 16), "smooth_quartz")
        for y in (5, 11):
            self.fill((7, y, 4, 24, y, 4), "light_blue_terracotta")
            self.fill((7, y, 16, 24, y, 16), "light_blue_terracotta")
            self.fill((7, y, 4, 7, y, 16), "light_blue_terracotta")
            self.fill((24, y, 4, 24, y, 16), "light_blue_terracotta")
        for x in (7, 24):
            for z in (4, 16):
                self.fill((x, 5, z, x, 15, z), "quartz_pillar", axis="y")
        # Ground-floor circulation: living room left, hallway middle, library wing right.
        self.fill((17, 5, 5, 17, 9, 15), "white_concrete")
        self.door(17, 5, 7, "east", wood="oak")
        self.mix((25, 4, 9, 30, 4, 14), FLOOR)
        for bounds in ((25, 5, 8, 31, 9, 8), (31, 5, 9, 31, 9, 15), (25, 5, 15, 30, 9, 15)):
            self.mix(bounds, PLASTER)
        self.fill((24, 10, 8, 31, 10, 15), "smooth_quartz")
        self.door(24, 5, 11, "east", wood="warped")
        # Six rises connect feet elevations 5 and 11. The upper landing is at z=15.
        self.fill((15, 10, 11, 16, 10, 14), "air")
        for z in range(9, 15):
            yy = z - 4
            if yy > 5:
                self.fill((15, 5, z, 16, yy - 1, z), "oak_planks")
            for x in (15, 16):
                self.p(x, yy, z, "oak_stairs", facing="south", half="bottom", shape="straight")
        # Upstairs plan preserves the kitchen on the left and brew/bedroom across the back.
        self.fill((14, 11, 5, 14, 15, 10), "white_concrete")
        self.fill((19, 11, 5, 19, 15, 10), "white_concrete")
        self.fill((15, 11, 10, 23, 15, 10), "white_concrete")
        self.door(14, 11, 8, "east", wood="birch")
        self.door(18, 11, 10, "south", wood="birch")
        self.door(22, 11, 10, "south", wood="birch")
        for x in (14, 17):
            for z in range(11, 15):
                self.p(x, 11, z, "oak_fence", north="true", south="true")
        # Front entrance, with a taller recessed stone surround.
        for x, hinge in ((21, "left"), (22, "right")):
            self.door(x, 5, 16, "south", hinge)
            self.p(x, 7, 16, "light_blue_stained_glass")
        for x in (20, 23):
            self.fill((x, 5, 17, x, 8, 17), "smooth_sandstone")
            self.p(x, 9, 17, "chiseled_sandstone")
        self.fill((20, 9, 17, 23, 9, 17), "smooth_sandstone")
        self.fill((21, 8, 17, 22, 8, 17), "smooth_sandstone")
        for x in (20, 23):
            self.planter(x, 5, 18, "potted_flowering_azalea_bush")
        # Cool blue plaster panels recall the reference's blue-and-white elevations.
        for bounds in ((7, 11, 5, 7, 15, 15), (24, 11, 5, 24, 15, 15), (8, 11, 16, 12, 15, 16), (31, 5, 9, 31, 9, 14), (25, 5, 15, 30, 9, 15)):
            self.mix(bounds, ["light_blue_concrete"] * 9 + ["light_blue_terracotta"])
        self.window(10, 6, 16, "south")
        self.window(10, 12, 16, "south")
        for x, hinge in ((14, "left"), (15, "right")):
            self.door(x, 11, 16, "south", hinge)
            self.p(x, 13, 16, "light_blue_stained_glass")
        self.window(20, 12, 16, "south")
        for y in (6, 12):
            self.window(10, y, 4, "north")
            self.window(7, y, 7, "west")
            self.window(7, y, 12, "west")
        self.window(20, 6, 4, "north")
        self.window(16, 12, 4, "north")
        self.window(21, 12, 4, "north", width=1)
        self.window(24, 12, 7, "east")
        self.window(27, 6, 15, "south")
        self.window(31, 6, 11, "east")
        self.window(27, 6, 8, "north")
        # Upper terrace, with real support columns and open railings.
        self.fill((7, 10, 17, 16, 10, 21), "oak_planks")
        for x in (7, 16):
            self.fill((x, 5, 21, x, 9, 21), "quartz_pillar", axis="y")
        self.fill((7, 9, 21, 16, 9, 21), "smooth_quartz")
        for x in range(8, 16):
            self.p(x, 11, 21, "oak_fence", east="true", west="true")
        for z in range(17, 21):
            self.p(7, 11, z, "oak_fence", north="true", south="true")
            self.p(16, 11, z, "oak_fence", north="true", south="true")
        self.pergola(7, 16, 17, 21, 10, 15)
        self.pergola(25, 32, 17, 20, 4, 10)
        self.roof(6, 25, 3, 17, 17)
        self.roof(25, 32, 7, 16, 11)
        self.fill((9, 19, 7, 9, 24, 7), "quartz_pillar", axis="y")
        self.p(9, 25, 7, "brick_slab", type="bottom")
        # Supported wall climbers stay on piers, away from glass and door openings.
        for x, z, facing, bottom, top in ((18, 17, "north", 5, 14), (6, 10, "east", 5, 13), (25, 5, "west", 5, 13)):
            for yy in range(bottom, top + 1):
                # Vine face points toward its supporting wall.
                support = self.get(x + DIRECTIONS[facing][0], yy, z + DIRECTIONS[facing][1])
                if support and support != "minecraft:air":
                    self.p(x, yy, z, "vine", **{facing: "true"})
        for x in (8, 9, 12, 13):
            self.leaf(x, 9, 22, flower=x % 2 == 0)
            self.p(x, 8, 22, "vine", up="true")
        for x in (18, 19):
            self.leaf(x, 10, 17, flower=True)
            self.p(x, 9, 17, "vine", north="true")

    def interiors(self) -> None:
        """Furnish all seven named rooms with usable furniture, storage and lighting."""
        # Living room: patterned rug, dining table, sofa, cabinets and a reading corner.
        for x in range(9, 15):
            for z in range(6, 11):
                self.p(x, 5, z, "orange_carpet" if (x + z) % 3 == 0 else "brown_carpet")
        for x in range(10, 13):
            for z in (7, 8):
                self.p(x, 5, z, "oak_fence", east="true", west="true")
                self.p(x, 6, z, "birch_slab", type="top")
        self.p(11, 7, 7, "white_candle", candles="3", lit="true")
        self.p(12, 7, 8, "potted_fern")
        for x in (10, 11, 12):
            self.p(x, 5, 5, "birch_stairs", facing="north", half="bottom", shape="straight")
            self.p(x, 5, 10, "oak_stairs", facing="south", half="bottom", shape="straight")
        self.fill((8, 5, 5, 8, 6, 6), "bookshelf")
        self.p(8, 7, 5, "potted_blue_orchid")
        self.p(8, 5, 14, "barrel", facing="south")
        self.p(8, 6, 14, "lantern", hanging="false")
        self.inventory(8, 5, 14, "barrel", [("bread", 12), ("apple", 6)])
        self.p(10, 5, 14, "chest", facing="south", type="single")
        self.inventory(10, 5, 14, "chest", [("book", 8), ("paper", 16)])
        self.frame(9, 7, 15, "north", "painting")
        self.lamp(11, 8, 8)
        # Ground hallway stays clear across the front door and both room doors.
        for z in range(7, 15):
            self.p(21, 5, z, "light_blue_carpet")
        self.p(18, 5, 5, "barrel", facing="south")
        self.p(18, 6, 5, "potted_bamboo")
        self.inventory(18, 5, 5, "barrel", [("torch", 24), ("lead", 2)])
        self.p(23, 5, 14, "chest", facing="west", type="single")
        self.inventory(23, 5, 14, "chest", [("oak_boat", 1), ("fishing_rod", 1)])
        self.frame(18, 7, 5, "south", "clock")
        self.lamp(21, 8, 8)
        # Enchanting room: two-high shelves with a one-block air gap around the table.
        shelves = {(x, z) for x in range(26, 31) for z in (9, 13)} | {(x, z) for x in (26, 30) for z in range(10, 13)}
        shelves -= {(26, 11), (28, 13)}
        for x, z in sorted(shelves):
            self.fill((x, 5, z, x, 6, z), "bookshelf")
        self.p(28, 5, 11, "enchanting_table")
        # Enchanting gaps remain air for bookshelf power; the rug lives at the entry.
        self.p(25, 5, 11, "purple_carpet")
        self.p(25, 5, 14, "lectern", facing="east", has_book="true")
        self.block_entities.append(Compound({"id": String("minecraft:lectern"), "x": Int(25), "y": Int(5), "z": Int(14), "Page": Int(0), "Book": Compound({"id": String("minecraft:written_book"), "count": Int(1), "components": Compound({"minecraft:written_book_content": Compound({"title": String("Villa al Mare"), "author": String("The Librarian"), "pages": NBTList[Compound]([Compound({"raw": String('{"text":"Notes on sea, stars and enchantments."}')})])})})})}))
        self.p(30, 5, 14, "chest", facing="west", type="single")
        self.inventory(30, 5, 14, "chest", [("lapis_lazuli", 32), ("book", 12), ("amethyst_shard", 8)])
        self.p(26, 7, 9, "white_candle", candles="3", lit="true")
        self.p(30, 7, 13, "potted_azalea_bush")
        self.frame(30, 8, 14, "north", "amethyst_shard")
        self.lamp(28, 8, 11)
        # Kitchen: L-shaped stone counters, a stocked pantry and smoker with hood.
        for x in range(8, 13):
            self.p(x, 11, 5, "smooth_quartz")
        for z in range(6, 10):
            self.p(8, 11, z, "smooth_quartz")
        self.p(8, 11, 6, "smoker", facing="east", lit="false")
        self.inventory(8, 11, 6, "smoker", [("potato", 8), ("coal", 16)])
        self.fill((8, 14, 6, 8, 15, 6), "smooth_quartz")
        self.p(10, 11, 5, "water_cauldron", level="3")
        self.p(11, 12, 5, "potted_fern")
        self.p(8, 12, 9, "cake", bites="0")
        self.fill((8, 11, 14, 8, 12, 14), "barrel", facing="east")
        self.inventory(8, 11, 14, "barrel", [("carrot", 16), ("wheat", 32), ("sugar", 8)])
        self.inventory(8, 12, 14, "barrel", [("bowl", 6), ("beetroot", 16)])
        self.p(10, 11, 12, "oak_fence")
        self.p(10, 12, 12, "birch_slab", type="top")
        self.p(10, 13, 12, "white_candle", candles="2", lit="true")
        self.p(10, 11, 14, "birch_stairs", facing="south", half="bottom", shape="straight")
        self.p(12, 11, 12, "yellow_carpet")
        self.frame(8, 13, 10, "east", "bread")
        self.lamp(11, 14, 10)
        # Brewing room along the north wall of the second floor.
        for z in range(5, 9):
            self.p(18, 11, z, "barrel", facing="west")
        for z in (6, 7):
            self.p(18, 12, z, "brewing_stand", has_bottle_0="true", has_bottle_1="true", has_bottle_2="true")
            self.inventory(18, 12, z, "brewing_stand", [("potion", 1), ("potion", 1), ("potion", 1), ("nether_wart", 6), ("blaze_powder", 16)])
        self.inventory(18, 11, 5, "barrel", [("glass_bottle", 16), ("nether_wart", 24), ("redstone", 12)])
        self.p(15, 11, 5, "water_cauldron", level="3")
        self.p(15, 11, 9, "chest", facing="east", type="single")
        self.inventory(15, 11, 9, "chest", [("glowstone_dust", 16), ("spider_eye", 8)])
        self.p(16, 11, 7, "blue_carpet")
        self.p(18, 12, 8, "potted_red_mushroom")
        self.frame(15, 13, 5, "south", "blaze_powder")
        self.lamp(17, 14, 7)
        # Bedroom: complete double bed, tall timber wardrobe and bedside light.
        for x in (20, 21):
            self.p(x, 11, 8, "cyan_bed", part="foot", facing="north", occupied="false")
            self.p(x, 11, 7, "cyan_bed", part="head", facing="north", occupied="false")
            self.fill((x, 11, 6, x, 12, 6), "stripped_oak_log", axis="y")
        for x in (22, 23):
            self.fill((x, 11, 5, x, 13, 5), "barrel", facing="south")
            for y in range(11, 14):
                self.p(x, y, 6, "spruce_trapdoor", facing="south", open="true", half="bottom")
        self.inventory(22, 11, 5, "barrel", [("leather_helmet", 1), ("leather_chestplate", 1)])
        self.p(20, 11, 5, "barrel", facing="south")
        self.p(20, 12, 5, "lantern", hanging="false")
        self.p(22, 11, 8, "cyan_carpet")
        self.p(22, 11, 9, "cyan_carpet")
        self.frame(20, 12, 7, "south", "nautilus_shell")
        self.lamp(21, 14, 8)
        # Upper hall and landing have a rug, writing desk and a supported potted plant.
        self.fill((20, 11, 12, 22, 11, 14), "cyan_carpet")
        self.p(23, 11, 11, "barrel", facing="west")
        self.p(23, 12, 11, "potted_blue_orchid")
        self.inventory(23, 11, 11, "barrel", [("map", 1), ("compass", 1)])
        self.frame(18, 13, 15, "north", "compass")
        self.lamp(21, 14, 13)

    def outdoors(self) -> None:
        """Furnish both terraces with dining furniture, loungers and planted railings."""
        # Dining beneath the upper balcony.
        for x in (9, 10):
            for z in (19, 20):
                self.p(x, 5, z, "oak_fence")
                self.p(x, 6, z, "birch_slab", type="top")
        self.p(9, 7, 19, "potted_blue_orchid")
        self.p(10, 7, 20, "white_candle", candles="2", lit="true")
        for z in (19, 20):
            self.p(8, 5, z, "oak_stairs", facing="west", half="bottom", shape="straight")
            self.p(11, 5, z, "oak_stairs", facing="east", half="bottom", shape="straight")
        self.lamp(13, 8, 19)
        # Upper balcony sofa and coffee table; the double doors remain accessible.
        for x in (9, 10, 11):
            self.p(x, 11, 18, "birch_stairs", facing="north", half="bottom", shape="straight")
        self.p(12, 11, 19, "oak_planks")
        self.p(12, 12, 19, "potted_cactus")
        self.planter(8, 11, 20, "potted_azalea_bush")
        self.planter(15, 11, 20, "potted_flowering_azalea_bush")
        # Two cyan sun loungers, set back from the pool under the lower pergola.
        for x in (27, 30):
            self.p(x, 5, 17, "quartz_stairs", facing="north", half="bottom", shape="straight")
            self.p(x, 5, 18, "cyan_wool")
            self.p(x, 5, 19, "smooth_quartz_slab", type="top")
            self.p(x, 6, 18, "cyan_carpet")
        self.p(28, 5, 18, "oak_planks")
        self.p(28, 6, 18, "potted_cactus")
        self.p(31, 5, 23, "barrel", facing="up")
        self.p(31, 6, 23, "lantern", hanging="false")
        self.inventory(31, 5, 23, "barrel", [("tropical_fish_bucket", 1), ("fishing_rod", 1)])
        for x in range(27, 32):
            self.leaf(x, 5, 16, flower=x % 3 == 0)

    def build(self) -> "Build":
        """Construct the reference-inspired Mediterranean villa.

        Returns:
            Build: This instance, ready to export.
        """
        self.grounds()
        self.structure()
        self.interiors()
        self.outdoors()
        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)
        root = schematic.write_to_nbt()
        region = root["Regions"][CONFIG["name"]]
        region["Entities"] = NBTList[Compound](self.entities)
        region["TileEntities"] = NBTList[Compound](self.block_entities)
        NBTFile(root).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"
        root = NBTFile.load_regardless_of_gzipped(path)
        assert len(root["Regions"]) == 1, "export must have exactly one region"
        region = root["Regions"][CONFIG["name"]]
        assert region["Entities"] == NBTList[Compound](self.entities), "item frames changed on reload"
        assert region["TileEntities"] == NBTList[Compound](self.block_entities), "furnished block entities changed on reload"
        return {
            "size_xyz": [size_x, size_y, size_z],
            "placed_blocks": int(root["Metadata"]["TotalBlocks"]),
            "palette_states": len(loaded.palette),
            "item_frames": len(self.entities),
            "block_entities": len(self.block_entities),
        }


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