use nalgebra_glm::{IVec3, Vec3};

use super::brick_map::{BRICK_CORNER_COUNT, BRICK_SIZE, BrickData, EMPTY_BRICK};
use super::bvh::SdfEditBvh;
use super::clipmap::SdfClipmap;
use super::primitives::{Aabb, SdfEdit};
use super::resources::TerrainConfig;

pub fn evaluate_sdf(edits: &[SdfEdit], point: Vec3) -> (f32, u32) {
    evaluate_sdf_dithered(edits, point, 0.5)
}

pub fn evaluate_sdf_dithered(edits: &[SdfEdit], point: Vec3, dither_threshold: f32) -> (f32, u32) {
    if edits.is_empty() {
        return (f32::MAX, 0);
    }

    let mut distance = f32::MAX;
    let mut material = 0u32;

    for edit in edits {
        let edit_distance = edit.evaluate(point);
        (distance, material) = edit.operation().apply_with_material_dithered(
            distance,
            material,
            edit_distance,
            edit.material_id(),
            dither_threshold,
        );
    }

    (distance, material)
}

pub fn evaluate_sdf_with_terrain_dithered(
    edits: &[SdfEdit],
    terrain: &TerrainConfig,
    point: Vec3,
    dither_threshold: f32,
) -> (f32, u32) {
    let (mut distance, mut material) = if terrain.enabled {
        (terrain.evaluate(point), terrain.material_id)
    } else {
        (f32::MAX, 0)
    };

    for edit in edits {
        let edit_distance = edit.evaluate(point);
        (distance, material) = edit.operation().apply_with_material_dithered(
            distance,
            material,
            edit_distance,
            edit.material_id(),
            dither_threshold,
        );
    }

    (distance, material)
}

struct PendingBrick {
    coord: IVec3,
    level: usize,
    priority: f32,
    voxel_size: f32,
    brick_world_origin: Vec3,
    old_pointer: i32,
}

pub struct GpuBrickDispatch {
    pub brick_coord: IVec3,
    pub level_index: usize,
    pub atlas_slot: u32,
    pub old_pointer: i32,
    pub world_origin: Vec3,
    pub voxel_size: f32,
}

pub fn prepare_gpu_brick_dispatches(
    clipmap: &mut SdfClipmap,
    bvh: &SdfEditBvh,
    terrain: &TerrainConfig,
    max_updates_per_frame: usize,
    camera_position: Vec3,
) -> Vec<GpuBrickDispatch> {
    let mut pending = Vec::new();

    for (level_index, level) in clipmap.levels.iter_mut().enumerate() {
        let dirty = level.take_dirty_bricks();
        let voxel_size = level.voxel_size;
        for coord in dirty {
            let brick_world_origin = level.brick_to_world_origin(coord);
            let distance = nalgebra_glm::length(&(brick_world_origin - camera_position));
            let old_pointer = level.get_pointer(coord);
            pending.push(PendingBrick {
                coord,
                level: level_index,
                priority: distance,
                voxel_size,
                brick_world_origin,
                old_pointer,
            });
        }
    }

    pending.sort_by(|a, b| a.priority.partial_cmp(&b.priority).unwrap());

    let to_process: Vec<_> = pending
        .drain(..max_updates_per_frame.min(pending.len()))
        .collect();

    for remaining in pending {
        if clipmap.levels[remaining.level].is_in_bounds(remaining.coord) {
            clipmap.levels[remaining.level].mark_dirty(remaining.coord);
        }
    }

    let mut dispatches = Vec::with_capacity(to_process.len());
    let mut scratch = Vec::new();

    for brick in to_process {
        if !clipmap.levels[brick.level].is_in_bounds(brick.coord) {
            if brick.old_pointer >= 0 {
                clipmap.allocator.deallocate(brick.old_pointer as u32);
            }
            continue;
        }

        let brick_half_size = brick.voxel_size * BRICK_CORNER_COUNT as f32 * 0.5;
        let brick_center =
            brick.brick_world_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),
        );

        scratch.clear();
        bvh.query_bounds(&brick_bounds.expand(brick.voxel_size * 4.0), &mut scratch);

        let has_terrain_surface = terrain.enabled && {
            let brick_min_y = brick.brick_world_origin.y;
            let brick_max_y = brick_min_y + brick.voxel_size * BRICK_CORNER_COUNT as f32;
            let terrain_min_y = terrain.base_height - terrain.max_surface_extent();
            let terrain_max_y = terrain.base_height + terrain.max_surface_extent();
            terrain_max_y >= brick_min_y && terrain_min_y <= brick_max_y
        };

        if scratch.is_empty() && !has_terrain_surface {
            if brick.old_pointer >= 0 {
                clipmap.allocator.deallocate(brick.old_pointer as u32);
                clipmap.levels[brick.level].set_pointer(brick.coord, EMPTY_BRICK);
            }
            continue;
        }

        let atlas_slot = if brick.old_pointer >= 0 {
            brick.old_pointer as u32
        } else {
            match clipmap.allocator.allocate() {
                Some(slot) => slot,
                None => {
                    clipmap.levels[brick.level].mark_dirty(brick.coord);
                    continue;
                }
            }
        };

        let new_pointer = atlas_slot as i32;
        clipmap.levels[brick.level].set_pointer(brick.coord, new_pointer);

        dispatches.push(GpuBrickDispatch {
            brick_coord: brick.coord,
            level_index: brick.level,
            atlas_slot,
            old_pointer: brick.old_pointer,
            world_origin: brick.brick_world_origin,
            voxel_size: brick.voxel_size,
        });
    }

    dispatches
}

