//! General level-authoring tools: a material palette, a data-oriented block
//! placement API, procedural generators, a command surface, snapping and the
//! work-plane grid and ground. None of this is specific to greyboxing, which
//! is just one activity that drives these tools (see the `greybox` module).

use crate::ecs::{EditorWorld, PlacementShape};
use nightshade::ecs::loading::load_texture_pack_from_image_bytes;
use nightshade::ecs::material::components::{Material, TextureTransform};
use nightshade::ecs::physics::components::{ColliderComponent, RigidBodyComponent};
use nightshade::prelude::*;
use nightshade::render::wgpu::texture_cache::{SamplerSettings, TextureUsage};

const PROTOTYPE_TEXTURES: &[(&str, &[u8])] = &[
    (
        "proto_light",
        include_bytes!("../../../assets/textures/prototype/light/texture_01.png"),
    ),
    (
        "proto_dark",
        include_bytes!("../../../assets/textures/prototype/dark/texture_01.png"),
    ),
    (
        "proto_green",
        include_bytes!("../../../assets/textures/prototype/green/texture_05.png"),
    ),
    (
        "proto_orange",
        include_bytes!("../../../assets/textures/prototype/orange/texture_05.png"),
    ),
    (
        "proto_purple",
        include_bytes!("../../../assets/textures/prototype/purple/texture_08.png"),
    ),
    (
        "proto_red",
        include_bytes!("../../../assets/textures/prototype/red/texture_05.png"),
    ),
];

#[derive(Clone, Copy)]
enum Swatch {
    Prototype {
        key: &'static str,
    },
    Color {
        rgb: [f32; 3],
        roughness: f32,
        metallic: f32,
    },
    Emissive {
        rgb: [f32; 3],
        strength: f32,
    },
}

struct PaletteEntry {
    label: &'static str,
    material_name: &'static str,
    swatch: Swatch,
}

const PROTOTYPE_TILING: f32 = 2.0;

const PALETTE: &[PaletteEntry] = &[
    PaletteEntry {
        label: "Proto Light",
        material_name: "greybox_proto_light",
        swatch: Swatch::Prototype { key: "proto_light" },
    },
    PaletteEntry {
        label: "Proto Dark",
        material_name: "greybox_proto_dark",
        swatch: Swatch::Prototype { key: "proto_dark" },
    },
    PaletteEntry {
        label: "Proto Green",
        material_name: "greybox_proto_green",
        swatch: Swatch::Prototype { key: "proto_green" },
    },
    PaletteEntry {
        label: "Proto Orange",
        material_name: "greybox_proto_orange",
        swatch: Swatch::Prototype {
            key: "proto_orange",
        },
    },
    PaletteEntry {
        label: "Proto Purple",
        material_name: "greybox_proto_purple",
        swatch: Swatch::Prototype {
            key: "proto_purple",
        },
    },
    PaletteEntry {
        label: "Proto Red",
        material_name: "greybox_proto_red",
        swatch: Swatch::Prototype { key: "proto_red" },
    },
    PaletteEntry {
        label: "Concrete",
        material_name: "greybox_concrete",
        swatch: Swatch::Color {
            rgb: [0.58, 0.58, 0.60],
            roughness: 0.9,
            metallic: 0.0,
        },
    },
    PaletteEntry {
        label: "Stone",
        material_name: "greybox_stone",
        swatch: Swatch::Color {
            rgb: [0.50, 0.47, 0.42],
            roughness: 0.85,
            metallic: 0.0,
        },
    },
    PaletteEntry {
        label: "Wood",
        material_name: "greybox_wood",
        swatch: Swatch::Color {
            rgb: [0.55, 0.40, 0.24],
            roughness: 0.7,
            metallic: 0.0,
        },
    },
    PaletteEntry {
        label: "Metal",
        material_name: "greybox_metal",
        swatch: Swatch::Color {
            rgb: [0.60, 0.62, 0.66],
            roughness: 0.35,
            metallic: 0.85,
        },
    },
    PaletteEntry {
        label: "White",
        material_name: "greybox_white",
        swatch: Swatch::Color {
            rgb: [0.90, 0.90, 0.90],
            roughness: 0.8,
            metallic: 0.0,
        },
    },
    PaletteEntry {
        label: "Emissive Cyan",
        material_name: "greybox_emissive_cyan",
        swatch: Swatch::Emissive {
            rgb: [0.12, 0.80, 1.0],
            strength: 5.0,
        },
    },
    PaletteEntry {
        label: "Emissive Orange",
        material_name: "greybox_emissive_orange",
        swatch: Swatch::Emissive {
            rgb: [1.0, 0.52, 0.16],
            strength: 5.0,
        },
    },
];

pub fn palette_count() -> usize {
    PALETTE.len()
}

/// The leading palette entries are textured prototype materials. Generators
/// pull floor, wall and stair materials from this range so a generated
/// building is always textured and the three read as distinct surfaces.
pub const PROTOTYPE_MATERIAL_COUNT: usize = 6;

pub fn palette_label(index: usize) -> &'static str {
    PALETTE[index % PALETTE.len()].label
}

pub fn palette_color(index: usize) -> [f32; 3] {
    match PALETTE[index % PALETTE.len()].swatch {
        Swatch::Prototype { .. } => [0.72, 0.72, 0.74],
        Swatch::Color { rgb, .. } => rgb,
        Swatch::Emissive { rgb, .. } => rgb,
    }
}

pub fn set_active_palette(editor_world: &mut EditorWorld, index: usize) {
    editor_world.resources.build.active_palette = index % PALETTE.len();
}

pub fn load_palette_textures(editor_world: &mut EditorWorld, world: &mut World) {
    if editor_world.resources.build.textures_loaded {
        return;
    }
    load_texture_pack_from_image_bytes(
        world,
        PROTOTYPE_TEXTURES,
        TextureUsage::Color,
        SamplerSettings::DEFAULT,
    );
    editor_world.resources.build.textures_loaded = true;
}

pub fn snap_step(editor_world: &EditorWorld) -> f32 {
    editor_world.resources.snap.translation_step.max(0.0001)
}

const GRID_HALF_CELLS: i32 = 40;
const GRID_COLOR: nightshade::prelude::Vec4 =
    nightshade::prelude::Vec4::new(0.32, 0.36, 0.44, 0.45);
const GRID_AXIS_COLOR: nightshade::prelude::Vec4 =
    nightshade::prelude::Vec4::new(0.55, 0.60, 0.72, 0.8);

pub fn update_grid(editor_world: &mut EditorWorld, world: &mut World) {
    if !editor_world.resources.placement.active && !editor_world.resources.push_pull.active {
        if let Some(entity) = editor_world.resources.build.grid_lines {
            world.core.set_lines(entity, Lines::default());
        }
        return;
    }
    let step = snap_step(editor_world);
    let entity = ensure_grid_entity(editor_world, world);
    world
        .core
        .set_lines(entity, Lines::new(build_grid_lines(step)));
}

fn ensure_grid_entity(editor_world: &mut EditorWorld, world: &mut World) -> Entity {
    use nightshade::ecs::world::{GLOBAL_TRANSFORM, LINES, VISIBILITY};
    if let Some(entity) = editor_world.resources.build.grid_lines
        && world.core.entity_has_components(entity, LINES)
    {
        return entity;
    }
    let entity = spawn_entities(world, LINES | VISIBILITY | GLOBAL_TRANSFORM, 1)[0];
    world.core.set_lines(entity, Lines::default());
    world
        .core
        .set_visibility(entity, Visibility { visible: true });
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    editor_world
        .resources
        .editor_scene
        .register_scaffolding(entity);
    editor_world.resources.build.grid_lines = Some(entity);
    entity
}

