amazeing 0.8.1

Amazeing is a maze generator/solver application with simulation/visualization.
use crate::cli::{AmazeingContext, Colors, ContextType};
use crate::maze::{BLOCK, Maze, Node, NodeFactory, OPEN, Rank, UnitShape, VOID};
use crate::render::helper::{current_millis, is_point_in_triangle};
use crate::render::unit::{
    HexagonRectangleUnitShapeFactory, HexagonUnitShapeFactory, OctagonSquareUnitShapeFactory, OctagonUnitShapeFactory,
    RhombusUnitShapeFactory, SquareUnitShapeFactory, TriangleUnitShapeFactory, UnitShapeFactory,
};
use crate::render::{BORDER, MARGIN};
use crate::util::IsDivisible;
use macroquad::prelude::{Color, Mesh, Vertex, clear_background, draw_line, draw_mesh, vec2, vec3};
use std::ops::{Index, IndexMut};

pub(crate) struct MazeScene {
    pub(crate) context: AmazeingContext,
    pub(crate) meshes: Vec<Vec<Mesh>>,
    pub(crate) maze: Maze,
    pub(crate) colors: Colors,
    pub(crate) wh: (u32, u32),
    pub(crate) bound: Option<Mesh>,
    pub(crate) rows: usize,
    pub(crate) cols: usize,
}

impl MazeScene {
    fn new_from(
        maze: &Maze,
        context: AmazeingContext,
        colors: &Colors,
        shape_factory: Box<dyn UnitShapeFactory>,
    ) -> Self {
        let (rows, cols) = (maze.rows(), maze.cols());
        let meshes = maze
            .data
            .iter()
            .enumerate()
            .map(|(row, row_data)| {
                row_data
                    .iter()
                    .enumerate()
                    .map(|(col, &cell)| {
                        let color = match cell {
                            OPEN => colors.color_open,
                            BLOCK => colors.color_block,
                            VOID => colors.color_bg,
                            _ => colors.color_bg,
                        };
                        shape_factory.shape(row, col, rows, cols, color)
                    })
                    .collect::<Vec<Mesh>>()
            })
            .collect::<Vec<Vec<Mesh>>>();

        let mut scene = Self {
            context,
            meshes,
            maze: maze.clone(),
            colors: colors.clone(),
            wh: shape_factory.wh_for(maze.rows(), maze.cols()),
            bound: None,
            rows: maze.rows(),
            cols: maze.cols(),
        };

        if scene.context.context_type == ContextType::Create || scene.context.show_perimeter {
            scene.set_bound();
        }

        scene
    }

    pub(crate) fn new_from_maze(maze: &Maze, context: &AmazeingContext, colors: &Colors) -> Self {
        let shape_factory = MazeScene::shape_factory(maze.unit_shape, context.zoom);
        MazeScene::new_from(maze, context.clone(), colors, shape_factory)
    }

    pub(crate) fn new_from_dimension(unit_shape: UnitShape, context: &AmazeingContext, colors: &Colors) -> Self {
        let shape_factory = MazeScene::shape_factory(unit_shape, context.zoom);
        let (m_rows, m_cols) = MazeScene::adjust_dimension(unit_shape, context.rows, context.cols);
        MazeScene::new_from(&Maze::new_void(unit_shape, m_rows, m_cols), context.clone(), colors, shape_factory)
    }

    pub(crate) fn w(&self) -> u32 {
        self.wh.0
    }

    pub(crate) fn h(&self) -> u32 {
        self.wh.1
    }

    pub(crate) fn update(&mut self) {
        let node_factory = NodeFactory::new(self.rows, self.cols);
        let color_open = self.colors.color_open;
        let color_block = self.colors.color_block;
        let color_bg = self.colors.color_bg;

        // Collect all updates first
        let updates: Vec<(Node, Color)> = self
            .maze
            .data
            .iter()
            .enumerate()
            .flat_map(|(row, row_data)| {
                row_data
                    .iter()
                    .enumerate()
                    .map(move |(col, &cell)| {
                        let color = match cell {
                            OPEN => color_open,
                            BLOCK => color_block,
                            VOID => color_bg,
                            _ => color_bg,
                        };
                        (node_factory.at(row, col).unwrap(), color)
                    })
                    .collect::<Vec<_>>()
            })
            .collect();

        // Apply all updates without borrowing self.maze during the operation
        for (node, color) in updates {
            self.update_color(node, color);
        }
    }

