use nalgebra_glm::{IVec3, Vec3};
use rayon::prelude::*;

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;

fn position_hash(pos: Vec3) -> f32 {
    let x = (pos.x * 127.1 + pos.y * 311.7 + pos.z * 74.7).sin() * 43758.5453;
    x.fract().abs()
}

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(
    edits: &[SdfEdit],
    terrain: &TerrainConfig,
    point: Vec3,
) -> (f32, u32) {
    evaluate_sdf_with_terrain_dithered(edits, terrain, point, 0.5)
}

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)
}

pub fn evaluate_sdf_culled(edits: &[SdfEdit], point: Vec3, search_radius: f32) -> (f32, u32) {
    let point_bounds = Aabb::from_center_half_extents(
        point,
        Vec3::new(search_radius, search_radius, search_radius),
    );

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

    for edit in edits {
        let edit_bounds = edit.effective_bounds();
        if !point_bounds.intersects(&edit_bounds.expand(search_radius)) {
            continue;
        }

        let edit_distance = edit.evaluate(point);
        (distance, material) = edit.operation().apply_with_material(
            distance,
            material,
            edit_distance,
            edit.material_id(),
        );
    }

    (distance, material)
}

pub fn evaluate_sdf_with_bvh(
    edits: &[SdfEdit],
    bvh: &SdfEditBvh,
    point: Vec3,
    scratch: &mut Vec<usize>,
) -> (f32, u32) {
    if edits.is_empty() || bvh.is_empty() {
        return (f32::MAX, 0);
    }

    bvh.query_point(point, scratch);

    if scratch.is_empty() {
        return (f32::MAX, 0);
    }

    scratch.sort_unstable();

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

    for &edit_idx in scratch.iter() {
        let edit = &edits[edit_idx];
        let edit_distance = edit.evaluate(point);
        (distance, material) = edit.operation().apply_with_material(
            distance,
            material,
            edit_distance,
            edit.material_id(),
        );
    }

    (distance, material)
}

pub fn populate_brick(
    edits: &[SdfEdit],
    brick_world_origin: Vec3,
    voxel_size: f32,
) -> Option<BrickData> {
    let mut brick = BrickData::default();
    let mut has_surface = false;
    let mut first_sign: Option<bool> = None;

    for z in 0..BRICK_CORNER_COUNT {
        for y in 0..BRICK_CORNER_COUNT {
            for x in 0..BRICK_CORNER_COUNT {
                let corner_world_pos =
                    brick_world_origin + Vec3::new(x as f32, y as f32, z as f32) * voxel_size;

                let (distance, _material) = evaluate_sdf(edits, corner_world_pos);
                brick.set_corner_distance(x, y, z, distance);

                let is_positive = distance > 0.0;
                match first_sign {
                    None => first_sign = Some(is_positive),
                    Some(first) if first != is_positive => has_surface = true,
                    _ => {}
                }
            }
        }
    }

    for z in 0..BRICK_SIZE {
        for y in 0..BRICK_SIZE {
            for x in 0..BRICK_SIZE {
                let center_world_pos = brick_world_origin
                    + Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5) * voxel_size;

                let dither = position_hash(center_world_pos);
                let (_distance, material) = evaluate_sdf_dithered(edits, center_world_pos, dither);
                brick.material_ids[x + y * BRICK_SIZE + z * BRICK_SIZE * BRICK_SIZE] = material;
            }
        }
    }

    if has_surface { Some(brick) } else { None }
}

