use nalgebra::{Point2, Vector2, point, vector};

use crate::{TileIndex, TileIndexOffset};

#[derive(Clone, Copy, Debug)]
pub struct TileRect {
    pub origin: TileIndex,
    pub size: Vector2<usize>,
}

impl Default for TileRect {
    fn default() -> Self {
        Self {
            origin: point![0, 0],
            size: vector![0, 0],
        }
    }
}

pub struct Rect {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
}

impl Rect {
    pub fn left(&self) -> f32 {
        self.x
    }

    pub fn right(&self) -> f32 {
        self.x + self.w
    }

    pub fn top(&self) -> f32 {
        self.y
    }

    pub fn bottom(&self) -> f32 {
        self.y + self.h
    }
}

impl TileRect {
    pub fn from_rect_inclusive(rect: Rect) -> TileRect {
        TileRect::from_rect(rect, f32::floor, f32::ceil)
    }

    pub fn from_rect_exclusive(rect: Rect) -> TileRect {
        TileRect::from_rect(rect, f32::ceil, f32::floor)
    }

    pub fn from_rect_round_to_min(rect: Rect) -> TileRect {
        TileRect::from_rect(rect, f32::floor, f32::floor)
    }

    fn from_rect(rect: Rect, min_fn: impl Fn(f32) -> f32, max_fn: impl Fn(f32) -> f32) -> TileRect {
        let min_corner = point![min_fn(rect.left()) as isize, min_fn(rect.top()) as isize];
        let max_corner = point![
            max_fn(rect.right()) as isize,
            max_fn(rect.bottom()) as isize
        ];

        TileRect {
            origin: min_corner,
            size: (max_corner - min_corner).map(|x| x.max(0) as usize),
        }
    }

    #[must_use]
    pub fn translate(&self, translation: TileIndexOffset) -> Self {
        Self {
            origin: self.origin + translation,
            size: self.size,
        }
    }

    pub fn linear_index_to_tile_index(&self, linear_index: usize) -> Option<TileIndex> {
        if linear_index < self.area() {
            let x_offset = (linear_index % self.size.x) as isize;
            let y_offset = (linear_index / self.size.x) as isize;

            Some(self.origin + vector![x_offset, y_offset])
        } else {
            None
        }
    }

    pub fn min_corner(&self) -> TileIndex {
        self.origin
    }

    pub fn max_corner(&self) -> TileIndex {
        Vector2::from_fn(|i, _| self.origin[i].checked_add_unsigned(self.size[i]).unwrap()).into()
    }

    pub fn left(&self) -> isize {
        self.origin.x
    }

    pub fn right(&self) -> isize {
        self.origin.x.wrapping_add_unsigned(self.size.x) - 1
    }

    pub fn top(&self) -> isize {
        self.origin.y
    }

    pub fn bottom(&self) -> isize {
        self.origin.y.wrapping_add_unsigned(self.size.y) - 1
    }

    pub fn intersects(&self, rhs: &TileRect) -> bool {
        self.left() <= rhs.right()
            && rhs.left() <= self.right()
            && self.top() <= rhs.bottom()
            && rhs.top() <= self.bottom()
    }

    pub fn contains_point(&self, point: TileIndex) -> bool {
        self.left() <= point.x
            && self.right() >= point.x
            && self.top() <= point.y
            && self.bottom() >= point.y
    }

    pub fn contains(&self, rhs: &TileRect) -> bool {
        self.left() <= rhs.left()
            && self.right() >= rhs.right()
            && self.top() <= rhs.top()
            && self.bottom() >= rhs.bottom()
    }

    pub fn area(&self) -> usize {
        self.size.x * self.size.y
    }

    pub fn is_empty(&self) -> bool {
        self.size.x == 0 || self.size.y == 0
    }

    pub fn expand_to_include_index(
        &mut self,
        index: TileIndex,
        minimum_nonzero_expansion: Vector2<usize>,
    ) -> bool {
        self.expand_to_include_bounds(
            TileRect {
                origin: index,
                size: vector![1, 1],
            },
            minimum_nonzero_expansion,
        )
    }

    pub fn expand_to_include_bounds(
        &mut self,
        bounds: TileRect,
        minimum_nonzero_expansion: Vector2<usize>,
    ) -> bool {
        let mut expanded = false;
        let min_expansion = minimum_nonzero_expansion;

        if self.is_empty() {
            expanded = true;
            *self = bounds;
        }

        let origin: Point2<_> = Vector2::from_fn(|i, _| {
            if bounds.origin[i] < self.origin[i] {
                expanded = true;
                bounds.origin[i].min(self.origin[i].saturating_sub(min_expansion[i] as isize))
            } else {
                self.origin[i]
            }
        })
        .into();

        let end_corner = self.max_corner();
        let bounds_end_corner = bounds.max_corner();

        let end_corner: Point2<_> = Vector2::from_fn(|i, _| {
            if bounds_end_corner[i] > end_corner[i] {
                expanded = true;
                bounds_end_corner[i].max(end_corner[i].saturating_add(min_expansion[i] as isize))
            } else {
                end_corner[i]
            }
        })
        .into();

        self.origin = origin;
        self.size = Vector2::from_fn(|i, _| origin[i].abs_diff(end_corner[i]));

        expanded
    }
}

pub struct Indices {
    bounds: TileRect,
    linear_index: usize,
}

impl Iterator for Indices {
    type Item = TileIndex;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(index) = self.bounds.linear_index_to_tile_index(self.linear_index) {
            self.linear_index += 1;
            Some(index)
        } else {
            None
        }
    }
}

impl TileRect {
    pub fn indices(&self) -> Indices {
        Indices {
            bounds: *self,
            linear_index: 0,
        }
    }
}