fn build_grid_lines(step: f32) -> Vec<Line> {
    let step = step.max(0.01);
    let extent = step * GRID_HALF_CELLS as f32;
    let mut lines = Vec::with_capacity((GRID_HALF_CELLS as usize * 2 + 1) * 2);
    for index in -GRID_HALF_CELLS..=GRID_HALF_CELLS {
        let offset = index as f32 * step;
        let color = if index == 0 {
            GRID_AXIS_COLOR
        } else {
            GRID_COLOR
        };
        lines.push(Line {
            start: Vec3::new(offset, 0.0, -extent),
            end: Vec3::new(offset, 0.0, extent),
            color,
        });
        lines.push(Line {
            start: Vec3::new(-extent, 0.0, offset),
            end: Vec3::new(extent, 0.0, offset),
            color,
        });
    }
    lines
}

pub fn snap_translation(position: Vec3, step: f32) -> Vec3 {
    if step <= 0.0 {
        return position;
    }
    Vec3::new(
        (position.x / step).round() * step,
        (position.y / step).round() * step,
        (position.z / step).round() * step,
    )
}

/// A single block to place, expressed as plain data so the placement UI and
/// the procedural generator both produce and consume the same values. This is
/// the unit an external driver (an MCP tool) would emit.
#[derive(Clone, Copy)]
pub struct PlacedBlock {
    pub shape: PlacementShape,
    pub translation: Vec3,
    pub scale: Vec3,
    pub rotation: Quat,
    pub material: usize,
    /// Hidden blocks still get a collider, used for invisible ramp colliders.
    pub visible: bool,
}

/// The full world-space AABB extent of each primitive's unit mesh, so a size
/// in meters maps to the same bounding box for every shape. Cube, cylinder and
/// cone span one meter; sphere and torus are larger; the plane is flat.
pub fn shape_full_extent(shape: PlacementShape) -> Vec3 {
    match shape {
        PlacementShape::Cube => Vec3::new(1.0, 1.0, 1.0),
        PlacementShape::Plane => Vec3::new(2.0, 0.0, 2.0),
        PlacementShape::Cylinder => Vec3::new(1.0, 1.0, 1.0),
        PlacementShape::Cone => Vec3::new(1.0, 1.0, 1.0),
        PlacementShape::Sphere => Vec3::new(2.0, 2.0, 2.0),
        PlacementShape::Torus => Vec3::new(2.6, 0.6, 2.6),
    }
}

pub fn axis_scale(size: f32, full: f32) -> f32 {
    if full > 1.0e-6 { size / full } else { 1.0 }
}

/// Turn a world-space axis-aligned box into a placeable block, resting flat
/// shapes on their minimum corner and scaling every other shape to fill the box.
pub fn block_from_aabb(
    shape: PlacementShape,
    min: Vec3,
    max: Vec3,
    material: usize,
) -> PlacedBlock {
    let full = shape_full_extent(shape);
    let size = max - min;
    let axis_center = |min_value: f32, max_value: f32, full_value: f32| {
        if full_value > 1.0e-6 {
            (min_value + max_value) * 0.5
        } else {
            min_value
        }
    };
    PlacedBlock {
        shape,
        translation: Vec3::new(
            axis_center(min.x, max.x, full.x),
            axis_center(min.y, max.y, full.y),
            axis_center(min.z, max.z, full.z),
        ),
        scale: Vec3::new(
            axis_scale(size.x, full.x),
            axis_scale(size.y, full.y),
            axis_scale(size.z, full.z),
        ),
        rotation: Quat::identity(),
        material,
        visible: true,
    }
}

pub fn spawn_shape(world: &mut World, shape: PlacementShape, position: Vec3) -> Entity {
    match shape {
        PlacementShape::Cube => spawn_cube_at(world, position),
        PlacementShape::Plane => spawn_plane_at(world, position),
        PlacementShape::Cylinder => spawn_cylinder_at(world, position),
        PlacementShape::Cone => spawn_cone_at(world, position),
        PlacementShape::Sphere => spawn_sphere_at(world, position),
        PlacementShape::Torus => spawn_torus_at(world, position),
    }
}

/// Spawn one block, set its transform and apply its material. Registration,
/// grouping, undo and selection are the caller's job.
pub fn spawn_block(world: &mut World, block: &PlacedBlock) -> Entity {
    use nightshade::ecs::world::VISIBILITY;
    let entity = spawn_shape(world, block.shape, block.translation);
    if let Some(local) = world.core.get_local_transform_mut(entity) {
        local.translation = block.translation;
        local.scale = block.scale;
        local.rotation = block.rotation;
    }
    mark_local_transform_dirty(world, entity);
    apply_to_entity(world, block.material, entity);
    world.core.add_components(entity, VISIBILITY);
    world.core.set_visibility(
        entity,
        Visibility {
            visible: block.visible,
        },
    );
    add_block_collider(world, entity, block);
    entity
}

/// Give a block a static cuboid collider matching its world-space half extents
/// so the player and physics collide with it. Rotation is taken from the
/// entity transform by the physics sync.
fn add_block_collider(world: &mut World, entity: Entity, block: &PlacedBlock) {
    use nightshade::ecs::world::{COLLIDER, RIGID_BODY};
    let full = shape_full_extent(block.shape);
    let half = Vec3::new(
        (block.scale.x * full.x * 0.5).max(0.05),
        (block.scale.y * full.y * 0.5).max(0.05),
        (block.scale.z * full.z * 0.5).max(0.05),
    );
    world.core.add_components(entity, RIGID_BODY | COLLIDER);
    if let Some(rigid_body) = world.core.get_rigid_body_mut(entity) {
        *rigid_body = RigidBodyComponent::new_static().with_translation(
            block.translation.x,
            block.translation.y,
            block.translation.z,
        );
    }
    if let Some(collider) = world.core.get_collider_mut(entity) {
        *collider = ColliderComponent::new_cuboid(half.x, half.y, half.z).with_friction(0.9);
    }
}

fn spawn_group(world: &mut World, name: &str) -> Entity {
    use nightshade::ecs::world::{GLOBAL_TRANSFORM, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, NAME};
    let entity = spawn_entities(
        world,
        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
        1,
    )[0];
    world.core.set_name(
        entity,
        nightshade::ecs::world::components::Name(name.to_string()),
    );
    mark_local_transform_dirty(world, entity);
    entity
}

/// Place a batch of blocks as one undoable operation. A batch of more than one
/// block is parented under a named group entity so it selects as a unit; a
/// single block stays at the root. Selects the result.
pub fn place_blocks(
    editor_world: &mut EditorWorld,
    world: &mut World,
    blocks: &[PlacedBlock],
    label: &str,
) {
    let mut entities = Vec::new();
    for block in blocks {
        if block.scale.x.abs() < 1.0e-4
            || block.scale.y.abs() < 1.0e-4
            || block.scale.z.abs() < 1.0e-4
        {
            continue;
        }
        entities.push(spawn_block(world, block));
    }
    if entities.is_empty() {
        return;
    }

    let root = if entities.len() > 1 {
        let group = spawn_group(world, label);
        for &entity in &entities {
            update_parent(
                world,
                entity,
                Some(nightshade::ecs::transform::components::Parent(Some(group))),
            );
        }
        group
    } else {
        entities[0]
    };

    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        root,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        label,
    );
    crate::systems::selection::set_primary(editor_world, Some(root));
}

