use pumpkin_data::block_properties::is_air;
use pumpkin_data::{Block, BlockDirection, BlockStateId};
use pumpkin_util::HeightMap;
use std::collections::HashMap;
use std::iter;
use std::sync::LazyLock;

use pumpkin_util::biome::FOLIAGE_NOISE;
use pumpkin_util::math::int_provider::IntProvider;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::random::{RandomGenerator, RandomImpl};

use crate::generation::block_predicate::BlockPredicate;
use crate::generation::height_provider::HeightProvider;
use crate::generation::proto_chunk::GenerationCache;
use crate::world::WorldPortalExt;

use super::configured_features::{CONFIGURED_FEATURES, ConfiguredFeature};

pub static PLACED_FEATURES: LazyLock<
    HashMap<pumpkin_data::placed_feature::PlacedFeature, PlacedFeature>,
> = LazyLock::new(build_placed_features);

pub enum PlacedFeatureWrapper {
    Direct(PlacedFeature),
    Named(pumpkin_data::placed_feature::PlacedFeature),
}

impl PlacedFeatureWrapper {
    #[allow(clippy::expect_used)]
    pub fn get(&self) -> &PlacedFeature {
        match self {
            Self::Named(name) => PLACED_FEATURES.get(name).expect("Unknown placed feature"),
            Self::Direct(feature) => feature,
        }
    }
}

pub struct PlacedFeature {
    /// The name of the configuired feature
    pub feature: Feature,
    pub placement: Vec<PlacementModifier>,
}

pub enum Feature {
    Named(pumpkin_data::configured_feature::ConfiguredFeature),
    Inlined(Box<ConfiguredFeature>),
}

impl PlacedFeature {
    pub fn generate_in_proto_chunk(
        &self,
        chunk: &mut crate::ProtoChunk,
        block_registry: &dyn crate::world::WorldPortalExt,
        feature_name: pumpkin_data::placed_feature::PlacedFeature,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        let Some(feature) = (match &self.feature {
            Feature::Named(name) => CONFIGURED_FEATURES.get(name),
            Feature::Inlined(feature) => Some(feature.as_ref()),
        }) else {
            return false;
        };
        if let ConfiguredFeature::SculkPatch(feature) = feature {
            feature.generate_in_proto_chunk(chunk, random, pos)
        } else {
            let min_y = chunk.generation_bottom_y();
            let height = chunk.generation_height();
            self.generate(
                chunk,
                block_registry,
                min_y,
                height,
                feature_name,
                random,
                pos,
            )
        }
    }

    #[expect(clippy::too_many_arguments)]
    pub fn generate<T: GenerationCache>(
        &self,
        chunk: &mut T,
        block_registry: &dyn WorldPortalExt,
        min_y: i8,
        height: u16,
        feature_name: pumpkin_data::placed_feature::PlacedFeature, // This placed feature
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        let mut stream: Vec<BlockPos> = vec![pos];
        for modifier in &self.placement {
            let mut new_stream = Vec::with_capacity(stream.len());

            for block_pos in stream {
                let positions = modifier.get_positions(
                    chunk,
                    block_registry,
                    min_y,
                    height,
                    feature_name,
                    random,
                    block_pos,
                );
                new_stream.extend(positions);
            }

            stream = new_stream;
        }

        let Some(feature) = (match &self.feature {
            Feature::Named(name) => CONFIGURED_FEATURES.get(name),
            Feature::Inlined(feature) => Some(feature.as_ref()),
        }) else {
            return false;
        };

        let mut ret = false;
        for pos in stream {
            if feature.generate(
                chunk,
                block_registry,
                min_y,
                height,
                feature_name,
                random,
                pos,
            ) {
                ret = true;
            }
        }
        ret
    }
}

pub enum PlacementModifier {
    BlockPredicateFilter(BlockFilterPlacementModifier),
    RarityFilter(RarityFilterPlacementModifier),
    SurfaceRelativeThresholdFilter(SurfaceThresholdFilterPlacementModifier),
    SurfaceWaterDepthFilter(SurfaceWaterDepthFilterPlacementModifier),
    Biome(BiomePlacementModifier),
    Count(CountPlacementModifier),
    NoiseBasedCount(NoiseBasedCountPlacementModifier),
    NoiseThresholdCount(NoiseThresholdCountPlacementModifier),
    CountOnEveryLayer(CountOnEveryLayerPlacementModifier),
    EnvironmentScan(EnvironmentScanPlacementModifier),
    Heightmap(HeightmapPlacementModifier),
    HeightRange(HeightRangePlacementModifier),
    InSquare(SquarePlacementModifier),
    RandomOffset(RandomOffsetPlacementModifier),
    FixedPlacement(Vec<BlockPos>),
}

