use direction::*;
use grid_2d::*;
use grid::SolidGrid;
use path::PathWalk;
use bfs::*;
use distance_map::*;
use error::*;
use config::*;

fn grid_from_strings(strings: &Vec<&str>) -> (Grid<bool>, Coord, Coord) {
    let width = strings[0].len() as u32;
    let height = strings.len() as u32;
    let size = Size::new(width, height);
    let mut grid = Grid::new_clone(size, false);
    let mut start = None;
    let mut goal = None;
    for (i, line) in strings.into_iter().enumerate() {
        for (j, ch) in line.chars().enumerate() {
            let coord = Coord::new(j as i32, i as i32);
            match ch {
                '.' => (),
                '#' => *grid.get_mut(coord).unwrap() = true,
                's' => start = Some(coord),
                'g' => goal = Some(coord),
                'B' => {
                    goal = Some(coord);
                    start = Some(coord);
                }
                'G' => {
                    goal = Some(coord);
                    *grid.get_mut(coord).unwrap() = true;
                }
                'S' => {
                    start = Some(coord);
                    *grid.get_mut(coord).unwrap() = true;
                }
                _ => panic!(),
            }
        }
    }

    (grid, start.unwrap(), goal.unwrap())
}

impl SolidGrid for Grid<bool> {
    fn is_solid(&self, coord: Coord) -> Option<bool> {
        self.get(coord).cloned()
    }
}

fn common_test<V, D>(strings: &Vec<&str>, directions: D, length: usize)
where
    V: Into<Direction>,
    D: Copy + IntoIterator<Item = V>,
{
    let (grid, start, goal) = grid_from_strings(strings);
    let mut ctx = BfsContext::new(grid.size());
    let mut path = Vec::new();
    let metadata = ctx.bfs(
        &grid,
        start,
        goal,
        directions,
        Default::default(),
        &mut path,
    ).unwrap();

    println!("{:?}", metadata);

    assert_eq!(path.len(), length);

    let walk = PathWalk::new(start, &path);

    let (should_be_goal, _) = walk.inspect(|&(coord, _)| {
        assert_eq!(grid.is_solid(coord), Some(false));
    }).last()
        .unwrap_or((start, Direction::North));

    assert_eq!(should_be_goal, goal);
}

#[test]
fn wall() {
    let strings = vec![
        "..........",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#...g.",
        "....#.....",
        "..........",
        "..........",
        "..........",
    ];
    common_test(&strings, CardinalDirections, 12);
    common_test(&strings, Directions, 7);
}

#[test]
fn no_path() {
    let strings = vec![
        "....#.....",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#...g.",
        "....######",
        "..........",
        "..........",
        "..........",
    ];

    let (grid, start, goal) = grid_from_strings(&strings);
    let mut ctx = BfsContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.bfs(
        &grid,
        start,
        goal,
        Directions,
        Default::default(),
        &mut path,
    );

    assert_eq!(result, Err(Error::NoPath));
}

#[test]
fn start_is_goal() {
    let strings = vec![
        "..........",
        "....#.....",
        ".B..#.....",
        "..........",
        "..........",
    ];
    common_test(&strings, CardinalDirections, 0);
    common_test(&strings, Directions, 0);
}

#[test]
fn goal_is_solid() {
    let strings = vec![
        "....#.....",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#.....",
        "....###G##",
        "..........",
        "..........",
        "..........",
    ];

    let (grid, start, goal) = grid_from_strings(&strings);
    let mut ctx = BfsContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.bfs(
        &grid,
        start,
        goal,
        Directions,
        Default::default(),
        &mut path,
    );

    assert_eq!(result, Err(Error::NoPath));
}

#[test]
fn start_is_solid() {
    let strings = vec![
        "....#.....",
        "....#.....",
        "....#.....",
        "....#.....",
        ".S..#.....",
        "....#.....",
        "....######",
        "..........",
        ".......g..",
        "..........",
    ];

    let (grid, start, goal) = grid_from_strings(&strings);
    let mut ctx = BfsContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.bfs(
        &grid,
        start,
        goal,
        Directions,
        SearchConfig {
            allow_solid_start: false,
        },
        &mut path,
    );

    assert_eq!(result, Err(Error::StartSolid));
}

