# Bounding box: X 0..60, Y 0..72, Z 0..58 (61 x 73 x 59 blocks).
# Generate Lanterncrag Cottage, a furnished cliff house reconstructed from reference.png.
# Reference reading: three levels, steep flared slate gables, timber frame with cream infill.
# A solid rocky crag carries terraced stone stairs, spruce planting, ivy and warm lanterns.
from __future__ import annotations

import argparse
import math
from collections import deque
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": (61, 73, 59), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Lanterncrag Cottage",
    "author": "generator",
    "description": "Three furnished levels, slate gables, timber and plaster, terraced crag and winding 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] = {}
        self.routes = []
        self.rooms = {}
        self.windows = []
        self.doors = []
        self.beds = []
        self.decorations = []
        self.lights = []
        self.reserved = set()
        self.ground = {}

    def inside(self, x: int, y: int, z: int) -> bool:
        """Report whether a coordinate is inside the volume.

        Args:
            x (int): Width coordinate.
            y (int): Height coordinate.
            z (int): Depth coordinate.

        Returns:
            bool: True when every axis is within range.
        """
        return 0 <= x < self.size_x and 0 <= y < self.size_y and 0 <= z < self.size_z

    def put(self, x: int, y: int, z: int, block: str | Material, **properties: str) -> None:
        """Place one block, in XYZ order, with an immediate bounds check.

        Args:
            x (int): Width coordinate.
            y (int): Height coordinate.
            z (int): Depth coordinate.
            block (str | Material): Namespaced block name, a full blockstate string, or a Material.
            **properties: Blockstate properties such as facing or half.

        Raises:
            AssertionError: When the coordinate is outside the volume, naming the coordinate.
        """
        assert self.inside(x, y, z), f"out of bounds: (x={x}, y={y}, z={z}) in {self.size_x}x{self.size_y}x{self.size_z}"
        material = self.material(block, **properties) if isinstance(block, str) else block
        self.canvas.block((y, z, x), material)
        self.placed[(x, y, z)] = material.blockstate

    def material(self, name: str, **properties: str) -> Material:
        """Build a Material from a block name and its properties.

        Args:
            name (str): Namespaced block name, or a full blockstate string.
            **properties: Blockstate properties.

        Returns:
            Material: Material ready for placement.
        """
        if "[" in name:
            return parse_blockstate(name)
        return self.canvas.material(name, **dict(sorted(properties.items())))

    def box(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None:
        """Fill an inclusive box.

        Args:
            bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive on both ends.
            block (str | Material): Block to place.
            **properties: Blockstate properties.
        """
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(min(x0, x1), max(x0, x1) + 1):
            for y in range(min(y0, y1), max(y0, y1) + 1):
                for z in range(min(z0, z1), max(z0, z1) + 1):
                    self.put(x, y, z, block, **properties)

    def shell(self, bounds: Tuple[int, int, int, int, int, int], block: str | Material, **properties: str) -> None:
        """Fill only the faces of a box, leaving the inside empty.

        Args:
            bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive.
            block (str | Material): Block to place.
            **properties: Blockstate properties.
        """
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(min(x0, x1), max(x0, x1) + 1):
            for y in range(min(y0, y1), max(y0, y1) + 1):
                for z in range(min(z0, z1), max(z0, z1) + 1):
                    on_face = x in (x0, x1) or y in (y0, y1) or z in (z0, z1)
                    if on_face:
                        self.put(x, y, z, block, **properties)

    def texture(self, bounds: Tuple[int, int, int, int, int, int], palette: Iterable[str]) -> None:
        """Fill a box with a weighted mix of related blocks, so the surface is not flat.

        Args:
            bounds (Tuple[int, int, int, int, int, int]): x0, y0, z0, x1, y1, z1, inclusive.
            palette (Iterable[str]): Block names. Repeat a name to make it more common.
        """
        choices = list(palette)
        x0, y0, z0, x1, y1, z1 = bounds
        for x in range(min(x0, x1), max(x0, x1) + 1):
            for y in range(min(y0, y1), max(y0, y1) + 1):
                for z in range(min(z0, z1), max(z0, z1) + 1):
                    self.put(x, y, z, self.rng.choice(choices))

    def get(self, x: int, y: int, z: int) -> Optional[str]:
        """Read back what was placed at a coordinate.

        Args:
            x (int): Width coordinate.
            y (int): Height coordinate.
            z (int): Depth coordinate.

        Returns:
            Optional[str]: Blockstate string, or None when the cell is empty.
        """
        return self.placed.get((x, y, z))

    def build(self) -> "Build":
        """Build the rocky terrain, timber house, rooms, approach and planting.

        Returns:
            Build: Completed volume.
        """
        self.terrain()
        self.approach()
        self.house()
        self.roof()
        self.furnish()
        self.exterior_details()
        self.rock_details()
        self.landscape()
        self.prune_unattached_foliage()
        self.connect_fences()
        return self

    def terrain(self) -> None:
        """Shape a solid, irregular crag with vertical stone seams and moss caps."""
        for x in range(2, 59):
            for z in range(3, 57):
                rim = ((x - 30) / 28)**2 + ((z - 30) / 26)**2
                if rim > 1 + 0.045 * math.sin(x * 1.7 + z * 0.8):
                    continue
                q = math.hypot((x - 28) / 23, (z - 22) / 20)
                front = math.hypot((x - 19) / 12, (z - 44) / 12)
                east = math.hypot((x - 46) / 10, (z - 32) / 14)
                noise = 1.2 * math.sin(x * .9) + .8 * math.cos(z * 1.3)
                h = max(1, round(26 - max(0, q - .56) * 31 + noise), round(18 - max(0, front - .30) * 20 + noise), round(22 - max(0, east - .35) * 24 + noise))
                h = min(26, h)
                if 10 <= x <= 42 and 12 <= z <= 33:
                    h = 26
                self.ground[x, z] = h
                seam = self.rng.choice(['stone', 'stone', 'andesite', 'tuff'])
                for y in range(h + 1):
                    name = self.rng.choice([seam] * 9 + ['stone'] * 5 + ['andesite'] * 2 + ['cobblestone'])
                    if y == h:
                        name = self.rng.choice(['moss_block'] * 3 + ['grass_block'] * 3 + ['coarse_dirt', 'stone'])
                    elif y == h - 1 and self.rng.random() < .45:
                        name = 'dirt'
                    self.put(x, y, z, 'minecraft:' + name)

    def landing(self, x0: int, x1: int, z0: int, z1: int, y: int) -> None:
        """Build a supported stone landing and reserve its walking surface.

        Args:
            x0, x1, z0, z1 (int): Inclusive horizontal limits.
            y (int): Walking surface block height.
        """
        for x in range(x0, x1 + 1):
            for z in range(z0, z1 + 1):
                self.texture((x, 0, z, x, y - 1, z), ['minecraft:stone'] * 5 + ['minecraft:andesite', 'minecraft:cobblestone'])
                self.put(x, y, z, self.rng.choice(['minecraft:stone_bricks'] * 5 + ['minecraft:andesite', 'minecraft:mossy_stone_bricks']))
                self.box((x, y + 1, z, x, max(y + 3, self.ground.get((x, z), 0) + 1), z), 'minecraft:air')
                self.reserved.add((x, z))
                self.routes.append((x, y + 1, z))
                self.ground[x, z] = y

    def flight(self, start: Position, count: int, facing: str, width: int = 4) -> None:
        """Build a one-block-rise stair flight, its foundation and low stone edges.

        Args:
            start (Position): First stair block in XYZ order.
            count (int): Number of treads.
            facing (str): Direction of ascent.
            width (int): Number of blocks across each tread.
        """
        dx, dz = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}[facing]
        for i in range(count):
            x, y, z = start[0] + i * dx, start[1] + i, start[2] + i * dz
            for j in range(-1, width + 1):
                xx, zz = x + (j if dz else 0), z + (j if dx else 0)
                self.texture((xx, 0, zz, xx, y - 1, zz), ['minecraft:stone'] * 5 + ['minecraft:andesite', 'minecraft:cobblestone'])
                top = max(y + 4, self.ground.get((xx, zz), 0) + 1)
                self.box((xx, y, zz, xx, top, zz), 'minecraft:air')
                self.reserved.add((xx, zz))
                if 0 <= j < width:
                    self.put(xx, y, zz, self.rng.choice(['minecraft:stone_brick_stairs'] * 5 + ['minecraft:andesite_stairs', 'minecraft:mossy_stone_brick_stairs']), facing=facing, half='bottom', shape='straight')
                    self.routes.append((xx, y + 1, zz))
                else:
                    self.put(xx, y, zz, 'minecraft:cobblestone')
                    self.put(xx, y + 1, zz, 'minecraft:stone_brick_wall', up='true')
                self.ground[xx, zz] = y

    def approach(self) -> None:
        """Lay out the switchback ascent, kitchen stair and fenced lookout."""
        self.landing(45, 50, 53, 55, 1)
        self.flight((46, 2, 52), 9, 'north')
        self.landing(43, 49, 40, 43, 11)
        self.flight((42, 12, 40), 9, 'west')
        self.landing(29, 33, 40, 43, 21)
        self.flight((30, 22, 39), 5, 'north')
        self.landing(21, 29, 33, 36, 26)
        self.landing(30, 33, 33, 34, 26)
        self.landing(10, 20, 34, 37, 26)
        self.flight((11, 16, 47), 10, 'north')
        self.landing(10, 25, 48, 51, 15)
        self.landing(11, 14, 37, 37, 26)
        # Each final riser occupies the landing edge, so no full-block jump is needed.
        for x in range(46, 50):
            self.put(x, 11, 43, 'minecraft:stone_brick_stairs', facing='north', half='bottom', shape='straight')
        for z in range(40, 44):
            self.put(33, 21, z, 'minecraft:stone_brick_stairs', facing='west', half='bottom', shape='straight')
        for x in range(11, 15):
            self.put(x, 26, 37, 'minecraft:stone_brick_stairs', facing='north', half='bottom', shape='straight')
        # Railings remain outside all stair arrival and departure cells.
        for x in range(10, 26):
            self.put(x, 16, 51, 'minecraft:oak_fence')
        for z in range(48, 51):
            for x in (10, 25):
                self.put(x, 16, z, 'minecraft:oak_fence')
        for x in range(16, 26):
            self.put(x, 16, 48, 'minecraft:oak_fence')
        for x, z, y in [(10, 51, 16), (25, 51, 16), (25, 48, 16), (43, 43, 12), (49, 40, 12), (29, 43, 22), (29, 40, 22), (10, 34, 27), (20, 36, 27)]:
            self.post_light(x, y, z)

    def post_light(self, x: int, y: int, z: int) -> None:
        """Set a short timber lamp post on an existing supported path edge.

        Args:
            x, y, z (int): Base of the post.
        """
        if self.get(x, y - 1, z) in (None, 'minecraft:air'):
            self.box((x, 0, z, x, y - 1, z), 'minecraft:cobblestone')
        self.put(x, y, z, 'minecraft:stripped_spruce_log', axis='y')
        self.put(x, y + 1, z, 'minecraft:oak_fence')
        self.light(x, y + 2, z)

    def light(self, x: int, y: int, z: int, hanging: bool = False) -> None:
        """Place and record a warm lantern.

        Args:
            x, y, z (int): Lantern cell.
            hanging (bool): Whether supported from above.
        """
        self.put(x, y, z, 'minecraft:lantern', hanging=str(hanging).lower())
        self.lights.append((x, y, z))
        self.decorations.append(((x, y, z), (x, y + (1 if hanging else -1), z)))

    def timber(self, bounds: Tuple[int, int, int, int, int, int], axis: str = 'y') -> None:
        """Place exposed warm oak framing.

        Args:
            bounds (tuple): Inclusive XYZ box.
            axis (str): Log grain direction.
        """
        self.box(bounds, 'minecraft:stripped_spruce_log', axis=axis)

    def window(self, x: int, y: int, z: int, width: int, height: int, face: str) -> None:
        """Glaze an opening, add wooden sill, lintel, and side shutters.

        Args:
            x, y, z (int): Lower left window cell.
            width, height (int): Glazed opening dimensions.
            face (str): Outward wall face.
        """
        dx, dz = (1, 0) if face in ('north', 'south') else (0, 1)
        ox, oz = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}[face]
        for i in range(width):
            xx, zz = x + i * dx, z + i * dz
            for yy in range(y, y + height):
                self.put(xx, yy, zz, 'minecraft:yellow_stained_glass')
                self.windows.append(((xx, yy, zz), (ox, oz)))
            self.put(xx, y - 1, zz, 'minecraft:stripped_oak_log', axis='x' if dx else 'z')
            self.put(xx, y + height, zz, 'minecraft:dark_oak_planks')
            self.put(xx + ox, y - 1, zz + oz, 'minecraft:spruce_slab', type='top')
        for i in (-1, width):
            xx, zz = x + i * dx, z + i * dz
            self.timber((xx, y - 1, zz, xx, y + height, zz))
            for yy in range(y, y + height):
                self.put(xx + ox, yy, zz + oz, 'minecraft:spruce_trapdoor', facing=face, half='bottom', open='true')

    def door(self, x: int, y: int, z: int, face: str, hinge: str = 'left') -> None:
        """Place matching door halves and record their entry.

        Args:
            x, y, z (int): Lower door cell.
            face (str): Door facing.
            hinge (str): Hinge side.
        """
        for half, yy in [('lower', y), ('upper', y + 1)]:
            self.put(x, yy, z, 'minecraft:spruce_door', facing=face, half=half, hinge=hinge, open='false')
        self.doors.append((x, y, z))

    def house(self) -> None:
        """Build two framed floors, a connected vaulted kitchen, and glazed openings."""
        plaster = ['minecraft:smooth_sandstone'] * 9 + ['minecraft:cut_sandstone'] * 2 + ['minecraft:birch_planks']
        self.texture((19, 25, 13, 41, 26, 32), ['minecraft:stone_bricks'] * 5 + ['minecraft:mossy_stone_bricks', 'minecraft:andesite'])
        self.box((20, 27, 14, 40, 64, 31), 'minecraft:air')
        for y in range(27, 43):
            self.texture((19, y, 13, 41, y, 13), plaster)
            self.texture((19, y, 32, 41, y, 32), plaster)
            self.texture((19, y, 14, 19, y, 31), plaster)
            self.texture((41, y, 14, 41, y, 31), plaster)
        for y in (26, 34, 42):
            self.texture((20, y, 14, 40, y, 31), ['minecraft:spruce_planks'] * 11 + ['minecraft:dark_oak_planks'])
            self.timber((19, y, 13, 41, y, 13), 'x')
            self.timber((19, y, 32, 41, y, 32), 'x')
            self.timber((19, y, 14, 19, y, 31), 'z')
            self.timber((41, y, 14, 41, y, 31), 'z')
        for x in (19, 24, 30, 36, 41):
            for z in (13, 32):
                self.timber((x, 27, z, x, 42, z))
        for z in (13, 19, 25, 32):
            for x in (19, 41):
                self.timber((x, 27, z, x, 42, z))
        # Floor beams are exposed overhead and terminate at the load-bearing frame.
        for y in (33, 41):
            for z in (18, 25, 30):
                self.timber((20, y, z, 40, y, z), 'x')
        for x, y, width, height in [(32, 28, 3, 3), (21, 36, 2, 4), (26, 36, 3, 4), (32, 36, 3, 4), (38, 36, 2, 4)]:
            self.window(x, y, 32, width, height, 'south')
        for y in (28, 36):
            self.window(41, y, 15, 3, 3, 'east')
            self.window(41, y, 27, 3, 3, 'east')
            self.window(21, y, 13, 2, 3, 'north')
            self.window(27, y, 13, 3, 3, 'north')
            self.window(33, y, 13, 2, 3, 'north')
        self.window(19, 28, 15, 3, 3, 'west')
        self.window(19, 36, 15, 3, 3, 'west')
        self.window(19, 36, 27, 3, 3, 'west')
        self.door(26, 27, 32, 'south')
        self.door(27, 27, 32, 'south', 'right')
        self.box((26, 29, 32, 27, 30, 32), 'minecraft:yellow_stained_glass')
        self.timber((25, 27, 32, 25, 31, 32))
        self.timber((28, 27, 32, 28, 31, 32))
        # Lower, attached kitchen wing gives the left silhouette its second gable.
        self.texture((10, 25, 22, 18, 26, 33), ['minecraft:cobblestone', 'minecraft:stone_bricks'])
        self.box((11, 27, 23, 18, 42, 32), 'minecraft:air')
        for y in range(27, 35):
            self.texture((10, y, 22, 18, y, 22), plaster)
            self.texture((10, y, 33, 18, y, 33), plaster)
            self.texture((10, y, 23, 10, y, 32), plaster)
        self.box((11, 26, 23, 18, 26, 32), 'minecraft:spruce_planks')
        for x in (10, 14, 18):
            self.timber((x, 27, 33, x, 34, 33))
        self.timber((10, 27, 22, 10, 34, 22))
        self.timber((10, 34, 22, 18, 34, 22), 'x')
        self.timber((10, 34, 33, 18, 34, 33), 'x')
        self.timber((10, 34, 22, 10, 34, 33), 'z')
        self.window(11, 28, 33, 2, 3, 'south')
        self.window(10, 28, 27, 3, 3, 'west')
        self.door(16, 27, 33, 'south')
        self.box((19, 27, 26, 19, 30, 28), 'minecraft:air')
        self.timber((19, 31, 25, 19, 31, 29), 'z')
        # Covered front stoop.
        for x in (22, 29):
            self.timber((x, 27, 35, x, 31, 35))
        self.timber((22, 31, 35, 29, 31, 35), 'x')
        for z, y in [(33, 33), (34, 32), (35, 32), (36, 31)]:
            for x in range(21, 31):
                self.put(x, y - 1, z, 'minecraft:dark_oak_planks')
                self.put(x, y, z, 'minecraft:deepslate_tile_stairs', facing='north', half='bottom', shape='straight')
        self.light(23, 30, 35, True)
        self.light(28, 30, 35, True)

    def roof(self) -> None:
        """Create the high flared slate roof, timber gables, dormer and chimney."""
        heights = [64, 64, 62, 60, 58, 56, 54, 52, 50, 48, 46, 44, 43, 42, 42]
        slate = ['minecraft:deepslate_tiles'] * 9 + ['minecraft:deepslate_bricks'] * 3 + ['minecraft:cobbled_deepslate']
        plaster = ['minecraft:smooth_sandstone'] * 6 + ['minecraft:cut_sandstone']
        for x in range(19, 42):
            h = heights[abs(x - 30)]
            for z in (13, 32):
                self.texture((x, 43, z, x, h - 1, z), plaster)
                self.timber((x, h - 2, z, x, h - 1, z))
        self.timber((30, 43, 13, 30, 63, 13))
        self.timber((30, 43, 32, 30, 63, 32))
        self.timber((24, 48, 32, 36, 48, 32), 'x')
        self.timber((27, 55, 32, 33, 55, 32), 'x')
        self.window(27, 45, 32, 2, 5, 'south')
        self.window(32, 45, 32, 2, 5, 'south')
        self.window(29, 56, 32, 2, 3, 'south')
        self.window(27, 45, 13, 2, 4, 'north')
        self.window(32, 45, 13, 2, 4, 'north')
        for x in range(16, 45):
            d = abs(x - 30)
            h = heights[d]
            lower = heights[min(14, d + 1)] - 1
            for z in range(10, 36):
                self.texture((x, lower, z, x, h - 1, z), slate)
                self.put(x, h, z, self.rng.choice(['minecraft:deepslate_tile_stairs'] * 6 + ['minecraft:deepslate_brick_stairs']), facing='east' if x < 30 else 'west', half='bottom', shape='straight')
                if z in (10, 35):
                    self.put(x, h - 1, z, 'minecraft:polished_deepslate')
                    if d > 2 and d % 2 == 0:
                        self.put(x, h - 2, z, 'minecraft:deepslate_brick_wall', up='true')
        for z in range(10, 36):
            self.put(30, 65, z, 'minecraft:deepslate_tile_slab', type='bottom')
        for z in (10, 22, 35):
            self.box((30, 65, z, 30, 67, z), 'minecraft:dark_oak_fence')
            self.put(30, 68, z, 'minecraft:lightning_rod', facing='up')
        # Wing roof, kept outside the main interior.
        wing_h = [42, 41, 39, 37, 35, 34, 34]
        for x in range(8, 20):
            d = abs(x - 14)
            h = wing_h[d]
            low = wing_h[min(6, d + 1)] - 1
            if 10 <= x <= 18:
                for z in (22, 33):
                    if h > 35:
                        self.texture((x, 35, z, x, h - 1, z), plaster)
                        self.put(x, h - 1, z, 'minecraft:stripped_oak_log')
            for z in range(20, 36):
                self.texture((x, low, z, x, h - 1, z), slate)
                self.put(x, h, z, 'minecraft:deepslate_tile_stairs', facing='east' if x < 14 else 'west', half='bottom', shape='straight')
        self.window(13, 36, 33, 2, 2, 'south')
        self.timber((14, 40, 33, 14, 41, 33))
        # A glazed east dormer opens into the attic via two broad steps.
        self.box((36, 43, 20, 43, 50, 24), 'minecraft:air')
        self.box((38, 43, 20, 43, 44, 24), 'minecraft:spruce_planks')
        for x in range(37, 44):
            for z in (20, 24):
                self.texture((x, 45, z, x, 51, z), plaster)
        self.texture((43, 45, 20, 43, 52, 24), plaster)
        for z in range(20, 25):
            h = 55 - abs(z - 22)
            self.texture((43, 51, z, 43, h - 1, z), plaster)
            for x in range(36, 46):
                self.texture((x, h - 2, z, x, h - 1, z), slate)
                self.put(x, h, z, 'minecraft:deepslate_tile_stairs', facing='south' if z < 22 else 'north', half='bottom', shape='straight')
        # Close eaves and dormer cheeks into the original roof.
        for z in (19, 25):
            self.texture((37, 45, z, 43, 51, z), plaster)
            self.timber((37, 45, z, 43, 45, z), 'x')
            self.timber((37, 51, z, 43, 51, z), 'x')
            for x in (39, 43):
                self.timber((x, 46, z, x, 50, z))
            self.box((36, 51, z, 45, 52, z), 'minecraft:deepslate_tiles')
        for z in (20, 24):
            self.timber((43, 45, z, 43, 52, z))
        self.window(43, 47, 21, 3, 4, 'east')
        for x, y in [(36, 43), (37, 44)]:
            self.box((x, 42, 21, x, y - 1, 23), 'minecraft:spruce_planks')
            for z in range(21, 24):
                self.put(x, y, z, 'minecraft:spruce_stairs', facing='east', half='bottom', shape='straight')
                self.box((x, y + 1, z, x, y + 2, z), 'minecraft:air')
        self.rooms['dormer reading nook'] = (40, 45, 22)
        # Chimney grows from the kitchen hearth foundation; campfire makes real smoke.
        self.texture((11, 26, 20, 13, 54, 22), ['minecraft:stone_bricks'] * 8 + ['minecraft:cracked_stone_bricks', 'minecraft:andesite'])
        for y in (39, 49, 54):
            self.box((10, y, 19, 14, y, 23), 'minecraft:stone_bricks')
        self.box((11, 55, 20, 13, 55, 22), 'minecraft:polished_andesite')
        self.put(12, 56, 21, 'minecraft:campfire', lit='true', signal_fire='false', facing='north')
        for x in (11, 13):
            for z in (20, 22):
                self.put(x, 56, z, 'minecraft:cobblestone_wall', up='true')
        self.box((10, 57, 19, 14, 57, 23), 'minecraft:stone_brick_slab', type='bottom')

    def interior_stair(self, xs: range, start_z: int, floor: int, direction: int) -> None:
        """Carve and support a full-width eight-rise stair between house floors.

        Args:
            xs (range): Tread width coordinates.
            start_z (int): First tread depth.
            floor (int): Lower floor height.
            direction (int): Positive or negative Z ascent.
        """
        for i in range(8):
            z, y = start_z + direction * i, floor + i + 1
            for x in xs:
                if y > floor + 1:
                    self.box((x, floor + 1, z, x, y - 1, z), 'minecraft:spruce_planks')
                self.put(x, y, z, 'minecraft:spruce_stairs', facing='south' if direction > 0 else 'north', half='bottom', shape='straight')
                self.box((x, y + 1, z, x, max(y + 3, floor + 9), z), 'minecraft:air')
                self.routes.append((x, y + 1, z))

    def bed(self, x: int, y: int, z: int, facing: str = 'north') -> None:
        """Place a complete two-part bed.

        Args:
            x, y, z (int): Bed foot location.
            facing (str): Direction toward its head.
        """
        dx, dz = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}[facing]
        self.put(x, y, z, 'minecraft:red_bed', facing=facing, part='foot', occupied='false')
        self.put(x + dx, y, z + dz, 'minecraft:red_bed', facing=facing, part='head', occupied='false')
        self.beds.append((x, y, z))

    def chair(self, x: int, y: int, z: int, facing: str) -> None:
        """Set an oak stair chair on the room floor.

        Args:
            x, y, z (int): Seat block.
            facing (str): Chair block orientation.
        """
        self.put(x, y, z, 'minecraft:dark_oak_stairs', facing=facing, half='bottom', shape='straight')

    def table(self, x: int, y: int, z: int, width: int = 1, depth: int = 1) -> None:
        """Make a plank-topped table with joined fence legs.

        Args:
            x, y, z (int): First leg at floor level.
            width, depth (int): Table dimensions.
        """
        self.box((x, y, z, x + width - 1, y, z + depth - 1), 'minecraft:dark_oak_fence')
        self.box((x, y + 1, z, x + width - 1, y + 1, z + depth - 1), 'minecraft:spruce_slab', type='top')

    def furnish(self) -> None:
        """Furnish the kitchen, dining hall, bedrooms and attic map workshop."""
        self.interior_stair(range(37, 40), 27, 26, -1)
        self.interior_stair(range(21, 24), 18, 34, 1)
        # Stairwell guards leave the upper arrivals open.
        for z in range(20, 28):
            for x in (36, 40):
                self.put(x, 35, z, 'minecraft:spruce_fence')
        for x in range(37, 40):
            self.put(x, 35, 28, 'minecraft:spruce_fence')
        for z in range(18, 26):
            for x in (20, 24):
                if self.get(x, 43, z) in (None, 'minecraft:air'):
                    self.put(x, 43, z, 'minecraft:spruce_fence')
        for x in range(21, 24):
            self.put(x, 43, 17, 'minecraft:spruce_fence')
        self.rooms.update({
            'entry and dining hall': (27, 27, 29),
            'fireside sitting room': (33, 27, 28),
            'vaulted kitchen': (16, 27, 30),
            'bedchamber': (29, 35, 20),
            'upstairs study': (32, 35, 28),
            'attic map workshop': (30, 43, 24),
            'attic storage': (26, 43, 19),
        })
        # Ground floor dining table and chairs.
        self.table(27, 27, 20, 3, 2)
        for x in (27, 29):
            self.chair(x, 27, 18, 'north')
            self.chair(x, 27, 23, 'south')
        self.light(28, 29, 20)
        for x in range(25, 35):
            for z in range(27, 31):
                if (x, z) not in ((27, 29), (33, 28)):
                    self.put(x, 27, z, 'minecraft:brown_carpet' if x in (25, 34) or z in (27, 30) else 'minecraft:red_carpet')
        for x in (30, 31, 32, 33):
            self.chair(x, 27, 25, 'north')
        self.table(31, 27, 28, 2, 1)
        self.put(31, 29, 28, 'minecraft:potted_fern')
        self.decorations.append(((31, 29, 28), (31, 28, 28)))
        # A masonry fireplace against the north wall, away from all windows.
        self.box((36, 27, 14, 39, 30, 16), 'minecraft:stone_bricks')
        self.box((37, 27, 16, 38, 28, 16), 'minecraft:air')
        for x in (37, 38):
            self.put(x, 27, 15, 'minecraft:magma_block')
            self.put(x, 28, 15, 'minecraft:iron_bars', east='true', west='true')
        self.box((36, 30, 16, 39, 30, 16), 'minecraft:polished_andesite')
        self.put(36, 31, 16, 'minecraft:potted_azalea_bush')
        self.decorations.append(((36, 31, 16), (36, 30, 16)))
        self.box((21, 27, 20, 21, 29, 23), 'minecraft:bookshelf')
        for x, z in [(23, 15), (34, 15), (35, 30), (22, 30)]:
            self.put(x, 27, z, 'minecraft:barrel', facing='up')
            self.light(x, 28, z)
        self.put(23, 27, 17, 'minecraft:chest', facing='east', type='single')
        self.put(22, 27, 25, 'minecraft:crafting_table')
        # Kitchen counters, working ovens, washbasin, chopping block and pantry.
        for x, block in [(11, 'barrel'), (12, 'smoker'), (13, 'furnace'), (14, 'water_cauldron'), (15, 'crafting_table'), (16, 'barrel'), (17, 'barrel')]:
            props = {'level': '3'} if block == 'water_cauldron' else ({'facing': 'south'} if block in ('smoker', 'furnace', 'barrel') else {})
            self.put(x, 27, 23, 'minecraft:' + block, **props)
        self.put(12, 28, 23, 'minecraft:smoker', facing='south', lit='true')
        self.put(13, 28, 23, 'minecraft:furnace', facing='south', lit='true')
        self.light(17, 28, 23)
        self.box((17, 27, 24, 18, 29, 24), 'minecraft:barrel', facing='south')
        self.table(14, 27, 28, 2, 1)
        self.chair(14, 27, 30, 'south')
        self.timber((11, 33, 28, 18, 33, 28), 'x')
        self.light(14, 32, 28, True)
        self.put(15, 29, 28, 'minecraft:potted_red_mushroom')
        self.decorations.append(((15, 29, 28), (15, 28, 28)))
        # Upper floor has two broad beds, wardrobes, desk, and wash corner.
        for x in (25, 26, 31, 32):
            self.bed(x, 35, 17)
        self.box((24, 35, 15, 27, 36, 15), 'minecraft:dark_oak_planks')
        self.box((30, 35, 15, 33, 36, 15), 'minecraft:dark_oak_planks')
        for x in (24, 28, 30, 34):
            self.put(x, 35, 17, 'minecraft:barrel', facing='up')
            self.light(x, 36, 17)
        self.box((35, 35, 15, 36, 38, 16), 'minecraft:barrel', facing='south')
        self.box((27, 35, 22, 34, 35, 25), 'minecraft:orange_carpet')
        self.box((28, 35, 23, 33, 35, 24), 'minecraft:red_carpet')
        self.table(29, 35, 29, 4, 1)
        self.chair(30, 35, 27, 'north')
        self.light(29, 37, 29)
        self.put(32, 37, 29, 'minecraft:potted_dead_bush')
        self.decorations.append(((32, 37, 29), (32, 36, 29)))
        self.box((35, 35, 28, 35, 38, 30), 'minecraft:bookshelf')
        self.put(25, 35, 29, 'minecraft:water_cauldron', level='3')
        self.put(25, 35, 30, 'minecraft:barrel', facing='up')
        self.light(25, 36, 30)
        # Attic rafters and low hanging lamps reveal its tall, steep volume.
        for z in (16, 24, 30):
            self.timber((24, 52, z, 36, 52, z), 'x')
            for y in range(46, 52):
                self.put(30, y, z, 'minecraft:chain', axis='y')
            self.light(30, 45, z, True)
        self.box((25, 43, 17, 25, 46, 21), 'minecraft:bookshelf')
        self.box((34, 43, 16, 35, 44, 18), 'minecraft:barrel', facing='west')
        for x, block in [(28, 'cartography_table'), (29, 'crafting_table'), (31, 'loom'), (32, 'smithing_table')]:
            self.put(x, 43, 15, 'minecraft:' + block)
        self.table(28, 43, 20, 4, 1)
        self.chair(29, 43, 22, 'south')
        self.put(30, 43, 27, 'minecraft:lectern', facing='south', has_book='false')
        self.table(32, 43, 28, 2, 1)
        self.put(33, 45, 28, 'minecraft:potted_blue_orchid')
        self.decorations.append(((33, 45, 28), (33, 44, 28)))
        self.put(26, 43, 28, 'minecraft:chest', facing='east', type='single')
        self.put(26, 43, 29, 'minecraft:chest', facing='east', type='single')
        self.put(35, 45, 17, 'minecraft:brewing_stand')
        self.decorations.append(((35, 45, 17), (35, 44, 17)))
        self.chair(41, 45, 23, 'south')
        self.put(42, 45, 21, 'minecraft:barrel', facing='up')
        self.light(42, 46, 21)

    def exterior_details(self) -> None:
        """Add supply porch, timber braces, window flowers, stacked logs and ivy."""
        # Open side porch with a pitched slate canopy.
        self.landing(42, 47, 25, 33, 26)
        self.landing(34, 47, 33, 35, 26)
        for x, z in [(47, 25), (47, 32), (43, 32)]:
            self.timber((x, 27, z, x, 32, z))
        for x in range(42, 49):
            y = 34 - (x - 42) // 3
            self.box((x, y - 1, 24, x, y - 1, 33), 'minecraft:dark_oak_planks')
            for z in range(24, 34):
                self.put(x, y, z, 'minecraft:deepslate_tile_stairs', facing='west', half='bottom', shape='straight')
        self.timber((47, 31, 25, 47, 31, 32), 'z')
        self.light(47, 30, 27, True)
        self.light(43, 30, 32, True)
        self.put(43, 31, 32, 'minecraft:dark_oak_planks')
        self.door(41, 27, 23, 'east')
        self.landing(42, 44, 22, 24, 26)
        for x, z in [(44, 26), (45, 26), (44, 27), (46, 31)]:
            self.put(x, 27, z, 'minecraft:barrel', facing='south')
        self.put(44, 28, 26, 'minecraft:barrel', facing='south')
        self.box((45, 27, 28, 46, 28, 30), 'minecraft:oak_log', axis='z')
        for x in (44, 45, 46):
            self.put(x, 27, 33, 'minecraft:oak_fence')
        # Inset braces make the timber visible at the sides of plaster panels.
        for y in (33, 41):
            for x in (20, 23, 31, 35, 37, 40):
                self.put(x, y, 33, 'minecraft:oak_fence')
            for z in (14, 18, 26, 31):
                if self.get(42, y, z) in (None, 'minecraft:air'):
                    self.put(42, y, z, 'minecraft:oak_fence')
        # Window boxes are held by the sill and keep glazed cells unobstructed.
        for x in (32, 33, 34):
            self.put(x, 27, 33, 'minecraft:grass_block')
            self.put(x, 28, 33, 'minecraft:poppy' if x % 2 else 'minecraft:cornflower')
            self.decorations.append(((x, 28, 33), (x, 27, 33)))
        for x in (11, 12):
            self.put(x, 27, 34, 'minecraft:grass_block')
            self.put(x, 28, 34, 'minecraft:azure_bluet')
            self.decorations.append(((x, 28, 34), (x, 27, 34)))
        # Trailing leaves cling to the main posts; vines touch the wall behind.
        for x in (19, 24, 36):
            for y in range(29, 49 if x == 24 else 42):
                if self.rng.random() < .86 and self.get(x, y, 32) not in (None, 'minecraft:air'):
                    if self.get(x, y, 33) in (None, 'minecraft:air'):
                        self.put(x, y, 33, 'minecraft:vine', north='true')
                        self.decorations.append(((x, y, 33), (x, y, 32)))
        for x, y, z in [(20, 32, 34), (20, 35, 33), (23, 41, 33), (24, 44, 33), (35, 32, 34), (36, 35, 33), (19, 38, 31)]:
            if self.get(x, y, z) in (None, 'minecraft:air'):
                self.put(x, y, z, 'minecraft:oak_leaves', persistent='true', distance='1')
        # Landing supplies and supported flowers.
        for x, z in [(18, 35), (18, 36), (24, 35)]:
            self.put(x, 27, z, 'minecraft:barrel', facing='up')
        self.put(18, 28, 35, 'minecraft:potted_fern')
        self.decorations.append(((18, 28, 35), (18, 27, 35)))
        self.table(19, 16, 49, 2, 1)
        self.chair(18, 16, 49, 'west')
        self.chair(22, 16, 49, 'east')
        self.light(20, 18, 49)

    def rock_details(self) -> None:
        """Break sheer terrace foundations into buttresses, fractures and small ledges."""
        rock = ['minecraft:stone'] * 5 + ['minecraft:andesite'] * 3 + ['minecraft:cobblestone', 'minecraft:tuff']
        # These solid outcrops grow from the toe of each retaining face.
        for x, z, high in [(9, 50, 13), (12, 52, 12), (15, 52, 14), (19, 52, 11), (22, 52, 13), (25, 52, 9), (27, 49, 15), (28, 46, 18), (28, 42, 19), (35, 37, 22), (39, 37, 20), (43, 36, 24), (49, 35, 20), (50, 39, 12), (9, 37, 22)]:
            for dx in (-1, 0, 1):
                for dz in (-1, 0, 1):
                    xx, zz = x + dx, z + dz
                    top = high - abs(dx) * 2 - abs(dz) * 2 + self.rng.randint(-1, 1)
                    if (xx, zz) in self.reserved:
                        continue
                    for y in range(max(0, top + 1)):
                        if self.get(xx, y, zz) in (None, 'minecraft:air'):
                            self.put(xx, y, zz, self.rng.choice(rock))
                    if self.get(xx, top + 1, zz) in (None, 'minecraft:air'):
                        self.put(xx, top, zz, 'minecraft:moss_block' if self.rng.random() < .45 else 'minecraft:andesite')
                        self.ground[xx, zz] = max(self.ground.get((xx, zz), 0), top)
        # Short pale mineral seams and ledge stones add relief to the tallest faces.
        for x in range(12, 47, 3):
            for z in (36, 52):
                if (x, z) in self.reserved:
                    continue
                for y in range(3, 23):
                    if self.get(x, y, z) not in (None, 'minecraft:air') and self.get(x, y, z + 1) in (None, 'minecraft:air'):
                        if y % 7 < 5:
                            self.put(x, y, z, 'minecraft:andesite')
                        if y % 7 == 1 and (x, z + 1) not in self.reserved:
                            self.put(x, y, z + 1, 'minecraft:cobblestone')
                            self.put(x, y + 1, z + 1, 'minecraft:moss_carpet')
                            self.decorations.append(((x, y + 1, z + 1), (x, y, z + 1)))

    def tree(self, x: int, z: int, height: int, radius: int) -> None:
        """Grow an irregular tiered spruce from the rock surface.

        Args:
            x, z (int): Trunk ground position.
            height (int): Trunk height above terrain.
            radius (int): Widest branch radius.
        """
        base = self.ground[x, z]
        self.put(x, base, z, 'minecraft:rooted_dirt')
        self.box((x, base + 1, z, x, base + height, z), 'minecraft:spruce_log', axis='y')
        for dy in range(4, height + 3):
            frac = (height + 2 - dy) / height
            spread = max(0, round(radius * frac))
            if dy % 4 == 0:
                spread += 1
            elif dy % 4 == 2:
                spread = max(0, spread - 2)
            elif dy % 4 == 3:
                spread = max(0, spread - 1)
            if dy >= height:
                spread = 0 if dy == height + 2 else 1
            for dx in range(-spread, spread + 1):
                for dz in range(-spread, spread + 1):
                    if abs(dx) + abs(dz) > spread + max(0, spread // 2):
                        continue
                    if spread > 1 and abs(dx) == spread and abs(dz) == spread:
                        continue
                    xx, yy, zz = x + dx, base + dy, z + dz
                    if not self.inside(xx, yy, zz):
                        continue
                    if (xx, zz) in self.reserved:
                        continue
                    if 9 <= xx <= 45 and 10 <= zz <= 36 and yy >= 26:
                        continue
                    if self.get(xx, yy, zz) not in (None, 'minecraft:air'):
                        continue
                    if spread >= 2 and self.rng.random() < .13:
                        continue
                    self.put(xx, yy, zz, 'minecraft:spruce_leaves', persistent='true', distance='1')
            if spread >= 2 and dy % 4 == 0:
                for dx, dz, axis in [(1, 0, 'x'), (-1, 0, 'x'), (0, 1, 'z'), (0, -1, 'z')]:
                    xx, zz = x + dx, z + dz
                    if (xx, zz) not in self.reserved and not (9 <= xx <= 45 and 10 <= zz <= 36 and base + dy >= 26):
                        self.put(xx, base + dy, zz, 'minecraft:spruce_log', axis=axis)

    def landscape(self) -> None:
        """Dress the crag with conifers, cliff shrubs, ferns and scattered flowers."""
        for x, z, h, r in [(7, 19, 33, 5), (7, 30, 22, 4), (47, 17, 33, 5), (52, 31, 21, 4), (7, 43, 20, 4), (31, 49, 16, 4), (42, 49, 13, 3), (51, 44, 15, 3), (18, 7, 24, 4), (38, 7, 23, 4), (22, 54, 10, 3)]:
            if (x, z) in self.ground:
                self.tree(x, z, h, r)
        for (x, z), y in sorted(self.ground.items()):
            if (x, z) in self.reserved or (9 <= x <= 48 and 10 <= z <= 36):
                continue
            if self.get(x, y + 1, z) not in (None, 'minecraft:air'):
                continue
            r = self.rng.random()
            if r < .11:
                for dx, dz in [(0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)]:
                    xx, zz = x + dx, z + dz
                    if not self.inside(xx, y + 1, zz) or (xx, zz) in self.reserved:
                        continue
                    if self.get(xx, y, zz) not in (None, 'minecraft:air') and self.get(xx, y + 1, zz) in (None, 'minecraft:air'):
                        self.put(xx, y + 1, zz, 'minecraft:azalea_leaves' if r < .035 else 'minecraft:oak_leaves', persistent='true', distance='1')
                continue
            if r < .43:
                self.put(x, y, z, 'minecraft:grass_block')
                plant = self.rng.choice(['fern'] * 5 + ['short_grass'] * 5 + ['sweet_berry_bush', 'dandelion', 'azure_bluet', 'poppy', 'cornflower'])
                self.put(x, y + 1, z, 'minecraft:' + plant, **({'age': '3'} if plant == 'sweet_berry_bush' else {}))
                self.decorations.append(((x, y + 1, z), (x, y, z)))
            elif r < .47:
                self.put(x, y + 1, z, self.rng.choice(['minecraft:mossy_cobblestone', 'minecraft:andesite']))
        # Moss seams and hanging roots interrupt the exposed vertical rock faces.
        for (x, z), top in sorted(self.ground.items()):
            if top < 9 or (x, z) in self.reserved:
                continue
            for dx, dz, side in [(1, 0, 'west'), (-1, 0, 'east'), (0, 1, 'north'), (0, -1, 'south')]:
                if self.ground.get((x + dx, z + dz), 0) > top - 5 or self.rng.random() > .17:
                    continue
                length = self.rng.randint(2, 7)
                for y in range(top - length, top + 1):
                    if self.inside(x + dx, y, z + dz) and (x + dx, z + dz) not in self.reserved and self.get(x + dx, y, z + dz) in (None, 'minecraft:air'):
                        self.put(x + dx, y, z + dz, 'minecraft:vine', **{side: 'true'})
                        self.decorations.append(((x + dx, y, z + dz), (x, y, z)))

    def connect_fences(self) -> None:
        """Resolve rail connections explicitly for a schematic rendered without updates."""
        for (x, y, z), state in list(self.placed.items()):
            name = state.split('[')[0]
            if not (name.endswith('_fence') or name.endswith('_wall')):
                continue
            wall = name.endswith('_wall')
            props = {'up': 'true'} if wall else {}
            for face, dx, dz in [('east', 1, 0), ('west', -1, 0), ('south', 0, 1), ('north', 0, -1)]:
                neighbor = self.get(x + dx, y, z + dz)
                connect = neighbor is not None and neighbor != 'minecraft:air' and not any(k in neighbor for k in ('lantern', 'vine', 'leaves', 'glass', 'trapdoor'))
                props[face] = ('low' if connect else 'none') if wall else str(connect).lower()
            self.put(x, y, z, name, **props)

    def prune_unattached_foliage(self) -> None:
        """Remove random leaf tips that have no connected support back to the ground."""
        blocks = {p for p, state in self.placed.items() if state != 'minecraft:air'}
        connected = {p for p in blocks if p[1] == 0}
        queue = deque(connected)
        while queue:
            x, y, z = queue.popleft()
            for point in ((x - 1, y, z), (x + 1, y, z), (x, y - 1, z), (x, y + 1, z), (x, y, z - 1), (x, y, z + 1)):
                if point in blocks and point not in connected:
                    connected.add(point)
                    queue.append(point)
        for point in sorted(blocks - connected):
            assert '_leaves' in self.get(*point), f'Unattached structure at {point}'
            self.put(*point, 'minecraft:air')

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