use nalgebra_glm::{IVec3, Vec3};

use super::brick_map::{BRICK_SIZE, BrickAllocator, BrickPointerGrid, EMPTY_BRICK, GRID_SIZE};
use super::bvh::SdfEditBvh;
use super::primitives::Aabb;
use super::resources::TerrainConfig;

pub const DEFAULT_LEVEL_COUNT: usize = 10;
pub const BASE_VOXEL_SIZE: f32 = 0.125;

pub struct SdfClipmap {
    pub levels: Vec<BrickPointerGrid>,
    pub center: Vec3,
    pub allocator: BrickAllocator,
}

impl SdfClipmap {
    pub fn new(level_count: usize, base_voxel_size: f32, max_bricks: u32) -> Self {
        let mut levels = Vec::with_capacity(level_count);
        for level_index in 0..level_count {
            let voxel_size = base_voxel_size * (1 << level_index) as f32;
            levels.push(BrickPointerGrid::new(voxel_size));
        }

        Self {
            levels,
            center: Vec3::zeros(),
            allocator: BrickAllocator::new(max_bricks),
        }
    }

    pub fn update_center(&mut self, new_center: Vec3) {
        self.update_center_with_bvh_and_terrain(new_center, None, None);
    }

    pub fn update_center_with_bvh(&mut self, new_center: Vec3, bvh: Option<&SdfEditBvh>) {
        self.update_center_with_bvh_and_terrain(new_center, bvh, None);
    }

