# Generate a furnished forest cottage, glass conservatory, and planted stone terrace.
# BOUNDING BOX: X=0..46, Y=0..43, Z=0..42 (47 wide x 44 high x 43 deep).
# Reference: two timber/plaster storeys, inhabitable steep dark gable attic, tall pale
# chimney, east glass conservatory, curved eaves, dense conifers and flowering terrace.
# Final front faces north (-Z), with conservatory to the west (viewer right). Ground / upper / attic finished floor blocks are Y=6/13/20.
# The hidden rear repeats the framing, glazing and planting of the visible facade.
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.schematic import load_schematic # noqa: E402
from mcio.sketch import LitematicCanvas, Material, parse_blockstate # noqa: E402

CONFIG = {
    "seed": 20260923,
    "size_xyz": (47, 44, 43), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Fernlight Cottage",
    "author": "generator",
    "description": "Furnished timber cottage with steep slate gable, attic library, glass winter garden, and woodland terrace. Java 1.21.1.",
}

# Material families stay restrained; weighted variants supply natural weathering.
STONE = ['stone_bricks'] * 5 + ['mossy_stone_bricks'] * 3 + ['cobblestone', 'andesite']
PATH = ['stone_bricks'] * 5 + ['andesite', 'mossy_stone_bricks']
GROUND = ['grass_block'] * 7 + ['moss_block'] * 2 + ['coarse_dirt']
PLASTER = ['smooth_sandstone'] * 12 + ['sandstone', 'stripped_birch_log']
FLOOR = ['spruce_planks'] * 8 + ['oak_planks'] * 2
ROOF = ['deepslate_tiles'] * 8 + ['deepslate_bricks'] * 2 + ['cobbled_deepslate']
CHIMNEY = ['stone_bricks'] * 4 + ['andesite'] * 2 + ['polished_diorite'] * 2
HOUSE_CENTER = 18
ROOF_PROFILE = [32, 31, 30, 28, 26, 24, 22, 20, 19, 18, 17, 16]

Position = Tuple[int, int, int]


