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

const DEFAULT_ORDINAL_MULTIPLIER: u32 = 2;

struct TestGrid {
    grid: Grid<Option<u32>>,
    ordinal_multiplier: u32,
}

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

impl SolidGrid for TestGrid {
    fn is_solid(&self, coord: Coord) -> Option<bool> {
        self.grid.get(coord).map(Option::is_none)
    }
}

impl CostGrid for TestGrid {
    type Cost = u32;
    fn cost(&self, coord: Coord, direction: Direction) -> Option<CostCell<Self::Cost>> {
        if let Some(cost) = self.grid.get(coord).cloned()? {
            if direction.is_ordinal() {
                Some(CostCell::Cost(cost * self.ordinal_multiplier))
            } else {
                Some(CostCell::Cost(cost))
            }
        } else {
            None
        }
    }
}

fn grid_from_strings(strings: &Vec<&str>, ordinal_multiplier: u32) -> (TestGrid, 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, Some(1));
    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() = Some(10),
                '#' => *grid.get_mut(coord).unwrap() = None,
                's' => start = Some(coord),
                'g' => goal = Some(coord),
                'B' => {
                    goal = Some(coord);
                    start = Some(coord);
                }
                'G' => {
                    goal = Some(coord);
                    *grid.get_mut(coord).unwrap() = None;
                }
                'S' => {
                    start = Some(coord);
                    *grid.get_mut(coord).unwrap() = None;
                }
                _ => panic!(),
            }
        }
    }

    let grid = TestGrid {
        grid,
        ordinal_multiplier,
    };

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

#[derive(Clone, Copy, PartialEq, Eq)]
enum WhichDirections {
    Cardinal,
    All,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum GridWeights {
    Weighted,
    Uniform,
}

use self::WhichDirections::*;
use self::GridWeights::*;

fn common_test(
    strings: &Vec<&str>,
    ordinal_multiplier: u32,
    which_directions: WhichDirections,
    grid_weights: GridWeights,
    length: usize,
    cost: u32,
) {
    let (grid, start, goal) = grid_from_strings(strings, ordinal_multiplier);
    let mut ctx = SearchContext::new(grid.size());

    let check_result = |path: &Vec<_>| {
        assert_eq!(path.len(), length);

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

        let (should_be_goal, total_cost) =
            walk.fold((start, 0), |(_, total_cost), (coord, direction)| {
                if let CostCell::Cost(cost) = grid.cost(coord, direction).unwrap() {
                    (coord, total_cost + cost)
                } else {
                    panic!("Path goes through wall");
                }
            });

        assert_eq!(should_be_goal, goal);
        assert_eq!(total_cost, cost);
    };

    let mut path = Vec::new();

    let (with_heuristic, without_heuristic, jps) = if which_directions == Cardinal {
        let metadata = ctx.dijkstra(
            &grid,
            start,
            goal,
            DirectionsCardinal,
            Default::default(),
            &mut path,
        ).unwrap();

        let without_heuristic = metadata.num_nodes_visited;
        check_result(&path);

        let metadata = ctx.astar_cardinal_manhatten_distance_heuristic(
            &grid,
            start,
            goal,
            Default::default(),
            &mut path,
        ).unwrap();
        let with_heuristic = metadata.num_nodes_visited;
        check_result(&path);

        let jps = if grid_weights == Uniform {
            let metadata = ctx.jump_point_search_cardinal_manhatten_distance_heuristic(
                &grid,
                start,
                goal,
                Default::default(),
                &mut path,
            ).unwrap();
            check_result(&path);
            Some(metadata.num_nodes_visited)
        } else {
            None
        };

        (with_heuristic, without_heuristic, jps)
    } else {
        let metadata = ctx.dijkstra(
            &grid,
            start,
            goal,
            Directions,
            Default::default(),
            &mut path,
        ).unwrap();
        check_result(&path);
        let without_heuristic = metadata.num_nodes_visited;

        let weights = HeuristicDirectionWeights::new(1, ordinal_multiplier);

        let metadata = ctx.astar_diagonal_distance_heuristic(
            &grid,
            start,
            goal,
            weights,
            Default::default(),
            &mut path,
        ).unwrap();
        check_result(&path);
        let with_heuristic = metadata.num_nodes_visited;

        let jps = if grid_weights == Uniform {
            let mut jps_ctx: SearchContext<f64> = SearchContext::new(grid.size());
            let metadata = jps_ctx
                .jump_point_search_octile_distance_heuristic(
                    &grid,
                    start,
                    goal,
                    Default::default(),
                    &mut path,
                )
                .unwrap();
            check_result(&path);
            Some(metadata.num_nodes_visited)
        } else {
            None
        };

        (with_heuristic, without_heuristic, jps)
    };

    println!(
        "Nodes visited: {}, With heuristic: {}, With JPS: {:?}",
        without_heuristic, with_heuristic, jps
    );
}

#[test]
fn uniform_wall() {
    let strings = vec![
        "..........",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#...g.",
        "....#.....",
        "..........",
        "..........",
        "..........",
    ];
    common_test(&strings, 1, Cardinal, Uniform, 12, 12);
    common_test(&strings, 1, All, Uniform, 7, 7);
}

#[test]
fn uniform_corridors() {
    let strings = vec![
        "#####.####",
        "#........#",
        "###.#.##.#",
        "###.#.##.#",
        "#s#.#..###",
        "#.#.#.##g#",
        "#.#.#.##.#",
        "#...#....#",
        "##.####.##",
        ".####.####",
    ];
    common_test(&strings, 1, Cardinal, Uniform, 24, 24);
    common_test(&strings, 1, All, Uniform, 18, 18);
}

#[test]
fn non_uniform_wall() {
    let strings = vec![
        "....,.....",
        "....#.....",
        "....#.....",
        "....#.....",
        ".s..#.....",
        "....#...g.",
        "....#,....",
        "....,,....",
        "....,,....",
        "....,,....",
    ];
    common_test(&strings, 1, Cardinal, Weighted, 16, 25);
    common_test(&strings, 1, All, Weighted, 9, 18);
}

#[test]
fn cheap_route() {
    let strings = vec![
        "..........",
        "..........",
        "..........",
        ",,,.,,,,..",
        ".s,...,,..",
        "..,..,,...",
        "..,..,g...",
        ".....,,...",
        "....,,,...",
        "....,,,...",
    ];
    common_test(&strings, 1, Cardinal, Weighted, 9, 18);
    common_test(&strings, 1, All, Weighted, 15, 15);
}

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