    pub(crate) fn set_bound(&mut self) {
        let (x_min, x_max) = (MARGIN - BORDER, self.w() as f32 - MARGIN + BORDER);
        let (y_min, y_max) = (MARGIN - BORDER, self.h() as f32 - MARGIN + BORDER);
        self.bound = Some(Mesh {
            vertices: vec![
                Vertex::new2(vec3(x_min, y_min, 0.), vec2(0., 0.), self.colors.color_perimeter),
                Vertex::new2(vec3(x_max, y_min, 0.), vec2(0., 0.), self.colors.color_perimeter),
                Vertex::new2(vec3(x_max, y_max, 0.), vec2(0., 0.), self.colors.color_perimeter),
                Vertex::new2(vec3(x_min, y_max, 0.), vec2(0., 0.), self.colors.color_perimeter),
            ],
            indices: vec![0, 1, 2, 0, 2, 3],
            texture: None,
        });

        if self.context.context_type == ContextType::Create {
            let node_factory = NodeFactory::new(self.rows, self.cols);

            for r in 0..self.rows {
                for c in 0..self.cols {
                    if self.is_mesh_in_bound(&self.meshes[r][c]) {
                        self.update_color(node_factory.at(r, c).unwrap(), self.colors.color_block);
                        self.maze[node_factory.at(r, c).unwrap()] = BLOCK;
                    }
                }
            }
        }
    }

    pub(crate) fn clear_and_draw(&self) {
        clear_background(self.colors.color_bg);
        self.draw();
        self.draw_bound();
    }

    pub(crate) fn draw(&self) {
        self.meshes.iter().for_each(|row| row.iter().for_each(draw_mesh));
    }

    pub(crate) fn draw_bound(&self) {
        if !self.context.show_perimeter {
            return;
        }

        if let Some(bound) = &self.bound {
            for i in 0..bound.vertices.len() {
                if i < bound.vertices.len() - 1 {
                    draw_line(
                        bound.vertices[i].position.x,
                        bound.vertices[i].position.y,
                        bound.vertices[i + 1].position.x,
                        bound.vertices[i + 1].position.y,
                        1.,
                        self.colors.color_perimeter,
                    )
                } else {
                    draw_line(
                        bound.vertices[i].position.x,
                        bound.vertices[i].position.y,
                        bound.vertices[0].position.x,
                        bound.vertices[0].position.y,
                        1.,
                        self.colors.color_perimeter,
                    )
                }
            }
        }
    }

    pub(crate) fn clicked_on(&self, (x, y): (f32, f32)) -> Option<Node> {
        for (row_idx, row) in self.meshes.iter().enumerate() {
            for (col_idx, mesh) in row.iter().enumerate() {
                if self.is_point_in_mesh(mesh, x, y) {
                    return NodeFactory::new(self.meshes.len(), row.len()).at(row_idx, col_idx);
                }
            }
        }
        None
    }

    pub(crate) fn update_node(&mut self, node: Node, value: i8, color: Color) {
        self[node].vertices.iter_mut().for_each(|vertex| vertex.color = color.into());
        self.maze[node] = value;
    }

    pub(crate) fn update_color(&mut self, node: Node, color: Color) {
        self[node].vertices.iter_mut().for_each(|vertex| vertex.color = color.into())
    }

    #[allow(dead_code)]
    pub(crate) fn display_void(&mut self, node: Node) {
        self.update_color(node, self.colors.color_bg)
    }

    pub(crate) fn display_block(&mut self, node: Node) {
        self.update_color(node, self.colors.color_block)
    }

    pub(crate) fn display_open(&mut self, node: Node) {
        self.update_color(node, self.colors.color_open)
    }

    #[allow(dead_code)]
    pub(crate) fn display_visiting(&mut self, node: Node) {
        self.update_color(node, self.colors.color_visiting)
    }

    pub(crate) fn display_visiting_gradient(&mut self, node: Node, rank: &Rank) {
        self.update_color(node, *self.colors.shed_color(rank))
    }

    pub(crate) fn display_path(&mut self, node: Node) {
        self.update_color(node, self.colors.color_path)
    }

    pub(crate) fn display_source(&mut self, node: Node) {
        self.update_color(node, self.colors.color_source)
    }

    pub(crate) fn display_destination(&mut self, node: Node) {
        self.update_color(node, self.colors.color_destination)
    }

    pub(crate) fn display_traversed(&mut self, node: Node) {
        self.update_color(node, self.colors.color_traversed)
    }