class Build:
    """Hold a block volume and write it out as a Litematica schematic."""

    def __init__(self, size_xyz: Tuple[int, int, int] = CONFIG["size_xyz"], seed: int = CONFIG["seed"]) -> None:
        """Create an empty volume with one seeded random source.

        Args:
            size_xyz (Tuple[int, int, int]): Volume size as width X, height Y, depth Z.
            seed (int): Seed for every random decision in this build.
        """
        self.size_x, self.size_y, self.size_z = size_xyz
        self.rng = random.Random(seed)
        self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
        self.placed: Dict[Position, str] = {}

    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, **props):
        """Place a vanilla block using its short name.

        Args:
            x, y, z (int): Block coordinate.
            name (str): Unqualified vanilla block name.
            **props (str): Block properties.
        """
        self.put(x, y, z, 'minecraft:' + name, **props)

    def fill(self, bounds, name, **props):
        """Fill an inclusive box with a vanilla block.

        Args:
            bounds (tuple): Inclusive XYZ corners.
            name (str): Unqualified block name.
            **props (str): Block properties.
        """
        self.box(bounds, 'minecraft:' + name, **props)

    def mix(self, bounds, names):
        """Texture an inclusive box from a weighted palette.

        Args:
            bounds (tuple): Inclusive XYZ corners.
            names (list): Unqualified block names, repeated for weights.
        """
        self.texture(bounds, ['minecraft:' + name for name in names])

    def clear(self, bounds):
        """Clear a volume for circulation.

        Args:
            bounds (tuple): Inclusive XYZ corners.
        """
        self.fill(bounds, 'air')

    def beam(self, bounds, axis='y'):
        """Place exposed structural oak.

        Args:
            bounds (tuple): Inclusive XYZ corners.
            axis (str): Log axis.
        """
        self.fill(bounds, 'stripped_spruce_log', axis=axis)

    def lamp(self, x, y, z, hanging=False):
        """Place and record a supported lantern.

        Args:
            x, y, z (int): Lantern coordinate.
            hanging (bool): Attach to the block above.
        """
        self.block(x, y, z, 'lantern', hanging=str(hanging).lower())
        self.lights.append((x, y, z))

    def door(self, x, y, z, facing, hinge='left'):
        """Place both matching halves of an oak door.

        Args:
            x, y, z (int): Lower half coordinate.
            facing (str): Door facing.
            hinge (str): Hinge side.
        """
        for dy, half in [(0, 'lower'), (1, 'upper')]:
            self.block(x, y + dy, z, 'spruce_door', facing=facing, hinge=hinge, half=half, open='false', powered='false')
        self.doors.append((x, y, z))

    def bed(self, x, y, z, color='white', facing='south'):
        """Place a complete bed on a solid floor.

        Args:
            x, y, z (int): Bed foot coordinate.
            color (str): Bed wool color.
            facing (str): Foot-to-head direction.
        """
        dx, dz = {'south': (0, 1), 'north': (0, -1), 'east': (1, 0), 'west': (-1, 0)}[facing]
        self.block(x, y, z, color + '_bed', part='foot', facing=facing, occupied='false')
        self.block(x + dx, y, z + dz, color + '_bed', part='head', facing=facing, occupied='false')

    def window(self, bounds):
        """Glaze and record an unobstructed window opening.

        Args:
            bounds (tuple): Inclusive glazing coordinates.
        """
        self.fill(bounds, 'glass')
        self.windows.append(bounds)

    def planter(self, x0, x1, y, z):
        """Build a supported timber window box with flowers.

        Args:
            x0, x1 (int): Planter endpoints.
            y (int): Soil height.
            z (int): Depth.
        """
        for x in range(x0, x1 + 1):
            self.block(x, y, z, 'grass_block')
            self.block(x, y, z - 1, 'spruce_trapdoor', facing='south', half='bottom', open='true')
            self.block(x, y + 1, z, ['poppy', 'azure_bluet', 'dandelion', 'oxeye_daisy'][(x - x0) % 4])
        self.block(x0 - 1, y, z, 'spruce_trapdoor', facing='east', half='bottom', open='true')
        self.block(x1 + 1, y, z, 'spruce_trapdoor', facing='west', half='bottom', open='true')
        for x in (x0, x1):
            self.block(x, y - 1, z, 'spruce_stairs', facing='south', half='top')
            self.block(x, y - 2, z, 'spruce_planks')

    def roof_height(self, x):
        """Return the curved, steep cottage roof profile.

        Args:
            x (int): Width coordinate.

        Returns:
            int: Upper roof block height.
        """
        return ROOF_PROFILE[abs(x - HOUSE_CENTER)]

    def terrain(self):
        """Build a rough stone garden island and continuous front approach."""
        for x in range(1, 46):
            for z in range(1, 41):
                corner = max(0, 6 - x) + max(0, x - 40) + max(0, 6 - z) + max(0, z - 35)
                if corner > 7 + self.rng.randrange(2):
                    continue
                self.mix((x, 0, z, x, 1, z), STONE)
                if 4 <= x <= 42 and 5 <= z <= 37 and corner <= 4:
                    self.mix((x, 2, z, x, 3, z), STONE)
                    edge = x in (4, 42) or z in (5, 37) or corner >= 3
                    self.block(x, 4, z, self.rng.choice(STONE if edge else GROUND))
                    self.garden_surface[(x, z)] = 4
                else:
                    self.block(x, 1, z, self.rng.choice(GROUND + STONE))
                    self.garden_surface[(x, z)] = 1
        # Formal front retaining line and broad, supported central stair.
        for x in range(6, 41):
            if 21 <= x <= 29:
                continue
            self.mix((x, 2, 5, x, 3, 5), STONE)
            self.block(x, 4, 5, 'stone_brick_slab', type='bottom')
        for z, y in [(2, 1), (3, 2), (4, 3), (5, 4)]:
            self.fill((21, 0, z, 29, y - 1, z), 'stone_bricks')
            self.fill((21, y, z, 29, y, z), 'stone_brick_stairs', facing='south')
            self.clear((21, y + 1, z, 29, y + 3, z))
            self.stair_runs['garden'].append([(x, y, z) for x in range(21, 30)])
        # Walk from the right-hand garden stair towards the cottage door.
        for x in range(16, 36):
            for z in range(6, 10):
                if z <= 7 and x < 21:
                    continue
                self.block(x, 4, z, self.rng.choice(PATH))
                self.paths.add((x, z))
        for z, y in [(10, 5), (11, 6)]:
            self.fill((16, 2, z, 20, y - 1, z), 'stone_bricks')
            self.fill((16, y, z, 20, y, z), 'stone_brick_stairs', facing='south')
            self.stair_runs['porch'].append([(x, y, z) for x in range(16, 21)])
        self.mix((14, 6, 12, 22, 6, 12), PATH)
        self.fill((14, 2, 12, 22, 5, 12), 'stone_bricks')
        for x in (19, 31):
            self.mix((x, 2, 4, x, 4, 4), STONE)
            self.block(x, 5, 4, 'chiseled_stone_bricks')
            self.lamp(x, 6, 4)
        # Exterior side service entrance climbs west towards the greenhouse.
        for x, y in [(42, 4), (41, 5), (40, 6)]:
            self.fill((x, 2, 24, x, y - 1, 25), 'stone_bricks')
            self.fill((x, y, 24, x, y, 25), 'stone_brick_stairs', facing='west')
            self.stair_runs['side'].append([(x, y, z) for z in (24, 25)])

    def house_shell(self):
        """Build a stone-footed timber house with two storeys and a steep attic."""
        self.mix((10, 2, 13, 26, 5, 32), STONE)
        self.mix((10, 6, 13, 26, 6, 32), FLOOR)
        self.mix((10, 13, 13, 26, 13, 32), FLOOR)
        self.mix((12, 20, 13, 24, 20, 32), FLOOR)
        for z in (13, 32):
            for x in range(10, 27):
                self.mix((x, 7, z, x, self.roof_height(x) - 1, z), PLASTER)
        for x in (10, 26):
            self.mix((x, 7, 13, x, 18, 32), PLASTER)
        for x in (10, 15, 21, 26):
            for z in (13, 32):
                self.beam((x, 7, z, x, min(27, self.roof_height(x) - 1), z))
        for x in (10, 26):
            for z in (13, 20, 26, 32):
                self.beam((x, 7, z, x, 18, z))
        for y in (7, 12, 13, 19):
            for z in (13, 32):
                a, b = (11, 25) if y == 19 else (10, 26)
                self.beam((a, y, z, b, y, z), 'x')
            if y < 19:
                for x in (10, 26):
                    self.beam((x, y, 13, x, y, 32), 'z')
        # Gable king post and stepped timber braces, all in the wall plane.
        for z in (13, 32):
            self.beam((18, 20, z, 18, 31, z))
            for x in range(12, 18):
                y = 20 + (x - 12)
                self.beam((x, y, z, x, y + 1, z), 'x')
                self.beam((36 - x, y, z, 36 - x, y + 1, z), 'x')
            self.beam((14, 23, z, 22, 23, z), 'x')
        self.door(17, 7, 13, 'south', 'left')
        self.door(18, 7, 13, 'south', 'right')
        self.window((17, 9, 13, 18, 10, 13))
        for a, b in [(11, 13), (22, 24)]:
            self.window((a, 8, 13, b, 10, 13))
            self.fill((a, 7, 12, b, 7, 12), 'spruce_slab', type='top')
        for a, b in [(16, 17), (19, 20)]:
            self.window((a, 15, 13, b, 18, 13))
        self.window((17, 25, 13, 17, 27, 13))
        self.window((19, 25, 13, 19, 27, 13))
        for x in (10, 26):
            ground_bays = [(15, 17), (21, 22)] if x == 10 else [(15, 17), (22, 24), (28, 30)]
            for z0, z1 in ground_bays:
                self.window((x, 8, z0, x, 10, z1))
            for z0, z1 in [(16, 18), (22, 23)]:
                self.window((x, 15, z0, x, 17, z1))
        for a, b in [(12, 14), (17, 19), (22, 24)]:
            self.window((a, 8, 32, b, 10, 32))
        self.window((17, 15, 32, 19, 18, 32))
        self.window((17, 25, 32, 19, 27, 32))
        # Double internal doors are aligned along Z in the east wall.
        self.clear((26, 7, 18, 26, 9, 19))
        self.door(26, 7, 18, 'east', 'left')
        self.door(26, 7, 19, 'east', 'right')
        self.beam((26, 9, 18, 26, 9, 19), 'z')
        # Exaggerated eaves transition from shallow curves to a very steep pitch.
        for x in range(7, 30):
            top = self.roof_height(x)
            outside = x - 1 if x < 18 else x + 1
            low = self.roof_height(outside) if 7 <= outside <= 29 else top - 1
            low = min(low, top - 1)
            for z in range(11, 35):
                self.mix((x, low, z, x, top - 1, z), ROOF)
                if x == 18:
                    self.block(x, top, z, 'deepslate_tile_slab', type='bottom')
                else:
                    self.block(x, top, z, self.rng.choice(['deepslate_tile_stairs'] * 5 + ['deepslate_brick_stairs']), facing='east' if x < 18 else 'west')
        # Stone edge bands emphasize the gable curve without changing the roof family.
        for z in (11, 34):
            for x in range(7, 30):
                y = self.roof_height(x)
                self.block(x, y - 1, z, 'polished_deepslate')
        for z in (11, 34):
            self.block(18, 33, z, 'spruce_fence')
        # Entrance canopy spans the front, with supported posts framing the door.
        for x in range(10, 27):
            for z in range(10, 13):
                y = 11 + (z - 10) // 2
                self.block(x, y, z, 'deepslate_tiles')
                self.block(x, y + 1, z, 'deepslate_tile_slab', type='bottom')
        for x in (10, 26):
            self.beam((x, 5, 10, x, 10, 10))
            self.block(x, 4, 10, 'mossy_stone_bricks')
        self.beam((10, 10, 10, 26, 10, 10), 'x')
        for x in (15, 21):
            self.block(x, 9, 11, 'spruce_fence')
            self.block(x, 10, 11, 'spruce_planks')
            self.lamp(x, 8, 11, hanging=True)
        # Window box immediately below the large upper gable window.
        self.planter(16, 20, 13, 12)
        for x in (15, 21):
            self.fill((x, 15, 12, x, 18, 12), 'oak_trapdoor', facing='south', half='bottom', open='true')
        for x in (15, 21):
            self.block(x, 19, 12, 'spruce_planks')
            self.lamp(x, 18, 12, hanging=True)

    def dormer(self):
        """Open an east-facing attic reading bay and fit its miniature gable roof."""
        self.mix((23, 20, 18, 28, 20, 24), FLOOR)
        self.clear((23, 21, 19, 27, 27, 23))
        for z in (18, 24):
            self.mix((23, 21, z, 28, 25, z), PLASTER)
            self.beam((23, 21, z, 28, 21, z), 'x')
            self.beam((28, 21, z, 28, 24, z))
        for z in range(18, 25):
            self.mix((28, 21, z, 28, 27 - abs(z - 21), z), PLASTER)
        self.window((28, 22, 20, 28, 24, 22))
        self.beam((28, 25, 19, 28, 25, 23), 'z')
        self.beam((28, 26, 21, 28, 27, 21))
        for z in range(17, 26):
            y = 28 - abs(z - 21)
            for x in range(22, 30):
                # Do not replace the higher, continuous main roof behind the dormer.
                if x <= 23 and self.roof_height(x) >= y:
                    continue
                self.block(x, y - 1, z, 'deepslate_tiles')
                self.block(x, y, z, 'deepslate_tile_stairs', facing='south' if z < 21 else 'north')
        self.block(29, 26, 19, 'spruce_planks')
        self.lamp(29, 25, 19, hanging=True)
        self.block(27, 21, 20, 'spruce_stairs', facing='east')
        self.block(27, 21, 22, 'bookshelf')
        self.block(27, 22, 22, 'potted_fern')
        self.rooms['attic reading bay'] = (25, 21, 21)

    def conservatory(self):
        """Build a tall timber-and-glass winter garden with a hipped glass roof."""
        self.mix((27, 2, 13, 39, 5, 30), STONE)
        self.mix((27, 6, 13, 39, 6, 30), ['polished_andesite'] * 5 + ['stone_bricks', 'andesite'])
        for x in range(27, 40):
            for z in (13, 30):
                self.fill((x, 7, z, x, 14, z), 'glass')
        self.fill((39, 7, 13, 39, 14, 30), 'glass')
        for x in (27, 31, 35, 39):
            for z in (13, 30):
                self.beam((x, 7, z, x, 14, z))
                self.block(x, 5, z - 1 if z == 13 else z + 1, 'mossy_stone_bricks')
        for z in (13, 18, 23, 26, 30):
            self.beam((39, 7, z, 39, 14, z))
        for y in (7, 11, 14):
            self.beam((27, y, 13, 39, y, 13), 'x')
            self.beam((27, y, 30, 39, y, 30), 'x')
            self.beam((39, y, 13, 39, y, 30), 'z')
        # Clear, closed hip planes meet the wall top with no diagonal air gaps.
        for x in range(26, 40):
            for z in range(13, 31):
                y = 15 + min(x - 26, 39 - x, z - 13, 30 - z, 4)
                self.block(x, y, z, 'glass')
                self.glass_roof.append((x, y, z))
        # Glazed hip joints connect every rising glass ring to the one below.
        for rise in range(1, 5):
            for x in (26 + rise, 39 - rise):
                for z in (13 + rise, 30 - rise):
                    self.block(x, 14 + rise, z, 'glass')
        # A light sill around the perimeter picks up the pale glazing grid.
        for x in range(27, 40):
            for z in (12, 31):
                self.block(x, 14, z, 'birch_slab', type='top')
        for z in range(13, 31):
            self.block(40, 14, z, 'birch_slab', type='top')
        self.door(39, 7, 24, 'west', 'left')
        self.door(39, 7, 25, 'west', 'right')
        self.fill((39, 9, 24, 39, 10, 25), 'glass')
        for x in (29, 37):
            self.block(x, 13, 14, 'spruce_planks')
            self.block(x, 12, 14, 'chain', axis='y')
            self.lamp(x, 11, 14, hanging=True)
        self.block(38, 13, 28, 'spruce_planks')
        self.block(38, 12, 28, 'chain', axis='y')
        self.lamp(38, 11, 28, hanging=True)
        self.block(40, 13, 23, 'spruce_planks')
        self.block(41, 13, 23, 'spruce_fence')
        self.lamp(41, 12, 23, hanging=True)
        # Low planting benches preserve the wide window openings.
        for z in (15, 28):
            for x in range(29, 38):
                self.block(x, 7, z, 'spruce_slab', type='top')
                if x in (29, 33, 37):
                    self.block(x, 7, z, 'barrel', facing='south' if z == 15 else 'north')
                self.block(x, 8, z, self.rng.choice(['potted_fern', 'potted_azalea_bush', 'potted_dandelion', 'potted_poppy', 'potted_dead_bush']))
        self.fill((35, 7, 20, 37, 7, 20), 'spruce_stairs', facing='north')
        self.fill((35, 7, 23, 37, 7, 23), 'spruce_stairs', facing='south')
        self.block(36, 7, 21, 'spruce_fence')
        self.block(36, 8, 21, 'spruce_pressure_plate')
        self.block(34, 7, 26, 'barrel', facing='up')
        self.lamp(34, 8, 26)
        for x, z in [(28, 24), (38, 17)]:
            self.block(x, 7, z, 'composter', level='8')
            self.block(x, 8, z, 'azalea_leaves', persistent='true', distance='1')
            self.block(x, 9, z, 'flowering_azalea_leaves', persistent='true', distance='1')
        self.rooms['conservatory'] = (31, 7, 21)

    def chimney(self):
        """Raise a pale masonry chimney above a working hearth."""
        self.mix((22, 7, 27, 24, 37, 29), CHIMNEY)
        for y in (12, 25, 34, 37):
            self.fill((22, y, 27, 24, y, 29), 'stone_bricks')
        self.fill((21, 35, 26, 25, 35, 30), 'stone_brick_slab', type='top')
        self.fill((21, 38, 26, 25, 38, 30), 'stone_brick_slab', type='bottom')
        # Two chimney pots with actual lit campfires, no unsupported smoke blocks.
        for z in (27, 29):
            self.block(23, 38, z, 'bricks')
            self.block(23, 39, z, 'campfire', facing='north', lit='true', signal_fire='false')
        self.clear((22, 7, 26, 24, 10, 26))
        self.fill((21, 7, 26, 21, 10, 26), 'stone_bricks')
        self.fill((25, 7, 26, 25, 10, 26), 'stone_bricks')
        self.fill((21, 10, 25, 25, 10, 26), 'polished_andesite')
        for x in (22, 23, 24):
            self.block(x, 7, 27, 'campfire', facing='north', lit='true')
            self.block(x, 7, 26, 'iron_bars', east='true', west='true')
        self.block(21, 11, 25, 'potted_fern')
        self.lamp(25, 11, 25)

    def stairs(self):
        """Cut two continuous, supported internal stair flights with clear landings."""
        for name, xs, base, floor in [('ground', (11, 12), 7, 6), ('attic', (14, 15), 14, 13)]:
            for step, z in enumerate(range(23, 30)):
                y = base + step
                for x in xs:
                    self.fill((x, floor + 1, z, x, y - 1, z), 'spruce_planks') if y > floor + 1 else None
                    self.block(x, y, z, 'spruce_stairs', facing='south')
                    self.clear((x, y + 1, z, x, y + 3, z))
                self.stair_runs[name].append([(x, y, z) for x in xs])
            for x in xs:
                self.clear((x, base + 7, 30, x, base + 9, 31))
        # The first-floor landing remains open beside the attic flight.
        for z in range(23, 29):
            self.block(13, 14, z, 'spruce_fence', north='true', south='true')
        self.rooms['upstairs landing'] = (12, 14, 30)
        self.rooms['attic landing'] = (15, 21, 30)

    def interiors(self):
        """Furnish cooking, sitting, sleeping, bathing and attic study spaces."""
        # Living room: wall sofa, rug, low table, bookcase and entrance storage.
        self.fill((11, 7, 16, 11, 7, 19), 'spruce_stairs', facing='west')
        self.block(11, 7, 20, 'barrel', facing='up')
        self.lamp(11, 8, 20)
        for x in range(14, 19):
            for z in range(17, 22):
                self.block(x, 7, z, 'green_carpet' if x in (14, 18) or z in (17, 21) else 'moss_carpet')
        self.block(16, 7, 19, 'spruce_slab', type='bottom')
        self.block(17, 7, 19, 'spruce_slab', type='bottom')
        self.fill((23, 7, 15, 24, 8, 15), 'bookshelf')
        self.block(24, 9, 15, 'potted_fern')
        self.block(20, 7, 14, 'barrel', facing='south')
        self.lamp(20, 8, 14)
        self.rooms['living room'] = (19, 7, 20)
        # Kitchen worktop below rear windows, oven, washbasin and pantry.
        for x in range(16, 21):
            self.block(x, 7, 31, 'barrel', facing='north')
            # Worktop stays below the rear glazing sill.
        self.block(16, 7, 31, 'smoker', facing='north', lit='true')
        self.block(17, 7, 31, 'furnace', facing='north', lit='true')
        self.block(19, 7, 31, 'water_cauldron', level='3')
        self.clear((19, 8, 31, 19, 8, 31))
        self.block(20, 7, 30, 'crafting_table')
        self.block(16, 7, 28, 'barrel', facing='up')
        self.lamp(16, 8, 28)
        self.block(18, 7, 26, 'spruce_fence')
        self.block(18, 8, 26, 'spruce_pressure_plate')
        self.block(17, 7, 26, 'spruce_stairs', facing='west')
        self.block(19, 7, 26, 'spruce_stairs', facing='east')
        self.rooms['kitchen'] = (19, 7, 29)
        # Upper hall divides the front bedroom from a small rear guest room.
        self.fill((16, 14, 24, 25, 18, 24), 'spruce_planks')
        self.door(19, 14, 24, 'north')
        self.fill((19, 16, 24, 19, 18, 24), 'stripped_spruce_log', axis='y')
        self.bed(22, 14, 17, 'green', 'south')
        self.bed(23, 14, 17, 'green', 'south')
        self.block(24, 14, 18, 'barrel', facing='up')
        self.lamp(24, 15, 18)
        self.fill((22, 14, 22, 24, 15, 22), 'barrel', facing='north')
        self.block(21, 14, 15, 'spruce_stairs', facing='east')
        self.block(20, 14, 15, 'bookshelf')
        self.block(20, 15, 15, 'potted_poppy')
        self.fill((17, 14, 18, 19, 14, 21), 'white_carpet')
        self.rooms['main bedroom'] = (20, 14, 20)
        # Guest bedroom shares the upper hall and chimney breast.
        self.bed(18, 14, 29, 'white', 'south')
        self.block(17, 14, 30, 'barrel', facing='up')
        self.lamp(17, 15, 30)
        self.block(20, 14, 30, 'bookshelf')
        self.block(20, 15, 30, 'potted_fern')
        self.rooms['guest bedroom'] = (19, 14, 28)
        # Washroom in the upper west-front bay, with a clear doorway.
        self.fill((14, 14, 14, 14, 17, 19), 'spruce_planks')
        self.fill((11, 14, 19, 14, 17, 19), 'spruce_planks')
        self.door(13, 14, 19, 'south')
        self.block(11, 14, 14, 'water_cauldron', level='3')
        self.block(12, 14, 14, 'smooth_quartz')
        self.block(13, 14, 14, 'barrel', facing='south')
        self.lamp(13, 15, 14)
        self.rooms['washroom'] = (12, 14, 17)
        # Attic library and workbench below the ridge.
        self.fill((16, 21, 15, 16, 23, 17), 'bookshelf')
        self.fill((20, 21, 15, 20, 23, 17), 'bookshelf')
        self.block(18, 21, 16, 'lectern', facing='south', has_book='false')
        self.fill((20, 21, 29, 21, 21, 30), 'spruce_planks')
        self.block(20, 22, 29, 'cartography_table')
        self.block(21, 22, 30, 'potted_fern')
        self.block(19, 21, 29, 'spruce_stairs', facing='west')
        self.block(21, 21, 26, 'barrel', facing='up')
        self.lamp(21, 22, 26)
        self.block(18, 24, 19, 'spruce_planks')
        self.beam((18, 25, 19, 18, 30, 19))
        self.lamp(18, 23, 19, hanging=True)
        self.rooms['attic study'] = (18, 21, 23)
        # Additional nearby floor-height lamps in the large rooms and circulation.
        for x, y, z in [(13, 7, 22), (20, 14, 23), (16, 21, 31)]:
            self.block(x, y, z, 'barrel', facing='up')
            self.lamp(x, y + 1, z)
        self.block(13, 14, 31, 'barrel', facing='up')
        self.lamp(13, 15, 31)

    def spruce(self, x, z, height):
        """Grow a narrow, asymmetric spruce with connected branches.

        Args:
            x, z (int): Trunk coordinate.
            height (int): Height above garden surface.
        """
        ground = self.garden_surface.get((x, z), 4)
        top = ground + height
        self.fill((x, ground + 1, z, x, top, z), 'spruce_log', axis='y')
        # Sparse whorls, visible trunks and rising tips recreate the reference firs.
        for y in range(ground + 5, top - 1, 4):
            radius = min(4, max(1, (top - y) // 7))
            for dx, dz in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                length = max(1, radius - self.rng.randrange(2))
                for step in range(1, length + 1):
                    xx, zz = x + dx * step, z + dz * step
                    if self.inside(xx, y, zz) and self.get(xx, y, zz) in (None, 'minecraft:air'):
                        self.block(xx, y, zz, 'spruce_log', axis='x' if dx else 'z')
                    for dy, sx, sz in [(1, 0, 0), (2, 0, 0), (0, dz, dx), (0, -dz, -dx), (0, dx, dz)]:
                        px, py, pz = xx + sx, y + dy, zz + sz
                        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 dx, dz in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                if self.get(x + dx, y + 1, z + dz) in (None, 'minecraft:air'):
                    self.block(x + dx, y + 1, z + dz, 'spruce_leaves', persistent='true', distance='1')
        for y in range(top - 2, top + 2):
            self.block(x, y, z, 'spruce_leaves', persistent='true', distance='1')
        for dx, dz in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
            self.block(x + dx, top - 2, z + dz, 'spruce_leaves', persistent='true', distance='1')

    def landscape(self):
        """Add dense cottage planting, vines, garden props and framed forest backdrop."""
        for x, z, height in [(5, 29, 29), (8, 36, 34), (18, 38, 37), (33, 36, 32), (42, 32, 25), (3, 20, 21)]:
            self.spruce(x, z, height)
        # Shrubs hug the foundations and flower beds, with paths left open.
        shrub_sites = [(7, 9), (11, 8), (13, 10), (23, 10), (27, 10), (30, 11), (35, 10), (39, 9), (41, 16), (41, 20), (38, 33), (29, 34), (8, 15), (8, 21), (7, 25), (11, 35), (5, 12)]
        for x, z in shrub_sites:
            for dx, dz in [(0, 0), (-1, 0), (1, 0), (0, -1), (0, 1)]:
                xx, zz = x + dx, z + dz
                if self.get(xx, 5, zz) not in (None, 'minecraft:air'):
                    continue
                if (xx, zz) in self.paths:
                    continue
                self.block(xx, 4, zz, 'grass_block')
                self.block(xx, 5, zz, 'flowering_azalea_leaves' if self.rng.random() < .22 else 'azalea_leaves', persistent='true', distance='1')
            if self.get(x, 6, z) in (None, 'minecraft:air'):
                self.block(x, 6, z, 'oak_leaves', persistent='true', distance='1')
        for (x, z), y in sorted(self.garden_surface.items()):
            if (x, z) in self.paths or (15 <= x <= 21 and 8 <= z <= 12) or (20 <= x <= 30 and z < 7) or (x >= 39 and 23 <= z <= 26):
                continue
            if self.get(x, y + 1, z) not in (None, 'minecraft:air'):
                continue
            base = self.get(x, y, z) or ''
            if 'grass_block' not in base and 'moss_block' not in base:
                continue
            chance = .67 if z < 13 or x >= 40 or x < 9 else .3
            if self.rng.random() < chance:
                self.block(x, y + 1, z, self.rng.choice(['fern'] * 3 + ['short_grass'] * 2 + ['dandelion', 'poppy', 'azure_bluet', 'oxeye_daisy', 'cornflower', 'allium']))
        # Vines directly attach to existing masonry or timber, never across glazing.
        for x in (10, 15, 21, 26):
            for y in range(7, 20):
                z = 12
                support = self.get(x, y, z + 1) or ''
                if 'glass' in support or 'door' in support or support.endswith(':air'):
                    continue
                if self.get(x, y, z) in (None, 'minecraft:air') and self.rng.random() < .75:
                    self.block(x, y, z, 'vine', south='true')
        for z in (14, 18, 23, 26, 29):
            for y in range(7, 14):
                support = self.get(39, y, z) or ''
                if 'glass' not in support and 'door' not in support and self.get(40, y, z) in (None, 'minecraft:air'):
                    self.block(40, y, z, 'vine', west='true')
        # Ivy clumps cling to timber corners and soften the glasshouse eaves.
        for x, z, y in [(27, 12, 12), (27, 12, 13), (27, 12, 14), (35, 12, 13), (39, 12, 12), (39, 12, 14), (40, 18, 12), (40, 18, 13), (40, 26, 13)]:
            if self.get(x, y, z) in (None, 'minecraft:air') or 'vine' in (self.get(x, y, z) or ''):
                self.block(x, y, z, 'azalea_leaves', persistent='true', distance='1')
        for x in (8, 9, 11, 12, 14, 24, 26, 27):
            y = self.roof_height(x) - 1
            if self.get(x, y, 10) in (None, 'minecraft:air'):
                self.block(x, y, 10, 'oak_leaves', persistent='true', distance='1')
        # Small vines follow the lower gable roof edge on both sides.
        for x in range(8, 15):
            y = self.roof_height(x) - 1
            if self.get(x, y, 10) in (None, 'minecraft:air'):
                self.block(x, y, 10, 'vine', south='true')
        for x in range(23, 29):
            y = self.roof_height(x) - 1
            if self.get(x, y, 10) in (None, 'minecraft:air'):
                self.block(x, y, 10, 'vine', south='true')
        # Front greenhouse planters sit below the windows, with small lanterns.
        for x in (28, 32, 36):
            self.block(x, 5, 11, 'barrel', facing='up')
            self.block(x, 6, 11, 'potted_fern')
        for x, z in [(7, 7), (13, 9), (33, 9), (40, 12), (40, 29), (8, 32)]:
            self.block(x, 4, z, 'stone_bricks')
            self.block(x, 5, z, 'cobblestone_wall')
            self.lamp(x, 6, z)
        # Timber tool table and a stack of chopped firewood in the back garden.
        for x in range(26, 30):
            self.block(x, 5, 34, 'spruce_log', axis='z')
            if x < 29:
                self.block(x, 6, 34, 'spruce_log', axis='z')
        self.block(36, 5, 32, 'composter', level='7')
        self.block(37, 5, 32, 'barrel', facing='up')
        self.block(38, 5, 32, 'crafting_table')
        self.block(37, 6, 32, 'potted_dead_bush')
        # Short garden rail, with leaves and lanterns between broad stone piers.
        for x in range(32, 40):
            if self.get(x, 5, 6) in (None, 'minecraft:air'):
                self.block(x, 5, 6, 'spruce_fence', east='true', west='true')

    def orient_to_reference(self):
        """Reflect the plan so the front view has its conservatory on the right."""
        edge = self.size_x - 1
        blocks = list(self.placed.items())
        self.placed = {}
        self.canvas = LitematicCanvas((self.size_y, self.size_z, self.size_x))
        for (x, y, z), state in blocks:
            name, _, raw = state.partition('[')
            props = dict(item.split('=') for item in raw.rstrip(']').split(',') if item)
            if props.get('facing') in ('east', 'west'):
                props['facing'] = {'east': 'west', 'west': 'east'}[props['facing']]
            if 'hinge' in props:
                props['hinge'] = 'right' if props['hinge'] == 'left' else 'left'
            if 'east' in props or 'west' in props:
                east, west = props.pop('east', 'false'), props.pop('west', 'false')
                props['east'], props['west'] = west, east
            self.put(edge - x, y, z, name, **props)
        mirror = lambda p: (edge - p[0], p[1], p[2])
        self.lights = [mirror(p) for p in self.lights]
        self.doors = [mirror(p) for p in self.doors]
        self.glass_roof = [mirror(p) for p in self.glass_roof]
        self.rooms = {name: mirror(p) for name, p in self.rooms.items()}
        self.windows = [(edge - x1, y0, z0, edge - x0, y1, z1) for x0, y0, z0, x1, y1, z1 in self.windows]
        self.stair_runs = {name: [[mirror(p) for p in tread] for tread in run] for name, run in self.stair_runs.items()}
        self.floor_areas = [(edge - x1, y, z0, edge - x0, z1) for x0, y, z0, x1, z1 in self.floor_areas]
        self.interior_volumes = [(edge - x1, y0, z0, edge - x0, y1, z1) for x0, y0, z0, x1, y1, z1 in self.interior_volumes]
        self.foundations = [(edge - x1, z0, edge - x0, z1) for x0, z0, x1, z1 in self.foundations]
        self.entry_start = mirror(self.entry_start)

    def build(self) -> 'Build':
        """Generate the reference-inspired cottage and its furnished garden.

        Returns:
            Build: Completed build with structural verification metadata.
        """
        self.lights = []
        self.doors = []
        self.windows = []
        self.rooms = {}
        self.glass_roof = []
        self.garden_surface = {}
        self.paths = set()
        self.stair_runs = {name: [] for name in ('garden', 'porch', 'side', 'ground', 'attic')}
        self.terrain()
        self.house_shell()
        self.dormer()
        self.conservatory()
        self.chimney()
        self.stairs()
        self.interiors()
        self.landscape()
        self.entry_start = (25, 2, 2)
        self.floor_areas = [(11, 7, 14, 25, 31), (27, 7, 14, 38, 29), (11, 14, 14, 25, 31), (14, 21, 14, 22, 31), (23, 21, 19, 27, 23)]
        self.interior_volumes = [(11, 7, 14, 25, 17, 31), (12, 18, 14, 24, 19, 31), (14, 21, 14, 22, 23, 31), (16, 24, 14, 20, 27, 31), (23, 21, 19, 27, 25, 23), (27, 7, 14, 38, 14, 29)]
        self.foundations = [(10, 13, 26, 32), (27, 13, 39, 30)]
        self.orient_to_reference()
        return self

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

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

        Returns:
            Path: The path written.
        """
        schematic = self.canvas.to_litematica(
            name=CONFIG["name"],
            author=CONFIG["author"],
            description=CONFIG["description"],
            minecraft_data_version=CONFIG["data_version"],
        )
        # The metadata otherwise records the current wall clock, which changes the bytes on every run.
        schematic.metadata.time_created = 0
        schematic.metadata.time_modified = 0
        path.parent.mkdir(parents=True, exist_ok=True)
        schematic.save(path)
        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()
