use std::f64::consts;

use pumpkin_data::{Block, BlockId, BlockState, block_properties::is_air};
use pumpkin_util::{
    math::{position::BlockPos, vector3::Vector3},
    random::{RandomGenerator, RandomImpl},
};

use crate::{block::BlockStateCodec, generation::proto_chunk::GenerationCache};

const SEA_LEVEL: i32 = 63; // TODO: use getSeaLevel() instead of hardcoding

pub struct IcebergFeature {
    pub main_block: BlockStateCodec,
}

impl IcebergFeature {
    #[expect(clippy::too_many_lines)]
    pub fn generate<T: GenerationCache>(
        &self,
        chunk: &mut T,
        _min_y: i8,
        _height: u16,
        _feature: pumpkin_data::placed_feature::PlacedFeature,
        random: &mut RandomGenerator,
        pos: BlockPos,
    ) -> bool {
        let origin = Vector3::new(pos.0.x, SEA_LEVEL, pos.0.z);
        let main_block = self.main_block.get_state();

        let snow_on_top = random.next_f64() > 0.7;
        let shape_angle = random.next_f64() * 2.0 * consts::PI;
        let shape_ellipse_a = 11 - random.next_bounded_i32(5);
        let shape_ellipse_c = 3 + random.next_bounded_i32(3);
        let is_ellipse = random.next_f64() > 0.7;

        let mut over_water_height = if is_ellipse {
            random.next_bounded_i32(6) + 6
        } else {
            random.next_bounded_i32(15) + 3
        };
        if !is_ellipse && random.next_f64() > 0.9 {
            over_water_height += random.next_bounded_i32(19) + 7;
        }

        let under_water_height = (over_water_height + random.next_bounded_i32(11)).min(18);
        let width =
            (over_water_height + random.next_bounded_i32(7) - random.next_bounded_i32(5)).min(11);
        let a = if is_ellipse { shape_ellipse_a } else { 11 };

        // Above-water pass
        for xo in -a..a {
            for zo in -a..a {
                for y_off in 0..over_water_height {
                    let radius = if is_ellipse {
                        Self::height_dependent_radius_ellipse(y_off, over_water_height, width)
                    } else {
                        Self::height_dependent_radius_round(random, y_off, over_water_height, width)
                    };
                    if is_ellipse || xo < radius {
                        Self::generate_iceberg_block(
                            chunk,
                            random,
                            &origin,
                            over_water_height,
                            xo,
                            y_off,
                            zo,
                            radius,
                            a,
                            is_ellipse,
                            shape_ellipse_c,
                            shape_angle,
                            snow_on_top,
                            main_block,
                        );
                    }
                }
            }
        }

        Self::smooth(
            chunk,
            &origin,
            width,
            over_water_height,
            is_ellipse,
            shape_ellipse_a,
        );

        // Below-water pass
        for xo in -a..a {
            for zo in -a..a {
                for y_neg in 1..under_water_height {
                    let y_off = -y_neg;
                    let new_a = if is_ellipse {
                        let y_off_sq = (y_off as f64).powi(2) as f32;
                        let denom = under_water_height as f32 * 8.0f32;
                        (a as f32 * (1.0f32 - y_off_sq / denom)).ceil() as i32
                    } else {
                        a
                    };
                    let radius = Self::height_dependent_radius_steep(
                        random,
                        -y_off,
                        under_water_height,
                        width,
                    );
                    if xo < radius {
                        Self::generate_iceberg_block(
                            chunk,
                            random,
                            &origin,
                            under_water_height,
                            xo,
                            y_off,
                            zo,
                            radius,
                            new_a,
                            is_ellipse,
                            shape_ellipse_c,
                            shape_angle,
                            snow_on_top,
                            main_block,
                        );
                    }
                }
            }
        }

        let do_cut_out = if is_ellipse {
            random.next_f64() > 0.1
        } else {
            random.next_f64() > 0.7
        };
        if do_cut_out {
            Self::generate_cut_out(
                chunk,
                random,
                width,
                over_water_height,
                &origin,
                is_ellipse,
                shape_ellipse_a,
                shape_angle,
                shape_ellipse_c,
            );
        }

        true
    }