#[test]
fn start_outside_grid() {
    let strings = vec![
        "....#.....",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#.....",
        "....######",
        "..........",
        ".......g..",
        "..........",
    ];

    let (grid, _, goal) = grid_from_strings(&strings);

    let start = Coord::new(-1, -1);

    let mut ctx = BfsContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.bfs(
        &grid,
        start,
        goal,
        Directions,
        Default::default(),
        &mut path,
    );

    assert_eq!(result, Err(Error::StartOutsideGrid));
}

#[test]
fn distance_map() {
    let strings = vec!["..#.", "..##", ".#.B", ".#.."];

    let (grid, start, _) = grid_from_strings(&strings);

    let mut ctx = BfsContext::new(grid.size());
    let mut distance_map: DistanceMap<u32> = DistanceMap::new(ctx.size());

    let result = ctx.populate_distance_map(
        &grid,
        start,
        Directions,
        Default::default(),
        &mut distance_map,
    ).unwrap();

    assert_eq!(result.num_nodes_visited, 10);

    assert!(distance_map.get(Coord::new(10, 10)).is_outside());
    assert!(distance_map.get(Coord::new(3, 2)).is_origin());
    assert!(distance_map.get(Coord::new(3, 0)).is_unvisited());
    assert!(distance_map.get(Coord::new(1, 2)).is_unvisited());
    assert_eq!(distance_map.get(Coord::new(0, 3)).cell().unwrap().cost(), 4);
    assert_eq!(
        distance_map
            .get(Coord::new(0, 3))
            .cell()
            .unwrap()
            .direction(),
        Direction::North
    );
    assert_eq!(distance_map.get(Coord::new(1, 1)).cell().unwrap().cost(), 2);
    assert_eq!(
        distance_map
            .get(Coord::new(1, 1))
            .cell()
            .unwrap()
            .direction(),
        Direction::SouthEast
    );
}

#[test]
fn distance_map_cardinal() {
    let strings = vec!["..#.", "...#", ".#.B", ".#.."];

    let (grid, start, _) = grid_from_strings(&strings);

    let mut ctx = BfsContext::new(grid.size());
    let mut distance_map: DistanceMap<u32> = DistanceMap::new(ctx.size());

    let result = ctx.populate_distance_map(
        &grid,
        start,
        CardinalDirections,
        Default::default(),
        &mut distance_map,
    ).unwrap();

    assert_eq!(result.num_nodes_visited, 11);

    assert!(distance_map.get(Coord::new(10, 10)).is_outside());
    assert!(distance_map.get(Coord::new(3, 2)).is_origin());
    assert!(distance_map.get(Coord::new(3, 0)).is_unvisited());
    assert!(distance_map.get(Coord::new(1, 2)).is_unvisited());
    assert_eq!(distance_map.get(Coord::new(0, 3)).cell().unwrap().cost(), 6);
    assert_eq!(
        distance_map
            .get(Coord::new(0, 3))
            .cell()
            .unwrap()
            .direction(),
        Direction::North
    );
    assert_eq!(distance_map.get(Coord::new(1, 1)).cell().unwrap().cost(), 3);
    assert_eq!(
        distance_map
            .get(Coord::new(1, 1))
            .cell()
            .unwrap()
            .direction(),
        Direction::East
    );
}

#[test]
fn bfs_best() {
    let strings = vec![
        "....#.....",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#.....",
        "....######",
        "..........",
        ".......g..",
        "..........",
    ];

    let (grid, start, _) = grid_from_strings(&strings);

    let mut ctx = BfsContext::new(grid.size());
    let mut path = Vec::new();

    let score = |coord: Coord| Some(coord.x + coord.y);

    let metadata = ctx.bfs_best(
        &grid,
        start,
        score,
        Directions,
        Default::default(),
        ::std::usize::MAX,
        &mut path,
    ).unwrap();

    assert_eq!(metadata.length, 8);

    let walk = PathWalk::new(start, &path);

    let (should_be_bottom_right, _) = walk.inspect(|&(coord, _)| {
        assert_eq!(grid.is_solid(coord), Some(false));
    }).last()
        .unwrap_or((start, Direction::North));

    assert_eq!(should_be_bottom_right, Coord::new(9, 9));
}