const DOOR_WIDTH: f32 = 1.6;
const DOOR_HEIGHT: f32 = 2.4;
const WINDOW_WIDTH: f32 = 1.6;
const WINDOW_SILL: f32 = 1.1;
const WINDOW_HEIGHT: f32 = 1.4;

/// A centered vertical opening in a wall, measured from the wall base.
#[derive(Clone, Copy)]
struct Opening {
    width: f32,
    bottom: f32,
    height: f32,
}

fn box_block(min: Vec3, max: Vec3, material: usize) -> PlacedBlock {
    block_from_aabb(PlacementShape::Cube, min, max, material)
}

/// Push a wall box, optionally with a centered opening cut along its longer
/// horizontal axis (built from side jambs plus a sill and lintel).
fn push_wall(
    blocks: &mut Vec<PlacedBlock>,
    min: Vec3,
    max: Vec3,
    opening: Option<Opening>,
    material: usize,
) {
    let Some(opening) = opening else {
        blocks.push(box_block(min, max, material));
        return;
    };
    let length_axis = if (max.x - min.x) >= (max.z - min.z) {
        0
    } else {
        2
    };
    let low = min[length_axis];
    let high = max[length_axis];
    let center = (low + high) * 0.5;
    let half = (opening.width * 0.5)
        .min((high - low) * 0.5 - 0.05)
        .max(0.0);
    let open_low = center - half;
    let open_high = center + half;
    let open_bottom = (min.y + opening.bottom).clamp(min.y, max.y);
    let open_top = (min.y + opening.bottom + opening.height).clamp(min.y, max.y);

    if open_low > low + 1.0e-3 {
        let mut jamb_max = max;
        jamb_max[length_axis] = open_low;
        blocks.push(box_block(min, jamb_max, material));
    }
    if high > open_high + 1.0e-3 {
        let mut jamb_min = min;
        jamb_min[length_axis] = open_high;
        blocks.push(box_block(jamb_min, max, material));
    }
    if open_bottom > min.y + 1.0e-3 {
        let mut sill_min = min;
        let mut sill_max = max;
        sill_min[length_axis] = open_low;
        sill_max[length_axis] = open_high;
        sill_max.y = open_bottom;
        blocks.push(box_block(sill_min, sill_max, material));
    }
    if open_top < max.y - 1.0e-3 {
        let mut lintel_min = min;
        let mut lintel_max = max;
        lintel_min[length_axis] = open_low;
        lintel_max[length_axis] = open_high;
        lintel_min.y = open_top;
        blocks.push(box_block(lintel_min, lintel_max, material));
    }
}

/// Place a row of evenly spaced openings along a wall by splitting it into
/// bays and cutting one centered opening per bay.
fn push_wall_bays(
    blocks: &mut Vec<PlacedBlock>,
    min: Vec3,
    max: Vec3,
    opening: Opening,
    spacing: f32,
    material: usize,
) {
    let length_axis = if (max.x - min.x) >= (max.z - min.z) {
        0
    } else {
        2
    };
    let length = max[length_axis] - min[length_axis];
    let bays = ((length / spacing.max(0.5)).floor() as u32).max(1);
    let bay = length / bays as f32;
    for index in 0..bays {
        let mut bay_min = min;
        let mut bay_max = max;
        bay_min[length_axis] = min[length_axis] + index as f32 * bay;
        bay_max[length_axis] = bay_min[length_axis] + bay;
        push_wall(blocks, bay_min, bay_max, Some(opening), material);
    }
}

/// A tiny deterministic hash-based PRNG so generators vary with a seed without
/// pulling in a dependency. Free functions over a plain u32 state.
fn rng_step(state: &mut u32) -> u32 {
    let mut value = *state;
    value ^= value << 13;
    value ^= value >> 17;
    value ^= value << 5;
    *state = value.max(1);
    *state
}

fn rng_unit(state: &mut u32) -> f32 {
    (rng_step(state) & 0x00ff_ffff) as f32 / 0x0100_0000 as f32
}

fn rng_range(state: &mut u32, low: f32, high: f32) -> f32 {
    low + rng_unit(state) * (high - low)
}

/// Inputs for the procedural building generator. Plain data so a generator can
/// be driven from the UI today and an external tool later.
#[derive(Clone, Copy)]
pub struct BuildingParams {
    pub min_corner: Vec3,
    pub footprint: Vec2,
    pub stories: u32,
    pub story_height: f32,
    pub wall_thickness: f32,
    pub floor_thickness: f32,
    pub window_spacing: f32,
    pub floor_material: usize,
    pub wall_material: usize,
    pub stair_material: usize,
}

/// A floor slab spanning [min, max] in XZ with a rectangular hole cut for a
/// stairwell, emitted as up to four strips around the hole.
fn push_floor_with_hole(
    blocks: &mut Vec<PlacedBlock>,
    min: Vec3,
    max: Vec3,
    hole_min: Vec3,
    hole_max: Vec3,
    material: usize,
) {
    let top = max.y;
    if hole_min.z > min.z + 1.0e-3 {
        blocks.push(box_block(min, Vec3::new(max.x, top, hole_min.z), material));
    }
    if hole_max.z < max.z - 1.0e-3 {
        blocks.push(box_block(
            Vec3::new(min.x, min.y, hole_max.z),
            max,
            material,
        ));
    }
    if hole_min.x > min.x + 1.0e-3 {
        blocks.push(box_block(
            Vec3::new(min.x, min.y, hole_min.z),
            Vec3::new(hole_min.x, top, hole_max.z),
            material,
        ));
    }
    if hole_max.x < max.x - 1.0e-3 {
        blocks.push(box_block(
            Vec3::new(hole_max.x, min.y, hole_min.z),
            Vec3::new(max.x, top, hole_max.z),
            material,
        ));
    }
}

/// An invisible collision-only ramp box, rotated to the given slope.
fn ramp_block(center: Vec3, size: Vec3, rotation: Quat, material: usize) -> PlacedBlock {
    PlacedBlock {
        shape: PlacementShape::Cube,
        translation: center,
        scale: size,
        rotation,
        material,
        visible: false,
    }
}

/// A single solid flight of steps spanning the `[min, max]` box (where `min.y`
/// is the floor it starts on and `max.y` the level it reaches), plus a hidden
/// smooth ramp collider over the nosings so the player walks up easily. The
/// flight climbs toward `+Z` when `ascending_positive_z`, otherwise toward
/// `-Z`.
fn push_flight(
    blocks: &mut Vec<PlacedBlock>,
    min: Vec3,
    max: Vec3,
    ascending_positive_z: bool,
    material: usize,
) {
    let (x0, x1) = (min.x, max.x);
    let (z0, z1) = (min.z, max.z);
    let (low_y, high_y) = (min.y, max.y);
    let depth = (z1 - z0).max(0.5);
    let climb = (high_y - low_y).max(0.1);
    let steps = (climb / 0.18).round().max(4.0) as u32;
    let rise = climb / steps as f32;
    let run = depth / steps as f32;
    for step in 0..steps {
        let height = low_y + (step as f32 + 1.0) * rise;
        let (front, back) = if ascending_positive_z {
            (z0 + step as f32 * run, z1)
        } else {
            (z0, z1 - step as f32 * run)
        };
        blocks.push(box_block(
            Vec3::new(x0, low_y, front),
            Vec3::new(x1, height, back),
            material,
        ));
    }
    let angle = climb.atan2(depth);
    let length = (depth * depth + climb * climb).sqrt();
    let sign = if ascending_positive_z { -1.0 } else { 1.0 };
    let rotation = nalgebra_glm::quat_angle_axis(sign * angle, &Vec3::new(1.0, 0.0, 0.0));
    let center = Vec3::new((x0 + x1) * 0.5, low_y + climb * 0.5 - 0.1, (z0 + z1) * 0.5);
    blocks.push(ramp_block(
        center,
        Vec3::new(x1 - x0, 0.3, length),
        rotation,
        material,
    ));
}

