use direction::*;
use grid_2d::*;
use num_traits::Zero;

#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy)]
pub struct DistanceMapCell<Cost> {
    pub(crate) seen: u64,
    pub(crate) visited: u64,
    pub(crate) cost: Cost,
    pub(crate) direction: Direction,
    pub(crate) coord: Coord,
}

impl<Cost: Zero> DistanceMapCell<Cost> {
    fn new(coord: Coord) -> Self {
        Self {
            seen: 0,
            visited: 0,
            cost: Zero::zero(),
            direction: Direction::North,
            coord,
        }
    }
}

impl<Cost> DistanceMapCell<Cost>
where
    Cost: Copy,
{
    pub fn cost(&self) -> Cost {
        self.cost
    }
    pub fn direction(&self) -> Direction {
        self.direction
    }
}

#[derive(Debug, Clone)]
pub enum DistanceMapEntry<'a, Cost: 'a> {
    Origin,
    Unvisited,
    Outside,
    Cell(&'a DistanceMapCell<Cost>),
}

impl<'a, Cost> DistanceMapEntry<'a, Cost> {
    pub fn cell(self) -> Option<&'a DistanceMapCell<Cost>> {
        match self {
            DistanceMapEntry::Cell(c) => Some(c),
            _ => None,
        }
    }
    pub fn is_origin(self) -> bool {
        match self {
            DistanceMapEntry::Origin => true,
            _ => false,
        }
    }
    pub fn is_unvisited(self) -> bool {
        match self {
            DistanceMapEntry::Unvisited => true,
            _ => false,
        }
    }
    pub fn is_outside(self) -> bool {
        match self {
            DistanceMapEntry::Outside => true,
            _ => false,
        }
    }
}

#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct DistanceMap<Cost> {
    pub(crate) seq: u64,
    pub(crate) grid: Grid<DistanceMapCell<Cost>>,
    pub(crate) origin: Coord,
}

impl<Cost> DistanceMap<Cost>
where
    Cost: Zero + Copy,
{
    pub fn new(size: Size) -> Self {
        Self {
            seq: 1,
            grid: Grid::new_fn(size, DistanceMapCell::new),
            origin: Coord::new(0, 0),
        }
    }

    pub fn width(&self) -> u32 {
        self.grid.width()
    }

    pub fn height(&self) -> u32 {
        self.grid.height()
    }

    pub fn size(&self) -> Size {
        self.grid.size()
    }

    pub fn get(&self, coord: Coord) -> DistanceMapEntry<Cost> {
        if let Some(cell) = self.grid.get(coord) {
            if cell.seen == self.seq {
                if coord == self.origin {
                    DistanceMapEntry::Origin
                } else {
                    DistanceMapEntry::Cell(cell)
                }
            } else {
                DistanceMapEntry::Unvisited
            }
        } else {
            DistanceMapEntry::Outside
        }
    }

    pub fn cost(&self, coord: Coord) -> Option<Cost> {
        match self.get(coord) {
            DistanceMapEntry::Cell(cell) => Some(cell.cost),
            DistanceMapEntry::Origin => Some(Zero::zero()),
            DistanceMapEntry::Unvisited => None,
            DistanceMapEntry::Outside => None,
        }
    }
}

#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct UniformDistanceMap<Cost, Directions> {
    pub(crate) distance_map: DistanceMap<Cost>,
    pub(crate) directions: Directions,
}

impl<Cost, IntoDirection, Directions> UniformDistanceMap<Cost, Directions>
where
    Cost: Zero + Copy,
    IntoDirection: Into<Direction>,
    Directions: Copy + IntoIterator<Item = IntoDirection>,
{
    pub fn new(size: Size, directions: Directions) -> Self {
        let distance_map = DistanceMap::new(size);
        Self {
            distance_map,
            directions,
        }
    }

    pub fn width(&self) -> u32 {
        self.distance_map.width()
    }

    pub fn height(&self) -> u32 {
        self.distance_map.height()
    }

    pub fn size(&self) -> Size {
        self.distance_map.size()
    }

    pub fn get(&self, coord: Coord) -> DistanceMapEntry<Cost> {
        self.distance_map.get(coord)
    }

    pub fn cost(&self, coord: Coord) -> Option<Cost> {
        self.distance_map.cost(coord)
    }
}
