use pumpkin_data::{
    structures::{Structure, StructureKeys},
    tag::{RegistryKey, get_tag_ids},
};

use crate::{
    ProtoChunk,
    biome::BiomeSupplier,
    generation::{
        biome_coords,
        noise::router::multi_noise_sampler::MultiNoiseSampler,
        structure::structures::{
            StructureGenerator, StructureGeneratorContext, StructurePosition,
            buried_treasure::BuriedTreasureGenerator,
            create_chunk_random,
            desert_pyramid::DesertPyramidGenerator,
            end_city::EndCityGenerator,
            igloo::IglooGenerator,
            jigsaw::JigsawGenerator,
            jungle_temple::JungleTempleGenerator,
            mansion::MansionGenerator,
            mineshaft::MineshaftGenerator,
            nether_fortress::NetherFortressGenerator,
            nether_fossil::NetherFossilGenerator,
            ocean_monument::{OceanMonumentGenerator, has_valid_biomes},
            ocean_ruin::OceanRuinGenerator,
            ruined_portal::RuinedPortalGenerator,
            shipwreck::ShipwreckGenerator,
            stronghold::StrongholdGenerator,
            swamp_hut::SwampHutGenerator,
        },
    },
};

pub(crate) mod height_sampler;
pub mod piece;
pub mod placement;
pub mod shiftable_piece;
pub mod structures;
pub mod template;

/// Creates a structure position by dispatching to the appropriate generator.
/// Does NOT perform biome validation — callers that require it should add
/// their own check after calling this function.
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn generate_structure_position(
    key: &StructureKeys,
    structure: &Structure,
    context: StructureGeneratorContext<'_>,
) -> Option<StructurePosition> {
    match key {
        StructureKeys::BuriedTreasure => {
            BuriedTreasureGenerator::get_structure_position(&BuriedTreasureGenerator, context)
        }
        StructureKeys::SwampHut => {
            SwampHutGenerator::get_structure_position(&SwampHutGenerator, context)
        }
        StructureKeys::Stronghold => {
            StrongholdGenerator::get_structure_position(&StrongholdGenerator, context)
        }
        StructureKeys::Fortress => {
            NetherFortressGenerator::get_structure_position(&NetherFortressGenerator, context)
        }
        StructureKeys::NetherFossil => {
            NetherFossilGenerator::get_structure_position(&NetherFossilGenerator, context)
        }
        StructureKeys::Igloo => IglooGenerator::get_structure_position(&IglooGenerator, context),
        StructureKeys::DesertPyramid => DesertPyramidGenerator.get_structure_position(context),
        StructureKeys::JunglePyramid => JungleTempleGenerator.get_structure_position(context),
        StructureKeys::VillagePlains
        | StructureKeys::VillageDesert
        | StructureKeys::VillageSavanna
        | StructureKeys::VillageSnowy
        | StructureKeys::VillageTaiga => {
            let (Some(start_pool), Some(size)) = (structure.start_pool, structure.size) else {
                return None;
            };
            let generator = JigsawGenerator::new(start_pool, size);
            generator.get_structure_position(context)
        }
        StructureKeys::AncientCity
        | StructureKeys::BastionRemnant
        | StructureKeys::PillagerOutpost
        | StructureKeys::TrailRuins
        | StructureKeys::TrialChambers => {
            let (Some(start_pool), Some(size)) = (structure.start_pool, structure.size) else {
                return None;
            };
            let mut generator = JigsawGenerator::new(start_pool, size);
            if *key == StructureKeys::PillagerOutpost {
                generator = generator.with_expansion_hack(true);
            }
            if let Some(start_jigsaw_name) = structure.start_jigsaw_name {
                generator = generator.with_start_jigsaw(start_jigsaw_name);
            }
            generator.get_structure_position(context)
        }
        StructureKeys::Shipwreck | StructureKeys::ShipwreckBeached => {
            let generator = ShipwreckGenerator {
                is_beached: *key == StructureKeys::ShipwreckBeached,
            };
            generator.get_structure_position(context)
        }
        StructureKeys::RuinedPortal
        | StructureKeys::RuinedPortalDesert
        | StructureKeys::RuinedPortalJungle
        | StructureKeys::RuinedPortalSwamp
        | StructureKeys::RuinedPortalMountain
        | StructureKeys::RuinedPortalOcean
        | StructureKeys::RuinedPortalNether => {
            let generator = RuinedPortalGenerator { variant: *key };
            generator.get_structure_position(context)
        }
        StructureKeys::OceanRuinCold | StructureKeys::OceanRuinWarm => {
            let generator = OceanRuinGenerator {
                is_warm: *key == StructureKeys::OceanRuinWarm,
            };
            generator.get_structure_position(context)
        }
        StructureKeys::EndCity => EndCityGenerator.get_structure_position(context),
        StructureKeys::Mansion => MansionGenerator.get_structure_position(context),
        StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context),
        StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => {
            let generator = MineshaftGenerator {
                is_mesa: *key == StructureKeys::MineshaftMesa,
            };
            generator.get_structure_position(context)
        }
    }
}

