use std::collections::HashSet;

use nalgebra_glm::{IVec3, Vec3};

pub const BRICK_SIZE: usize = 8;
pub const BRICK_CORNER_COUNT: usize = 9;
pub const GRID_SIZE: usize = 128;

#[cfg(target_arch = "wasm32")]
pub const ATLAS_SIZE: usize = 360;

#[cfg(not(target_arch = "wasm32"))]
pub const ATLAS_SIZE: usize = 450;

pub const BRICKS_PER_ATLAS_DIM: usize = ATLAS_SIZE / BRICK_CORNER_COUNT;
pub const MAX_BRICKS: usize = BRICKS_PER_ATLAS_DIM * BRICKS_PER_ATLAS_DIM * BRICKS_PER_ATLAS_DIM;

pub const EMPTY_BRICK: i32 = -1;

#[derive(Clone)]
pub struct BrickData {
    pub distances: [f32; BRICK_CORNER_COUNT * BRICK_CORNER_COUNT * BRICK_CORNER_COUNT],
    pub material_ids: [u32; BRICK_SIZE * BRICK_SIZE * BRICK_SIZE],
}

impl Default for BrickData {
    fn default() -> Self {
        Self {
            distances: [f32::MAX; BRICK_CORNER_COUNT * BRICK_CORNER_COUNT * BRICK_CORNER_COUNT],
            material_ids: [0; BRICK_SIZE * BRICK_SIZE * BRICK_SIZE],
        }
    }
}

impl BrickData {
    pub fn get_corner_distance(&self, x: usize, y: usize, z: usize) -> f32 {
        self.distances[x + y * BRICK_CORNER_COUNT + z * BRICK_CORNER_COUNT * BRICK_CORNER_COUNT]
    }

    pub fn set_corner_distance(&mut self, x: usize, y: usize, z: usize, distance: f32) {
        self.distances[x + y * BRICK_CORNER_COUNT + z * BRICK_CORNER_COUNT * BRICK_CORNER_COUNT] =
            distance;
    }

    pub fn has_surface(&self) -> bool {
        let mut has_positive = false;
        let mut has_negative = false;

        for &distance in &self.distances {
            if distance > 0.0 {
                has_positive = true;
            } else if distance < 0.0 {
                has_negative = true;
            }
            if has_positive && has_negative {
                return true;
            }
        }
        false
    }
}

#[derive(Clone)]
pub struct BrickPointerGrid {
    pub pointers: Vec<i32>,
    pub origin_brick: IVec3,
    pub voxel_size: f32,
    pub dirty_bricks: HashSet<(i32, i32, i32)>,
    pub allocated_coords: HashSet<(i32, i32, i32)>,
    pub pointer_dirty: bool,
}

impl BrickPointerGrid {
    pub fn new(voxel_size: f32) -> Self {
        Self {
            pointers: vec![EMPTY_BRICK; GRID_SIZE * GRID_SIZE * GRID_SIZE],
            origin_brick: IVec3::zeros(),
            voxel_size,
            dirty_bricks: HashSet::new(),
            allocated_coords: HashSet::new(),
            pointer_dirty: true,
        }
    }

    pub fn world_to_brick_coord(&self, world_pos: Vec3) -> IVec3 {
        let brick_world_size = self.voxel_size * BRICK_SIZE as f32;
        IVec3::new(
            (world_pos.x / brick_world_size).floor() as i32,
            (world_pos.y / brick_world_size).floor() as i32,
            (world_pos.z / brick_world_size).floor() as i32,
        )
    }

