use super::math::calculate_new_node_position;
use super::spatial_index::SpatialIndex;
use super::Attraction;
use super::{Attractor, Node, Point};
use log::info;
use rand::thread_rng;
use rand::Rng;
use std::collections::HashMap;

pub struct SpaceColonization<F>
where
    F: Fn(&Node, &Node),
{
    max_point: Point,
    /// When a node grows within kill_distance of an attractor, the attractor is killed
    kill_distance: f64,
    /// Maximum distance between an attractor and a node for the node to
    /// be affected by the attractor.
    ///
    /// Must be greater than 2 * sqrt((density)**2 + (density)**2) + kill_distance
    /// The edge case that must be covered is :
    ///
    /// - There are two attractors and one node like this :
    ///   ```text
    ///   ---------------
    ///   |A     |      |
    ///   |      |      |
    ///   |      |      |
    ///   ---------------
    ///   |      |      |
    ///   |      |      |
    ///   |      |     A|
    ///   --------------- N
    ///   ```
    ///
    /// - An attractor is at the top-left corner of its cell
    /// - The other attractor is at the bottom-right corner of its cell
    /// - The Node is placed at the bottom-right limit of the kill distance of the bottom-right
    /// attractor
    /// - On the next iteration, the bottom-right attractor will be dead so the nearest attractor
    /// is ( 2 * cell\_diagonal + kill\_distance ) away.
    attraction_distance: i32,
    segment_length: u16,
    /// Size of the cells on which attractors are placed.
    ///
    /// If density is 10, then there will be an average distance of 10 between attractors
    density: i32,
    pub attractors: SpatialIndex<Attractor>,
    render_fn: F,
}