pub fn populate_brick_with_bvh(
    edits: &[SdfEdit],
    bvh: &SdfEditBvh,
    brick_world_origin: Vec3,
    voxel_size: f32,
) -> Option<BrickData> {
    let mut brick = BrickData::default();
    let mut has_surface = false;
    let mut first_sign: Option<bool> = None;

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

    let mut relevant_edit_indices = Vec::new();
    bvh.query_bounds(
        &brick_bounds.expand(voxel_size * 4.0),
        &mut relevant_edit_indices,
    );

    if relevant_edit_indices.is_empty() {
        return None;
    }

    for z in 0..BRICK_CORNER_COUNT {
        for y in 0..BRICK_CORNER_COUNT {
            for x in 0..BRICK_CORNER_COUNT {
                let corner_world_pos =
                    brick_world_origin + Vec3::new(x as f32, y as f32, z as f32) * voxel_size;

                let (distance, _material) =
                    evaluate_sdf_subset(edits, &relevant_edit_indices, corner_world_pos);
                brick.set_corner_distance(x, y, z, distance);

                let is_positive = distance > 0.0;
                match first_sign {
                    None => first_sign = Some(is_positive),
                    Some(first) if first != is_positive => has_surface = true,
                    _ => {}
                }
            }
        }
    }

    if !has_surface {
        return None;
    }

    for z in 0..BRICK_SIZE {
        for y in 0..BRICK_SIZE {
            for x in 0..BRICK_SIZE {
                let center_world_pos = brick_world_origin
                    + Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5) * voxel_size;

                let dither = position_hash(center_world_pos);
                let (_distance, material) = evaluate_sdf_subset_dithered(
                    edits,
                    &relevant_edit_indices,
                    center_world_pos,
                    dither,
                );
                brick.material_ids[x + y * BRICK_SIZE + z * BRICK_SIZE * BRICK_SIZE] = material;
            }
        }
    }

    Some(brick)
}

fn evaluate_sdf_subset(edits: &[SdfEdit], indices: &[usize], point: Vec3) -> (f32, u32) {
    evaluate_sdf_subset_dithered(edits, indices, point, 0.5)
}

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

    let mut sorted_indices: Vec<usize> = indices.to_vec();
    sorted_indices.sort_unstable();

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

    for &edit_idx in &sorted_indices {
        let edit = &edits[edit_idx];
        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)
}

fn evaluate_sdf_subset_with_terrain(
    edits: &[SdfEdit],
    indices: &[usize],
    terrain: &TerrainConfig,
    point: Vec3,
) -> (f32, u32) {
    evaluate_sdf_subset_with_terrain_dithered(edits, indices, terrain, point, 0.5)
}

fn evaluate_sdf_subset_with_terrain_dithered(
    edits: &[SdfEdit],
    indices: &[usize],
    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)
    };

    let mut sorted_indices: Vec<usize> = indices.to_vec();
    sorted_indices.sort_unstable();

    for &edit_idx in &sorted_indices {
        let edit = &edits[edit_idx];
        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 populate_brick_with_terrain(
    edits: &[SdfEdit],
    bvh: &SdfEditBvh,
    terrain: &TerrainConfig,
    brick_world_origin: Vec3,
    voxel_size: f32,
) -> Option<BrickData> {
    let mut brick = BrickData::default();
    let mut has_surface = false;
    let mut first_sign: Option<bool> = None;

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

    let mut relevant_edit_indices = Vec::new();
    bvh.query_bounds(
        &brick_bounds.expand(voxel_size * 4.0),
        &mut relevant_edit_indices,
    );

    let has_terrain_surface = if terrain.enabled {
        let brick_min_y = brick_world_origin.y;
        let brick_max_y = brick_world_origin.y + voxel_size * BRICK_CORNER_COUNT as f32;
        terrain.height >= brick_min_y && terrain.height <= brick_max_y
    } else {
        false
    };

    if relevant_edit_indices.is_empty() && !has_terrain_surface {
        return None;
    }

    for z in 0..BRICK_CORNER_COUNT {
        for y in 0..BRICK_CORNER_COUNT {
            for x in 0..BRICK_CORNER_COUNT {
                let corner_world_pos =
                    brick_world_origin + Vec3::new(x as f32, y as f32, z as f32) * voxel_size;

                let (distance, _material) = evaluate_sdf_subset_with_terrain(
                    edits,
                    &relevant_edit_indices,
                    terrain,
                    corner_world_pos,
                );
                brick.set_corner_distance(x, y, z, distance);

                let is_positive = distance > 0.0;
                match first_sign {
                    None => first_sign = Some(is_positive),
                    Some(first) if first != is_positive => has_surface = true,
                    _ => {}
                }
            }
        }
    }

    if !has_surface {
        return None;
    }

    for z in 0..BRICK_SIZE {
        for y in 0..BRICK_SIZE {
            for x in 0..BRICK_SIZE {
                let center_world_pos = brick_world_origin
                    + Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5) * voxel_size;

                let dither = position_hash(center_world_pos);
                let (_distance, material) = evaluate_sdf_subset_with_terrain_dithered(
                    edits,
                    &relevant_edit_indices,
                    terrain,
                    center_world_pos,
                    dither,
                );
                brick.material_ids[x + y * BRICK_SIZE + z * BRICK_SIZE * BRICK_SIZE] = material;
            }
        }
    }

    Some(brick)
}

