use super::Point;

#[derive(Debug)]
pub struct SpatialIndex<T> {
    elements: Vec<Vec<T>>,
    /// The number of cells between a cell and the cell right below it.
    ///
    /// For a table of 100x100 with cell_size = 10, number_cells_x is 11 (100/10+1)
    number_cells_x: i32,
    cell_size: i32,
    size: u32,
    max_point: Point,
}

impl<T> SpatialIndex<T>
where
    T: std::fmt::Debug,
{
    pub fn new(max_point: Point, cell_size: i32) -> SpatialIndex<T> {
        // Here we add 1 to number cells x and y to handle the right and bottom edges where
        // x = max_point.x or y = max_point.y
        // For example, with max_point of (100,100)
        // the point 100,100 is valid but 100/10 = 10, which is out of bounds of a zero based
        // array of length 10. So we add 1 to the length and 10 is a valid index in a zero based
        // array of length 11
        let number_cells_x = (max_point.x / cell_size) + 1;
        let number_cells = number_cells_x * ((max_point.y / cell_size) + 1);
        let mut elements = Vec::with_capacity(number_cells as usize);
        for _ in 0..number_cells {
            elements.push(Vec::new());
        }
        SpatialIndex {
            elements,
            cell_size,
            number_cells_x,
            size: 0,
            max_point,
        }
    }

    /// Adds an element in the cell of point
    ///
    /// panics if the point is outside of the grid
    pub fn add(&mut self, point: &Point, element: T) {
        let element_index = self.get_index_from_position(point);
        self.elements
            .get_mut(element_index as usize)
            .unwrap()
            .push(element);
        self.size += 1;
    }

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

    /// Fetches elements in the cell corresponding to point and all the adjacent cells
    /// filtering returned elements with filter
    ///
    /// May panic or return wrong values if the point is outside this SpatialIndex max point
    pub fn get_surrounding_elements_with_filter<'a, F>(
        self: &'a mut Self,
        point: &Point,
        mut filter: F,
    ) -> Vec<*mut T>
    where
        F: FnMut(&T) -> bool,
    {
        let surrounding_indices: Vec<usize> = self.get_surrounding_indices(point);
        let mut surrounding_elements: Vec<*mut T> = Vec::new();
        for i in surrounding_indices.iter() {
            println!("elements len {}", self.elements.len());
            let elements_for_index = self
                .elements
                .get_mut(*i as usize)
                .expect(
                    format!(
                        "point {:?} is in range for spatial_index with max_point {:?}, currently getting index {}, among surrounding indices {:?}",
                        point, self.max_point, i, surrounding_indices
                    )
                    .as_str(),
                );
            let elements_for_index = elements_for_index
                .iter_mut()
                .filter(|el| filter(el))
                .map(|el| el as *mut T);
            surrounding_elements.extend(elements_for_index);
        }
        surrounding_elements
    }

    fn get_index_from_position(&self, point: &Point) -> usize {
        ((point.x / self.cell_size) + ((point.y / self.cell_size) * self.number_cells_x)) as usize
    }

    fn get_surrounding_indices(&self, point: &Point) -> Vec<usize> {
        let element_index = self.get_index_from_position(point);
        let mut indices = Vec::from([element_index]);
        let row_offset = self.number_cells_x as usize;

        let last_cell_x_start = self.max_point.x - (self.max_point.x % self.cell_size);
        let last_row_y_min = self.max_point.y - (self.max_point.y % self.cell_size);

        // top row
        if point.y >= self.cell_size {
            // top left
            if point.x >= self.cell_size {
                indices.push(element_index - row_offset - 1);
            }
            // top middle
            indices.push(element_index - row_offset);
            // top right
            if point.x < last_cell_x_start {
                indices.push(element_index - row_offset + 1);
            }
        }

        // middle left
        if point.x >= self.cell_size {
            indices.push(element_index - 1);
        }
        // middle middle can be skipped, already added

        // middle right
        if point.x < last_cell_x_start {
            indices.push(element_index + 1);
        }

        if point.y < last_row_y_min {
            // bottom left
            if point.x >= self.cell_size {
                indices.push(element_index + row_offset - 1);
            }

            // bottom middle
            indices.push(element_index + row_offset);
            // bottom right
            if point.x < last_cell_x_start {
                indices.push(element_index + row_offset + 1);
            }
        }

        indices
    }
}