impl<F> SpaceColonization<F>
where
    F: Fn(&Node, &Node),
{
    pub fn new(width: i32, height: i32, render_fn: F) -> SpaceColonization<F>
    where
        F: Fn(&Node, &Node),
    {
        let attraction_distance = 100;
        let attractors = SpatialIndex::new(Point::new((width, height)), attraction_distance);

        let mut sc = SpaceColonization {
            max_point: Point {
                x: width,
                y: height,
            },
            kill_distance: 5.0,
            attraction_distance,
            segment_length: 5,
            density: 30,
            attractors,
            render_fn,
        };

        sc.place_attractors();

        return sc;
    }

    #[cfg(test)]
    pub fn new_for_tests(
        width: i32,
        height: i32,
        attractors: SpatialIndex<Attractor>,
        render_fn: F,
    ) -> SpaceColonization<F>
    where
        F: Fn(&Node, &Node),
    {
        SpaceColonization {
            max_point: Point {
                x: width,
                y: height,
            },
            kill_distance: 5.0,
            attraction_distance: 12,
            segment_length: 3,
            density: 3,
            attractors,
            render_fn,
        }
    }

    pub fn render_all_nodes<G>(&self, nodes: &Vec<Node>, render_id: f64, render_fn: G)
    where
        G: Copy + Fn(&Node, &Node),
    {
        info!("Rendering {} nodes", nodes.len());
        for n in nodes.iter() {
            n.render(render_id, render_fn);
        }
    }

    fn place_attractors(&mut self) {
        let mut x_pos = 0;
        let mut y_pos = 0;
        while x_pos < self.max_point.x {
            while y_pos < self.max_point.y {
                let point = self.get_random_point(x_pos.into(), y_pos.into());
                self.attractors.add(&point, Attractor::new(point));
                y_pos += self.density;
            }
            x_pos += self.density;
            y_pos = 0;
        }
    }

    fn get_random_point(&self, x_pos: i32, y_pos: i32) -> Point {
        let half_density: i32 = (self.density / 2).into();
        let mut x_min = x_pos - half_density;
        if x_min < 0 {
            x_min = 0;
        }

        let mut y_min = y_pos - half_density;
        if y_min < 0 {
            y_min = 0;
        }

        Point {
            x: thread_rng()
                .gen_range(x_min..x_pos + half_density)
                .try_into()
                .unwrap(),
            y: thread_rng()
                .gen_range(y_min..y_pos + half_density)
                .try_into()
                .unwrap(),
        }
    }

    pub fn grow<'b>(&mut self, mut nodes: &'b mut Vec<Node>) {
        self.grow_nodes(&mut nodes)
    }

    pub fn grow_nodes(&mut self, nodes: &mut Vec<Node>) {
        // iterate through attractors
        // find closest node within attraction range
        // build a map of nodes to affecting attractors
        // attractors within the attraction range that this node is the closest to
        //
        // calculate new node position

        // ------------ START OF BLOCK ----------
        // DO NOT MODIFY THE NODES VEC AFTER THIS
        // We are taking raw pointers to Node to be dereferenced later, if the Vec of nodes is
        // modified it will cause wrong behavior or segmentation faults and crash
        let mut attractor_to_closest_node: HashMap<*mut Attractor, Attraction> = HashMap::new();

        for n in nodes.iter_mut() {
            self.build_attractor_to_closest_node(&mut attractor_to_closest_node, n);
        }

        let mut node_to_attractors: HashMap<*mut Node, Vec<*mut Attractor>> = HashMap::new();

        attractor_to_closest_node
            .drain()
            .for_each(|(attractor, attraction)| {
                let mut node_attractors = match node_to_attractors.remove(&attraction.node) {
                    Some(node_a) => node_a,
                    None => Vec::new(),
                };
                node_attractors.push(attractor);
                node_to_attractors.insert(attraction.node, node_attractors);
            });

        let mut dead_attractors: Vec<*mut Attractor> = Vec::new();
        node_to_attractors.iter().for_each(|(node, attractor)| {
            // Unsafe is used here for two main reasons :
            //
            // PERFORMANCE : Using unsafe here allows to store multiple mutable references to a
            // Node and save a few bytes of memory and cpu cycles to store a struct that holds
            // a fake reference to the Node, such as the path in the tree, and then resolve it
            // handling the Option<> every step of the way.
            //
            // Using a raw pointer we can Oh I actually just realised having a raw pointer deep
            // in a tree of Vec that are getting pushed into might very well cause the pointer
            // to become invalid when its parent gets pushed into and moved to another memory
            // space
            //
            // Using raw fixed length arrays would solve that but its a fine line between too
            // large memory usage and enough children nodes
            //
            // LEARNING : using unsafe here sounded like not the worst idea and was a nice
            // opportunity to learn, which did happen. I am closing in now and I can say that I
            // have learned quite a lot about how rust programs work and should be designed.

            unsafe {
                let new_point =
                    calculate_new_node_position(&(**node), attractor, self.segment_length);
                if let Some(new_point) = new_point {
                    let new_node = Node::new(new_point);
                    attractor.iter().for_each(|a| {
                        if (**a).position.distance(&new_node.position) <= self.kill_distance {
                            dead_attractors.push(*a);
                            (**a).dead = true;
                        }
                    });
                    (self.render_fn)(&**node, &new_node);
                    (**node).children.push(new_node);
                }
            }
        });
    }

    fn build_attractor_to_closest_node<'b>(
        &'b mut self,
        mut attractor_to_closest_node: &mut HashMap<*mut Attractor, Attraction>,
        n: &mut Node,
    ) {
        for child in n.children.iter_mut() {
            self.build_attractor_to_closest_node(&mut attractor_to_closest_node, child);
        }

        if !n.growing {
            return;
        }

        let attractors_in_range = self.find_attractors_in_range(&n);

        if attractors_in_range.is_empty() {
            n.growing = false;
            return;
        }

        for a in attractors_in_range {
            if let Some(closest) = attractor_to_closest_node.get(&a.0) {
                if a.1 < closest.distance {
                    attractor_to_closest_node.insert(a.0, Attraction::new(n, a.1));
                }
            } else {
                attractor_to_closest_node.insert(a.0, Attraction::new(n, a.1));
            }
        }
    }

    fn find_attractors_in_range(&mut self, n: &Node) -> Vec<(*mut Attractor, f64)> {
        let mut attractors_in_range = Vec::new();
        for a in self
            .attractors
            .get_surrounding_elements_with_filter(&n.position, |a| !a.dead)
            .iter()
        {
            let distance;
            unsafe {
                distance = n.position.distance(&(**a).position);
            }
            if distance < self.attraction_distance as f64 {
                attractors_in_range.push((*a, distance));
            }
        }
        attractors_in_range
    }
}

