use std::{hash::Hash, mem};

use nalgebra::{Vector2, vector};

use crate::{
    TileIndex, TileIndexOffset,
    tile_grid::{self, Empty, TileGrid},
    tile_rect::TileRect,
};

#[derive(Clone, Debug)]
pub struct MultiTileGrid<T: Tile, S: TileShape> {
    data: TileGrid<TileEntry<(T, S)>>,
}

impl<T: Tile, S: TileShape> Default for MultiTileGrid<T, S> {
    fn default() -> Self {
        Self {
            data: TileGrid::default(),
        }
    }
}

#[derive(Clone, Copy, Default, Debug)]
enum TileEntry<T> {
    #[default]
    Empty,
    Origin(T),
    Offset(Vector2<i8>),
}

impl<T: 'static> Empty for TileEntry<T> {
    fn empty() -> &'static Self {
        &TileEntry::Empty
    }

    /// Returns `true` if the tile entry is [`Empty`].
    ///
    /// [`Empty`]: TileEntry::Empty
    fn is_empty(&self) -> bool {
        matches!(self, Self::Empty)
    }
}

impl<T> TileEntry<T> {
    fn offset(&self) -> Option<TileIndexOffset> {
        match self {
            TileEntry::Empty => None,
            TileEntry::Origin(_) => Some(vector![0, 0]),
            TileEntry::Offset(offset) => Some(offset.map(isize::from)),
        }
    }
}

pub trait Tile: 'static {}

impl<T: 'static> Tile for T {}

pub trait TileShape: 'static {
    /// Returned offsets must fit within a `Vector2<i8>`. Returning `[0, 0]` has no effect, as it
    /// is implied to be present no matter what is returned. Although the offsets do not need to be
    /// disjoint, returning fewer duplicates improves performance.
    fn offsets(&self) -> impl Iterator<Item = TileIndexOffset>;

    fn bounds_hint(&self) -> Option<TileRect> {
        None
    }
}

impl<T: Tile, S: TileShape> MultiTileGrid<T, S> {
    pub fn shift(&mut self, offset: TileIndexOffset) {
        self.data.shift(offset)
    }

    pub fn bounds(&self) -> TileRect {
        self.data.bounds()
    }

    /// Returns whether or not any expansion occurred
    pub fn expand_to_fit_index(&mut self, index: TileIndex) -> bool {
        self.data.expand_to_fit_index(index)
    }

    /// Returns whether or not any expansion occurred
    pub fn expand_to_fit_bounds(&mut self, new_bounds: TileRect) -> bool {
        self.data.expand_to_fit_bounds(new_bounds)
    }

    pub fn origin_of(&self, index: TileIndex) -> Option<TileIndex> {
        let offset = self.data[index].offset()?;

        Some(index + offset)
    }

    pub fn get_tile(&self, index: TileIndex) -> Option<IndexedTile<'_, T, S>> {
        if let Some(origin) = self.origin_of(index) {
            let TileEntry::Origin((tile, shape)) = &self.data[origin] else {
                unreachable!("Origin index should contain an origin entry")
            };

            Some(IndexedTile {
                tile,
                shape,
                origin,
            })
        } else {
            None
        }
    }

    pub fn get_tile_mut(&mut self, index: TileIndex) -> Option<IndexedTileMut<'_, T, S>> {
        if let Some(origin) = self.origin_of(index) {
            let TileEntry::Origin((tile, shape)) = &mut self.data[origin] else {
                unreachable!("Origin index should contain an origin entry")
            };

            Some(IndexedTileMut {
                tile,
                shape,
                origin,
            })
        } else {
            None
        }
    }

    pub fn insert_tile(
        &mut self,
        origin: TileIndex,
        tile: T,
        shape: S,
    ) -> Result<(), TileInsertError> {
        for offset in shape.offsets().chain([vector![0, 0]]) {
            let index = origin + offset;
            if !self.data[index].is_empty() {
                return Err(TileInsertError::Overlap { conflict: index });
            }
        }

        unsafe { self.insert_tile_unchecked(origin, tile, shape) };

        Ok(())
    }

    pub fn insert_tile_overwriting(
        &mut self,
        origin: TileIndex,
        tile: T,
        shape: S,
    ) -> Vec<RemovedTile<T, S>> {
        let mut removed_tiles = Vec::new();

        for offset in shape.offsets().chain([vector![0, 0]]) {
            let index = origin + offset;
            if let Some(removed_tile) = self.remove_tile(index) {
                removed_tiles.push(removed_tile);
            }
        }

        unsafe { self.insert_tile_unchecked(origin, tile, shape) };

        removed_tiles
    }

    pub unsafe fn insert_tile_unchecked(&mut self, origin: TileIndex, tile: T, shape: S) {
        if let Some(bounds) = shape.bounds_hint() {
            self.data
                .expand_to_fit_bounds(bounds.translate(origin.coords));
        }

        for offset in shape.offsets() {
            self.data[origin + offset] =
                TileEntry::Offset(-offset.map(|x| i8::try_from(x).expect("Shape is too big")));
        }

        self.data[origin] = TileEntry::Origin((tile, shape));
    }

    pub fn remove_tile(&mut self, index: TileIndex) -> Option<RemovedTile<T, S>> {
        let origin = self.origin_of(index)?;

        let entry = mem::take(&mut self.data[origin]);

        let TileEntry::Origin((tile, shape)) = entry else {
            unreachable!("Origin index should contain an origin entry")
        };

        for offset in shape.offsets() {
            let index = origin + offset;
            self.data[index] = TileEntry::Empty;
        }

        Some(RemovedTile {
            tile,
            shape,
            origin,
        })
    }

    pub fn shrink_to_fit(&mut self) {
        self.data.shrink_to_fit();
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct RemovedTile<T: Tile, S: TileShape> {
    pub tile: T,
    pub shape: S,
    pub origin: TileIndex,
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct IndexedTile<'a, T: Tile, S: TileShape> {
    pub tile: &'a T,
    pub shape: &'a S,
    pub origin: TileIndex,
}

#[derive(PartialEq, Eq, Hash, Debug)]
pub struct IndexedTileMut<'a, T: Tile, S: TileShape> {
    pub tile: &'a mut T,
    pub shape: &'a S,
    pub origin: TileIndex,
}