#[cfg(test)]
mod test {
    use super::*;

    fn assert_vec_values(actual: Vec<*mut usize>, expected: Vec<usize>) {
        let resolved_actual: Vec<usize>;
        unsafe {
            resolved_actual = actual.iter().map(|n| **n).collect();
        }

        assert_eq!(resolved_actual, expected);
    }

    #[test]
    fn when_no_element_surrounding_nodes_returns_empty_vec() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        assert_eq!(
            index.get_surrounding_elements_with_filter(&Point::new((0, 0)), |_| true),
            Vec::<*mut usize>::new()
        );
    }

    #[test]
    fn added_point_is_surrounding_itself() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((50, 50)), 132);
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((50, 50)), |_| true),
            vec![132],
        );
    }

    #[test]
    fn multiple_points_in_every_surrounding_cell() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((40, 40)), 4040);
        index.add(&Point::new((45, 45)), 4040);
        index.add(&Point::new((50, 40)), 5040);
        index.add(&Point::new((60, 40)), 6040);
        index.add(&Point::new((40, 50)), 4050);
        index.add(&Point::new((50, 50)), 5050);
        index.add(&Point::new((60, 50)), 6050);
        index.add(&Point::new((50, 50)), 5050);
        index.add(&Point::new((51, 51)), 5151);
        index.add(&Point::new((60, 60)), 6060);
        index.add(&Point::new((40, 60)), 4060);
        index.add(&Point::new((50, 60)), 5060);
        index.add(&Point::new((60, 60)), 6060);
        assert_eq!(
            index
                .get_surrounding_elements_with_filter(&Point::new((50, 50)), |_| true)
                .sort(),
            vec![
                &4040, &4545, &5040, &6040, &4050, &5050, &6050, &5050, &5151, &6060, &4060, &5060,
                &6060,
            ]
            .sort()
        );
    }

    #[test]
    fn point_on_top_edge_is_close_to_first_cell() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((50, 0)), 132);
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((50, 9)), |_| true),
            vec![132],
        );
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((50, 0)), |_| true),
            vec![132],
        );
    }

    #[test]
    fn point_on_bottom_edge_is_close_to_bottom_cell() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((50, 100)), 132);
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((50, 95)), |_| true),
            vec![132],
        );
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((50, 100)), |_| true),
            vec![132],
        );
    }

    #[test]
    fn point_on_right_edge_is_close_to_first_cell() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((100, 50)), 132);
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((95, 50)), |_| true),
            vec![132],
        );
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((100, 50)), |_| true),
            vec![132],
        );
    }
    #[test]
    fn point_on_left_edge_is_close_to_first_cell() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((0, 50)), 132);
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((9, 50)), |_| true),
            vec![132],
        );
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((0, 50)), |_| true),
            vec![132],
        );
    }

    #[test]
    fn when_elements_too_far_surrounding_is_empty() {
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((100, 100)), 10);
        index.add(&Point::new((0, 0)), 132);
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((99, 99)), |_| true),
            Vec::<usize>::new(),
        );
    }

    #[test]
    fn works_when_max_point_is_not_multiple_of_cell_size() {
        let cell_size = 100;
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((1920, 580)), cell_size);
        for x_cell in 0..19 as usize {
            for y_cell in 0..5 as usize {
                index.add(
                    &Point::new((x_cell as i32 * cell_size, y_cell as i32 * cell_size)),
                    x_cell * 100 + y_cell,
                );
            }
        }
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((1833, 502)), |_| true),
            vec![1905, 1904, 1805, 1804]
        );
    }

    #[test]
    fn works_when_max_point_is_not_multiple_of_cell_size() {
        let cell_size = 100;
        let mut index: SpatialIndex<usize> = SpatialIndex::new(Point::new((1920, 580)), cell_size);
        for x_cell in 0..19 as usize {
            for y_cell in 0..5 as usize {
                index.add(
                    &Point::new((x_cell as i32 * cell_size, y_cell as i32 * cell_size)),
                    x_cell * 100 + y_cell,
                );
            }
        }
        assert_vec_values(
            index.get_surrounding_elements_with_filter(&Point::new((1833, 502)), |_| true),
            vec![1905, 1904, 1805, 1804]
        );
    }
}