    pub fn brick_to_world_origin(&self, brick_coord: IVec3) -> Vec3 {
        let brick_world_size = self.voxel_size * BRICK_SIZE as f32;
        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,
        )
    }

    fn wrap_coord(&self, coord: IVec3) -> IVec3 {
        let grid_size = GRID_SIZE as i32;
        IVec3::new(
            ((coord.x % grid_size) + grid_size) % grid_size,
            ((coord.y % grid_size) + grid_size) % grid_size,
            ((coord.z % grid_size) + grid_size) % grid_size,
        )
    }

    fn coord_to_index(&self, wrapped_coord: IVec3) -> usize {
        wrapped_coord.x as usize
            + wrapped_coord.y as usize * GRID_SIZE
            + wrapped_coord.z as usize * GRID_SIZE * GRID_SIZE
    }

    pub fn get_pointer(&self, brick_coord: IVec3) -> i32 {
        let relative = brick_coord - self.origin_brick;
        let grid_size = GRID_SIZE as i32;

        if relative.x < 0
            || relative.x >= grid_size
            || relative.y < 0
            || relative.y >= grid_size
            || relative.z < 0
            || relative.z >= grid_size
        {
            return EMPTY_BRICK;
        }

        let wrapped = self.wrap_coord(brick_coord);
        self.pointers[self.coord_to_index(wrapped)]
    }

    pub fn set_pointer(&mut self, brick_coord: IVec3, pointer: i32) {
        let wrapped = self.wrap_coord(brick_coord);
        let index = self.coord_to_index(wrapped);
        let old_pointer = self.pointers[index];
        if old_pointer != pointer {
            self.pointers[index] = pointer;
            self.pointer_dirty = true;
        }

        let coord_tuple = (brick_coord.x, brick_coord.y, brick_coord.z);
        if pointer != EMPTY_BRICK && old_pointer == EMPTY_BRICK {
            self.allocated_coords.insert(coord_tuple);
        } else if pointer == EMPTY_BRICK && old_pointer != EMPTY_BRICK {
            self.allocated_coords.remove(&coord_tuple);
        }
    }

    pub fn mark_dirty(&mut self, brick_coord: IVec3) {
        self.dirty_bricks
            .insert((brick_coord.x, brick_coord.y, brick_coord.z));
    }

    pub fn take_dirty_bricks(&mut self) -> Vec<IVec3> {
        let result: Vec<IVec3> = self
            .dirty_bricks
            .iter()
            .map(|&(x, y, z)| IVec3::new(x, y, z))
            .collect();
        self.dirty_bricks.clear();
        result
    }

    pub fn world_extent(&self) -> f32 {
        self.voxel_size * BRICK_SIZE as f32 * GRID_SIZE as f32
    }

    pub fn is_in_bounds(&self, brick_coord: IVec3) -> bool {
        let relative = brick_coord - self.origin_brick;
        let grid_size = GRID_SIZE as i32;
        relative.x >= 0
            && relative.x < grid_size
            && relative.y >= 0
            && relative.y < grid_size
            && relative.z >= 0
            && relative.z < grid_size
    }

    pub fn get_allocated_brick_coords(&self) -> impl Iterator<Item = IVec3> + '_ {
        let grid_size = GRID_SIZE as i32;
        (0..grid_size).flat_map(move |z| {
            (0..grid_size).flat_map(move |y| {
                (0..grid_size).filter_map(move |x| {
                    let coord = IVec3::new(
                        self.origin_brick.x + x,
                        self.origin_brick.y + y,
                        self.origin_brick.z + z,
                    );
                    if self.get_pointer(coord) != EMPTY_BRICK {
                        Some(coord)
                    } else {
                        None
                    }
                })
            })
        })
    }
}

pub struct BrickAllocator {
    free_list: Vec<u32>,
    next_slot: u32,
    max_bricks: u32,
}

impl BrickAllocator {
    pub fn new(max_bricks: u32) -> Self {
        Self {
            free_list: Vec::new(),
            next_slot: 0,
            max_bricks,
        }
    }

    pub fn allocate(&mut self) -> Option<u32> {
        if let Some(slot) = self.free_list.pop() {
            return Some(slot);
        }
        if self.next_slot < self.max_bricks {
            let slot = self.next_slot;
            self.next_slot += 1;
            return Some(slot);
        }
        None
    }

    pub fn deallocate(&mut self, slot: u32) {
        self.free_list.push(slot);
    }

    pub fn allocated_count(&self) -> u32 {
        self.next_slot - self.free_list.len() as u32
    }

    pub fn capacity(&self) -> u32 {
        self.max_bricks
    }
}

pub fn brick_index_to_atlas_coord(brick_index: u32) -> IVec3 {
    let bricks_per_row = BRICKS_PER_ATLAS_DIM as u32;
    let bricks_per_slice = bricks_per_row * bricks_per_row;

    let z = brick_index / bricks_per_slice;
    let remainder = brick_index % bricks_per_slice;
    let y = remainder / bricks_per_row;
    let x = remainder % bricks_per_row;

    IVec3::new(
        (x * BRICK_CORNER_COUNT as u32) as i32,
        (y * BRICK_CORNER_COUNT as u32) as i32,
        (z * BRICK_CORNER_COUNT as u32) as i32,
    )
}