impl PlacementModifier {
    #[expect(clippy::too_many_arguments)]
    pub fn get_positions<T: GenerationCache>(
        &self,
        chunk: &T,
        block_registry: &dyn WorldPortalExt,
        min_y: i8,
        height: u16,
        feature: pumpkin_data::placed_feature::PlacedFeature,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        match self {
            Self::BlockPredicateFilter(modifier) => {
                modifier.get_positions(block_registry, chunk, feature, random, pos)
            }
            Self::RarityFilter(modifier) => {
                modifier.get_positions(block_registry, chunk, feature, random, pos)
            }
            Self::SurfaceRelativeThresholdFilter(modifier) => {
                modifier.get_positions(block_registry, chunk, feature, random, pos)
            }
            Self::SurfaceWaterDepthFilter(modifier) => {
                modifier.get_positions(block_registry, chunk, feature, random, pos)
            }
            Self::Biome(modifier) => {
                modifier.get_positions(block_registry, chunk, feature, random, pos)
            }
            Self::Count(modifier) => modifier.get_positions(random, pos),
            Self::NoiseBasedCount(modifier) => Box::new(modifier.get_positions(random, pos)),
            Self::NoiseThresholdCount(modifier) => modifier.get_positions(random, pos),
            Self::CountOnEveryLayer(modifier) => modifier.get_positions(random, chunk, pos),
            Self::EnvironmentScan(modifier) => modifier.get_positions(chunk, block_registry, pos),
            Self::Heightmap(modifier) => modifier.get_positions(chunk, min_y, height, random, pos),
            Self::HeightRange(modifier) => modifier.get_positions(min_y, height, random, pos),
            Self::InSquare(_) => SquarePlacementModifier::get_positions(random, pos),
            Self::RandomOffset(modifier) => modifier.get_positions(random, pos),
            Self::FixedPlacement(positions) => Box::new(positions.clone().into_iter()),
        }
    }
}

pub struct NoiseBasedCountPlacementModifier {
    pub to_count_ratio: i32,
    pub factor: f64,
    pub offset: f64,
}

impl CountPlacementModifierBase for NoiseBasedCountPlacementModifier {
    fn get_count(&self, _random: &mut RandomGenerator, pos: BlockPos) -> i32 {
        let noise = FOLIAGE_NOISE.sample(
            pos.0.x as f64 / self.factor,
            pos.0.z as f64 / self.factor,
            false,
        );
        ((noise + self.offset) * self.to_count_ratio as f64).ceil() as i32
    }
}

pub struct NoiseThresholdCountPlacementModifier {
    pub noise_level: f64,
    pub below_noise: i32,
    pub above_noise: i32,
}

impl CountPlacementModifierBase for NoiseThresholdCountPlacementModifier {
    fn get_count(&self, _random: &mut RandomGenerator, pos: BlockPos) -> i32 {
        let noise = FOLIAGE_NOISE.sample(pos.0.x as f64 / 200.0, pos.0.z as f64 / 200.0, false);
        if noise < self.noise_level {
            self.below_noise
        } else {
            self.above_noise
        }
    }
}

pub struct EnvironmentScanPlacementModifier {
    pub direction_of_search: BlockDirection,
    pub target_condition: BlockPredicate,
    pub allowed_search_condition: Option<BlockPredicate>,
    pub max_steps: i32,
}

impl EnvironmentScanPlacementModifier {
    pub fn get_positions<T: GenerationCache>(
        &self,
        chunk: &T,
        block_registry: &dyn WorldPortalExt,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let allowed_search_condition = self
            .allowed_search_condition
            .as_ref()
            .unwrap_or(&BlockPredicate::AlwaysTrue);

        if !allowed_search_condition.test(block_registry, chunk, &pos) {
            return Box::new(iter::empty());
        }
        let mut pos = pos;
        for _ in 0..self.max_steps {
            if self.target_condition.test(block_registry, chunk, &pos) {
                return Box::new(iter::once(pos));
            }
            pos = pos.offset(self.direction_of_search.to_offset());

            if chunk.out_of_height(pos.0.y as i16) {
                return Box::new(iter::empty());
            }

            if !allowed_search_condition.test(block_registry, chunk, &pos) {
                break;
            }
        }
        if self.target_condition.test(block_registry, chunk, &pos) {
            return Box::new(iter::once(pos));
        }

        Box::new(iter::empty())
    }
}