pub fn trilinear_interpolate(brick: &BrickData, local_pos: Vec3) -> f32 {
    let x = local_pos.x.clamp(0.0, BRICK_SIZE as f32);
    let y = local_pos.y.clamp(0.0, BRICK_SIZE as f32);
    let z = local_pos.z.clamp(0.0, BRICK_SIZE as f32);

    let x0 = x.floor() as usize;
    let y0 = y.floor() as usize;
    let z0 = z.floor() as usize;

    let x1 = (x0 + 1).min(BRICK_CORNER_COUNT - 1);
    let y1 = (y0 + 1).min(BRICK_CORNER_COUNT - 1);
    let z1 = (z0 + 1).min(BRICK_CORNER_COUNT - 1);

    let fx = x - x0 as f32;
    let fy = y - y0 as f32;
    let fz = z - z0 as f32;

    let d000 = brick.get_corner_distance(x0, y0, z0);
    let d100 = brick.get_corner_distance(x1, y0, z0);
    let d010 = brick.get_corner_distance(x0, y1, z0);
    let d110 = brick.get_corner_distance(x1, y1, z0);
    let d001 = brick.get_corner_distance(x0, y0, z1);
    let d101 = brick.get_corner_distance(x1, y0, z1);
    let d011 = brick.get_corner_distance(x0, y1, z1);
    let d111 = brick.get_corner_distance(x1, y1, z1);

    let c00 = d000 * (1.0 - fx) + d100 * fx;
    let c10 = d010 * (1.0 - fx) + d110 * fx;
    let c01 = d001 * (1.0 - fx) + d101 * fx;
    let c11 = d011 * (1.0 - fx) + d111 * fx;

    let c0 = c00 * (1.0 - fy) + c10 * fy;
    let c1 = c01 * (1.0 - fy) + c11 * fy;

    c0 * (1.0 - fz) + c1 * fz
}

pub fn trilinear_gradient(brick: &BrickData, local_pos: Vec3, voxel_size: f32) -> Vec3 {
    let x = local_pos.x.clamp(0.0, BRICK_SIZE as f32);
    let y = local_pos.y.clamp(0.0, BRICK_SIZE as f32);
    let z = local_pos.z.clamp(0.0, BRICK_SIZE as f32);

    let x0 = x.floor() as usize;
    let y0 = y.floor() as usize;
    let z0 = z.floor() as usize;

    let x1 = (x0 + 1).min(BRICK_CORNER_COUNT - 1);
    let y1 = (y0 + 1).min(BRICK_CORNER_COUNT - 1);
    let z1 = (z0 + 1).min(BRICK_CORNER_COUNT - 1);

    let fx = x - x0 as f32;
    let fy = y - y0 as f32;
    let fz = z - z0 as f32;

    let d000 = brick.get_corner_distance(x0, y0, z0);
    let d100 = brick.get_corner_distance(x1, y0, z0);
    let d010 = brick.get_corner_distance(x0, y1, z0);
    let d110 = brick.get_corner_distance(x1, y1, z0);
    let d001 = brick.get_corner_distance(x0, y0, z1);
    let d101 = brick.get_corner_distance(x1, y0, z1);
    let d011 = brick.get_corner_distance(x0, y1, z1);
    let d111 = brick.get_corner_distance(x1, y1, z1);

    let y0_x = lerp(d100 - d000, d110 - d010, fy);
    let y1_x = lerp(d101 - d001, d111 - d011, fy);
    let grad_x = lerp(y0_x, y1_x, fz);

    let x0_y = lerp(d010 - d000, d110 - d100, fx);
    let x1_y = lerp(d011 - d001, d111 - d101, fx);
    let grad_y = lerp(x0_y, x1_y, fz);

    let x0_z = lerp(d001 - d000, d101 - d100, fx);
    let x1_z = lerp(d011 - d010, d111 - d110, fx);
    let grad_z = lerp(x0_z, x1_z, fy);

    let gradient = Vec3::new(grad_x, grad_y, grad_z) / voxel_size;
    let len = nalgebra_glm::length(&gradient);
    if len > 0.0001 {
        gradient / len
    } else {
        Vec3::new(0.0, 1.0, 0.0)
    }
}

fn lerp(a: f32, b: f32, t: f32) -> f32 {
    a + (b - a) * t
}