    #[expect(clippy::too_many_arguments, clippy::useless_let_if_seq)]
    fn generate_cut_out<T: GenerationCache>(
        chunk: &mut T,
        random: &mut RandomGenerator,
        width: i32,
        height: i32,
        global_origin: &Vector3<i32>,
        is_ellipse: bool,
        shape_ellipse_a: i32,
        shape_angle: f64,
        shape_ellipse_c: i32,
    ) {
        let random_sign_x: i32 = if random.next_bool() { -1 } else { 1 };
        let random_sign_z: i32 = if random.next_bool() { -1 } else { 1 };

        let mut x_off = if random.next_bool() {
            width / 2 + 1 - random.next_bounded_i32((width - width / 2 - 1).max(1))
        } else {
            random.next_bounded_i32((width / 2 - 2).max(1))
        };

        let mut z_off = if random.next_bool() {
            width / 2 + 1 - random.next_bounded_i32((width - width / 2 - 1).max(1))
        } else {
            random.next_bounded_i32((width / 2 - 2).max(1))
        };
        if is_ellipse {
            let v = random.next_bounded_i32((shape_ellipse_a - 5).max(1));
            x_off = v;
            z_off = v;
        }

        let local_origin = Vector3::new(random_sign_x * x_off, 0, random_sign_z * z_off);
        let angle = if is_ellipse {
            shape_angle + consts::FRAC_PI_2
        } else {
            random.next_f64() * 2.0 * consts::PI
        };

        // Above-water carve
        for y_off in 0..(height - 3) {
            let radius = Self::height_dependent_radius_round(random, y_off, height, width);
            Self::carve(
                chunk,
                radius,
                y_off,
                global_origin,
                false,
                angle,
                &local_origin,
                shape_ellipse_a,
                shape_ellipse_c,
            );
        }

        // Below-water carve
        let mut y_off = -1i32;
        while y_off > -height + random.next_bounded_i32(5) {
            let radius = Self::height_dependent_radius_steep(random, -y_off, height, width);
            Self::carve(
                chunk,
                radius,
                y_off,
                global_origin,
                true,
                angle,
                &local_origin,
                shape_ellipse_a,
                shape_ellipse_c,
            );
            y_off -= 1;
        }
    }

    #[expect(clippy::too_many_arguments)]
    fn carve<T: GenerationCache>(
        chunk: &mut T,
        radius: i32,
        y_off: i32,
        global_origin: &Vector3<i32>,
        under_water: bool,
        angle: f64,
        local_origin: &Vector3<i32>,
        shape_ellipse_a: i32,
        shape_ellipse_c: i32,
    ) {
        let a = radius + 1 + shape_ellipse_a / 3;
        let c = (radius - 3).min(3) + shape_ellipse_c / 2 - 1;

        for xo in -a..a {
            for zo in -a..a {
                let signed_dist = Self::signed_distance_ellipse(
                    xo,
                    zo,
                    local_origin.x,
                    local_origin.z,
                    a,
                    c,
                    angle,
                );
                if signed_dist < 0.0 {
                    let pos = global_origin.add(&Vector3::new(xo, y_off, zo));
                    let raw = GenerationCache::get_block_state(chunk, &pos);
                    let bid = raw.to_block_id();
                    if Self::is_iceberg_state(bid) || bid == Block::SNOW_BLOCK.id {
                        if under_water {
                            chunk.set_block_state(&pos, Block::WATER.default_state);
                        } else {
                            chunk.set_block_state(&pos, Block::AIR.default_state);
                            Self::remove_floating_snow_layer(chunk, &pos);
                        }
                    }
                }
            }
        }
    }

    fn remove_floating_snow_layer<T: GenerationCache>(chunk: &mut T, pos: &Vector3<i32>) {
        let above = pos.add(&Vector3::new(0, 1, 0));
        let raw = GenerationCache::get_block_state(chunk, &above);
        if raw.to_block_id() == Block::SNOW.id {
            chunk.set_block_state(&above, Block::AIR.default_state);
        }
    }

    #[expect(clippy::too_many_arguments)]
    fn generate_iceberg_block<T: GenerationCache>(
        chunk: &mut T,
        random: &mut RandomGenerator,
        origin: &Vector3<i32>,
        height: i32,
        xo: i32,
        y_off: i32,
        zo: i32,
        radius: i32,
        a: i32,
        is_ellipse: bool,
        shape_ellipse_c: i32,
        shape_angle: f64,
        snow_on_top: bool,
        main_block: &'static BlockState,
    ) {
        let signed_dist = if is_ellipse {
            Self::signed_distance_ellipse(
                xo,
                zo,
                0,
                0,
                a,
                Self::get_ellipse_c(y_off, height, shape_ellipse_c),
                shape_angle,
            )
        } else {
            Self::signed_distance_circle(xo, zo, 0, 0, radius, random)
        };

        if signed_dist < 0.0 {
            let pos = origin.add(&Vector3::new(xo, y_off, zo));
            let compare_val: f64 = if is_ellipse {
                -0.5
            } else {
                -6.0 - random.next_bounded_i32(3) as f64
            };
            if signed_dist > compare_val && random.next_f64() > 0.9 {
                return;
            }
            Self::set_iceberg_block(
                chunk,
                random,
                &pos,
                height - y_off,
                height,
                is_ellipse,
                snow_on_top,
                main_block,
            );
        }
    }

    #[expect(clippy::too_many_arguments)]
    fn set_iceberg_block<T: GenerationCache>(
        chunk: &mut T,
        random: &mut RandomGenerator,
        pos: &Vector3<i32>,
        h_diff: i32,
        height: i32,
        is_ellipse: bool,
        snow_on_top: bool,
        main_block: &'static BlockState,
    ) {
        let state_id = GenerationCache::get_block_state(chunk, pos);
        let bid = state_id.to_block_id();
        if is_air(state_id)
            || bid == Block::SNOW_BLOCK.id
            || bid == Block::ICE.id
            || bid == Block::WATER.id
        {
            let randomness = !is_ellipse || random.next_f64() > 0.05;
            let divisor = if is_ellipse { 3 } else { 2 };
            let threshold =
                random.next_bounded_i32((height / divisor).max(1)) as f64 + height as f64 * 0.6;
            if snow_on_top && bid != Block::WATER.id && h_diff as f64 <= threshold && randomness {
                chunk.set_block_state(pos, Block::SNOW_BLOCK.default_state);
            } else {
                chunk.set_block_state(pos, main_block);
            }
        }
    }