pub struct RandomOffsetPlacementModifier {
    pub xz_spread: IntProvider,
    pub y_spread: IntProvider,
}

impl RandomOffsetPlacementModifier {
    pub fn get_positions(
        &self,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let x = pos.0.x + self.xz_spread.get(random);
        let y = pos.0.y + self.y_spread.get(random);
        let z = pos.0.z + self.xz_spread.get(random);
        Box::new(iter::once(BlockPos(Vector3::new(x, y, z))))
    }
}

pub struct CountOnEveryLayerPlacementModifier {
    pub count: IntProvider,
}

impl CountOnEveryLayerPlacementModifier {
    pub fn get_positions<T: GenerationCache>(
        &self,
        random: &mut RandomGenerator,
        chunk: &T,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let mut positions = Vec::new(); // Using a Vec to collect results, analogous to Stream.builder()
        let mut i = 0; // Represents the 'targetY' in findPos
        let mut bl;

        loop {
            bl = false;
            for _j in 0..self.count.get(random) {
                let x = random.next_bounded_i32(16) + pos.0.x;
                let z = random.next_bounded_i32(16) + pos.0.z;
                let y = chunk.top_motion_blocking_block_height_exclusive(x, z);

                let n = Self::find_pos(chunk, x, y, z, i);

                if n == i32::MAX {
                    continue;
                }
                positions.push(BlockPos::new(x, n, z));
                bl = true;
            }
            i += 1;
            if !bl {
                break;
            }
        }
        Box::new(positions.into_iter())
    }

    fn find_pos<T: GenerationCache>(chunk: &T, x: i32, y: i32, z: i32, target_y: i32) -> i32 {
        let mut mutable_pos = BlockPos::new(x, y, z);
        let mut found_count = 0;
        let mut current_block_state = GenerationCache::get_block_state(chunk, &mutable_pos.0);

        for j in (chunk.bottom_y() as i32 + 1..=y).rev() {
            mutable_pos.0.y = j - 1;
            let next_block_state = GenerationCache::get_block_state(chunk, &mutable_pos.0);

            if !Self::blocks_spawn(next_block_state)
                && Self::blocks_spawn(current_block_state)
                && next_block_state.to_block_id() != Block::BEDROCK
            {
                if found_count == target_y {
                    return mutable_pos.0.y + 1;
                }
                found_count += 1;
            }
            current_block_state = next_block_state;
        }
        i32::MAX
    }

    fn blocks_spawn(state: BlockStateId) -> bool {
        let block = state.to_block_id();
        is_air(state) || block == Block::WATER || block == Block::LAVA
    }
}

pub struct BlockFilterPlacementModifier {
    pub predicate: BlockPredicate,
}

impl ConditionalPlacementModifier for BlockFilterPlacementModifier {
    fn should_place<T: GenerationCache>(
        &self,
        block_registry: &dyn WorldPortalExt,
        _feature: pumpkin_data::placed_feature::PlacedFeature,
        chunk: &T,
        _random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        self.predicate.test(block_registry, chunk, &pos)
    }
}

pub struct SurfaceThresholdFilterPlacementModifier {
    pub heightmap: HeightMap,
    pub min_inclusive: Option<i32>,
    pub max_inclusive: Option<i32>,
}

impl ConditionalPlacementModifier for SurfaceThresholdFilterPlacementModifier {
    fn should_place<T: GenerationCache>(
        &self,
        _block_registry: &dyn WorldPortalExt,
        _feature: pumpkin_data::placed_feature::PlacedFeature,
        chunk: &T,
        _random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        let y = chunk.get_top_y(&self.heightmap, pos.0.x, pos.0.z);
        let min = y.saturating_add(self.min_inclusive.unwrap_or(i32::MIN));
        let max = y.saturating_add(self.max_inclusive.unwrap_or(i32::MAX));
        min <= pos.0.y && pos.0.y <= max
    }
}

pub struct RarityFilterPlacementModifier {
    pub chance: u32,
}

impl ConditionalPlacementModifier for RarityFilterPlacementModifier {
    fn should_place<T: GenerationCache>(
        &self,
        _block_registry: &dyn WorldPortalExt,
        _feature: pumpkin_data::placed_feature::PlacedFeature,
        _chunk: &T,
        random: &mut RandomGenerator,
        _pos: BlockPos,
    ) -> bool {
        random.next_f32() < 1.0 / self.chance as f32
    }
}

