# Lanternwatch cottage, a furnished reconstruction of reference.png.
# Bounding box: X 0..50, Y 0..61, Z 0..46 (51 x 62 x 47 blocks).
# Two cottage levels, one kitchen level, four stone tower rooms and an open lookout.
# Steep deepslate roofs, sandstone infill, exposed timber, terraced stone and conifers.
# Everything that has to be right for the harness is already right here: bounds checked placement,
# one seeded random source, and a byte reproducible export.
from __future__ import annotations

import argparse
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": (51, 62, 47), # width X, height Y, depth Z
    "minecraft_version": "1.21.1",
    "data_version": 3955,
    "output": "output.litematic",
    "name": "Lanternwatch Cottage",
    "author": "generator",
    "description": "Furnished timber cottage and stone watchtower, terraced woodland garden, Java 1.21.1",
}

Position = Tuple[int, int, int]


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def stair(self, x, y, z, block="stone_brick_stairs", facing="north", half="bottom"):
        """Place a stair with explicit geometry properties."""
        self.put(x, y, z, "minecraft:" + block, facing=facing, half=half, shape="straight", waterlogged="false")

    def slab(self, x, y, z, block="stone_brick_slab", kind="bottom"):
        """Place a slab with explicit height."""
        self.put(x, y, z, "minecraft:" + block, type=kind, waterlogged="false")

    def log(self, bounds, axis="y", block="stripped_spruce_log"):
        """Place a timber with the grain following its span."""
        self.box(bounds, "minecraft:" + block, axis=axis)

    def door(self, x, y, z, facing="south", hinge="left"):
        """Install both halves of a closed oak door."""
        for dy, half in enumerate(("lower", "upper")):
            self.put(x, y + dy, z, "minecraft:oak_door", facing=facing, hinge=hinge, half=half, open="false", powered="false")
        self.doors.append((x, y, z))

    def bed(self, x, y, z, facing="north", color="red"):
        """Install a supported bed with matching foot and head."""
        dx, dz = {"north": (0, -1), "south": (0, 1), "east": (1, 0), "west": (-1, 0)}[facing]
        for xx, zz, part in ((x, z, "foot"), (x + dx, z + dz, "head")):
            self.put(xx, y, zz, f"minecraft:{color}_bed", facing=facing, part=part, occupied="false")

    def lantern(self, x, y, z, hanging=False):
        """Place a lantern and register the lighting design."""
        self.put(x, y, z, "minecraft:lantern", hanging=str(hanging).lower(), waterlogged="false")

    def window(self, axis, fixed, a0, a1, y0, y1):
        """Glaze a framed opening and remember its exact cells."""
        cells = []
        for a in range(a0, a1 + 1):
            for y in range(y0, y1 + 1):
                x, z = (a, fixed) if axis == "z" else (fixed, a)
                self.put(x, y, z, "minecraft:yellow_stained_glass")
                cells.append((x, y, z))
        self.windows.append(cells)
        self.window_specs.append((axis, cells))

    def pot(self, x, y, z, plant="fern"):
        """Set a flower pot on a solid sill or table."""
        self.put(x, y, z, "minecraft:potted_" + plant)

    def terrain(self):
        """Build an irregular island, raised terrace and continuous front stair."""
        rock = ["minecraft:stone"] * 5 + ["minecraft:andesite"] * 2 + ["minecraft:mossy_cobblestone"]
        path = ["minecraft:stone_bricks"] * 5 + ["minecraft:andesite"] * 2 + ["minecraft:cobblestone", "minecraft:mossy_stone_bricks"]
        self.ground = {}
        for x in range(2, 49):
            for z in range(3, 46):
                r = ((x - 25) / 24)**2 + ((z - 24) / 22)**2
                if r > 1.0 + self.rng.uniform(-0.075, 0.075):
                    continue
                h = 1 if r > .79 else (2 if r > .60 else 3)
                for y in range(h):
                    self.put(x, y, z, self.rng.choice(rock) if y == 0 else "minecraft:dirt")
                self.put(x, h, z, self.rng.choice(["minecraft:grass_block"] * 6 + ["minecraft:moss_block", "minecraft:coarse_dirt", "minecraft:podzol"]))
                self.ground[x, z] = h
        # Raised paved terrace. Rough outer faces continue down to bedrock.
        for x in range(8, 45):
            for z in range(12, 38):
                if (x < 11 and z < 16) or (x > 41 and z > 34):
                    continue
                self.texture((x, 0, z, x, 5, z), rock)
                self.put(x, 6, z, self.rng.choice(path))
                self.ground[x, z] = 6
        # Low retaining wall and capped stone piers around the forecourt.
        for x in list(range(9, 28)) + list(range(34, 41)):
            self.put(x, 7, 37, self.rng.choice(path))
            self.slab(x, 8, 37)
        for x in (9, 15, 21, 27, 34, 40):
            self.texture((x, 7, 37, x, 9, 37), path)
            self.slab(x, 10, 37)
        for z in range(25, 37):
            self.put(8, 7, z, self.rng.choice(path))
            self.slab(8, 8, z)
        for z in (26, 31, 36):
            self.box((8, 7, z, 8, 9, z), "minecraft:stone_bricks")
            self.slab(8, 10, z)
        # Solid fill under each tread, one block rise per step.
        for z in range(39, 45):
            y = 45 - z
            for x in range(29, 33):
                self.box((x, 0, z, x, y - 1, z), "minecraft:stone_bricks")
                self.stair(x, y, z)
                self.ground[x, z] = y
                self.approach.append((x, y, z))
            for x in (28, 33):
                self.box((x, 0, z, x, y, z), "minecraft:cobblestone")
                self.slab(x, y + 1, z)
        for x in range(29, 33):
            self.box((x, 0, 38, x, 6, 38), "minecraft:stone_bricks")
        self.start = (30, 2, 45)
        self.box((28, 0, 45, 34, 1, 46), "minecraft:gravel")
        self.room_targets["front door landing"] = (28, 7, 34)
        for x, z in ((27, 37), (34, 37), (8, 31)):
            self.put(x, 10, z, "minecraft:chiseled_stone_bricks")
            self.lantern(x, 11, z)
        # Garden beds at the foot of the retaining work.
        for x, z in ((12, 39), (18, 39), (23, 39), (37, 39), (42, 34)):
            h = self.ground.get((x, z), 2)
            self.box((x, h, z, x + 1, h, z + 1), "minecraft:moss_block")
            for dx in (0, 1):
                self.put(x + dx, h + 1, z, "minecraft:azalea_leaves", persistent="true")
                self.put(x + dx, h + 1, z + 1, "minecraft:flowering_azalea_leaves", persistent="true")

    def cottage(self):
        """Construct the timber cottage, roof and lower east kitchen wing."""
        plaster = ["minecraft:smooth_sandstone"] * 9 + ["minecraft:sandstone", "minecraft:cut_sandstone"]
        stone = ["minecraft:stone_bricks"] * 6 + ["minecraft:andesite", "minecraft:mossy_stone_bricks"]
        self.texture((20, 4, 15, 36, 6, 32), stone)
        self.box((21, 6, 16, 35, 6, 31), "minecraft:spruce_planks")
        self.box((20, 14, 15, 36, 14, 32), "minecraft:spruce_planks")
        for z in (15, 32):
            self.texture((20, 7, z, 36, 20, z), plaster)
        for x in (20, 36):
            self.texture((x, 7, 15, x, 20, 32), plaster)
        for x in (20, 25, 31, 36):
            for z in (15, 32):
                self.log((x, 7, z, x, 20, z))
        for x in (20, 36):
            for z in (20, 26):
                self.log((x, 7, z, x, 20, z))
        for y in (7, 14, 20):
            for z in (15, 32):
                self.log((20, y, z, 36, y, z), "x")
            for x in (20, 36):
                self.log((x, y, 15, x, y, 32), "z")
        # Ground-floor front and side windows.
        for a, b in ((22, 23), (33, 34)):
            self.window("z", 32, a, b, 9, 11)
            for x in range(a, b + 1):
                self.stair(x, 8, 33, "oak_stairs", "north", "top")
                self.slab(x, 12, 33, "spruce_slab")
        for z in (17, 28):
            self.window("x", 36, z, z + 1, 9, 11)
        self.window("z", 15, 23, 25, 9, 11)
        self.window("z", 15, 32, 34, 9, 11)
        # Tall, amber-lit, divided front-gable window.
        self.window("z", 32, 26, 27, 16, 19)
        self.window("z", 32, 29, 30, 16, 19)
        self.log((28, 15, 32, 28, 29, 32))
        self.window("z", 15, 26, 30, 17, 19)
        self.window("x", 36, 16, 17, 16, 18)
        self.door(28, 7, 32)
        self.window("z", 32, 27, 29, 10, 11)
        self.log((26, 7, 33, 26, 11, 33), block="oak_log")
        self.log((30, 7, 33, 30, 11, 33), block="oak_log")
        self.log((26, 12, 33, 30, 12, 33), "x", "oak_log")
        for x in range(25, 32):
            for z in (33, 34):
                self.stair(x, 13, z, "dark_oak_stairs", "north")
        self.put(26, 12, 34, "minecraft:chain", axis="y")
        self.put(30, 12, 34, "minecraft:chain", axis="y")
        self.lantern(26, 11, 34, True)
        self.lantern(30, 11, 34, True)
        # Curving, steep deepslate roof, with a solid underlayer at every rise.
        heights = {0: 32, 1: 31, 2: 30, 3: 28, 4: 26, 5: 24, 6: 23, 7: 22, 8: 21, 9: 20, 10: 20}
        self.roof_heights = heights
        for x in range(18, 39):
            d = abs(x - 28)
            h = heights[d]
            outer_h = heights[min(10, d + 1)]
            for z in range(13, 35):
                for y in range(outer_h, h + 1):
                    self.put(x, y, z, self.rng.choice(["minecraft:deepslate_tiles"] * 7 + ["minecraft:deepslate_bricks", "minecraft:cobbled_deepslate"]))
                if d:
                    material = "cobbled_deepslate_stairs" if z in (13, 34) else self.rng.choice(["deepslate_tile_stairs"] * 5 + ["deepslate_brick_stairs"])
                    self.stair(x, h + 1, z, material, "east" if x < 28 else "west")
                else:
                    self.slab(x, h + 1, z, "deepslate_tile_slab")
        # Plaster gables meet the roof exactly; stepped oak braces follow the pitch.
        for z in (15, 32):
            for x in range(20, 37):
                top = heights[abs(x - 28)] - 1
                if top >= 21:
                    self.texture((x, 21, z, x, top, z), plaster)
                if top >= 22:
                    self.put(x, top, z, "minecraft:stripped_oak_log", axis="x")
            self.log((28, 20, z, 28, 31, z))
            self.log((23, 22, z, 33, 22, z), "x")
            for x in (25, 31):
                self.log((x, 20, z, x, 26, z))
        for z in (13, 34):
            self.log((28, 32, z, 28, 34, z), block="oak_log")
        # An enclosed dormer lights the tall upper chamber.
        self.box((32, 23, 21, 37, 26, 25), "minecraft:smooth_sandstone")
        self.box((32, 23, 22, 36, 25, 24), "minecraft:air")
        self.window("x", 37, 22, 24, 24, 25)
        for z in (21, 25):
            self.log((37, 23, z, 37, 26, z))
        for z in range(20, 27):
            y = 29 - abs(z - 23)
            self.box((32, y - 1, z, 38, y, z), "minecraft:deepslate_tiles")
            for x in range(32, 39):
                self.stair(x, y + 1, z, "deepslate_tile_stairs", "south" if z < 23 else "north")
        self.box((37, 26, 22, 37, 28, 24), "minecraft:smooth_sandstone")
        self.log((37, 26, 23, 37, 28, 23))
        # A low, overlapping gable covers a kitchen and east porch.
        self.texture((37, 3, 19, 43, 6, 31), stone)
        self.box((37, 6, 20, 42, 6, 30), "minecraft:oak_planks")
        for z in (19, 31):
            self.texture((37, 7, z, 43, 14, z), plaster)
        self.texture((43, 7, 19, 43, 14, 31), plaster)
        for x in (37, 43):
            for z in (19, 31):
                self.log((x, 7, z, x, 14, z))
        for z in (19, 31):
            self.log((37, 14, z, 43, 14, z), "x")
        self.log((43, 14, 19, 43, 14, 31), "z")
        self.window("z", 31, 39, 41, 9, 11)
        self.window("x", 43, 21, 23, 9, 11)
        self.window("x", 43, 28, 29, 9, 11)
        self.door(36, 7, 25, "east")
        self.door(43, 7, 26, "east")
        for x in range(35, 46):
            h = 20 - abs(x - 39)
            for z in range(18, 33):
                # Preserve the taller main roof where the two roofs overlap.
                if x <= 36 and self.get(x, h, z) and "deepslate" in self.get(x, h, z):
                    continue
                self.put(x, h, z, self.rng.choice(["minecraft:deepslate_tiles"] * 5 + ["minecraft:deepslate_bricks"]))
                self.stair(x, h + 1, z, "deepslate_tile_stairs", "east" if x < 39 else "west")
            if 37 <= x <= 43:
                for z in (19, 31):
                    self.texture((x, 15, z, x, h - 1, z), plaster)
                    self.put(x, h - 1, z, "minecraft:stripped_spruce_log", axis="x")
        # Porch ties back into the courtyard at floor height.
        self.box((44, 0, 24, 46, 5, 33), "minecraft:stone_bricks")
        self.box((44, 6, 24, 46, 6, 33), "minecraft:spruce_planks")
        self.box((42, 0, 32, 46, 5, 35), "minecraft:stone_bricks")
        self.box((42, 6, 32, 46, 6, 35), "minecraft:stone_bricks")
        for z in (24, 32):
            self.log((45, 7, z, 45, 14, z), block="oak_log")
            self.lantern(44, 13, z, True)
            self.put(44, 14, z, "minecraft:oak_planks")
        # Hanging golden roadside pennant.
        self.log((43, 12, 29, 48, 12, 29), "x", "oak_log")
        self.box((47, 9, 29, 47, 11, 29), "minecraft:yellow_wool")
        self.put(47, 10, 29, "minecraft:orange_wool")
        self.put(47, 8, 29, "minecraft:orange_wool")
        self.room_targets["hearth hall"] = (28, 7, 29)
        self.room_targets["upper sleeping chamber"] = (28, 15, 28)
        self.room_targets["kitchen wing"] = (40, 7, 25)
        self.room_targets["east porch"] = (44, 7, 26)

    def tower(self):
        """Raise a stone watchtower with four rooms and a timber lookout."""
        stone = ["minecraft:stone_bricks"] * 6 + ["minecraft:andesite"] * 2 + ["minecraft:cobblestone"]
        for y in range(6, 41):
            for x in range(11, 21):
                for z in range(15, 25):
                    if x in (11, 20) or z in (15, 24):
                        self.put(x, y, z, self.rng.choice(stone))
        # Masonry corner piers and thin floor bands.
        for x in (11, 20):
            for z in (15, 24):
                self.box((x, 6, z, x, 40, z), "minecraft:stone_bricks")
        for y in (6, 14, 23, 32, 41):
            self.box((11, y, 15, 20, y, 24), "minecraft:spruce_planks")
            for x in range(11, 21):
                for z in (15, 24):
                    self.put(x, y, z, "minecraft:stone_bricks")
            for z in range(15, 25):
                for x in (11, 20):
                    self.put(x, y, z, "minecraft:stone_bricks")
        for y in (17, 26, 35):
            self.window("z", 24, 15, 16, y, y + 2)
            self.window("x", 11, 20, 21, y, y + 2)
            self.window("z", 15, 15, 16, y, y + 2)
            self.window("x", 20, 19, 19, y, y + 2)
            for x in (15, 16):
                self.stair(x, y - 1, 25, "stone_brick_stairs", "north", "top")
                self.slab(x, y + 3, 25)
            for z in (20, 21):
                self.stair(10, y - 1, z, "stone_brick_stairs", "east", "top")
        self.window("z", 24, 14, 15, 9, 11)
        for y in (7, 15):
            self.door(20, y, 21, "east")
        # Ladder shaft remains clear through every deck, capped by a hatch.
        for y in range(7, 42):
            self.put(12, y, 17, "minecraft:ladder", facing="east", waterlogged="false")
        self.put(12, 42, 17, "minecraft:spruce_trapdoor", facing="west", half="bottom", open="false", powered="false", waterlogged="false")
        self.ladder_column = (12, 17, 7, 41)
        # Heavy cantilever brackets and a full, walkable balcony deck.
        for x in (10, 21):
            for z in (15, 19, 24):
                self.stair(x, 38, z, "stone_brick_stairs", "east" if x == 10 else "west", "top")
                self.put(x, 39, z, "minecraft:stone_bricks")
                self.stair(x + (-1 if x == 10 else 1), 40, z, "oak_stairs", "east" if x == 10 else "west", "top")
        for z in (14, 25):
            for x in (11, 15, 20):
                self.stair(x, 39, z, "stone_brick_stairs", "south" if z == 14 else "north", "top")
        for x in range(8, 24):
            for z in range(12, 28):
                if (x, z) != (12, 17):
                    self.put(x, 41, z, "minecraft:oak_planks")
        for x in range(8, 24):
            for z in (12, 27):
                self.put(x, 42, z, "minecraft:oak_fence", east="true", west="true")
        for z in range(13, 27):
            for x in (8, 23):
                self.put(x, 42, z, "minecraft:oak_fence", north="true", south="true")
        for x in (10, 21):
            for z in (14, 25):
                self.log((x, 42, z, x, 47, z), block="stripped_oak_log")
                self.stair(x, 46, z + (1 if z == 14 else -1), "oak_stairs", "south" if z == 14 else "north", "top")
        for z in (14, 25):
            self.log((10, 47, z, 21, 47, z), "x", "oak_log")
            self.log((15, 42, z, 15, 47, z), block="stripped_oak_log")
        for x in (10, 21):
            self.log((x, 47, 14, x, 47, 25), "z", "oak_log")
        self.box((9, 48, 13, 22, 48, 26), "minecraft:spruce_planks")
        self.lantern(15, 46, 20, True)
        self.put(15, 47, 20, "minecraft:oak_planks")
        self.box((18, 42, 19, 19, 42, 20), "minecraft:cartography_table")
        self.put(18, 43, 19, "minecraft:lantern", hanging="false")
        self.put(11, 42, 22, "minecraft:barrel", facing="up")
        # Square hipped roof with flared eaves, tiny summit and brass flagpole.
        for k in range(8):
            xmin, xmax, zmin, zmax = 7 + k, 24 - k, 11 + k, 28 - k
            y = 49 + k
            self.box((xmin, y, zmin, xmax, y, zmax), "minecraft:deepslate_tiles")
            for x in range(xmin, xmax + 1):
                self.stair(x, y, zmin, "cobbled_deepslate_stairs", "south")
                self.stair(x, y, zmax, "cobbled_deepslate_stairs", "north")
            for z in range(zmin + 1, zmax):
                self.stair(xmin, y, z, "deepslate_tile_stairs", "east")
                self.stair(xmax, y, z, "deepslate_tile_stairs", "west")
        self.box((15, 57, 19, 16, 57, 20), "minecraft:deepslate_tiles")
        self.put(15, 58, 19, "minecraft:deepslate_tile_wall", up="true")
        self.put(15, 59, 19, "minecraft:iron_bars")
        self.box((22, 50, 19, 22, 59, 19), "minecraft:oak_fence")
        self.put(22, 60, 19, "minecraft:lantern", hanging="false")
        for x in range(23, 28):
            for y in (57, 58, 59):
                if not (x == 27 and y == 58):
                    self.put(x, y, 19, "minecraft:yellow_wool" if (x + y) % 3 else "minecraft:orange_wool")
        for name, y in (("tower pantry", 7), ("tower archive", 15), ("watchkeeper bunk", 24), ("map room", 33), ("open lookout", 42)):
            self.room_targets[name] = (14, y, 20)

    def interiors(self):
        """Furnish every room and install the cottage stair and tower ladder landings."""
        # Eight-step, two-wide flight along the west hall, with a clear landing.
        for i in range(8):
            y, z = 7 + i, 29 - i
            for x in (22, 23):
                self.box((x, 7, z, x, y, z), "minecraft:spruce_planks")
                self.stair(x, y, z, "spruce_stairs", "north")
                self.box((x, y + 1, z, x, max(16, y + 3), z), "minecraft:air")
                self.interior_stairs.append((x, y, z))
        for x in (22, 23):
            self.box((x, 14, 21, x, 14, 21), "minecraft:spruce_planks")
            self.box((x, 15, 21, x, 17, 21), "minecraft:air")
        # Rail along the stairwell; entry remains free at each end.
        for z in range(24, 30):
            self.put(24, 15, z, "minecraft:spruce_fence", north="true", south="true")
        # Ground floor hearth, cushioned seating and long dining table.
        self.box((27, 7, 16, 30, 10, 17), "minecraft:stone_bricks")
        self.box((28, 7, 17, 29, 8, 17), "minecraft:air")
        for x in (28, 29):
            self.put(x, 7, 16, "minecraft:shroomlight")
            self.put(x, 7, 17, "minecraft:iron_bars", east="true", west="true")
        self.log((27, 10, 18, 30, 10, 18), "x", "dark_oak_log")
        self.pot(27, 11, 18, "dead_bush")
        self.lantern(30, 11, 18)
        self.box((30, 7, 23, 30, 7, 25), "minecraft:oak_planks")
        for z in (23, 25):
            self.stair(29, 7, z, "spruce_stairs", "east")
            self.stair(31, 7, z, "spruce_stairs", "west")
        self.pot(30, 8, 24, "poppy")
        self.lantern(30, 8, 25)
        for z in range(25, 29):
            self.put(34, 7, z, "minecraft:bookshelf")
        self.stair(32, 7, 29, "oak_stairs", "north")
        self.put(33, 7, 29, "minecraft:barrel", facing="up")
        self.lantern(33, 8, 29)
        self.box((26, 7, 26, 28, 7, 28), "minecraft:orange_carpet")
        self.put(21, 7, 18, "minecraft:chest", facing="east", type="single")
        self.put(21, 7, 19, "minecraft:crafting_table")
        # Ceiling beams and low hanging lights stay above all walking headroom.
        for z in (20, 28):
            self.log((24, 13, z, 35, 13, z), "x")
        for x, z in ((25, 21), (32, 28)):
            self.put(x, 13, z, "minecraft:spruce_planks")
            self.put(x, 12, z, "minecraft:chain", axis="y")
            self.lantern(x, 11, z, True)
        # Fireside bench and a crockery sideboard make the hall a lived-in room.
        for x in (25, 26):
            self.stair(x, 7, 20, "dark_oak_stairs", "north")
        self.put(24, 7, 20, "minecraft:dark_oak_planks")
        self.put(27, 7, 20, "minecraft:dark_oak_planks")
        self.put(34, 7, 20, "minecraft:barrel", facing="up")
        self.put(33, 7, 20, "minecraft:crafting_table")
        self.pot(33, 8, 20, "fern")
        self.lantern(34, 8, 20)
        # Upper chamber includes two beds, wardrobes, washstand and writing desk.
        self.bed(33, 15, 19, "north", "red")
        self.bed(34, 15, 19, "north", "red")
        self.put(32, 15, 18, "minecraft:barrel", facing="up")
        self.lantern(32, 16, 18)
        self.box((26, 15, 16, 27, 16, 16), "minecraft:bookshelf")
        self.put(29, 15, 16, "minecraft:chest", facing="south", type="single")
        self.put(30, 15, 16, "minecraft:chest", facing="south", type="single")
        self.box((33, 15, 30, 34, 15, 30), "minecraft:spruce_planks")
        self.put(33, 16, 30, "minecraft:lectern", facing="north", has_book="false", powered="false")
        self.lantern(34, 16, 30)
        self.stair(33, 15, 28, "spruce_stairs", "south")
        self.put(21, 15, 30, "minecraft:water_cauldron", level="3")
        self.put(22, 15, 30, "minecraft:barrel", facing="up")
        self.pot(22, 16, 30, "blue_orchid")
        self.box((27, 15, 25, 30, 15, 28), "minecraft:red_carpet")
        self.box((28, 20, 24, 28, 21, 24), "minecraft:chain", axis="y")
        self.log((20, 22, 24, 36, 22, 24), "x", "oak_log")
        self.lantern(28, 19, 24, True)
        # Bookshelves screen the bed alcove, with a reading bench beside the rug.
        self.box((29, 15, 21, 30, 16, 21), "minecraft:bookshelf")
        self.pot(29, 17, 21, "fern")
        self.lantern(30, 17, 21)
        for z in (27, 28, 29):
            self.stair(26, 15, z, "spruce_stairs", "east")
        self.put(26, 15, 30, "minecraft:barrel", facing="up")
        self.lantern(26, 16, 30)
        self.box((21, 15, 28, 21, 17, 29), "minecraft:barrel", facing="east")
        self.put(21, 15, 18, "minecraft:barrel", facing="up")
        self.lantern(21, 16, 18)
        # Kitchen: cooking range, pantry, preparation table and wash basin.
        for z in (20, 21):
            self.put(37, 7, z, "minecraft:smoker", facing="east", lit="true")
        self.box((37, 8, 20, 37, 13, 21), "minecraft:bricks")
        self.put(38, 7, 20, "minecraft:crafting_table")
        self.put(39, 7, 20, "minecraft:barrel", facing="up")
        self.put(40, 7, 20, "minecraft:water_cauldron", level="3")
        self.lantern(39, 8, 20)
        self.box((38, 7, 29, 40, 7, 29), "minecraft:oak_planks")
        self.pot(38, 8, 29, "dandelion")
        self.put(40, 8, 29, "minecraft:sea_pickle", pickles="2", waterlogged="false")
        self.stair(39, 7, 27, "oak_stairs", "south")
        self.put(42, 7, 30, "minecraft:barrel", facing="north")
        self.put(42, 8, 30, "minecraft:barrel", facing="north")
        self.lantern(42, 9, 30)
        # Slender masonry chimney behind the ridge.
        self.box((31, 15, 16, 32, 34, 17), "minecraft:stone_bricks")
        self.box((30, 35, 15, 33, 35, 18), "minecraft:stone_brick_slab", type="bottom")
        self.put(31, 36, 16, "minecraft:cobblestone_wall", up="true")
        # Tower fittings leave the west ladder and central route unoccupied.
        for y in (7, 15, 24, 33):
            self.put(18, y, 22, "minecraft:barrel", facing="up")
            self.lantern(18, y + 1, 22)
            self.put(13, y, 22, "minecraft:barrel", facing="up")
            self.lantern(13, y + 1, 22)
        for x in (14, 16, 18):
            self.put(x, 7, 16, "minecraft:barrel", facing="south")
            self.put(x, 8, 16, "minecraft:barrel", facing="south")
        self.put(18, 7, 19, "minecraft:crafting_table")
        self.put(17, 7, 19, "minecraft:water_cauldron", level="3")
        self.put(14, 7, 22, "minecraft:chest", facing="north", type="single")
        self.put(18, 7, 18, "minecraft:brewing_stand", has_bottle_0="false", has_bottle_1="false", has_bottle_2="false")
        self.box((14, 15, 16, 18, 16, 16), "minecraft:bookshelf")
        self.put(17, 15, 19, "minecraft:lectern", facing="west", has_book="false")
        self.stair(16, 15, 21, "oak_stairs", "north")
        self.box((19, 15, 18, 19, 18, 18), "minecraft:bookshelf")
        self.put(14, 24, 18, "minecraft:oak_planks")
        self.pot(14, 25, 18, "dead_bush")
        self.stair(14, 24, 19, "oak_stairs", "north")
        self.bed(17, 24, 18, "north", "green")
        self.bed(18, 24, 18, "north", "green")
        self.put(14, 24, 22, "minecraft:chest", facing="north", type="single")
        self.box((16, 33, 19, 18, 33, 20), "minecraft:cartography_table")
        self.stair(15, 33, 20, "spruce_stairs", "east")
        self.put(18, 33, 16, "minecraft:bookshelf")
        self.put(18, 34, 16, "minecraft:bookshelf")

    def fir(self, x, z, height, radius):
        """Grow a tapered conifer with hanging, irregular branch tiers."""
        ground = self.ground.get((x, z), 1)
        base = ground + 1
        self.log((x, base, z, x, base + height, z), block="spruce_log")
        for level in range(3, height + 1, 3):
            r = max(1, round(radius * (1 - level / (height + 3))))
            for dx in range(-r, r + 1):
                for dz in range(-r, r + 1):
                    if abs(dx) + abs(dz) > r + 1 or (dx == dz == 0):
                        continue
                    if abs(dx) == r and abs(dz) == r and self.rng.random() < .65:
                        continue
                    for dy in (-1, 0):
                        if dy == -1 and abs(dx) + abs(dz) < r:
                            continue
                        if abs(dx) > 0 and abs(dz) > 0 and self.rng.random() < .33:
                            continue
                        pos = (x + dx, base + level + dy, z + dz)
                        if self.inside(*pos) and not self.get(*pos):
                            self.put(*pos, "minecraft:spruce_leaves", persistent="true", distance="1")
        for y in range(base + height - 2, base + height + 3):
            self.put(x, y, z, "minecraft:spruce_leaves", persistent="true", distance="1")
        self.trees.append((x, z, base, height))

    def gardens(self):
        """Dress the site with tall conifers, groundcover, climbing ivy and props."""
        for x, z, height, radius in ((7, 17, 26, 4), (6, 29, 19, 4), (8, 8, 25, 4), (31, 7, 36, 5), (41, 11, 29, 4), (46, 18, 24, 3), (5, 37, 12, 3), (43, 39, 13, 3)):
            self.fir(x, z, height, radius)
        flowers = ["fern"] * 5 + ["short_grass"] * 4 + ["dandelion", "poppy", "oxeye_daisy", "azure_bluet"]
        for (x, z), h in self.ground.items():
            if h >= 6 or self.get(x, h + 1, z) or (self.get(x, h, z) or "").split("[")[0] not in {"minecraft:grass_block", "minecraft:podzol", "minecraft:coarse_dirt", "minecraft:moss_block"}:
                continue
            if self.rng.random() < .26:
                self.put(x, h + 1, z, "minecraft:" + self.rng.choice(flowers))
        # Leaf vines hug masonry and frame rather than hovering in space.
        for x, z, top in ((20, 33, 16), (35, 33, 12), (10, 24, 20), (44, 30, 11)):
            for y in range(7, top + 1):
                if self.rng.random() < .86:
                    if not self.get(x, y, z):
                        self.put(x, y, z, "minecraft:oak_leaves", persistent="true", distance="1")
                if y % 3 == 0 and not self.get(x - 1, y, z):
                    self.put(x - 1, y, z, "minecraft:oak_leaves", persistent="true", distance="1")
        for x, z in ((10, 32), (16, 33), (38, 34), (42, 32)):
            self.put(x, 7, z, "minecraft:barrel", facing="up")
            self.pot(x, 8, z, "fern" if x % 2 else "azalea_bush")
        for x in (11, 12, 13):
            self.stair(x, 7, 34, "spruce_stairs", "north")
        self.box((15, 7, 28, 16, 7, 29), "minecraft:hay_block", axis="y")
        self.put(15, 8, 28, "minecraft:hay_block", axis="y")
        self.put(17, 7, 28, "minecraft:barrel", facing="south")
        self.put(17, 8, 28, "minecraft:barrel", facing="south")
        self.lantern(17, 9, 28)
        # Front planters are supported on stone plinths.
        for x, z in ((23, 34), (33, 34), (38, 32)):
            self.put(x, 7, z, "minecraft:moss_block")
            self.put(x, 8, z, "minecraft:flowering_azalea")

    def detailing(self):
        """Add joinery, warm window trim, wall weathering and garden furniture."""
        # Projecting carved braces in the front gable.
        for x, y in ((21, 16), (22, 17), (23, 18), (24, 19), (32, 19), (33, 18), (34, 17), (35, 16)):
            self.put(x, y, 33, "minecraft:stripped_oak_log", axis="x")
            self.stair(x, y - 1, 33, "oak_stairs", "east" if x < 28 else "west", "top")
        for x in (20, 25, 31, 36):
            self.stair(x, 13, 33, "spruce_stairs", "north", "top")
        # Window-box sill and shutters on the broad upper window.
        for x in range(25, 32):
            self.stair(x, 15, 33, "oak_stairs", "north", "top")
        for x in (25, 31):
            self.pot(x, 16, 33, "fern")
        for x in (25, 31):
            for y in (17, 18, 19):
                self.put(x, y, 33, "minecraft:oak_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false")
        for x in (21, 24, 32, 35):
            for y in (9, 10, 11):
                self.put(x, y, 33, "minecraft:spruce_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false")
        # Warm sconces on front corner beams and next to the wing entry.
        for x in (24, 32):
            self.put(x, 11, 34, "minecraft:oak_fence")
            self.put(x, 11, 33, "minecraft:oak_planks")
            self.lantern(x, 10, 34, True)
        for y in (18, 27, 36):
            for x in (14, 17):
                for yy in (y, y + 1):
                    self.put(x, yy, 25, "minecraft:oak_trapdoor", facing="south", half="bottom", open="true", powered="false", waterlogged="false")
        # Timber fascia gives the stone tower a strongly expressed wooden crown.
        self.log((8, 41, 27, 23, 41, 27), "x", "stripped_oak_log")
        self.log((8, 41, 12, 23, 41, 12), "x", "stripped_oak_log")
        for x in (8, 23):
            self.log((x, 41, 13, x, 41, 26), "z", "stripped_oak_log")
        for x in (11, 15, 20):
            self.log((x, 38, 25, x, 40, 25), block="stripped_oak_log")
            self.stair(x, 40, 26, "oak_stairs", "north", "top")
        for z in (15, 19, 24):
            self.log((10, 38, z, 10, 40, z), block="stripped_oak_log")
        # Damp masonry and buttresses interrupt the retaining wall's broad face.
        for x in range(9, 41):
            if 28 <= x <= 33:
                continue
            for y in range(2, 7):
                if self.rng.random() < .31:
                    self.put(x, y, 37, self.rng.choice(["minecraft:mossy_stone_bricks", "minecraft:cobblestone", "minecraft:andesite"]))
        for x in (10, 18, 25, 36, 40):
            self.texture((x, 1, 38, x, 5, 38), ["minecraft:stone_bricks", "minecraft:mossy_cobblestone", "minecraft:andesite"])
            self.stair(x, 6, 38, "stone_brick_stairs", "north")
        # A barrel beside a small workbench, and split logs below the tower window.
        self.put(13, 7, 27, "minecraft:crafting_table")
        self.put(12, 7, 27, "minecraft:barrel", facing="up")
        self.lantern(12, 8, 27)
        self.log((10, 7, 28, 10, 7, 30), "z", "spruce_log")
        self.log((11, 7, 28, 11, 7, 30), "z", "spruce_log")
        self.log((10, 8, 28, 10, 8, 30), "z", "spruce_log")

    def prune_detached_leaves(self):
        """Remove decorative leaf fragments that do not join any supported geometry."""
        remaining = {p for p, state in self.placed.items() if state != "minecraft:air"}
        while remaining:
            seed = min(remaining)
            component, pending = {seed}, deque([seed])
            remaining.remove(seed)
            while pending:
                x, y, z = pending.popleft()
                for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)):
                    p = (x + dx, y + dy, z + dz)
                    if p in remaining:
                        remaining.remove(p)
                        component.add(p)
                        pending.append(p)
            if all("_leaves" in self.get(*p) for p in component):
                for p in sorted(component):
                    self.put(*p, "minecraft:air")

    def connect_fences(self):
        """Join rail corners and wooden brackets using their actual neighbours."""
        for (x, y, z), state in list(self.placed.items()):
            name = state.split("[")[0]
            if not name.endswith("_fence"):
                continue
            props = {}
            for direction, dx, dz in (("north", 0, -1), ("south", 0, 1), ("west", -1, 0), ("east", 1, 0)):
                neighbor = (self.get(x + dx, y, z + dz) or "minecraft:air").split("[")[0]
                connect = neighbor.endswith(("_fence", "_log", "_planks", "_wool")) or neighbor == "minecraft:stone_bricks"
                props[direction] = str(connect).lower()
            self.put(x, y, z, name, waterlogged="false", **props)

    def build(self) -> "Build":
        """Build the complete, furnished Lanternwatch cottage and tower."""
        self.doors, self.windows, self.trees = [], [], []
        self.window_specs = []
        self.approach, self.interior_stairs = [], []
        self.room_targets = {}
        self.terrain()
        self.cottage()
        self.tower()
        self.interiors()
        self.gardens()
        self.detailing()
        for axis, cells in self.window_specs:
            dx, dz = (0, 1) if axis == "z" else (1, 0)
            for x, y, z in cells:
                for sign in (-1, 1):
                    p = (x + sign * dx, y, z + sign * dz)
                    if "_leaves" in (self.get(*p) or ""):
                        self.put(*p, "minecraft:air")
        self.prune_detached_leaves()
        self.connect_fences()
        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()