/// A multi-story building: a floor slab per story (upper floors cut out for a
/// stairwell), four perimeter walls with a ground-floor door and windows
/// elsewhere, a stairwell connecting the floors, and a roof.
pub fn generate_building(params: &BuildingParams) -> Vec<PlacedBlock> {
    let thickness = params.wall_thickness.max(0.1);
    let width = params.footprint.x.max(thickness * 4.0 + DOOR_WIDTH);
    let depth = params.footprint.y.max(thickness * 4.0 + DOOR_WIDTH);
    let floor_thickness = params.floor_thickness.max(0.1);
    let story_height = params.story_height.max(2.4);
    let stories = params.stories.max(1);

    let x0 = params.min_corner.x;
    let z0 = params.min_corner.z;
    let x1 = x0 + width;
    let z1 = z0 + depth;
    let window = Opening {
        width: WINDOW_WIDTH,
        bottom: WINDOW_SILL,
        height: WINDOW_HEIGHT,
    };
    let door = Opening {
        width: DOOR_WIDTH,
        bottom: 0.0,
        height: DOOR_HEIGHT,
    };

    let spacing = params.window_spacing.max(2.0);

    let climb = story_height + floor_thickness;
    let available_depth = (depth - thickness * 2.0 - 0.5).max(1.5);
    let stair_depth = (climb * 1.9).clamp(1.5, available_depth);
    let available_width = (width - thickness * 2.0 - 0.5).max(1.5);
    let stair_width = (width * 0.5).clamp(2.6, 4.6).min(available_width);
    let sx0 = x0 + thickness;
    let sx1 = (sx0 + stair_width).min(x1 - thickness);
    let sxm = (sx0 + sx1) * 0.5;
    let sz0 = z0 + thickness;
    let sz1 = (sz0 + stair_depth).min(z1 - thickness);

    let mut blocks = Vec::with_capacity(stories as usize * 20);
    let mut base_y = params.min_corner.y;
    for story in 0..stories {
        let floor_top = base_y + floor_thickness;
        if story == 0 {
            blocks.push(box_block(
                Vec3::new(x0, base_y, z0),
                Vec3::new(x1, floor_top, z1),
                params.floor_material,
            ));
        } else {
            let hole_on_left = story % 2 == 1;
            let (hole_x0, hole_x1) = if hole_on_left { (sx0, sxm) } else { (sxm, sx1) };
            push_floor_with_hole(
                &mut blocks,
                Vec3::new(x0, base_y, z0),
                Vec3::new(x1, floor_top, z1),
                Vec3::new(hole_x0, base_y, sz0),
                Vec3::new(hole_x1, floor_top, sz1),
                params.floor_material,
            );
        }
        let wall_top = floor_top + story_height;
        let front_min = Vec3::new(x0, floor_top, z0);
        let front_max = Vec3::new(x1, wall_top, z0 + thickness);
        if story == 0 {
            push_wall(
                &mut blocks,
                front_min,
                front_max,
                Some(door),
                params.wall_material,
            );
        } else {
            push_wall_bays(
                &mut blocks,
                front_min,
                front_max,
                window,
                spacing,
                params.wall_material,
            );
        }
        push_wall_bays(
            &mut blocks,
            Vec3::new(x0, floor_top, z1 - thickness),
            Vec3::new(x1, wall_top, z1),
            window,
            spacing,
            params.wall_material,
        );
        push_wall_bays(
            &mut blocks,
            Vec3::new(x0, floor_top, z0 + thickness),
            Vec3::new(x0 + thickness, wall_top, z1 - thickness),
            window,
            spacing,
            params.wall_material,
        );
        push_wall_bays(
            &mut blocks,
            Vec3::new(x1 - thickness, floor_top, z0 + thickness),
            Vec3::new(x1, wall_top, z1 - thickness),
            window,
            spacing,
            params.wall_material,
        );
        if story + 1 < stories {
            let on_left = story % 2 == 0;
            let (flight_x0, flight_x1) = if on_left { (sx0, sxm) } else { (sxm, sx1) };
            push_flight(
                &mut blocks,
                Vec3::new(flight_x0, floor_top, sz0),
                Vec3::new(flight_x1, floor_top + climb, sz1),
                on_left,
                params.stair_material,
            );
        }
        base_y = wall_top;
    }
    blocks.push(box_block(
        Vec3::new(x0, base_y, z0),
        Vec3::new(x1, base_y + floor_thickness, z1),
        params.floor_material,
    ));
    blocks
}

#[derive(Clone, Copy)]
pub struct TowerParams {
    pub min_corner: Vec3,
    pub footprint: Vec2,
    pub stories: u32,
    pub story_height: f32,
    pub shrink_per_story: f32,
    pub floor_thickness: f32,
    pub floor_material: usize,
    pub wall_material: usize,
}

/// A stepped tower: each story is a thin floor plate plus a solid-ish ring of
/// four walls, and the footprint shrinks as it rises, capped with crenellations.
pub fn generate_tower(params: &TowerParams) -> Vec<PlacedBlock> {
    let stories = params.stories.max(1);
    let story_height = params.story_height.max(2.0);
    let floor_thickness = params.floor_thickness.max(0.1);
    let thickness = 0.3_f32;
    let mut blocks = Vec::new();
    let mut base_y = params.min_corner.y;
    let center_x = params.min_corner.x + params.footprint.x * 0.5;
    let center_z = params.min_corner.z + params.footprint.y * 0.5;
    for story in 0..stories {
        let inset = params.shrink_per_story * story as f32;
        let half_x = (params.footprint.x * 0.5 - inset).max(1.5);
        let half_z = (params.footprint.y * 0.5 - inset).max(1.5);
        let x0 = center_x - half_x;
        let x1 = center_x + half_x;
        let z0 = center_z - half_z;
        let z1 = center_z + half_z;
        let floor_top = base_y + floor_thickness;
        blocks.push(box_block(
            Vec3::new(x0, base_y, z0),
            Vec3::new(x1, floor_top, z1),
            params.floor_material,
        ));
        let wall_top = floor_top + story_height;
        let window = Opening {
            width: WINDOW_WIDTH,
            bottom: WINDOW_SILL,
            height: WINDOW_HEIGHT,
        };
        for (a, b) in [
            (
                Vec3::new(x0, floor_top, z0),
                Vec3::new(x1, wall_top, z0 + thickness),
            ),
            (
                Vec3::new(x0, floor_top, z1 - thickness),
                Vec3::new(x1, wall_top, z1),
            ),
            (
                Vec3::new(x0, floor_top, z0 + thickness),
                Vec3::new(x0 + thickness, wall_top, z1 - thickness),
            ),
            (
                Vec3::new(x1 - thickness, floor_top, z0 + thickness),
                Vec3::new(x1, wall_top, z1 - thickness),
            ),
        ] {
            push_wall(&mut blocks, a, b, Some(window), params.wall_material);
        }
        base_y = wall_top;
    }
    blocks
}