pub struct SquarePlacementModifier;

impl SquarePlacementModifier {
    pub fn get_positions(
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let x = random.next_bounded_i32(16) + pos.0.x;
        let z = random.next_bounded_i32(16) + pos.0.z;
        Box::new(iter::once(BlockPos(Vector3::new(x, pos.0.y, z))))
    }
}

pub struct CountPlacementModifier {
    pub count: IntProvider,
}

impl CountPlacementModifierBase for CountPlacementModifier {
    fn get_count(&self, random: &mut RandomGenerator, _pos: BlockPos) -> i32 {
        self.count.get(random)
    }
}

pub struct SurfaceWaterDepthFilterPlacementModifier {
    pub max_water_depth: i32,
}

impl ConditionalPlacementModifier for SurfaceWaterDepthFilterPlacementModifier {
    fn should_place<T: GenerationCache>(
        &self,
        _block_registry: &dyn WorldPortalExt,
        _feature: pumpkin_data::placed_feature::PlacedFeature,
        chunk: &T,
        _random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        let world_top = chunk.top_block_height_exclusive(pos.0.x, pos.0.z);
        let ocean = chunk.ocean_floor_height_exclusive(pos.0.x, pos.0.z);
        world_top - ocean <= self.max_water_depth
    }
}

pub struct BiomePlacementModifier;

impl ConditionalPlacementModifier for BiomePlacementModifier {
    fn should_place<T: GenerationCache>(
        &self,
        _block_registry: &dyn WorldPortalExt,
        this_feature: pumpkin_data::placed_feature::PlacedFeature,
        chunk: &T,
        _random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        let biome = chunk.get_biome_for_terrain_gen(pos.0.x, pos.0.y, pos.0.z);

        for step in biome.features {
            if step.contains(&this_feature) {
                return true;
            }
        }
        false
    }
}

pub struct HeightRangePlacementModifier {
    pub height: HeightProvider,
}

impl HeightRangePlacementModifier {
    pub fn get_positions(
        &self,
        min_y: i8,
        height: u16,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let mut pos = pos;
        pos.0.y = self.height.get(random, min_y, height);
        Box::new(iter::once(pos))
    }
}

pub struct HeightmapPlacementModifier {
    pub heightmap: HeightMap,
}

impl HeightmapPlacementModifier {
    pub fn get_positions<T: GenerationCache>(
        &self,
        chunk: &T,
        min_y: i8,
        _height: u16,
        _random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let x = pos.0.x;
        let z = pos.0.z;
        let top = chunk.get_top_y(&self.heightmap, x, z);
        if top > min_y as i32 {
            return Box::new(iter::once(BlockPos(Vector3::new(x, top, z))));
        }
        Box::new(iter::empty())
    }
}

pub trait CountPlacementModifierBase {
    fn get_positions(
        &self,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        let count = self.get_count(random, pos).max(0); // Vanilla treats negative counts as 0, but `i32 as usize` in Rust would wrap.
        Box::new(std::iter::repeat_n(pos, count as usize))
    }

    fn get_count(&self, random: &mut RandomGenerator, pos: BlockPos) -> i32;
}

pub trait ConditionalPlacementModifier {
    fn get_positions<T: GenerationCache>(
        &self,
        block_registry: &dyn WorldPortalExt,
        chunk: &T,
        feature: pumpkin_data::placed_feature::PlacedFeature,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> Box<dyn Iterator<Item = BlockPos>> {
        if self.should_place(block_registry, feature, chunk, random, pos) {
            Box::new(iter::once(pos))
        } else {
            Box::new(iter::empty())
        }
    }

    fn should_place<T: GenerationCache>(
        &self,
        block_registry: &dyn WorldPortalExt,
        feature: pumpkin_data::placed_feature::PlacedFeature,
        chunk: &T,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool;
}

// generated code is now placed alongside other codegen outputs
// in `src/generated` so it's easier to find when upgrading MC versions.
// the path is relative to this file (up two levels to reach `src`).

/// Returns all placed feature names for tab-completion in `/place feature`.
#[must_use]
pub const fn all_placed_feature_names() -> &'static [&'static str] {
    pumpkin_data::placed_feature::PlacedFeature::all_names()
}
include!("../../../../pumpkin-data/src/generated/placed_features_generated.rs");