    let (grid, start, goal) = grid_from_strings(&strings, DEFAULT_ORDINAL_MULTIPLIER);
    let mut ctx = SearchContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.dijkstra(
        &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,
        DEFAULT_ORDINAL_MULTIPLIER,
        Cardinal,
        Uniform,
        0,
        0,
    );
    common_test(&strings, DEFAULT_ORDINAL_MULTIPLIER, All, Uniform, 0, 0);
}

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

    let (grid, start, goal) = grid_from_strings(&strings, DEFAULT_ORDINAL_MULTIPLIER);
    let mut ctx = SearchContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.dijkstra(
        &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, DEFAULT_ORDINAL_MULTIPLIER);
    let mut ctx = SearchContext::new(grid.size());
    let mut path = Vec::new();
    let result = ctx.dijkstra(
        &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, DEFAULT_ORDINAL_MULTIPLIER);

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

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

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

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

    common_test(&strings, 20, All, Weighted, 2, 2);
    common_test(&strings, 1, All, Uniform, 1, 1);
}

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

    let (grid, start, goal) = grid_from_strings(&strings, DEFAULT_ORDINAL_MULTIPLIER);

    let mut ctx: SearchContext<f32> = SearchContext::new(grid.size());
    let mut path = Vec::new();
    ctx.jump_point_search_octile_distance_heuristic(
        &grid,
        start,
        goal,
        Default::default(),
        &mut path,
    ).unwrap();
    assert_eq!(path.len(), 11);
}

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

    let (grid, start, goal) = grid_from_strings(&strings, DEFAULT_ORDINAL_MULTIPLIER);

    let mut ctx: SearchContext<u32> = SearchContext::new(grid.size());
    let mut path = Vec::new();
    ctx.jump_point_search_cardinal_manhatten_distance_heuristic(
        &grid,
        start,
        goal,
        Default::default(),
        &mut path,
    ).unwrap();
    assert_eq!(path.len(), 18);
}

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

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

    let mut ctx: SearchContext<u32> = SearchContext::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(), 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::SouthEast
    );
}

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

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

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

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

    assert_eq!(distance_map.get(Coord::new(0, 0)).cell().unwrap().cost(), 2);
}

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

    let (grid, start, goal) = grid_from_strings(&strings, DEFAULT_ORDINAL_MULTIPLIER);
    let mut ctx = SearchContext::new(grid.size());
    let mut path = Vec::new();
    let predicate = |c| c == goal;
    let result = ctx.dijkstra_predicate(
        &grid,
        start,
        predicate,
        Directions,
        Default::default(),
        &mut path,
    );

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

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

    let (grid, start, goal) = grid_from_strings(&strings, DEFAULT_ORDINAL_MULTIPLIER);

    let mut search_ctx: SearchContext<i32> = SearchContext::new(grid.size());
    let mut bfs_ctx = BfsContext::new(grid.size());
    let mut distance_map: UniformDistanceMap<i32, _> =
        UniformDistanceMap::new(grid.size(), CardinalDirections);
    let mut path = Vec::new();

    bfs_ctx
        .populate_uniform_distance_map(&grid, goal, Default::default(), &mut distance_map)
        .unwrap();
    let _metadata = search_ctx
        .best_search_uniform_distance_map(
            &grid,
            start,
            Default::default(),
            3,
            &distance_map,
            &mut path,
        )
        .unwrap();

    assert_eq!(path[0], Direction::South);
}