#[derive(Clone, Copy)]
pub struct StairsParams {
    pub min_corner: Vec3,
    pub width: f32,
    pub steps: u32,
    pub rise: f32,
    pub run: f32,
    pub material: usize,
}

/// A solid run of stairs climbing along +Z, each step a full-height box so the
/// staircase reads as filled underneath.
pub fn generate_stairs(params: &StairsParams) -> Vec<PlacedBlock> {
    let steps = params.steps.max(1);
    let rise = params.rise.max(0.05);
    let run = params.run.max(0.1);
    let width = params.width.max(0.5);
    let x0 = params.min_corner.x;
    let x1 = x0 + width;
    let y0 = params.min_corner.y;
    let z0 = params.min_corner.z;
    let mut blocks = Vec::with_capacity(steps as usize);
    for step in 0..steps {
        let step_front = z0 + step as f32 * run;
        blocks.push(box_block(
            Vec3::new(x0, y0, step_front),
            Vec3::new(x1, y0 + (step as f32 + 1.0) * rise, z0 + steps as f32 * run),
            params.material,
        ));
    }
    blocks
}

#[derive(Clone, Copy)]
pub struct ColumnsParams {
    pub min_corner: Vec3,
    pub footprint: Vec2,
    pub spacing: f32,
    pub column: f32,
    pub height: f32,
    pub floor_thickness: f32,
    pub floor_material: usize,
    pub column_material: usize,
}

/// A floor slab studded with a grid of columns (a hypostyle hall).
pub fn generate_columns(params: &ColumnsParams) -> Vec<PlacedBlock> {
    let spacing = params.spacing.max(1.0);
    let column = params.column.max(0.2);
    let height = params.height.max(1.0);
    let floor_thickness = params.floor_thickness.max(0.1);
    let x0 = params.min_corner.x;
    let z0 = params.min_corner.z;
    let x1 = x0 + params.footprint.x.max(spacing);
    let z1 = z0 + params.footprint.y.max(spacing);
    let floor_top = params.min_corner.y + floor_thickness;
    let mut blocks = vec![box_block(
        Vec3::new(x0, params.min_corner.y, z0),
        Vec3::new(x1, floor_top, z1),
        params.floor_material,
    )];
    let columns_x = ((x1 - x0) / spacing).floor().max(1.0) as u32;
    let columns_z = ((z1 - z0) / spacing).floor().max(1.0) as u32;
    let margin = spacing * 0.5;
    for ix in 0..=columns_x {
        for iz in 0..=columns_z {
            let cx = x0 + margin + ix as f32 * spacing;
            let cz = z0 + margin + iz as f32 * spacing;
            if cx + column * 0.5 > x1 || cz + column * 0.5 > z1 {
                continue;
            }
            blocks.push(box_block(
                Vec3::new(cx - column * 0.5, floor_top, cz - column * 0.5),
                Vec3::new(cx + column * 0.5, floor_top + height, cz + column * 0.5),
                params.column_material,
            ));
        }
    }
    blocks
}

#[derive(Clone, Copy)]
pub struct PerimeterParams {
    pub min_corner: Vec3,
    pub footprint: Vec2,
    pub height: f32,
    pub thickness: f32,
    pub gate_width: f32,
    pub material: usize,
}

/// A perimeter wall enclosing a courtyard, with a gate opening on the front.
pub fn generate_perimeter(params: &PerimeterParams) -> Vec<PlacedBlock> {
    let thickness = params.thickness.max(0.1);
    let height = params.height.max(1.0);
    let x0 = params.min_corner.x;
    let z0 = params.min_corner.z;
    let x1 = x0 + params.footprint.x.max(thickness * 4.0 + params.gate_width);
    let z1 = z0 + params.footprint.y.max(thickness * 4.0);
    let y0 = params.min_corner.y;
    let gate = Opening {
        width: params.gate_width.max(1.0),
        bottom: 0.0,
        height: (height - 0.3).max(1.0),
    };
    let mut blocks = Vec::with_capacity(4);
    push_wall(
        &mut blocks,
        Vec3::new(x0, y0, z0),
        Vec3::new(x1, y0 + height, z0 + thickness),
        Some(gate),
        params.material,
    );
    push_wall(
        &mut blocks,
        Vec3::new(x0, y0, z1 - thickness),
        Vec3::new(x1, y0 + height, z1),
        None,
        params.material,
    );
    push_wall(
        &mut blocks,
        Vec3::new(x0, y0, z0 + thickness),
        Vec3::new(x0 + thickness, y0 + height, z1 - thickness),
        None,
        params.material,
    );
    push_wall(
        &mut blocks,
        Vec3::new(x1 - thickness, y0, z0 + thickness),
        Vec3::new(x1, y0 + height, z1 - thickness),
        None,
        params.material,
    );
    blocks
}

#[derive(Clone, Copy)]
pub struct CityBlockParams {
    pub min_corner: Vec3,
    pub columns: u32,
    pub rows: u32,
    pub lot: Vec2,
    pub street: f32,
    pub max_stories: u32,
    pub story_height: f32,
    pub wall_thickness: f32,
    pub floor_thickness: f32,
    pub window_spacing: f32,
    pub seed: u32,
    pub variation: f32,
    pub floor_material: usize,
    pub wall_material: usize,
}

/// A grid of buildings separated by streets, their heights varied by the seed.
/// `variation` blends between uniform max height (0) and fully random (1).
pub fn generate_city_block(params: &CityBlockParams) -> Vec<PlacedBlock> {
    let columns = params.columns.max(1);
    let rows = params.rows.max(1);
    let max_stories = params.max_stories.max(1);
    let variation = params.variation.clamp(0.0, 1.0);
    let mut state = params.seed.max(1);
    let mut blocks = Vec::new();
    for row in 0..rows {
        for column in 0..columns {
            let x = params.min_corner.x + column as f32 * (params.lot.x + params.street);
            let z = params.min_corner.z + row as f32 * (params.lot.y + params.street);
            let random = 1.0 + rng_unit(&mut state) * (max_stories as f32 - 1.0);
            let stories =
                (max_stories as f32 * (1.0 - variation) + random * variation).round() as u32;
            let inset = rng_range(
                &mut state,
                0.0,
                params.lot.x.min(params.lot.y) * 0.2 * variation,
            );
            let footprint = Vec2::new(params.lot.x - inset, params.lot.y - inset);
            blocks.extend(generate_building(&BuildingParams {
                min_corner: Vec3::new(x + inset * 0.5, params.min_corner.y, z + inset * 0.5),
                footprint,
                stories: stories.max(1),
                story_height: params.story_height,
                wall_thickness: params.wall_thickness,
                floor_thickness: params.floor_thickness,
                window_spacing: params.window_spacing,
                floor_material: params.floor_material,
                wall_material: params.wall_material,
                stair_material: (params.wall_material + 3) % PROTOTYPE_MATERIAL_COUNT,
            }));
        }
    }
    blocks
}