#[derive(Clone, Copy, Debug)]
pub enum TileInsertError {
    Overlap { conflict: TileIndex },
}

#[derive(Clone, Debug)]
pub struct IntoIter<T: Tile, S: TileShape> {
    inner: tile_grid::IntoIter<TileEntry<(T, S)>>,
}

impl<T: Tile, S: TileShape> Iterator for IntoIter<T, S> {
    type Item = RemovedTile<T, S>;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some((origin, entry)) = self.inner.next() {
            match entry {
                TileEntry::Origin((tile, shape)) => {
                    return Some(RemovedTile {
                        tile,
                        shape,
                        origin,
                    });
                }
                TileEntry::Empty | TileEntry::Offset(_) => (),
            }
        }

        None
    }
}

impl<T: Tile, S: TileShape> IntoIterator for MultiTileGrid<T, S> {
    type Item = RemovedTile<T, S>;

    type IntoIter = IntoIter<T, S>;

    fn into_iter(self) -> Self::IntoIter {
        IntoIter {
            inner: self.data.into_iter(),
        }
    }
}

#[derive(Clone, Debug)]
pub struct Iter<'a, T: Tile, S: TileShape> {
    inner: tile_grid::Iter<'a, TileEntry<(T, S)>>,
}

impl<'a, T: Tile, S: TileShape> Iterator for Iter<'a, T, S> {
    type Item = IndexedTile<'a, T, S>;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some((origin, entry)) = self.inner.next() {
            match entry {
                TileEntry::Origin((tile, shape)) => {
                    return Some(IndexedTile {
                        tile,
                        shape,
                        origin,
                    });
                }
                TileEntry::Empty | TileEntry::Offset(_) => (),
            }
        }

        None
    }
}

impl<T: Tile, S: TileShape> MultiTileGrid<T, S> {
    pub fn iter(&self) -> Iter<'_, T, S> {
        Iter {
            inner: self.data.iter(),
        }
    }
}

impl<'a, T: Tile, S: TileShape> IntoIterator for &'a MultiTileGrid<T, S> {
    type Item = IndexedTile<'a, T, S>;

    type IntoIter = Iter<'a, T, S>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

#[derive(Debug)]
pub struct IterMut<'a, T: Tile, S: TileShape> {
    inner: tile_grid::IterMut<'a, TileEntry<(T, S)>>,
}

impl<'a, T: Tile, S: TileShape> Iterator for IterMut<'a, T, S> {
    type Item = IndexedTileMut<'a, T, S>;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some((origin, entry)) = self.inner.next() {
            match entry {
                TileEntry::Origin((tile, shape)) => {
                    return Some(IndexedTileMut {
                        tile,
                        shape,
                        origin,
                    });
                }
                TileEntry::Empty | TileEntry::Offset(_) => (),
            }
        }

        None
    }
}

impl<T: Tile, S: TileShape> MultiTileGrid<T, S> {
    pub fn iter_mut(&mut self) -> IterMut<'_, T, S> {
        IterMut {
            inner: self.data.iter_mut(),
        }
    }
}

impl<'a, T: Tile, S: TileShape> IntoIterator for &'a mut MultiTileGrid<T, S> {
    type Item = IndexedTileMut<'a, T, S>;

    type IntoIter = IterMut<'a, T, S>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<T: Tile + Hash, S: TileShape + Hash> Hash for MultiTileGrid<T, S> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        for (i, tile) in self.iter().enumerate() {
            (i, tile).hash(state);
        }
    }
}

impl<T: Tile + Eq, S: TileShape + Eq> PartialEq for MultiTileGrid<T, S> {
    fn eq(&self, other: &Self) -> bool {
        self.iter().zip(other.iter()).all(|(a, b)| a == b)
    }
}

impl<T: Tile + Eq, S: TileShape + Eq> Eq for MultiTileGrid<T, S> {}