    pub(crate) fn delay_till_next_frame(&self, current_frame_start_time: u128) {
        let current_frame_time = (current_millis() - current_frame_start_time) as f32;
        let minimum_frame_time = (1. / self.context.fps) * 1000.;
        #[allow(unused_assignments)]
        let mut time_to_sleep: f32 = 0.;
        if current_frame_time < minimum_frame_time {
            time_to_sleep = minimum_frame_time - current_frame_time;
            std::thread::sleep(std::time::Duration::from_millis(time_to_sleep as u64));
        }
        // println!("Min: {}ms | Current: {}ms | Sleep: {}ms", minimum_frame_time, current_frame_time, time_to_sleep);
    }

    fn shape_factory(unit_shape: UnitShape, zoom: f32) -> Box<dyn UnitShapeFactory> {
        match unit_shape {
            UnitShape::Triangle => Box::new(TriangleUnitShapeFactory::new(zoom)),
            UnitShape::Square => Box::new(SquareUnitShapeFactory::new(zoom)),
            UnitShape::Rhombus => Box::new(RhombusUnitShapeFactory::new(zoom)),
            UnitShape::Hexagon => Box::new(HexagonUnitShapeFactory::new(zoom)),
            UnitShape::HexagonRectangle => Box::new(HexagonRectangleUnitShapeFactory::new(zoom)),
            UnitShape::Octagon => Box::new(OctagonUnitShapeFactory::new(zoom)),
            UnitShape::OctagonSquare => Box::new(OctagonSquareUnitShapeFactory::new(zoom)),
        }
    }

    fn is_point_in_mesh(&self, mesh: &Mesh, x: f32, y: f32) -> bool {
        let vertices = &mesh.vertices;
        let indices = &mesh.indices;

        if indices.len() >= 3 {
            for i in (0..indices.len()).step_by(3) {
                if i + 2 < indices.len()
                    && is_point_in_triangle(
                        (x, y),
                        (vertices[indices[i] as usize].position.x, vertices[indices[i] as usize].position.y),
                        (vertices[indices[i + 1] as usize].position.x, vertices[indices[i + 1] as usize].position.y),
                        (vertices[indices[i + 2] as usize].position.x, vertices[indices[i + 2] as usize].position.y),
                    )
                {
                    return true;
                }
            }
        }

        false
    }

    fn is_mesh_in_bound(&self, mesh: &Mesh) -> bool {
        // Return early if no bound is set
        let Some(bound) = &self.bound else {
            return false;
        };

        // Check if all vertices of the mesh are within the bound mesh
        // Calculate the center of the mesh
        let (sum_x, sum_y) = mesh
            .vertices
            .iter()
            .fold((0.0, 0.0), |acc, vertex| (acc.0 + vertex.position.x, acc.1 + vertex.position.y));
        let center_x = sum_x / mesh.vertices.len() as f32;
        let center_y = sum_y / mesh.vertices.len() as f32;
        let center_point = (center_x, center_y);

        // Check if the center point is inside any triangle of the bound mesh
        let bound_vertices = &bound.vertices;
        let bound_indices = &bound.indices;

        for i in (0..bound_indices.len()).step_by(3) {
            if i + 2 < bound_indices.len() {
                let v1 = (
                    bound_vertices[bound_indices[i] as usize].position.x,
                    bound_vertices[bound_indices[i] as usize].position.y,
                );
                let v2 = (
                    bound_vertices[bound_indices[i + 1] as usize].position.x,
                    bound_vertices[bound_indices[i + 1] as usize].position.y,
                );
                let v3 = (
                    bound_vertices[bound_indices[i + 2] as usize].position.x,
                    bound_vertices[bound_indices[i + 2] as usize].position.y,
                );

                if is_point_in_triangle(center_point, v1, v2, v3) {
                    return true;
                }
            }
        }

        false
    }

    #[allow(clippy::borrowed_box)]
    fn adjust_dimension(unit_shape: UnitShape, rows: usize, cols: usize) -> (usize, usize) {
        match unit_shape {
            UnitShape::Triangle | UnitShape::HexagonRectangle | UnitShape::OctagonSquare => {
                ((rows * 2).odd_floor(), cols)
            }
            _ => (rows, cols),
        }
    }
}

impl Index<Node> for MazeScene {
    type Output = Mesh;

    fn index(&self, index: Node) -> &Self::Output {
        &self.meshes[index.row][index.col]
    }
}

impl IndexMut<Node> for MazeScene {
    fn index_mut(&mut self, index: Node) -> &mut Self::Output {
        &mut self.meshes[index.row][index.col]
    }
}