#[derive(Clone, Copy)]
pub struct CourtyardParams {
    pub min_corner: Vec3,
    pub footprint: Vec2,
    pub wing_depth: f32,
    pub stories: u32,
    pub story_height: f32,
    pub wall_thickness: f32,
    pub floor_thickness: f32,
    pub window_spacing: f32,
    pub floor_material: usize,
    pub wall_material: usize,
}

/// Four building wings around an open courtyard.
pub fn generate_courtyard(params: &CourtyardParams) -> Vec<PlacedBlock> {
    let width = params.footprint.x.max(8.0);
    let depth = params.footprint.y.max(8.0);
    let wing = params.wing_depth.clamp(2.0, width.min(depth) * 0.5 - 1.0);
    let origin = params.min_corner;
    let building = |min_corner: Vec3, footprint: Vec2| BuildingParams {
        min_corner,
        footprint,
        stories: params.stories,
        story_height: params.story_height,
        wall_thickness: params.wall_thickness,
        floor_thickness: params.floor_thickness,
        window_spacing: params.window_spacing,
        floor_material: params.floor_material,
        wall_material: params.wall_material,
        stair_material: (params.wall_material + 3) % PROTOTYPE_MATERIAL_COUNT,
    };
    let mut blocks = Vec::new();
    blocks.extend(generate_building(&building(origin, Vec2::new(width, wing))));
    blocks.extend(generate_building(&building(
        Vec3::new(origin.x, origin.y, origin.z + depth - wing),
        Vec2::new(width, wing),
    )));
    let inner_depth = depth - 2.0 * wing;
    if inner_depth > 1.0 {
        blocks.extend(generate_building(&building(
            Vec3::new(origin.x, origin.y, origin.z + wing),
            Vec2::new(wing, inner_depth),
        )));
        blocks.extend(generate_building(&building(
            Vec3::new(origin.x + width - wing, origin.y, origin.z + wing),
            Vec2::new(wing, inner_depth),
        )));
    }
    blocks
}

#[derive(Clone, Copy)]
pub struct WfcParams {
    pub min_corner: Vec3,
    pub columns: u32,
    pub rows: u32,
    pub cell: f32,
    pub wall_height: f32,
    pub wall_thickness: f32,
    pub floor_thickness: f32,
    pub seed: u32,
    pub floor_material: usize,
    pub wall_material: usize,
}

#[derive(Clone, Copy)]
struct WfcTile {
    sockets: [u8; 4],
    floor: bool,
    weight: f32,
}

/// Modular tile set for wave function collapse. Sockets are [north, east,
/// south, west] where 1 is an open passage and 0 is a wall, so neighboring
/// tiles only fit when the touching sockets agree, producing connected layouts.
fn wfc_tiles() -> Vec<WfcTile> {
    let mut tiles = Vec::new();
    let mut push = |sockets: [u8; 4], floor: bool, weight: f32| {
        tiles.push(WfcTile {
            sockets,
            floor,
            weight,
        });
    };
    push([1, 1, 1, 1], true, 0.4);
    for sockets in [[1, 1, 1, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]] {
        push(sockets, true, 0.8);
    }
    push([1, 0, 1, 0], true, 1.4);
    push([0, 1, 0, 1], true, 1.4);
    for sockets in [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [1, 0, 0, 1]] {
        push(sockets, true, 1.2);
    }
    for sockets in [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] {
        push(sockets, true, 0.6);
    }
    push([0, 0, 0, 0], true, 0.5);
    push([0, 0, 0, 0], false, 0.6);
    tiles
}

const WFC_OPPOSITE: [usize; 4] = [2, 3, 0, 1];

fn wfc_side_sockets(sockets: &[[u8; 4]], mask: u32, side: usize) -> [bool; 2] {
    let mut present = [false; 2];
    for (index, tile) in sockets.iter().enumerate() {
        if mask & (1 << index) != 0 {
            present[tile[side] as usize] = true;
        }
    }
    present
}

/// Generic socket-based wave function collapse over a grid. Returns the chosen
/// tile index per cell, or None on contradiction. Shared by every WFC tile set.
fn wfc_solve(
    columns: usize,
    rows: usize,
    sockets: &[[u8; 4]],
    weights: &[f32],
    seed: u32,
) -> Option<Vec<usize>> {
    let count = columns * rows;
    let full = if sockets.len() >= 32 {
        u32::MAX
    } else {
        (1u32 << sockets.len()) - 1
    };
    let mut cells = vec![full; count];
    let mut state = seed.max(1);

    let allow_side = |cells: &mut [u32], index: usize, side: usize, value: u8| {
        let mut mask = 0u32;
        for (tile_index, tile) in sockets.iter().enumerate() {
            if tile[side] == value {
                mask |= 1 << tile_index;
            }
        }
        cells[index] &= mask;
    };
    for column in 0..columns {
        allow_side(&mut cells, column, 0, 0);
        allow_side(&mut cells, (rows - 1) * columns + column, 2, 0);
    }
    for row in 0..rows {
        allow_side(&mut cells, row * columns, 3, 0);
        allow_side(&mut cells, row * columns + columns - 1, 1, 0);
    }

    loop {
        let mut target = None;
        let mut best = u32::MAX;
        for (index, &mask) in cells.iter().enumerate() {
            let options = mask.count_ones();
            if options == 0 {
                return None;
            }
            if options > 1 && options < best {
                best = options;
                target = Some(index);
            }
        }
        let Some(index) = target else {
            break;
        };

        let mut total = 0.0;
        for (tile_index, weight) in weights.iter().enumerate() {
            if cells[index] & (1 << tile_index) != 0 {
                total += weight;
            }
        }
        let mut pick = rng_unit(&mut state) * total;
        let mut chosen = 0;
        for (tile_index, weight) in weights.iter().enumerate() {
            if cells[index] & (1 << tile_index) != 0 {
                pick -= weight;
                if pick <= 0.0 {
                    chosen = tile_index;
                    break;
                }
            }
        }
        cells[index] = 1 << chosen;

        let mut stack = vec![index];
        while let Some(current) = stack.pop() {
            let column = current % columns;
            let row = current / columns;
            let neighbors = [
                (row > 0).then(|| current - columns),
                (column + 1 < columns).then_some(current + 1),
                (row + 1 < rows).then(|| current + columns),
                (column > 0).then(|| current - 1),
            ];
            for (side, neighbor) in neighbors.into_iter().enumerate() {
                let Some(neighbor) = neighbor else {
                    continue;
                };
                let present = wfc_side_sockets(sockets, cells[current], side);
                let mut allowed = 0u32;
                for (tile_index, tile) in sockets.iter().enumerate() {
                    if present[tile[WFC_OPPOSITE[side]] as usize] {
                        allowed |= 1 << tile_index;
                    }
                }
                let next = cells[neighbor] & allowed;
                if next != cells[neighbor] {
                    cells[neighbor] = next;
                    if next == 0 {
                        return None;
                    }
                    stack.push(neighbor);
                }
            }
        }
    }

    Some(
        cells
            .iter()
            .map(|mask| mask.trailing_zeros() as usize)
            .collect(),
    )
}

