use crate::prelude::*;

pub struct WorldDimensions {
    pub rows: i32,
    pub cols: i32,
    pub cell_width: i32,
}

pub struct TileGrid {
    pub tiles: SmallVec<[TileType; 256]>,
    pub grid_dimensions: WorldDimensions,
}

#[derive(Copy, Clone, PartialEq, Debug)]
pub enum TileType {
    Transparent,
    Opaque,
}

/// Pivots are oriented along the game world, which also aligns with Octant::NorthOfEast
pub enum Pivot {
    _Center,
    TopRight,
    BottomRight,
    BottomLeft,
    TopLeft,
}

impl Pivot {
    pub fn abs_coords(&self, tile_coords: &IVec2, world_dimensions: &WorldDimensions) -> Vec3 {
        let translation = grid_position_to_translation(*tile_coords, &world_dimensions);

        match self {
            Pivot::_Center => translation,
            Pivot::TopRight => Vec3::new(
                translation.x + (world_dimensions.cell_width / 2) as f32,
                translation.y - (world_dimensions.cell_width / 2) as f32,
                translation.z,
            ),
            Pivot::BottomRight => Vec3::new(
                translation.x + (world_dimensions.cell_width / 2) as f32,
                translation.y + (world_dimensions.cell_width / 2) as f32,
                translation.z,
            ),
            Pivot::BottomLeft => Vec3::new(
                translation.x - (world_dimensions.cell_width / 2) as f32,
                translation.y + (world_dimensions.cell_width / 2) as f32,
                translation.z,
            ),
            Pivot::TopLeft => Vec3::new(
                translation.x - (world_dimensions.cell_width / 2) as f32,
                translation.y - (world_dimensions.cell_width / 2) as f32,
                translation.z,
            ),
        }
    }

    pub fn flip_x(&self) -> Pivot {
        match self {
            Pivot::_Center => Pivot::_Center,
            Pivot::TopRight => Pivot::TopLeft,
            Pivot::BottomRight => Pivot::BottomLeft,
            Pivot::BottomLeft => Pivot::BottomRight,
            Pivot::TopLeft => Pivot::TopRight,
        }
    }

    pub fn flip_y(&self) -> Pivot {
        match self {
            Pivot::_Center => Pivot::_Center,
            Pivot::TopRight => Pivot::BottomRight,
            Pivot::BottomRight => Pivot::TopRight,
            Pivot::BottomLeft => Pivot::TopLeft,
            Pivot::TopLeft => Pivot::BottomLeft,
        }
    }

    /// Rotates the pivot points clockwise.
    /// ┌──────▶┐
    /// ▲       │
    /// │       ▼
    /// └◀──────┘

    pub fn rotate_cw(&self) -> Pivot {
        match self {
            Pivot::_Center => Pivot::_Center,
            Pivot::TopRight => Pivot::BottomRight,
            Pivot::BottomRight => Pivot::BottomLeft,
            Pivot::BottomLeft => Pivot::TopLeft,
            Pivot::TopLeft => Pivot::TopRight,
        }
    }

    /// Rotates the pivot points counter-clockwise.
    /// ┌◀──────┐
    /// │       ▲
    /// ▼       │
    /// └──────▶┘
    pub fn rotate_ccw(&self) -> Pivot {
        match self {
            Pivot::_Center => Pivot::_Center,
            Pivot::TopRight => Pivot::TopLeft,
            Pivot::TopLeft => Pivot::BottomLeft,
            Pivot::BottomLeft => Pivot::BottomRight,
            Pivot::BottomRight => Pivot::TopRight,
        }
    }
}

/// 4\33333|22222/1
/// 44\3333|2222/11
/// 444\333|222/111
/// 4444\33|22/1111
/// 44444\3|2/11111
/// ------@_@------
/// 55555/6|7\88888
/// 5555/66|77\8888
/// 555/666|777\888
/// 55/6666|7777\88
/// 5/66666|77777\8
#[derive(Copy, Clone)]
pub enum Octant {
    NorthOfEast,
    EastOfNorth,
    WestOfNorth,
    NorthOfWest,
    SouthOfWest,
    WestOfSouth,
    EastOfSouth,
    SouthOfEast,
}