#[must_use]
pub fn try_generate_structure(
    key: &StructureKeys,
    structure: &Structure,
    seed: i64,
    chunk: &ProtoChunk,
    sea_level: i32,
    height_sampler: Option<&mut dyn crate::generation::structure::structures::HeightSampler>,
) -> Option<StructurePosition> {
    let random = create_chunk_random(seed, chunk.x, chunk.z);
    let context = StructureGeneratorContext {
        seed,
        chunk_x: chunk.x,
        chunk_z: chunk.z,
        random,
        sea_level,
        min_y: chunk.bottom_y() as i32,
        height_sampler,
        structure_key: Some(*key),
    };
    let structure_pos = generate_structure_position(key, structure, context);

    if let Some(pos) = structure_pos {
        // Get the biome at the structure's starting position.
        // Clamp biome Y to the chunk's valid range — structure start_pos.y may exceed
        // the chunk's logical height (e.g. nether fossils use full height 256 but
        // ProtoChunk only covers logical_height 128).
        let biome_y = biome_coords::from_block(pos.start_pos.0.y);
        let biome_height = (chunk.height() >> 2) as i32;
        let biome_bottom = biome_coords::from_block(chunk.bottom_y() as i32);
        let clamped_biome_y = biome_y.clamp(biome_bottom, biome_bottom + biome_height - 1);

        let current_biome = chunk.get_biome_id(
            biome_coords::from_block(pos.start_pos.0.x),
            clamped_biome_y,
            biome_coords::from_block(pos.start_pos.0.z),
        ) as u16;

        let biomes = get_tag_ids(
            RegistryKey::WorldgenBiome,
            structure
                .biomes
                .strip_prefix('#')
                .unwrap_or(structure.biomes),
        )?;

        // Check if the biome is allowed for this structure
        if biomes.contains(&current_biome) {
            return Some(pos);
        }
    }

    None
}

#[must_use]
pub fn lazily_generate_structure(
    key: &StructureKeys,
    structure: &Structure,
    mut context: StructureGeneratorContext, // Replaces 5 separate arguments!
    biome_supplier: &dyn BiomeSupplier,
    multi_noise_sampler: &mut MultiNoiseSampler,
) -> Option<StructurePosition> {
    if *key == StructureKeys::Monument {
        let center_x = crate::generation::positions::chunk_pos::get_center_x(context.chunk_x);
        let center_z = crate::generation::positions::chunk_pos::get_center_z(context.chunk_z);
        let start_y = context
            .height_sampler
            .as_deref_mut()
            .map_or(context.sea_level, |sampler| {
                sampler.estimate_ocean_floor_height(center_x, center_z)
            });
        if !has_valid_biomes(
            biome_supplier,
            multi_noise_sampler,
            context.chunk_x,
            context.chunk_z,
            context.sea_level,
            start_y,
        ) {
            return None;
        }
    }

    let structure_pos = generate_structure_position(key, structure, context);

    if let Some(pos) = structure_pos {
        // Get the biome mathematically, bypassing the chunk boundaries entirely!
        let biome_x = biome_coords::from_block(pos.start_pos.0.x);
        let biome_y = biome_coords::from_block(pos.start_pos.0.y);
        let biome_z = biome_coords::from_block(pos.start_pos.0.z);

        let biome = biome_supplier.biome(biome_x, biome_y, biome_z, multi_noise_sampler);

        if let Some(biomes) = get_tag_ids(
            RegistryKey::WorldgenBiome,
            structure
                .biomes
                .strip_prefix('#')
                .unwrap_or(structure.biomes),
        ) && biomes.contains(&(biome.id as u16))
        {
            return Some(pos);
        }
    }

    None
}
