// ShortestPathTest.cpp
//
//     Created on: 21. 01. 2018
//         Author: Jan Uhlik
//    Modified by:
//
// Copyright (c) 2017 Czech Technical University in Prague | Faculty of Information Technology. All rights reserved.
// Git repository: https://gitlab.fit.cvut.cz/algorithms-library-toolkit/automata-library

#include <catch2/catch.hpp>

#include <graph/GraphClasses.hpp>
#include <node/NodeClasses.hpp>
#include <edge/EdgeClasses.hpp>

#include <heuristic/SquareGridHeuristics.hpp>

#include <traverse/BFS.hpp>
#include <traverse/IDDFS.hpp>
#include <shortest_path/BellmanFord.hpp>
#include <shortest_path/SPFA.hpp>
#include "shortest_path/Dijkstra.hpp"
#include <shortest_path/AStar.hpp>
#include <shortest_path/IDAStar.hpp>
#include <shortest_path/MM.hpp>
#include <generate/RandomGraphFactory.hpp>
#include <generate/RandomGridFactory.hpp>

using namespace graph;

const double RELATIVE_ERROR = 0.0001; // 0.01 %

TEST_CASE ( "Shortest Path", "[unit][graph][shortest_path]" ) {
	SECTION ( "BFS" ) {
		graph::DirectedGraph<int, edge::WeightedEdge<int>> graph;

		graph.addNode(1);
		graph.addNode(2);
		graph.addNode(3);
		graph.addNode(4);
		graph.addNode(5);

		graph.addEdge(edge::WeightedEdge<int>(1, 2, 5));
		graph.addEdge(edge::WeightedEdge<int>(2, 3, 3));
		graph.addEdge(edge::WeightedEdge<int>(3, 4, 3));
		graph.addEdge(edge::WeightedEdge<int>(4, 5, 5));
		graph.addEdge(edge::WeightedEdge<int>(2, 4, 4));

		auto res1 = traverse::BFS::findPath(graph, 1, 5);

		auto res2 = traverse::BFS::findPathBidirectional(graph, 1, 5);

		ext::vector<int> res = {1, 2, 4, 5};
		CHECK(res1 == res);
		CHECK(res1.size() == res.size());
		CHECK(res2 == res);
		CHECK(res2.size() == res.size());
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "IDDFS" ) {
		graph::DirectedGraph<int, ext::pair<int, int>> graph;

		graph.addNode(1);
		graph.addNode(2);
		graph.addNode(3);
		graph.addNode(4);
		graph.addNode(5);
		graph.addNode(5);
		graph.addNode(6);
		graph.addNode(7);
		graph.addNode(8);

		graph.addEdge(1, 2);
		graph.addEdge(1, 3);
		graph.addEdge(1, 4);
		graph.addEdge(2, 5);
		graph.addEdge(2, 6);
		graph.addEdge(3, 7);
		graph.addEdge(6, 8);

		auto bfs = traverse::BFS::findPath(graph, 1, 8);
		auto iddfs = traverse::IDDFS::findPath(graph, 1, 8);
		auto iddfsBi = traverse::IDDFS::findPathBidirectional(graph, 1, 8);

		CHECK(bfs == iddfs);
		CHECK(iddfsBi == iddfs);
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "Dijkstra" ) {
		graph::UndirectedGraph<int, edge::WeightedEdge<int>> graph;

		graph.addNode(1);
		graph.addNode(2);
		graph.addNode(3);
		graph.addNode(4);
		graph.addNode(5);

		graph.addEdge(edge::WeightedEdge<int>(1, 2, 5));
		graph.addEdge(edge::WeightedEdge<int>(2, 3, 3));
		graph.addEdge(edge::WeightedEdge<int>(3, 4, 3));
		graph.addEdge(edge::WeightedEdge<int>(4, 5, 5));
		graph.addEdge(edge::WeightedEdge<int>(2, 4, 4));

		auto res1 = graph::shortest_path::Dijkstra::findPath(graph, 1, 5);

		auto res2 = graph::shortest_path::Dijkstra::findPathBidirectional(graph, 1, 5);

		ext::vector<int> res = {1, 2, 4, 5};
		CHECK(res1.first == res);
		CHECK(res1.second == 14);
		CHECK(res2.first == res);
		CHECK(res2.second == 14);
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "BFS grid" ) {
		grid::SquareGrid4<> graph(10, 10);
		using node_type = decltype(graph)::node_type;

		graph.addObstacle(3, 2);
		graph.addObstacle(3, 3);
		graph.addObstacle(3, 4);
		graph.addObstacle(3, 5);
		graph.addObstacle(4, 5);
		graph.addObstacle(5, 5);
		graph.addObstacle(6, 5);

		node_type start = ext::make_pair(8l, 2l);
		node_type goal = ext::make_pair(1l, 9l);

		auto res1 = traverse::BFS::findPath(graph, start, goal);
		auto res2 = traverse::BFS::findPathBidirectional(graph, start, goal);

		CHECK(res1.size() == 15);
		CHECK(res1.size() == res2.size());
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "Dijkstra Grid" ) {
		grid::WeightedSquareGrid8<> graph(10, 10);
		using node_type = decltype(graph)::node_type;

		graph.addObstacle(3, 2);
		graph.addObstacle(3, 3);
		graph.addObstacle(3, 4);
		graph.addObstacle(3, 5);
		graph.addObstacle(4, 5);
		graph.addObstacle(5, 5);
		graph.addObstacle(6, 5);

		node_type start = ext::make_pair(8l, 2l);
		node_type goal = ext::make_pair(1l, 9l);

		auto res1 = graph::shortest_path::Dijkstra::findPath(graph, start, goal);
		auto res2 = graph::shortest_path::Dijkstra::findPathBidirectional(graph, start, goal);

		CHECK(Approx(res1.second).epsilon(RELATIVE_ERROR) == (M_SQRT2 * 5 + 4));
		CHECK(res1.second == res2.second);
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "A* Grid" ) {
		grid::WeightedSquareGrid8<> graph(11, 11);
		using node_type = decltype(graph)::node_type;

		graph.addObstacle(2, 5);
		graph.addObstacle(3, 5);
		graph.addObstacle(4, 5);
		graph.addObstacle(5, 5);
		graph.addObstacle(6, 5);
		graph.addObstacle(7, 5);
		graph.addObstacle(5, 3);
		graph.addObstacle(5, 4);
		graph.addObstacle(5, 6);
		graph.addObstacle(5, 7);
		graph.addObstacle(5, 8);

		node_type start = ext::make_pair(9l, 1l);
		node_type goal = ext::make_pair(1l, 9l);

		auto f_heuristic_forward = [&](const node_type &n) -> double {
			return heuristic::DiagonalDistance::diagonalDistance(goal, n);
		};

		auto f_heuristic_backward = [&](const node_type &n) -> double {
			return heuristic::DiagonalDistance::diagonalDistance(start, n);
		};

		auto res1 = graph::shortest_path::AStar::findPath(graph,
				start,
				goal,
				f_heuristic_forward);

		auto res2 = graph::shortest_path::AStar::findPathBidirectional(graph,
				start,
				goal,
				f_heuristic_forward,
				f_heuristic_backward);

		auto res3 = graph::shortest_path::MM::findPathBidirectional(graph,
				start,
				goal,
				f_heuristic_forward,
				f_heuristic_backward);

		CHECK(Approx(res1.second).epsilon(RELATIVE_ERROR) == (M_SQRT2 * 4 + 8));
		CHECK(res1.second == res2.second);
		CHECK(res2.second == res3.second);
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "All" ) {
		graph::UndirectedGraph<int, edge::WeightedEdge<int, int>> graph;
		int start = 5;
		int goal = 17;

		auto f_heuristic = [&](const int &) -> int {
			return 0;
		};

		graph.addNode(1);
		graph.addNode(2);
		graph.addNode(3);
		graph.addNode(4);
		graph.addNode(5);
		graph.addNode(6);
		graph.addNode(7);
		graph.addNode(8);
		graph.addNode(9);
		graph.addNode(10);
		graph.addNode(11);
		graph.addNode(12);
		graph.addNode(13);
		graph.addNode(14);
		graph.addNode(15);
		graph.addNode(16);
		graph.addNode(17);
		graph.addNode(18);
		graph.addNode(19);
		graph.addNode(20);

		graph.addEdge(1, 5, 10);
		graph.addEdge(2, 3, 20);
		graph.addEdge(2, 4, 4);
		graph.addEdge(2, 6, 19);
		graph.addEdge(2, 5, 9);
		graph.addEdge(4, 7, 21);
		graph.addEdge(5, 6, 24);
		graph.addEdge(6, 7, 1);
		graph.addEdge(6, 8, 3);
		graph.addEdge(6, 9, 15);
		graph.addEdge(7, 10, 18);
		graph.addEdge(8, 12, 22);
		graph.addEdge(9, 11, 14);
		graph.addEdge(10, 18, 50); //
		graph.addEdge(10, 11, 8);
		graph.addEdge(11, 13, 2);
		graph.addEdge(12, 13, 5);
		graph.addEdge(13, 14, 13);
		graph.addEdge(13, 19, 7);
		graph.addEdge(14, 15, 16);
		graph.addEdge(15, 16, 6);
		graph.addEdge(15, 17, 25);
		graph.addEdge(16, 18, 17);
		graph.addEdge(17, 18, 12);
		graph.addEdge(17, 20, 11);
		graph.addEdge(19, 20, 23);

		auto dijkstra = graph::shortest_path::Dijkstra::findPath(graph, start, goal);
		auto dijkstraBi = graph::shortest_path::Dijkstra::findPathBidirectional(graph, start, goal);
		auto bellmanFord = graph::shortest_path::BellmanFord::findPath(graph, start, goal);
		auto spfa = graph::shortest_path::SPFA::findPath(graph, start, goal);
		auto astar = graph::shortest_path::AStar::findPath(graph, start, goal, f_heuristic);
		auto idastar = graph::shortest_path::IDAStar::findPath(graph, start, goal, f_heuristic);
		auto astarBi = graph::shortest_path::AStar::findPathBidirectional(graph, start, goal, f_heuristic, f_heuristic);
		auto mm = graph::shortest_path::MM::findPathBidirectional(graph, start, goal, f_heuristic, f_heuristic);

		CHECK(dijkstra.second == 94);

		CHECK(dijkstra == dijkstraBi);
		CHECK(bellmanFord == dijkstraBi);
		CHECK(bellmanFord == spfa);
		CHECK(astar == spfa);
		CHECK(astar == idastar);
		CHECK(astarBi == idastar);
		CHECK(astarBi == mm);

		auto BFS = traverse::BFS::findPath(graph, start, goal);
		auto BFSBi = traverse::BFS::findPathBidirectional(graph, start, goal);

		CHECK(BFS == BFSBi);
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "All Grid" ) {
		grid::WeightedSquareGrid8<int, edge::WeightedEdge<ext::pair<int, int>, double>> graph(11, 11);
		using node_type = ext::pair<int, int>;

		auto start = ext::make_pair(8, 2);
		auto goal = ext::make_pair(1, 9);

		graph.addObstacle(2, 5);
		graph.addObstacle(3, 5);
		graph.addObstacle(4, 5);
		graph.addObstacle(5, 5);
		graph.addObstacle(6, 5);
		graph.addObstacle(7, 5);
		graph.addObstacle(5, 3);
		graph.addObstacle(5, 4);
		graph.addObstacle(5, 6);
		graph.addObstacle(5, 7);
		graph.addObstacle(5, 8);

		auto f_heuristic_forward = [&](const node_type &n) -> double {
			return heuristic::DiagonalDistance::diagonalDistance(goal, n);
		};

		auto f_heuristic_backward = [&](const node_type &n) -> double {
			return heuristic::DiagonalDistance::diagonalDistance(start, n);
		};

		auto dijkstra = graph::shortest_path::Dijkstra::findPath(graph, start, goal);
		auto dijkstraBi = graph::shortest_path::Dijkstra::findPathBidirectional(graph, start, goal);
		auto bellmanFord = graph::shortest_path::BellmanFord::findPath(graph, start, goal);
		auto spfa = graph::shortest_path::SPFA::findPath(graph, start, goal);
		auto astar = graph::shortest_path::AStar::findPath(graph, start, goal, f_heuristic_forward);
		auto idastar = graph::shortest_path::IDAStar::findPath(graph, start, goal, f_heuristic_forward);
		auto astarBi = graph::shortest_path::AStar::findPathBidirectional(graph, start, goal, f_heuristic_forward, f_heuristic_backward);
		auto mm = graph::shortest_path::MM::findPathBidirectional(graph, start, goal, f_heuristic_forward, f_heuristic_backward);

		CHECK(Approx(dijkstra.second).epsilon(RELATIVE_ERROR) == (M_SQRT2 * 3 + 8));
		CHECK(Approx(dijkstra.second).epsilon(RELATIVE_ERROR) == dijkstraBi.second);
		CHECK(Approx(bellmanFord.second).epsilon(RELATIVE_ERROR) == dijkstraBi.second);
		CHECK(Approx(bellmanFord.second).epsilon(RELATIVE_ERROR) == spfa.second);
		CHECK(Approx(astar.second).epsilon(RELATIVE_ERROR) == spfa.second);
		CHECK(Approx(astar.second).epsilon(RELATIVE_ERROR) == idastar.second);
		CHECK(Approx(astarBi.second).epsilon(RELATIVE_ERROR) == idastar.second);
		CHECK(Approx(astarBi.second).epsilon(RELATIVE_ERROR) == mm.second);
	}

	// ---------------------------------------------------------------------------------------------------------------------

	SECTION ( "All Grid Random" ) {
		auto graph_tuple = generate::RandomGridFactory::randomGrid<grid::WeightedSquareGrid8<long, edge::WeightedEdge<ext::pair<long, long>>>>(50, 50, 100);
		auto graph = std::get<0>(graph_tuple);
		auto start = std::get<1>(graph_tuple);
		auto goal = std::get<2>(graph_tuple);

		using weight_type = decltype(graph)::edge_type::weight_type;
		using node_type = decltype(graph)::node_type;

		auto f_heuristic_forward = [&](const node_type &n) -> weight_type {
			return heuristic::DiagonalDistance::diagonalDistance(goal, n);
		};

		auto f_heuristic_backward = [&](const node_type &n) -> weight_type {
			return heuristic::DiagonalDistance::diagonalDistance(start, n);
		};

		auto dijkstra = graph::shortest_path::Dijkstra::findPath(graph, start, goal);
		auto dijkstraBi = graph::shortest_path::Dijkstra::findPathBidirectional(graph, start, goal);
		auto bellmanFord = graph::shortest_path::BellmanFord::findPath(graph, start, goal);
		auto spfa = graph::shortest_path::SPFA::findPath(graph, start, goal);
		auto astar = graph::shortest_path::AStar::findPath(graph, start, goal, f_heuristic_forward);
		auto astarBi = graph::shortest_path::AStar::findPathBidirectional(graph, start, goal, f_heuristic_forward, f_heuristic_backward);
		auto mm = graph::shortest_path::MM::findPathBidirectional(graph, start, goal, f_heuristic_forward, f_heuristic_backward);

		// CAPTURE (graph, start, goal);
		CAPTURE(dijkstra.second, dijkstraBi.second, bellmanFord.second, spfa.second, astar.second, astarBi.second, mm.second);

		// relative compares, two values can differ at most by 0.1 % of each other
		CHECK(Approx(dijkstra.second).epsilon(RELATIVE_ERROR) == dijkstraBi.second);
		CHECK(Approx(bellmanFord.second).epsilon(RELATIVE_ERROR) == dijkstraBi.second);
		CHECK(Approx(bellmanFord.second).epsilon(RELATIVE_ERROR) == spfa.second);
		CHECK(Approx(astar.second).epsilon(RELATIVE_ERROR) == spfa.second);
		CHECK(Approx(astarBi.second).epsilon(RELATIVE_ERROR) == astar.second);
		CHECK(Approx(astarBi.second).epsilon(RELATIVE_ERROR) == mm.second);
	}
}