impl Octant {
    /// Get the grid point on scan line per quadrant
    pub fn grid_point_on_scan_line(&self, observer: IVec2, depth: i32, slope: f32) -> IVec2 {
        match self {
            Self::SouthOfEast => {
                let x = depth;
                let y = (x as f32 * slope) as i32;

                IVec2 {
                    x: observer.x + x,
                    y: observer.y + y,
                }
            }
            Self::EastOfSouth => {
                let y = depth;
                let x = (y as f32 * slope) as i32;

                IVec2 {
                    x: observer.x + x,
                    y: observer.y + y,
                }
            }
            Self::WestOfSouth => {
                let y = depth;
                let x = (y as f32 * slope) as i32;

                IVec2 {
                    x: observer.x - x,
                    y: observer.y + y,
                }
            }
            Self::SouthOfWest => {
                let x = depth;
                let y = (x as f32 * slope) as i32;

                IVec2 {
                    x: observer.x - x,
                    y: observer.y + y,
                }
            }
            Octant::NorthOfWest => {
                let x = depth;
                let y = (x as f32 * slope) as i32;

                IVec2 {
                    x: observer.x - x,
                    y: observer.y - y,
                }
            }
            Octant::WestOfNorth => {
                let y = depth;
                let x = (y as f32 * slope) as i32;

                IVec2 {
                    x: observer.x - x,
                    y: observer.y - y,
                }
            }
            Octant::EastOfNorth => {
                let y = depth;
                let x = (y as f32 * slope) as i32;

                IVec2 {
                    x: observer.x + x,
                    y: observer.y - y,
                }
            }
            Octant::NorthOfEast => {
                let x = depth;
                let y = (x as f32 * slope) as i32;

                IVec2 {
                    x: observer.x + x,
                    y: observer.y - y,
                }
            }
        }
    }

    pub fn get_adjusted_pivot(&self, pivot: Pivot) -> Pivot {
        match self {
            Octant::NorthOfEast => pivot,
            Octant::EastOfNorth => pivot.flip_x().rotate_cw(),
            Octant::WestOfNorth => pivot.rotate_ccw(),
            Octant::NorthOfWest => pivot.flip_x(),
            Octant::SouthOfWest => pivot.flip_x().flip_y(),
            Octant::WestOfSouth => pivot.flip_y().rotate_cw(),
            Octant::EastOfSouth => pivot.rotate_cw(),
            Octant::SouthOfEast => pivot.flip_y(),
        }
    }

    /// Returns the absolute slope for an octant
    /// Octants (1, 4, 5, 8) and (2, 3, 6, 7) should have the same absolute slope
    pub fn slope_abs(
        &self,
        world_dimensions: &WorldDimensions,
        observer: &IVec2,
        tile: &IVec2,
        pivot: Pivot,
    ) -> f32 {
        let pivot = self.get_adjusted_pivot(pivot);
        let target = pivot.abs_coords(tile, &world_dimensions);
        let observer = grid_position_to_translation(*observer, &world_dimensions);

        match self {
            Octant::SouthOfEast
            | Octant::SouthOfWest
            | Octant::NorthOfWest
            | Octant::NorthOfEast => ((target.y - observer.y) / (target.x - observer.x)).abs(),
            Octant::EastOfSouth
            | Octant::WestOfSouth
            | Octant::WestOfNorth
            | Octant::EastOfNorth => ((target.x - observer.x) / (target.y - observer.y)).abs(),
        }
    }

    pub fn get_next_tile_on_scanline(&self, tile: &IVec2) -> IVec2 {
        match self {
            Octant::SouthOfEast | Octant::SouthOfWest => IVec2 {
                x: tile.x,
                y: tile.y + 1,
            },
            Octant::EastOfSouth | Octant::EastOfNorth => IVec2 {
                x: tile.x + 1,
                y: tile.y,
            },
            Octant::WestOfSouth | Octant::WestOfNorth => IVec2 {
                x: tile.x - 1,
                y: tile.y,
            },
            Octant::NorthOfWest | Octant::NorthOfEast => IVec2 {
                x: tile.x,
                y: tile.y - 1,
            },
        }
    }
}