    pub fn update_center_with_bvh_and_terrain(
        &mut self,
        new_center: Vec3,
        bvh: Option<&SdfEditBvh>,
        terrain: Option<&TerrainConfig>,
    ) {
        let grid_size = GRID_SIZE as i32;

        for (level_index, level) in self.levels.iter_mut().enumerate() {
            let voxel_size = BASE_VOXEL_SIZE * (1 << level_index) as f32;
            let brick_world_size = voxel_size * BRICK_SIZE as f32;

            let new_origin_brick = IVec3::new(
                ((new_center.x / brick_world_size).floor() as i32) - (grid_size / 2),
                ((new_center.y / brick_world_size).floor() as i32) - (grid_size / 2),
                ((new_center.z / brick_world_size).floor() as i32) - (grid_size / 2),
            );

            let old_origin = level.origin_brick;
            let delta = new_origin_brick - old_origin;
            let is_first_population =
                level.allocated_coords.is_empty() && level.dirty_bricks.is_empty();

            if delta.x != 0 || delta.y != 0 || delta.z != 0 || is_first_population {
                let freed_bricks = collect_scrolled_out_bricks(level, old_origin, new_origin_brick);
                let had_freed_bricks = !freed_bricks.is_empty();
                for brick_coord in freed_bricks {
                    let pointer = level.get_pointer(brick_coord);
                    if pointer != EMPTY_BRICK {
                        self.allocator.deallocate(pointer as u32);
                        level.set_pointer(brick_coord, EMPTY_BRICK);
                    }
                }
                if had_freed_bricks {
                    level.pointer_dirty = true;
                }

                let mut scratch = Vec::new();
                let has_bvh = bvh.filter(|b| !b.is_empty()).is_some();

                if is_first_population && has_bvh {
                    for brick_z in new_origin_brick.z..(new_origin_brick.z + grid_size) {
                        for brick_y in new_origin_brick.y..(new_origin_brick.y + grid_size) {
                            for brick_x in new_origin_brick.x..(new_origin_brick.x + grid_size) {
                                let brick_coord = IVec3::new(brick_x, brick_y, brick_z);
                                let brick_origin = Vec3::new(
                                    brick_coord.x as f32 * brick_world_size,
                                    brick_coord.y as f32 * brick_world_size,
                                    brick_coord.z as f32 * brick_world_size,
                                );

                                let brick_half_size = brick_world_size * 0.5;
                                let brick_center = brick_origin
                                    + Vec3::new(brick_half_size, brick_half_size, brick_half_size);
                                let brick_bounds = Aabb::from_center_half_extents(
                                    brick_center,
                                    Vec3::new(brick_half_size, brick_half_size, brick_half_size),
                                );

                                if let Some(bvh) = bvh.filter(|b| !b.is_empty()) {
                                    scratch.clear();
                                    bvh.query_bounds(
                                        &brick_bounds.expand(voxel_size * 4.0),
                                        &mut scratch,
                                    );
                                    if !scratch.is_empty() {
                                        level.mark_dirty(brick_coord);
                                    }
                                }
                            }
                        }
                    }
                } else if has_bvh {
                    let scroll_ranges =
                        collect_scrolled_in_brick_ranges(old_origin, new_origin_brick);

                    for (axis, &(primary_start, primary_end)) in scroll_ranges.iter().enumerate() {
                        if primary_start >= primary_end {
                            continue;
                        }

                        let (other1_start, other1_end, other2_start, other2_end) = match axis {
                            0 => (
                                new_origin_brick.y,
                                new_origin_brick.y + grid_size,
                                new_origin_brick.z,
                                new_origin_brick.z + grid_size,
                            ),
                            1 => (
                                new_origin_brick.x,
                                new_origin_brick.x + grid_size,
                                new_origin_brick.z,
                                new_origin_brick.z + grid_size,
                            ),
                            _ => (
                                new_origin_brick.x,
                                new_origin_brick.x + grid_size,
                                new_origin_brick.y,
                                new_origin_brick.y + grid_size,
                            ),
                        };

                        for primary in primary_start..primary_end {
                            for other1 in other1_start..other1_end {
                                for other2 in other2_start..other2_end {
                                    let brick_coord = match axis {
                                        0 => IVec3::new(primary, other1, other2),
                                        1 => IVec3::new(other1, primary, other2),
                                        _ => IVec3::new(other1, other2, primary),
                                    };

                                    let brick_origin = Vec3::new(
                                        brick_coord.x as f32 * brick_world_size,
                                        brick_coord.y as f32 * brick_world_size,
                                        brick_coord.z as f32 * brick_world_size,
                                    );

                                    let brick_half_size = brick_world_size * 0.5;
                                    let brick_center = brick_origin
                                        + Vec3::new(
                                            brick_half_size,
                                            brick_half_size,
                                            brick_half_size,
                                        );
                                    let brick_bounds = Aabb::from_center_half_extents(
                                        brick_center,
                                        Vec3::new(
                                            brick_half_size,
                                            brick_half_size,
                                            brick_half_size,
                                        ),
                                    );

                                    if let Some(bvh) = bvh.filter(|b| !b.is_empty()) {
                                        scratch.clear();
                                        bvh.query_bounds(
                                            &brick_bounds.expand(voxel_size * 4.0),
                                            &mut scratch,
                                        );
                                        if !scratch.is_empty() {
                                            level.mark_dirty(brick_coord);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                level.origin_brick = new_origin_brick;
            }
        }

        self.center = new_center;
    }

    pub fn mark_dirty_in_bounds(&mut self, bounds: &Aabb) {
        let grid_size = super::brick_map::GRID_SIZE as i32;

        for level in &mut self.levels {
            let expanded = bounds.expand(level.voxel_size * 4.0);
            let brick_min = level.world_to_brick_coord(expanded.min);
            let brick_max = level.world_to_brick_coord(expanded.max);

            let clamped_min = IVec3::new(
                brick_min.x.max(level.origin_brick.x),
                brick_min.y.max(level.origin_brick.y),
                brick_min.z.max(level.origin_brick.z),
            );
            let clamped_max = IVec3::new(
                brick_max.x.min(level.origin_brick.x + grid_size - 1),
                brick_max.y.min(level.origin_brick.y + grid_size - 1),
                brick_max.z.min(level.origin_brick.z + grid_size - 1),
            );

            for z in clamped_min.z..=clamped_max.z {
                for y in clamped_min.y..=clamped_max.y {
                    for x in clamped_min.x..=clamped_max.x {
                        let coord = IVec3::new(x, y, z);
                        level.mark_dirty(coord);
                    }
                }
            }
        }
    }

    pub fn level_world_extent(&self, level_index: usize) -> f32 {
        if level_index < self.levels.len() {
            self.levels[level_index].world_extent()
        } else {
            0.0
        }
    }

    pub fn select_level_for_distance(&self, distance_from_center: f32) -> usize {
        for (level_index, level) in self.levels.iter().enumerate() {
            let half_extent = level.world_extent() * 0.5;
            if distance_from_center < half_extent * 0.85 {
                return level_index;
            }
        }
        self.levels.len().saturating_sub(1)
    }

    pub fn level_count(&self) -> usize {
        self.levels.len()
    }

    pub fn voxel_sizes(&self) -> Vec<f32> {
        self.levels.iter().map(|level| level.voxel_size).collect()
    }

    pub fn mark_all_dirty(&mut self) {
        let grid_size = GRID_SIZE as i32;
        for level in &mut self.levels {
            for z in 0..grid_size {
                for y in 0..grid_size {
                    for x in 0..grid_size {
                        let coord = IVec3::new(
                            level.origin_brick.x + x,
                            level.origin_brick.y + y,
                            level.origin_brick.z + z,
                        );
                        level.mark_dirty(coord);
                    }
                }
            }
        }
    }

    pub fn force_reinitialize(&mut self) {
        for level in &mut self.levels {
            let coords: Vec<_> = level.allocated_coords.iter().copied().collect();
            let had_bricks = !coords.is_empty();
            for (x, y, z) in coords {
                let coord = IVec3::new(x, y, z);
                let pointer = level.get_pointer(coord);
                if pointer != EMPTY_BRICK {
                    self.allocator.deallocate(pointer as u32);
                    level.set_pointer(coord, EMPTY_BRICK);
                }
            }
            level.dirty_bricks.clear();
            if had_bricks {
                level.pointer_dirty = true;
            }
        }
    }

    pub fn mark_all_levels_pointer_dirty(&mut self) {
        for level in &mut self.levels {
            level.pointer_dirty = true;
        }
    }
}

fn collect_scrolled_out_bricks(
    level: &BrickPointerGrid,
    _old_origin: IVec3,
    new_origin: IVec3,
) -> Vec<IVec3> {
    let grid_size = GRID_SIZE as i32;

    level
        .allocated_coords
        .iter()
        .filter_map(|&(x, y, z)| {
            let relative_x = x - new_origin.x;
            let relative_y = y - new_origin.y;
            let relative_z = z - new_origin.z;

            if relative_x < 0
                || relative_x >= grid_size
                || relative_y < 0
                || relative_y >= grid_size
                || relative_z < 0
                || relative_z >= grid_size
            {
                Some(IVec3::new(x, y, z))
            } else {
                None
            }
        })
        .collect()
}

fn collect_scrolled_in_brick_ranges(old_origin: IVec3, new_origin: IVec3) -> [(i32, i32); 3] {
    let grid_size = GRID_SIZE as i32;
    let mut ranges = [(0i32, 0i32); 3];

    for axis in 0..3 {
        let delta = new_origin[axis] - old_origin[axis];

        if delta == 0 {
            ranges[axis] = (0, 0);
            continue;
        }

        let (range_start, range_end) = if delta > 0 {
            (
                new_origin[axis] + grid_size - delta.min(grid_size),
                new_origin[axis] + grid_size,
            )
        } else {
            (new_origin[axis], new_origin[axis] - delta.max(-grid_size))
        };

        ranges[axis] = (range_start, range_end);
    }

    ranges
}

pub struct LevelBlendInfo {
    pub level: usize,
    pub blend_factor: f32,
}

impl SdfClipmap {
    pub fn get_blend_info(&self, world_pos: Vec3) -> LevelBlendInfo {
        let diff = world_pos - self.center;
        let distance_from_center = diff.x.abs().max(diff.y.abs()).max(diff.z.abs());

        let level = self.select_level_for_distance(distance_from_center);
        let level_extent = self.level_world_extent(level);
        let half_extent = level_extent * 0.5;
        let transition_width = level_extent * 0.1;

        let edge_distance = half_extent - distance_from_center;
        let blend_factor = (edge_distance / transition_width).clamp(0.0, 1.0);

        LevelBlendInfo {
            level,
            blend_factor,
        }
    }
}