/// Wave function collapse over the modular tile set, emitting a connected
/// layout of floor cells bounded by walls where tiles meet a closed socket.
pub fn generate_wfc(params: &WfcParams) -> Vec<PlacedBlock> {
    let columns = params.columns.max(1) as usize;
    let rows = params.rows.max(1) as usize;
    let tiles = wfc_tiles();
    let sockets: Vec<[u8; 4]> = tiles.iter().map(|tile| tile.sockets).collect();
    let weights: Vec<f32> = tiles.iter().map(|tile| tile.weight).collect();
    let mut grid = None;
    for attempt in 0..16 {
        if let Some(solution) = wfc_solve(
            columns,
            rows,
            &sockets,
            &weights,
            params.seed.wrapping_add(attempt),
        ) {
            grid = Some(solution);
            break;
        }
    }
    let Some(grid) = grid else {
        return Vec::new();
    };

    let cell = params.cell.max(1.0);
    let thickness = params.wall_thickness.max(0.1);
    let floor_thickness = params.floor_thickness.max(0.1);
    let wall_height = params.wall_height.max(1.0);
    let mut blocks = Vec::new();
    for row in 0..rows {
        for column in 0..columns {
            let tile = tiles[grid[row * columns + column]];
            if !tile.floor {
                continue;
            }
            let x0 = params.min_corner.x + column as f32 * cell;
            let z0 = params.min_corner.z + row as f32 * cell;
            let x1 = x0 + cell;
            let z1 = z0 + cell;
            let base = params.min_corner.y;
            let floor_top = base + floor_thickness;
            blocks.push(box_block(
                Vec3::new(x0, base, z0),
                Vec3::new(x1, floor_top, z1),
                params.floor_material,
            ));
            let wall_top = floor_top + wall_height;
            let mut wall = |min: Vec3, max: Vec3| {
                blocks.push(box_block(min, max, params.wall_material));
            };
            if tile.sockets[0] == 0 {
                wall(
                    Vec3::new(x0, floor_top, z0),
                    Vec3::new(x1, wall_top, z0 + thickness),
                );
            }
            if tile.sockets[2] == 0 {
                wall(
                    Vec3::new(x0, floor_top, z1 - thickness),
                    Vec3::new(x1, wall_top, z1),
                );
            }
            if tile.sockets[3] == 0 {
                wall(
                    Vec3::new(x0, floor_top, z0),
                    Vec3::new(x0 + thickness, wall_top, z1),
                );
            }
            if tile.sockets[1] == 0 {
                wall(
                    Vec3::new(x1 - thickness, floor_top, z0),
                    Vec3::new(x1, wall_top, z1),
                );
            }
        }
    }
    blocks
}

#[derive(Clone, Copy, PartialEq)]
enum CityCell {
    Road,
    Plot,
    Park,
}

#[derive(Clone, Copy)]
pub struct WfcCityParams {
    pub min_corner: Vec3,
    pub columns: u32,
    pub rows: u32,
    pub cell: f32,
    pub road_width: f32,
    pub max_stories: u32,
    pub story_height: f32,
    pub wall_thickness: f32,
    pub floor_thickness: f32,
    pub window_spacing: f32,
    pub seed: u32,
    pub road_material: usize,
    pub park_material: usize,
    pub floor_material: usize,
    pub wall_material: usize,
}

/// The city tile set: socket 1 is a road connection, 0 is none. Road tiles form
/// a connected network; plot and park tiles sit off the roads.
fn city_tiles() -> (Vec<[u8; 4]>, Vec<f32>, Vec<CityCell>) {
    let mut sockets = Vec::new();
    let mut weights = Vec::new();
    let mut kinds = Vec::new();
    let mut push = |socket: [u8; 4], weight: f32, kind: CityCell| {
        sockets.push(socket);
        weights.push(weight);
        kinds.push(kind);
    };
    push([1, 1, 1, 1], 0.1, CityCell::Road);
    for socket in [[1, 1, 1, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]] {
        push(socket, 0.2, CityCell::Road);
    }
    push([1, 0, 1, 0], 0.8, CityCell::Road);
    push([0, 1, 0, 1], 0.8, CityCell::Road);
    for socket in [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [1, 0, 0, 1]] {
        push(socket, 0.4, CityCell::Road);
    }
    push([0, 0, 0, 0], 8.0, CityCell::Plot);
    push([0, 0, 0, 0], 1.5, CityCell::Park);
    (sockets, weights, kinds)
}

/// A large city laid out by wave function collapse: roads collapse into a
/// connected grid network, and the remaining plots fill with seeded buildings
/// while parks leave open green space.
pub fn generate_wfc_city(params: &WfcCityParams) -> Vec<PlacedBlock> {
    let columns = params.columns.max(2) as usize;
    let rows = params.rows.max(2) as usize;
    let (sockets, weights, kinds) = city_tiles();
    let mut grid = None;
    for attempt in 0..24 {
        if let Some(solution) = wfc_solve(
            columns,
            rows,
            &sockets,
            &weights,
            params.seed.wrapping_add(attempt),
        ) {
            grid = Some(solution);
            break;
        }
    }
    let Some(grid) = grid else {
        return Vec::new();
    };

    let cell = params.cell.max(6.0);
    let road = params.road_width.clamp(1.0, cell * 0.4);
    let floor_thickness = params.floor_thickness.max(0.1);
    let mut state = params.seed.max(1);
    let mut blocks = Vec::new();
    for row in 0..rows {
        for column in 0..columns {
            let kind = kinds[grid[row * columns + column]];
            let x0 = params.min_corner.x + column as f32 * cell;
            let z0 = params.min_corner.z + row as f32 * cell;
            let base = params.min_corner.y;
            let ground_top = base + floor_thickness;
            let ground_material = match kind {
                CityCell::Road => params.road_material,
                CityCell::Park => params.park_material,
                CityCell::Plot => params.floor_material,
            };
            blocks.push(box_block(
                Vec3::new(x0, base, z0),
                Vec3::new(x0 + cell, ground_top, z0 + cell),
                ground_material,
            ));
            if kind == CityCell::Plot {
                let footprint = Vec2::new(cell - road * 2.0, cell - road * 2.0);
                let stories =
                    1 + (rng_unit(&mut state) * (params.max_stories.max(1) as f32)) as u32;
                blocks.extend(generate_building(&BuildingParams {
                    min_corner: Vec3::new(x0 + road, ground_top, z0 + road),
                    footprint,
                    stories: stories.max(1),
                    story_height: params.story_height,
                    wall_thickness: params.wall_thickness,
                    floor_thickness,
                    window_spacing: params.window_spacing,
                    floor_material: params.floor_material,
                    wall_material: params.wall_material,
                    stair_material: (params.wall_material + 3) % PROTOTYPE_MATERIAL_COUNT,
                }));
            }
        }
    }
    blocks
}

fn block_world_aabb(block: &PlacedBlock) -> (Vec3, Vec3) {
    let full = shape_full_extent(block.shape);
    let half = Vec3::new(
        block.scale.x * full.x * 0.5,
        block.scale.y * full.y * 0.5,
        block.scale.z * full.z * 0.5,
    );
    (block.translation - half, block.translation + half)
}

fn aabb_overlap(a: (Vec3, Vec3), b: (Vec3, Vec3)) -> bool {
    let margin = 0.02;
    a.0.x < b.1.x - margin
        && a.1.x > b.0.x + margin
        && a.0.y < b.1.y - margin
        && a.1.y > b.0.y + margin
        && a.0.z < b.1.z - margin
        && a.1.z > b.0.z + margin
}

fn vec_min(a: Vec3, b: Vec3) -> Vec3 {
    Vec3::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z))
}

