# Generate a furnished alpine cottage with layered slate roofs and terraced gardens.
# Bounding box: X=0..54, Y=0..39, Z=0..52 (55 x 40 x 53 blocks).
# Reference reading: two timber-and-plaster storeys, usable roof loft, steep slate
# cross-gables, stone chimney, low wraparound veranda, masonry garden terraces.
# South (+Z) is the entrance. Floors at Y=5, 12, 20. All randomness is seeded.
from __future__ import annotations

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

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

from mcio.schematic import load_schematic # noqa: E402
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402

CONFIG = {
    "seed": 20260923,
    "size_xyz": (55, 40, 53), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Hearthwood Cottage",
    "author": "generator",
    "description": "Furnished alpine timber cottage, two storeys and storage loft, slate cross-gables, lantern veranda and terraced garden. Java 1.21.1.",
}

# Weighted families maintain coherent plaster, wood, stone, and roof surfaces.
STONE = ['stone_bricks'] * 6 + ['cobblestone'] * 3 + ['mossy_stone_bricks', 'andesite']
PLASTER = ['smooth_sandstone'] * 10 + ['sandstone'] * 2 + ['cut_sandstone']
WOOD_FLOOR = ['spruce_planks'] * 30 + ['dark_oak_planks']
SLATE = ['deepslate_tiles'] * 7 + ['deepslate_bricks'] * 3 + ['cobbled_deepslate']
SLATE_STAIRS = ['deepslate_tile_stairs'] * 7 + ['deepslate_brick_stairs'] * 3 + ['cobbled_deepslate_stairs']

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 block(self, x, y, z, name, **properties):
        """Place a vanilla block with a short material name."""
        self.put(x, y, z, 'minecraft:' + name, **properties)

    def fill(self, bounds, name, **properties):
        """Fill an inclusive box with a vanilla material."""
        self.box(bounds, 'minecraft:' + name, **properties)

    def mix(self, bounds, names):
        """Apply the seeded material palette to a surface."""
        self.texture(bounds, ['minecraft:' + name for name in names])

    def door(self, x, y, z, facing='north', hinge='left'):
        """Install matching operable door halves."""
        for half, dy in [('lower', 0), ('upper', 1)]:
            self.block(x, y + dy, z, 'spruce_door', half=half, facing=facing, hinge=hinge, open='false')

    def lamp(self, x, y, z, hanging=False):
        """Place a supported lantern and record the fixture."""
        self.block(x, y, z, 'lantern', hanging=str(hanging).lower())
        self.lights.append((x, y, z))

    def window(self, axis, wall, start, end, low, high, mullions=()):
        """Glaze a timber framed opening, recording its clear inner approach."""
        for t in range(start, end + 1):
            for y in range(low, high + 1):
                x, z = (t, wall) if axis == 'x' else (wall, t)
                if t in mullions:
                    self.block(x, y, z, 'stripped_spruce_log', axis='y')
                else:
                    props = dict(east='true', west='true') if axis == 'x' else dict(north='true', south='true')
                    self.block(x, y, z, 'yellow_stained_glass_pane', **props)
                    self.windows.append((x, y, z))
            for y in (low - 1, high + 1):
                x, z = (t, wall) if axis == 'x' else (wall, t)
                self.block(x, y, z, 'spruce_planks')
        for t in (start - 1, end + 1):
            for y in range(low - 1, high + 2):
                x, z = (t, wall) if axis == 'x' else (wall, t)
                self.block(x, y, z, 'stripped_spruce_log', axis='y')

    def stair_run(self, xs, first_z, first_y, count, direction, bottom):
        """Build a two-wide stair with full supports and generous clearance."""
        facing = 'north' if direction == -1 else 'south'
        for i in range(count):
            z, y = first_z + direction * i, first_y + i
            for x in xs:
                if y > bottom:
                    self.fill((x, bottom, z, x, y - 1, z), 'spruce_planks')
                self.block(x, y, z, 'spruce_stairs', facing=facing, half='bottom', shape='straight')
                self.fill((x, y + 1, z, x, y + 3, z), 'air')
                self.walking_stairs.append((x, y, z, facing))

    def bed(self, x, y, z, color='red', facing='north'):
        """Place a complete bed pair on the room floor."""
        dx, dz = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}[facing]
        self.block(x, y, z, color + '_bed', facing=facing, part='foot')
        self.block(x + dx, y, z + dz, color + '_bed', facing=facing, part='head')

    def flower_box(self, axis, fixed, start, end, y):
        """Build an attached planter of soil, wood edging, and flowers."""
        for t in range(start, end + 1):
            x, z = (t, fixed) if axis == 'x' else (fixed, t)
            self.block(x, y, z, 'dirt')
            self.block(x, y + 1, z, self.rng.choice(['fern', 'oxeye_daisy', 'azure_bluet', 'poppy']))
            dx, dz = (0, 1) if axis == 'x' else (1, 0)
            self.block(x + dx, y, z + dz, 'spruce_trapdoor', facing='south' if axis == 'x' else 'east', half='bottom', open='true')

    def roof_height(self, x, z):
        """Return the union of the main roof and its projecting front gables."""
        candidates = [(29 - abs(z - 23), 'south' if z < 23 else 'north')]
        if 12 <= x <= 28 and 25 <= z <= 37:
            candidates.append((26 - abs(x - 20), 'east' if x < 20 else 'west'))
        if 31 <= x <= 39 and 29 <= z <= 37:
            candidates.append((24 - abs(x - 35), 'east' if x < 35 else 'west'))
        return max(candidates, key=lambda item: item[0])

    def terrain(self):
        """Make the irregular earth island, raised terrace, and entrance stair."""
        for x in range(2, 53):
            for z in range(3, 51):
                if ((x - 27) / 28)**6 + ((z - 27) / 26)**6 > 1.33:
                    continue
                self.block(x, 0, z, self.rng.choice(['stone', 'andesite', 'stone']))
                self.block(x, 1, z, self.rng.choice(['grass_block'] * 9 + ['coarse_dirt', 'moss_block']))
        # Solid raised platform and deliberately uneven masonry retaining edges.
        self.fill((7, 2, 8, 47, 2, 44), 'dirt')
        self.fill((7, 3, 8, 47, 3, 44), 'grass_block')
        for x in range(7, 48):
            for z in (8, 44):
                if z == 44 and 25 <= x <= 31:
                    continue
                self.mix((x, 2, z, x, 3, z), STONE)
                if x % 4 != 0:
                    self.block(x, 4, z, 'stone_brick_slab', type='bottom')
        for z in range(9, 44):
            for x in (7, 47):
                self.mix((x, 2, z, x, 3, z), STONE)
                if z % 4:
                    self.block(x, 4, z, 'stone_brick_slab', type='bottom')
        # Terraced approach: a continuous one-block rise from the low garden.
        for z, y in [(48, 2), (47, 3), (46, 4), (45, 5)]:
            self.mix((25, 1, z, 31, y - 1, z), STONE)
            for x in range(25, 32):
                self.block(x, y, z, 'stone_brick_stairs' if x % 3 else 'andesite_stairs', facing='north')
                self.walking_stairs.append((x, y, z, 'north'))
        self.mix((24, 2, 44, 32, 5, 44), STONE)
        self.mix((24, 2, 43, 32, 5, 43), STONE)
        for z in range(49, 51):
            self.mix((24, 1, z, 32, 1, z), ['dirt_path', 'coarse_dirt', 'gravel', 'cobblestone'])
        # Stone paths round the raised terrace.
        for x in range(9, 46):
            for z in (9, 10, 42, 43):
                if 24 <= x <= 32 and z >= 42:
                    continue
                self.block(x, 3, z, self.rng.choice(['cobblestone', 'mossy_cobblestone', 'gravel', 'stone_bricks']))
        for x in (8, 9, 45, 46):
            for z in range(11, 42):
                self.block(x, 3, z, self.rng.choice(['cobblestone', 'gravel', 'mossy_cobblestone']))
        # Low buttresses at the entrance and corners.
        for x, z in [(8, 43), (20, 44), (23, 45), (33, 45), (39, 44), (46, 43), (7, 16), (47, 16)]:
            self.mix((x, 2, z, x, 4, z), STONE)
            self.block(x, 5, z, 'stone_brick_slab', type='bottom')
        for x in (23, 33):
            self.block(x, 5, 45, 'stone_bricks')
            self.lamp(x, 6, 45)

    def frame(self):
        """Raise the load-bearing timber and warm plaster envelope."""
        self.mix((12, 2, 13, 41, 4, 35), STONE)
        self.mix((12, 5, 13, 41, 5, 35), WOOD_FLOOR)
        self.mix((12, 12, 13, 41, 12, 35), WOOD_FLOOR)
        # The walls continue up to the roof underside, closing every gable.
        for x in range(12, 42):
            for z in range(13, 36):
                if x not in (12, 41) and z not in (13, 35):
                    continue
                self.mix((x, 6, z, x, 11, z), PLASTER)
                height = self.roof_height(x, z)[0]
                self.mix((x, 13, z, x, height - 2, z), PLASTER)
        for x in (12, 20, 27, 34, 41):
            for z in (13, 35):
                self.fill((x, 5, z, x, 11, z), 'spruce_log', axis='y')
                h = self.roof_height(x, z)[0]
                self.fill((x, 12, z, x, h - 2, z), 'spruce_log', axis='y')
        for z in (13, 20, 27, 35):
            for x in (12, 41):
                self.fill((x, 5, z, x, 18, z), 'spruce_log', axis='y')
        for y in (5, 11, 12):
            for z in (13, 35):
                self.fill((12, y, z, 41, y, z), 'spruce_log', axis='x')
            for x in (12, 41):
                self.fill((x, y, 13, x, y, 35), 'spruce_log', axis='z')
        # Main loft platform and the floor of the projecting front gable.
        self.mix((13, 20, 18, 40, 20, 28), WOOD_FLOOR)
        self.mix((16, 20, 29, 24, 20, 34), WOOD_FLOOR)
        # Ground-floor doors and glazed openings.
        self.door(28, 6, 35, 'north', 'left')
        self.door(29, 6, 35, 'north', 'right')
        self.window('x', 35, 15, 18, 7, 9)
        self.window('x', 35, 22, 24, 7, 9)
        self.window('x', 35, 33, 37, 7, 9, (35,))
        self.window('x', 13, 16, 19, 7, 9)
        self.window('x', 13, 31, 35, 7, 9, (33,))
        self.window('z', 41, 16, 18, 7, 9)
        self.window('z', 41, 24, 26, 7, 9)
        self.window('z', 41, 29, 32, 7, 9)
        self.window('z', 12, 27, 30, 7, 9)
        self.door(12, 6, 15, 'east')
        self.window('x', 35, 18, 22, 16, 19, (20,))
        self.window('x', 35, 34, 36, 16, 19, (35,))
        self.window('x', 13, 17, 21, 14, 16, (19,))
        self.window('x', 13, 32, 36, 14, 16, (34,))
        self.window('z', 41, 23, 27, 16, 19, (25,))
        self.window('z', 12, 27, 30, 16, 18, (29,))
        self.window('z', 41, 22, 24, 23, 25, (23,))
        self.window('x', 35, 20, 20, 22, 23)
        # Wooden shutters lie flat against solid plaster beside the glass.
        for start, end in [(18, 22), (34, 36)]:
            for x in (start - 1, end + 1):
                for y in (16, 17):
                    self.block(x, y, 36, 'spruce_trapdoor', facing='south', open='true')
        for x in (14, 26):
            for y in range(19, 23 - abs(x - 20) // 3):
                if y < self.roof_height(x, 35)[0] - 1:
                    self.block(x, y, 35, 'stripped_spruce_log')

    def roofs(self):
        """Lay varied slate courses over the intersecting gables and veranda."""
        for x in range(10, 44):
            for z in range(10, 38):
                if z == 37 and not (12 <= x <= 28 or 31 <= x <= 39):
                    continue
                h, facing = self.roof_height(x, z)
                self.block(x, h - 1, z, self.rng.choice(SLATE))
                self.block(x, h, z, self.rng.choice(SLATE_STAIRS), facing=facing, half='bottom', shape='straight')
        # Ridge caps and warm carved timber finials.
        for x in range(10, 44):
            self.block(x, 30, 23, 'deepslate_tile_slab', type='bottom')
        for x in (10, 11, 42, 43):
            self.block(x, 30, 23, 'spruce_slab', type='bottom')
        for x in (10, 43):
            self.block(x, 30, 23, 'spruce_planks')
            self.block(x, 31, 23, 'spruce_fence')
        for center, peak, z0 in [(20, 26, 27), (35, 24, 30)]:
            for z in range(z0, 38):
                if self.roof_height(center, z)[0] == peak:
                    self.block(center, peak + 1, z, 'deepslate_tile_slab', type='bottom')
            self.block(center, peak + 1, 37, 'spruce_planks')
            self.block(center, peak + 2, 37, 'spruce_fence')
        # Timber fascia follows the front gables beneath the slate.
        for x in range(13, 28):
            h = 26 - abs(x - 20)
            self.block(x, h - 1, 37, 'dark_oak_planks')
        for x in range(32, 39):
            h = 24 - abs(x - 35)
            self.block(x, h - 1, 37, 'dark_oak_planks')
        # Low continuous porch canopy, with half-block roof courses.
        for x in range(9, 48):
            for z in range(10, 43):
                is_front = z >= 35
                is_east = x >= 41
                is_west = x <= 12
                if not (is_front or is_east or is_west):
                    continue
                if z < 12 and x < 41:
                    continue
                distances = []
                if is_front:
                    distances.append(z - 35)
                if is_east:
                    distances.append(x - 41)
                if is_west:
                    distances.append(12 - x)
                d = max(distances)
                y = 15 - d // 2
                self.block(x, y - 1, z, self.rng.choice(SLATE))
                if d % 2 == 0:
                    self.block(x, y, z, self.rng.choice(SLATE_STAIRS), facing='north' if is_front else ('west' if is_east else 'east'))
                else:
                    self.block(x, y, z, 'deepslate_tile_slab', type='bottom')
        # A small high dormer, opening into the storage loft.
        self.fill((29, 23, 25, 33, 27, 28), 'air')
        self.mix((29, 23, 25, 29, 27, 28), PLASTER)
        self.mix((33, 23, 25, 33, 27, 28), PLASTER)
        for x in range(29, 34):
            h = 30 - abs(x - 31)
            self.mix((x, 23, 28, x, h - 2, 28), PLASTER)
        for x in range(28, 35):
            h = 30 - abs(x - 31)
            for z in range(24, 30):
                self.block(x, h - 1, z, self.rng.choice(SLATE))
                self.block(x, h, z, self.rng.choice(SLATE_STAIRS), facing='east' if x < 31 else 'west')
        self.window('x', 28, 31, 31, 24, 26)
        for z in range(24, 30):
            self.block(31, 31, z, 'deepslate_tile_slab', type='bottom')
        self.block(31, 31, 29, 'spruce_slab', type='bottom')
        self.flower_box('x', 36, 18, 22, 14)
        self.flower_box('x', 36, 34, 36, 14)

    def veranda(self):
        """Build the deep wraparound porch, railings, brackets, and fixtures."""
        self.mix((10, 2, 36, 44, 4, 42), STONE)
        self.mix((10, 5, 36, 44, 5, 42), WOOD_FLOOR)
        self.mix((42, 2, 12, 46, 4, 41), STONE)
        self.mix((42, 5, 12, 46, 5, 41), WOOD_FLOOR)
        self.mix((10, 2, 13, 11, 4, 35), STONE)
        self.mix((10, 5, 13, 11, 5, 35), WOOD_FLOOR)
        for x, z in [(10, 40), (19, 40), (25, 40), (32, 40), (44, 40), (45, 32), (45, 24), (45, 16), (10, 25), (10, 15)]:
            self.block(x, 6, z, 'stone_bricks')
            self.fill((x, 7, z, x, 11, z), 'stripped_spruce_log', axis='y')
            for dx in (-1, 1):
                if 9 <= x + dx <= 46:
                    self.block(x + dx, 11, z, 'spruce_stairs', facing='east' if dx == -1 else 'west', half='top')
        self.fill((10, 11, 40, 44, 11, 40), 'spruce_log', axis='x')
        self.fill((45, 11, 13, 45, 11, 40), 'spruce_log', axis='z')
        for x in list(range(11, 18)) + list(range(33, 44)):
            self.block(x, 6, 41, 'spruce_fence', east='true', west='true')
        for z in list(range(13, 22)) + list(range(26, 39)):
            self.block(46, 6, z, 'spruce_fence', north='true', south='true')
        for x, z in [(11, 39), (24, 39), (31, 39), (43, 39), (44, 17), (44, 30)]:
            self.block(x, 11, z, 'spruce_planks')
            self.block(x, 10, z, 'chain', axis='y')
            self.lamp(x, 9, z, True)
        # Lamps hang just below the outer eaves, visible from the approach.
        for x in (10, 24, 32, 44):
            self.block(x, 10, 42, 'chain', axis='y')
            self.lamp(x, 9, 42, True)
        for z in (16, 32):
            self.lamp(47, 10, z, True)
        # Porch bench and chest of wood beside the doorway.
        for x in range(15, 19):
            self.block(x, 6, 38, 'spruce_stairs', facing='north')
        self.block(22, 6, 37, 'barrel', facing='south')
        self.block(23, 6, 37, 'barrel', facing='south')
        self.block(23, 7, 37, 'barrel', facing='south')
        self.block(33, 6, 37, 'composter', level='6')
        self.block(42, 6, 20, 'crafting_table')
        self.block(43, 6, 20, 'barrel', facing='up')
        self.block(43, 7, 20, 'flower_pot')
        for z in range(27, 30):
            self.block(43, 6, z, 'spruce_stairs', facing='east')
        self.flower_box('x', 42, 12, 17, 6)
        self.flower_box('x', 42, 35, 40, 6)
        self.flower_box('z', 47, 27, 31, 6)
        # Fence planters sit over corbels attached to the porch foundation.
        for x in list(range(12, 18)) + list(range(35, 41)):
            self.block(x, 5, 42, 'spruce_planks')
        for z in range(27, 32):
            self.block(47, 5, z, 'spruce_stairs', facing='west', half='top')

    def chimney(self):
        """Add the high weathered stone chimney and a sealed, lit hearth."""
        self.mix((12, 5, 18, 15, 33, 21), STONE)
        self.fill((14, 6, 19, 15, 8, 20), 'air')
        for z in (19, 20):
            self.block(14, 6, z, 'campfire', facing='east', lit='true')
            self.block(13, 7, z, 'shroomlight')
            self.block(16, 5, z, 'polished_andesite')
            self.block(15, 6, z, 'iron_bars', north='true', south='true')
        for x in range(11, 17):
            for z in range(17, 23):
                self.block(x, 31, z, 'stone_brick_slab', type='top')
                self.block(x, 34, z, 'stone_brick_slab', type='top')
        for x in (12, 15):
            for z in (18, 21):
                self.block(x, 33, z, 'cobblestone_wall', up='true')
        for x in (13, 14):
            for z in (19, 20):
                self.block(x, 33, z, 'campfire', lit='true')
        # Weathered seams and a few mossy stones at the stack base.
        for y in (10, 17, 23, 28):
            self.block(12, y, 20, 'mossy_stone_bricks')

    def interiors(self):
        """Furnish all three levels and connect them with supported staircases."""
        # Ground level: living room, dining hall, working kitchen, and entry.
        self.fill((27, 6, 14, 27, 11, 25), 'spruce_planks')
        self.fill((27, 6, 20, 27, 9, 22), 'air')
        self.fill((28, 6, 23, 40, 11, 23), 'spruce_planks')
        self.door(31, 6, 23, 'north')
        self.fill((13, 10, 24, 26, 11, 24), 'spruce_log', axis='x')
        for x in (13, 26):
            self.fill((x, 6, 24, x, 9, 24), 'spruce_log')
        # Living room: sofa, chairs, wool rug, coffee table, and bookcases.
        for x in range(16, 23):
            for z in range(27, 32):
                self.block(x, 6, z, 'orange_carpet' if x in (16, 22) or z in (27, 31) else 'brown_carpet')
        for x in range(16, 21):
            self.block(x, 6, 26, 'spruce_stairs', facing='south')
        for z in (29, 30):
            self.block(23, 6, z, 'spruce_stairs', facing='west')
        self.fill((18, 6, 29, 20, 6, 29), 'dark_oak_slab', type='top')
        for z in (25, 26):
            self.fill((13, 6, z, 13, 8, z), 'bookshelf')
        self.block(14, 6, 32, 'barrel', facing='up')
        self.lamp(14, 7, 32)
        self.block(25, 6, 32, 'chest', facing='west', type='single')
        self.block(24, 6, 25, 'jukebox')
        # Dining table surrounded by supported wooden seats.
        self.fill((19, 6, 17, 22, 6, 19), 'spruce_planks')
        self.block(20, 7, 18, 'flower_pot')
        for x in (19, 21, 22):
            self.block(x, 6, 16, 'spruce_stairs', facing='south')
            self.block(x, 6, 20, 'spruce_stairs', facing='north')
        self.block(18, 6, 18, 'spruce_stairs', facing='east')
        self.block(23, 6, 18, 'spruce_stairs', facing='west')
        self.block(25, 6, 16, 'barrel', facing='south')
        self.block(25, 7, 16, 'potted_fern')
        self.block(17, 6, 22, 'spruce_planks')
        self.lamp(17, 7, 22)
        # Kitchen counters keep the north and east windows clear above them.
        for x in range(29, 39):
            self.block(x, 6, 14, 'barrel' if x % 2 else 'spruce_planks', **({'facing': 'south'} if x % 2 else {}))
        for z in range(15, 21):
            self.block(40, 6, z, 'spruce_planks')
        self.block(29, 7, 14, 'smoker', facing='south', lit='true')
        self.block(30, 7, 14, 'furnace', facing='south', lit='true')
        self.block(36, 7, 14, 'water_cauldron', level='3')
        self.block(38, 7, 14, 'crafting_table')
        self.block(40, 7, 20, 'potted_red_mushroom')
        self.block(29, 6, 18, 'barrel', facing='up')
        self.lamp(29, 7, 18)
        self.block(36, 6, 19, 'spruce_planks')
        self.block(37, 6, 19, 'spruce_planks')
        self.block(36, 7, 19, 'flower_pot')
        # Entry coat cabinet and hanging light below the upper floor.
        self.block(32, 6, 33, 'barrel', facing='west')
        self.lamp(32, 7, 33)
        self.block(29, 10, 31, 'chain', axis='y')
        self.block(29, 11, 31, 'spruce_planks')
        self.lamp(29, 9, 31, True)
        # Upper level: private master, guest room, and rear study.
        self.fill((27, 13, 14, 27, 19, 34), 'spruce_planks')
        self.door(27, 13, 29, 'west')
        self.fill((13, 13, 24, 26, 19, 24), 'spruce_planks')
        self.door(24, 13, 24, 'north')
        self.fill((31, 13, 22, 40, 19, 22), 'spruce_planks')
        self.door(33, 13, 22, 'north')
        # Master room under the large front gable.
        self.bed(16, 13, 29, 'red', 'north')
        self.bed(17, 13, 29, 'red', 'north')
        self.block(15, 13, 28, 'barrel', facing='up')
        self.lamp(15, 14, 28)
        self.fill((14, 13, 25, 16, 15, 25), 'bookshelf')
        self.block(23, 13, 26, 'chest', facing='south', type='single')
        self.block(24, 13, 32, 'spruce_stairs', facing='west')
        for x in range(19, 23):
            for z in range(28, 32):
                self.block(x, 13, z, 'red_carpet')
        self.block(23, 13, 32, 'spruce_planks')
        self.block(23, 14, 32, 'potted_poppy')
        # Guest chamber has two beds and luggage storage.
        self.bed(17, 13, 20, 'green', 'north')
        self.bed(23, 13, 20, 'green', 'north')
        self.block(20, 13, 22, 'barrel', facing='up')
        self.lamp(20, 14, 22)
        self.block(20, 13, 16, 'chest', facing='south', type='single')
        self.block(24, 13, 16, 'crafting_table')
        # Study: books, desk, chair, map-making station.
        self.fill((32, 13, 20, 35, 15, 20), 'bookshelf')
        self.fill((38, 13, 16, 38, 13, 19), 'spruce_planks')
        self.block(37, 13, 18, 'spruce_stairs', facing='east')
        self.block(38, 14, 17, 'lectern', facing='west', has_book='false')
        self.block(32, 13, 16, 'cartography_table')
        self.block(35, 13, 16, 'barrel', facing='up')
        self.lamp(35, 14, 16)
        self.block(32, 13, 32, 'barrel', facing='up')
        self.lamp(32, 14, 32)
        # Stairwell openings, rails, and both ascending runs.
        self.fill((36, 12, 25, 37, 12, 30), 'air')
        self.stair_run((36, 37), 30, 6, 7, -1, 6)
        for z in range(25, 31):
            for x in (35, 38):
                self.block(x, 13, z, 'spruce_fence', north='true', south='true')
        self.fill((28, 20, 18, 29, 20, 24), 'air')
        self.stair_run((28, 29), 18, 13, 8, 1, 13)
        for z in range(18, 25):
            self.block(30, 21, z, 'spruce_fence', north='true', south='true')
        # Attic storeroom, sewing bench, reading nook in the front gable.
        for x, z in [(17, 23), (18, 23), (17, 24), (38, 24), (38, 25)]:
            self.block(x, 21, z, 'barrel', facing='south')
        self.block(17, 22, 23, 'barrel', facing='south')
        self.block(19, 21, 19, 'chest', facing='south', type='single')
        self.block(35, 21, 19, 'loom', facing='south')
        self.block(36, 21, 19, 'crafting_table')
        self.block(22, 21, 25, 'barrel', facing='up')
        self.lamp(22, 22, 25)
        self.block(36, 21, 26, 'barrel', facing='up')
        self.lamp(36, 22, 26)
        self.block(20, 21, 32, 'spruce_stairs', facing='north')
        self.block(22, 21, 32, 'spruce_planks')
        self.lamp(22, 22, 32)
        self.fill((17, 21, 30, 17, 22, 31), 'bookshelf')
        # Additional low lights beside the large upper windows.
        self.block(21, 13, 33, 'spruce_planks')
        self.lamp(21, 14, 33)
        self.block(36, 13, 33, 'spruce_planks')
        self.lamp(36, 14, 33)

    def spruce(self, x, z, height):
        """Grow an irregular tiered spruce with supported persistent foliage."""
        self.fill((x, 2, z, x, height - 2, z), 'spruce_log', axis='y')
        # Separated whorls leave the trunk visible, as in the reference pines.
        for tier in range(6, height - 2, 4):
            radius = min(4, max(1, (height - tier + 4) // 7))
            for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                for step in range(1, radius):
                    px, pz = x + dx * step, z + dz * step
                    if self.get(px, tier, pz) in (None, 'minecraft:air'):
                        self.block(px, tier, pz, 'spruce_log', axis='x' if dx else 'z')
            for dy in (0, 1):
                r = radius if dy == 0 else max(1, radius - 1)
                for dx in range(-r, r + 1):
                    for dz in range(-r, r + 1):
                        if abs(dx) + abs(dz) > r + 1:
                            continue
                        if abs(dx) == abs(dz) == r and r > 1:
                            continue
                        px, py, pz = x + dx, tier + dy, z + dz
                        if self.inside(px, py, pz) and self.get(px, py, pz) in (None, 'minecraft:air'):
                            self.block(px, py, pz, 'spruce_leaves', persistent='true', distance='1')
        for y in range(height - 2, height + 3):
            r = 1 if y == height - 2 else 0
            for dx, dz in [(0, 0)] + ([(1, 0), (-1, 0), (0, 1), (0, -1)] if r else []):
                if self.get(x + dx, y, z + dz) in (None, 'minecraft:air'):
                    self.block(x + dx, y, z + dz, 'spruce_leaves', persistent='true', distance='1')

    def gardens(self):
        """Plant dense cottage borders and add wood piles, rocks, and ivy."""
        for x, z, h in [(7, 10, 28), (20, 6, 34), (47, 9, 31), (50, 29, 23), (4, 30, 21)]:
            self.spruce(x, z, h)
        # Dense low beds outside the retaining walls, leaving the approach clear.
        flowers = ['oxeye_daisy', 'azure_bluet', 'allium', 'lily_of_the_valley', 'poppy', 'fern', 'short_grass']
        for x in range(4, 51):
            for z in range(8, 49):
                outside = x <= 6 or x >= 48 or z >= 45
                if not outside or (23 <= x <= 33 and z >= 43):
                    continue
                if self.get(x, 1, z) is None or self.get(x, 2, z) not in (None, 'minecraft:air'):
                    continue
                chance = self.rng.random()
                if chance < .36:
                    self.block(x, 2, z, self.rng.choice(flowers))
                elif chance < .51:
                    self.block(x, 2, z, self.rng.choice(['azalea_leaves', 'oak_leaves', 'flowering_azalea_leaves']), persistent='true', distance='1')
                elif chance < .56:
                    self.block(x, 2, z, 'mossy_cobblestone')
        # Clusters on the upper garden terrace.
        for x0, z0 in [(9, 11), (9, 32), (12, 43), (18, 43), (36, 43), (42, 43), (46, 11)]:
            for dx in (-1, 0, 1):
                x = x0 + dx
                if self.get(x, 4, z0) in (None, 'minecraft:air'):
                    self.block(x, 4, z0, 'flowering_azalea_leaves' if dx == 0 else 'azalea_leaves', persistent='true', distance='1')
        # Small log stack at the west side, all resting on paving.
        self.mix((8, 3, 20, 9, 4, 23), STONE)
        for z in range(20, 24):
            for y in (5, 6):
                self.block(9, y, z, 'spruce_log', axis='x')
        # Front climbing vine stems and leaf clusters attached to corner posts.
        for x in (12, 27, 41):
            for y in range(6, 14):
                if self.get(x, y, 35) not in (None, 'minecraft:air') and self.get(x, y, 36) in (None, 'minecraft:air'):
                    self.block(x, y, 36, 'vine', north='true')
        for x, y, z in [(11, 10, 35), (12, 11, 36), (26, 10, 35), (27, 11, 36), (41, 11, 36), (42, 10, 35)]:
            if self.get(x, y, z) in (None, 'minecraft:air'):
                self.block(x, y, z, 'oak_leaves', persistent='true', distance='1')
        # Garden lamps on grounded posts, corresponding to the warm boundary lights.
        for x, z in [(7, 39), (47, 38), (9, 8), (46, 8)]:
            self.block(x, 4, z, 'stone_bricks')
            self.fill((x, 5, z, x, 7, z), 'spruce_fence')
            self.lamp(x, 8, z)

    def build(self) -> 'Build':
        """Build the reference cottage, grounds, and complete furnished interior."""
        self.windows = []
        self.lights = []
        self.walking_stairs = []
        self.terrain()
        self.frame()
        self.roofs()
        self.veranda()
        self.chimney()
        self.interiors()
        self.gardens()
        return self

    def export(self, path: Path) -> Path:
        """Write the schematic so that two runs produce identical bytes.

        Args:
            path (Path): Destination .litematic path.

        Returns:
            Path: The path written.
        """
        schematic = self.canvas.to_litematica(
            name=CONFIG["name"],
            author=CONFIG["author"],
            description=CONFIG["description"],
            minecraft_data_version=CONFIG["data_version"],
        )
        # The metadata otherwise records the current wall clock, which changes the bytes on every run.
        schematic.metadata.time_created = 0
        schematic.metadata.time_modified = 0
        path.parent.mkdir(parents=True, exist_ok=True)
        schematic.metadata.total_blocks = sum(state != 'minecraft:air' for state in self.placed.values())
        schematic.save(path)
        # Standard compressed Litematica NBT with a fixed gzip timestamp.
        path.write_bytes(gzip.compress(path.read_bytes(), mtime=0))
        return path

    def verify(self, path: Path) -> Dict[str, object]:
        """Reload the written file and confirm it matches what was placed.

        Args:
            path (Path): Schematic to reload.

        Returns:
            Dict[str, object]: Size, block count and palette size of the reloaded file.

        Raises:
            AssertionError: When a reloaded cell differs from what was placed.
        """
        loaded = load_schematic(path)
        size_y, size_z, size_x = loaded.size_yzx
        assert (size_x, size_y, size_z) == (self.size_x, self.size_y, self.size_z), "size changed on reload"
        ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx)
        for (x, y, z), state in self.placed.items():
            assert loaded.palette[int(ids[y, z, x])] == state, f"cell (x={x}, y={y}, z={z}) changed on reload"
        return {
            "size_xyz": [size_x, size_y, size_z],
            "placed_blocks": len(self.placed),
            "palette_states": len(loaded.palette),
        }


def main() -> None:
    """Generate the build, export it, and verify the exported file."""
    parser = argparse.ArgumentParser(description="Generate a Minecraft schematic.")
    parser.add_argument("--output", type=Path, default=Path(CONFIG["output"]))
    parser.add_argument("--seed", type=int, default=CONFIG["seed"])
    args = parser.parse_args()

    build = Build(seed=args.seed).build()
    path = build.export(args.output)
    report = build.verify(path)
    print(f"wrote {path}: {report}")


if __name__ == "__main__":
    main()