pub struct Visibility {
    _is_omniscient: bool,
    max_visible_distance: i32,
    visible_tiles: HashMap<IVec2, i32>,
    world_dimensions: WorldDimensions,
    pub observer: IVec2,
}

impl Visibility {
    pub fn new(
        world_dimensions: WorldDimensions,
        is_omniscient: bool,
        max_visible_distance: i32,
    ) -> Self {
        Self {
            _is_omniscient: is_omniscient,
            max_visible_distance,
            world_dimensions,
            visible_tiles: HashMap::new(),
            observer: IVec2 { x: 0, y: 0 },
        }
    }

    pub fn drain_visible_tiles(&mut self) {
        self.visible_tiles.drain();
    }

    fn get_tile_type(&self, world: &TileGrid, tile_coords: &IVec2) -> Option<TileType> {
        let maybe_idx = grid_pos_to_idx(tile_coords, &world.grid_dimensions);

        match maybe_idx {
            Some(idx) => {
                let tile = world.tiles.get(idx);

                if let Some(tile) = tile {
                    return Some(*tile);
                } else {
                    return Some(TileType::Opaque);
                }
            }
            // This allows out-of-bounds to become Opaque
            None => Some(TileType::Opaque),
        }
    }

    fn add_observer(&mut self) {
        self.visible_tiles.insert(self.observer, 0);
    }

    pub fn compute_visible_tiles(&mut self, world: &TileGrid) -> HashMap<IVec2, i32> {
        self.add_observer();
        self.compute_visible_tiles_in_octant(world, Octant::NorthOfEast, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::EastOfNorth, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::WestOfNorth, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::NorthOfWest, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::SouthOfWest, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::WestOfSouth, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::EastOfSouth, 1, 0., 1.);
        self.compute_visible_tiles_in_octant(world, Octant::SouthOfEast, 1, 0., 1.);
        self.visible_tiles.clone()
    }

    fn compute_visible_tiles_in_octant(
        &mut self,
        world: &TileGrid,
        octant: Octant,
        current_depth: i32,
        mut min_slope: f32,
        max_slope: f32,
    ) {
        let mut is_first = true;
        let mut previous = octant.grid_point_on_scan_line(self.observer, current_depth, min_slope);
        let mut current = octant.grid_point_on_scan_line(self.observer, current_depth, min_slope);

        loop {
            if self.observer.distance_squared(current) >= self.max_visible_distance.pow(2)
            // if the slope of the current cell exceeds max_slope, we can stop calculating
                || octant.slope_abs(
                    &self.world_dimensions,
                    &self.observer,
                    &current,
                    Pivot::BottomRight,
                ) >= max_slope
            {
                break;
            }

            self.visible_tiles
                .insert(current, self.observer.distance_squared(current));

            match is_first {
                false => {
                    match (
                        self.get_tile_type(world, &previous),
                        self.get_tile_type(world, &current),
                    ) {
                        // first opaque cell after at least one transparent
                        (Some(TileType::Transparent), Some(TileType::Opaque)) => {
                            let next_max_slope = octant.slope_abs(
                                &self.world_dimensions,
                                &self.observer,
                                &current,
                                Pivot::BottomRight,
                            );

                            self.compute_visible_tiles_in_octant(
                                world,
                                octant,
                                current_depth + 1,
                                min_slope,
                                next_max_slope,
                            );
                        }
                        // first transparent cell after at least one opaque
                        (Some(TileType::Opaque), Some(TileType::Transparent)) => {
                            min_slope = octant.slope_abs(
                                &self.world_dimensions,
                                &self.observer,
                                &current,
                                Pivot::BottomLeft,
                            );
                        }
                        (_, _) => {}
                    }
                }
                true => {
                    is_first = false;
                }
            }
            previous = current;
            current = octant.get_next_tile_on_scanline(&current);
        }

        // see through last group of transparent cells in a row
        if let Some(TileType::Transparent) = self.get_tile_type(world, &previous) {
            self.compute_visible_tiles_in_octant(
                world,
                octant,
                current_depth + 1,
                min_slope,
                max_slope,
            );
        }
    }
}