#[cfg(test)]
mod test {
    use std::cell::RefCell;

    use super::*;

    fn assert_vertices<'a, F>(
        sc: &SpaceColonization<F>,
        nodes: &'a Vec<Node>,
        mut expected_nodes: Vec<(Point, Point)>,
    ) where
        F: Copy + Fn(&Node, &Node),
    {
        let rendered_nodes = RefCell::new(Vec::new());
        sc.render_all_nodes(&nodes, 0.0, |n1: &Node, n2: &Node| {
            rendered_nodes.borrow_mut().push((n1.position, n2.position));
        });
        let sort_points = |line1: &(Point, Point), line2: &(Point, Point)| {
            if line1.0 != line2.0 {
                return line1.0.cmp(&line2.0);
            }

            return line1.1.cmp(&line2.1);
        };
        rendered_nodes.borrow_mut().sort_by(sort_points);
        expected_nodes.sort_by(sort_points);

        let rendered_nodes = rendered_nodes.take();

        assert_eq!(rendered_nodes, expected_nodes);
    }

    #[test]
    fn grow_should_reach_single_attractor_and_die() {
        let mut nodes = Vec::new();
        nodes.push(Node::new(Point::new((0, 0))));
        let mut attractors = SpatialIndex::new(Point::new((100, 100)), 10);
        let point = Point::new((10, 0));
        attractors.add(&point, Attractor::new(point));

        let mut sc = SpaceColonization::new_for_tests(100, 100, attractors, |_, _| {});

        assert_eq!(
            sc.attractors
                .get_surrounding_elements_with_filter(&point, |_| true)
                .len(),
            1
        );

        println!("before grow");
        dbg!(&nodes);
        sc.grow(&mut nodes);
        println!("after grow 1");
        dbg!(&nodes);

        assert_eq!(
            sc.attractors
                .get_surrounding_elements_with_filter(&point, |a| a.dead == false)
                .len(),
            1
        );


        sc.grow(&mut nodes);
        println!("after grow 2");
        dbg!(&nodes);

        assert_vertices(
            &sc,
            &nodes,
            Vec::from([
                (Point::new((0, 0)), Point::new((3, 0))),
                (Point::new((3, 0)), Point::new((6, 0))),
            ]),
        );
        assert_eq!(
            sc.attractors
                .get_surrounding_elements_with_filter(&point, |a| a.dead == false)
                .len(),
            0
        );
        assert_eq!(
            sc.attractors
                .get_surrounding_elements_with_filter(&point, |a| a.dead == true)
                .len(),
            1
        );
    }

    #[test]
    fn grow_should_ignore_dead_attractors() {
        let mut nodes = Vec::new();
        nodes.push(Node::new(Point::new((0, 0))));
        let mut attractors = SpatialIndex::new(Point::new((100, 100)), 10);
        let point = Point::new((10, 0));
        attractors.add(
            &point,
            Attractor {
                position: point,
                dead: true,
            },
        );

        let mut sc = SpaceColonization::new_for_tests(100, 100, attractors, |_, _| {});

        assert_eq!(
            sc.attractors
                .get_surrounding_elements_with_filter(&Point::new((10, 0)), |a| a.dead == true)
                .len(),
            1
        );

        sc.grow(&mut nodes);

        assert_vertices(&sc, &nodes, Vec::new());
    }
}