pub struct BrickUpdateResult {
    pub brick_coord: IVec3,
    pub level_index: usize,
    pub brick_data: Option<BrickData>,
    pub old_pointer: i32,
    pub new_pointer: i32,
    pub world_origin: Vec3,
    pub voxel_size: f32,
}

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

struct EvaluatedBrick {
    coord: IVec3,
    level: usize,
    brick_data: Option<BrickData>,
    old_pointer: i32,
    world_origin: Vec3,
    voxel_size: f32,
}

pub fn update_dirty_bricks(
    clipmap: &mut SdfClipmap,
    edits: &[SdfEdit],
    bvh: &SdfEditBvh,
    terrain: &TerrainConfig,
    max_updates_per_frame: usize,
    camera_position: Vec3,
) -> Vec<BrickUpdateResult> {
    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 {
        clipmap.levels[remaining.level].mark_dirty(remaining.coord);
    }

    let terrain_copy = *terrain;

    let evaluated: Vec<EvaluatedBrick> = to_process
        .into_par_iter()
        .map(|pending_brick| {
            let brick_data = populate_brick_with_terrain(
                edits,
                bvh,
                &terrain_copy,
                pending_brick.brick_world_origin,
                pending_brick.voxel_size,
            );
            EvaluatedBrick {
                coord: pending_brick.coord,
                level: pending_brick.level,
                brick_data,
                old_pointer: pending_brick.old_pointer,
                world_origin: pending_brick.brick_world_origin,
                voxel_size: pending_brick.voxel_size,
            }
        })
        .collect();

    let mut results = Vec::with_capacity(evaluated.len());

    for eval in evaluated {
        let new_pointer = match (&eval.brick_data, eval.old_pointer) {
            (Some(_), EMPTY_BRICK) => match clipmap.allocator.allocate() {
                Some(slot) => slot as i32,
                None => {
                    clipmap.levels[eval.level].mark_dirty(eval.coord);
                    continue;
                }
            },
            (Some(_), old) => old,
            (None, EMPTY_BRICK) => EMPTY_BRICK,
            (None, old) => {
                clipmap.allocator.deallocate(old as u32);
                EMPTY_BRICK
            }
        };

        clipmap.levels[eval.level].set_pointer(eval.coord, new_pointer);

        results.push(BrickUpdateResult {
            brick_coord: eval.coord,
            level_index: eval.level,
            brick_data: eval.brick_data,
            old_pointer: eval.old_pointer,
            new_pointer,
            world_origin: eval.world_origin,
            voxel_size: eval.voxel_size,
        });
    }

    results
}

pub fn mark_edits_dirty(clipmap: &mut SdfClipmap, edits: &[SdfEdit]) {
    for edit in edits {
        let bounds = edit.effective_bounds();
        clipmap.mark_dirty_in_bounds(&bounds);
    }
}

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
}