    fn smooth<T: GenerationCache>(
        chunk: &mut T,
        origin: &Vector3<i32>,
        width: i32,
        height: i32,
        is_ellipse: bool,
        shape_ellipse_a: i32,
    ) {
        let a = if is_ellipse {
            shape_ellipse_a
        } else {
            width / 2
        };
        for x in -a..=a {
            for z in -a..=a {
                for y_off in 0..=height {
                    let pos = origin.add(&Vector3::new(x, y_off, z));
                    let raw = GenerationCache::get_block_state(chunk, &pos);
                    let bid = raw.to_block_id();
                    if Self::is_iceberg_state(bid) || bid == Block::SNOW.id {
                        let below = pos.add(&Vector3::new(0, -1, 0));
                        let below_state_id = GenerationCache::get_block_state(chunk, &below);
                        if is_air(below_state_id) {
                            chunk.set_block_state(&pos, Block::AIR.default_state);
                            let above = pos.add(&Vector3::new(0, 1, 0));
                            chunk.set_block_state(&above, Block::AIR.default_state);
                        } else if Self::is_iceberg_state(bid) {
                            let sides = [
                                pos.add(&Vector3::new(-1, 0, 0)), // west
                                pos.add(&Vector3::new(1, 0, 0)),  // east
                                pos.add(&Vector3::new(0, 0, -1)), // north
                                pos.add(&Vector3::new(0, 0, 1)),  // south
                            ];
                            let non_iceberg_count = sides
                                .iter()
                                .filter(|side| {
                                    !Self::is_iceberg_state(
                                        GenerationCache::get_block_state(chunk, side).to_block_id(),
                                    )
                                })
                                .count();
                            if non_iceberg_count >= 3 {
                                chunk.set_block_state(&pos, Block::AIR.default_state);
                            }
                        }
                    }
                }
            }
        }
    }

    const fn get_ellipse_c(y_off: i32, height: i32, shape_ellipse_c: i32) -> i32 {
        if y_off > 0 && height - y_off <= 3 {
            return shape_ellipse_c - (4 - (height - y_off));
        }
        shape_ellipse_c
    }

    fn signed_distance_circle(
        xo: i32,
        zo: i32,
        ox: i32,
        oz: i32,
        radius: i32,
        random: &mut RandomGenerator,
    ) -> f64 {
        let off = 10.0f32 * random.next_f32().clamp(0.2, 0.8) / radius as f32;
        off as f64 + ((xo - ox) as f64).powi(2) + ((zo - oz) as f64).powi(2)
            - (radius as f64).powi(2)
    }

    fn signed_distance_ellipse(
        xo: i32,
        zo: i32,
        ox: i32,
        oz: i32,
        a: i32,
        c: i32,
        angle: f64,
    ) -> f64 {
        let dx = (xo - ox) as f64;
        let dz = (zo - oz) as f64;
        let cos = angle.cos();
        let sin = angle.sin();
        ((dx * cos - dz * sin) / a as f64).powi(2) + ((dx * sin + dz * cos) / c as f64).powi(2)
            - 1.0
    }

    fn height_dependent_radius_round(
        random: &mut RandomGenerator,
        y_off: i32,
        height: i32,
        width: i32,
    ) -> i32 {
        let k = 3.5f32 - random.next_f32();
        let y_off_sq = (y_off as f64).powi(2) as f32;
        let scale = if height > 15 + random.next_bounded_i32(5) {
            let temp_y_off = if y_off < 3 + random.next_bounded_i32(6) {
                y_off / 2
            } else {
                y_off
            };
            (1.0f32 - temp_y_off as f32 / (height as f32 * k * 0.4f32)) * width as f32
        } else {
            (1.0f32 - y_off_sq / (height as f32 * k)) * width as f32
        };

        (scale / 2.0f32).ceil() as i32
    }

    fn height_dependent_radius_ellipse(y_off: i32, height: i32, width: i32) -> i32 {
        let y_off_sq = (y_off as f64).powi(2) as f32;
        let scale = (1.0f32 - y_off_sq / height as f32) * width as f32;
        (scale / 2.0f32).ceil() as i32
    }

    fn height_dependent_radius_steep(
        random: &mut RandomGenerator,
        y_off: i32,
        height: i32,
        width: i32,
    ) -> i32 {
        let k = 1.0f32 + random.next_f32() / 2.0f32;
        let scale = (1.0f32 - y_off as f32 / (height as f32 * k)) * width as f32;
        (scale / 2.0f32).ceil() as i32
    }

    const fn is_iceberg_state(id: BlockId) -> bool {
        matches!(
            id,
            BlockId::PACKED_ICE | BlockId::SNOW_BLOCK | BlockId::BLUE_ICE
        )
    }
}