fn vec_max(a: Vec3, b: Vec3) -> Vec3 {
    Vec3::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z))
}

fn entity_world_aabb(world: &World, entity: Entity) -> Option<(Vec3, Vec3)> {
    let bounding_volume = world.core.get_bounding_volume(entity)?;
    let global = world.core.get_global_transform(entity)?;
    let corners = bounding_volume.transform(&global.0).obb.get_corners();
    let mut min = corners[0];
    let mut max = corners[0];
    for corner in corners {
        min = vec_min(min, corner);
        max = vec_max(max, corner);
    }
    Some((min, max))
}

/// Find a grid-aligned offset that keeps the batch from overlapping existing
/// geometry, searching outward in rings from the requested position.
fn find_clear_offset(
    editor_world: &EditorWorld,
    world: &World,
    blocks: &[PlacedBlock],
    step: f32,
) -> Vec3 {
    let mut batch_min = None;
    let mut batch_max = Vec3::zeros();
    for block in blocks {
        let (min, max) = block_world_aabb(block);
        match batch_min {
            None => {
                batch_min = Some(min);
                batch_max = max;
            }
            Some(current) => {
                batch_min = Some(vec_min(current, min));
                batch_max = vec_max(batch_max, max);
            }
        }
    }
    let Some(batch_min) = batch_min else {
        return Vec3::zeros();
    };
    let existing: Vec<(Vec3, Vec3)> = world
        .core
        .query_entities(nightshade::ecs::world::BOUNDING_VOLUME)
        .filter(|entity| !editor_world.resources.editor_scene.is_scaffolding(*entity))
        .filter_map(|entity| entity_world_aabb(world, entity))
        .collect();
    if existing.is_empty() {
        return Vec3::zeros();
    }
    let size = batch_max - batch_min;
    let clearance = step.max(1.0);
    let cell_x = size.x + clearance;
    let cell_z = size.z + clearance;
    for ring in 0..=24_i32 {
        for column in -ring..=ring {
            for row in -ring..=ring {
                if column.abs() != ring && row.abs() != ring {
                    continue;
                }
                let offset = Vec3::new(column as f32 * cell_x, 0.0, row as f32 * cell_z);
                let shifted = (batch_min + offset, batch_max + offset);
                if !existing.iter().any(|other| aabb_overlap(shifted, *other)) {
                    return offset;
                }
            }
        }
    }
    Vec3::zeros()
}

/// The data-oriented command surface for greybox authoring. A driver (the
/// editor UI today, an MCP tool later) builds one of these and hands it to
/// [`apply_command`]; nothing here depends on the UI.
pub enum BuildCommand {
    PlaceBlocks(Vec<PlacedBlock>),
    Building(BuildingParams),
    Tower(TowerParams),
    Stairs(StairsParams),
    Columns(ColumnsParams),
    Perimeter(PerimeterParams),
    CityBlock(CityBlockParams),
    Courtyard(CourtyardParams),
    Wfc(WfcParams),
    WfcCity(WfcCityParams),
}

pub fn apply_command(editor_world: &mut EditorWorld, world: &mut World, command: BuildCommand) {
    let relocate = !matches!(command, BuildCommand::PlaceBlocks(_));
    let (mut blocks, label): (Vec<PlacedBlock>, &str) = match command {
        BuildCommand::PlaceBlocks(blocks) => (blocks, "Place blocks"),
        BuildCommand::Building(params) => (generate_building(&params), "Generate building"),
        BuildCommand::Tower(params) => (generate_tower(&params), "Generate tower"),
        BuildCommand::Stairs(params) => (generate_stairs(&params), "Generate stairs"),
        BuildCommand::Columns(params) => (generate_columns(&params), "Generate columns"),
        BuildCommand::Perimeter(params) => (generate_perimeter(&params), "Generate perimeter"),
        BuildCommand::CityBlock(params) => (generate_city_block(&params), "Generate city block"),
        BuildCommand::Courtyard(params) => (generate_courtyard(&params), "Generate courtyard"),
        BuildCommand::Wfc(params) => (generate_wfc(&params), "Generate WFC level"),
        BuildCommand::WfcCity(params) => (generate_wfc_city(&params), "Generate WFC city"),
    };
    if relocate {
        let step = snap_step(editor_world);
        let offset = find_clear_offset(editor_world, world, &blocks, step);
        if offset != Vec3::zeros() {
            for block in &mut blocks {
                block.translation += offset;
            }
        }
    }
    place_blocks(editor_world, world, &blocks, label);
}

pub fn snap_selection_to_grid(editor_world: &mut EditorWorld, world: &mut World) {
    let step = snap_step(editor_world);
    let mut targets: Vec<Entity> = editor_world.resources.ui.selected_entities.clone();
    if targets.is_empty()
        && let Some(entity) = editor_world.resources.ui.selected_entity
    {
        targets.push(entity);
    }
    for entity in targets {
        if let Some(transform) = world.core.get_local_transform_mut(entity) {
            transform.translation = snap_translation(transform.translation, step);
        }
        mark_local_transform_dirty(world, entity);
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            entity,
        );
    }
}

pub fn apply_active_to_selection(editor_world: &mut EditorWorld, world: &mut World) {
    let index = editor_world.resources.build.active_palette;
    let mut targets: Vec<Entity> = editor_world.resources.ui.selected_entities.clone();
    if targets.is_empty()
        && let Some(entity) = editor_world.resources.ui.selected_entity
    {
        targets.push(entity);
    }
    for entity in targets {
        apply_to_entity(world, index, entity);
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            entity,
        );
    }
}

pub fn apply_to_entity(world: &mut World, index: usize, entity: Entity) {
    if world.core.get_material_ref(entity).is_none() {
        return;
    }
    let name = register_palette_material(world, index);
    world.core.set_material_ref(entity, MaterialRef::new(name));
    world.resources.mesh_render_state.request_full_rebuild();
}

fn register_palette_material(world: &mut World, index: usize) -> String {
    let entry = &PALETTE[index % PALETTE.len()];
    let material = build_material(&entry.swatch);
    let name = entry.material_name.to_string();
    material_registry_insert(
        &mut world.resources.assets.material_registry,
        name.clone(),
        material,
    );
    if let Some(&registry_index) = world
        .resources
        .assets
        .material_registry
        .registry
        .name_to_index
        .get(&name)
    {
        registry_add_reference(
            &mut world.resources.assets.material_registry.registry,
            registry_index,
        );
    }
    name
}

fn build_material(swatch: &Swatch) -> Material {
    match swatch {
        Swatch::Prototype { key } => Material {
            base_color: [0.95, 0.95, 0.95, 1.0],
            base_texture: Some((*key).to_string()),
            base_texture_transform: TextureTransform {
                scale: [PROTOTYPE_TILING, PROTOTYPE_TILING],
                ..Default::default()
            },
            roughness: 0.85,
            metallic: 0.0,
            ..Default::default()
        },
        Swatch::Color {
            rgb,
            roughness,
            metallic,
        } => Material {
            base_color: [rgb[0], rgb[1], rgb[2], 1.0],
            roughness: *roughness,
            metallic: *metallic,
            ..Default::default()
        },
        Swatch::Emissive { rgb, strength } => Material {
            base_color: [rgb[0], rgb[1], rgb[2], 1.0],
            emissive_factor: *rgb,
            emissive_strength: *strength,
            roughness: 0.6,
            metallic: 0.0,
            ..Default::default()
        },
    }
}
