// Copyright (c) 2017 Czech Technical University in Prague | Faculty of Information Technology. All rights reserved.

#pragma once

#include <alib/functional>
#include <alib/map>
#include <alib/set>
#include <limits>
#include <stdexcept>

#include <common/SupportFunction.hpp>

namespace graph {

namespace shortest_path {

class FloydWarshall {
// ---------------------------------------------------------------------------------------------------------------------

 public:
  /// Find the shortest paths using FloydWarshall algorithm in the \p graph.
  ///
  /// \param graph to explore.
  ///
  /// \returns distances matrix implemented with two ext::map.
  ///
  /// \note TEdge of \p graph must follow graph::edge::WeightedEdge interface.
  /// \sa graph::edge_type::WeightedEdge.
  ///
  /// \throws std::out_of_range if \p graph contains negative cycle.
  ///
  template<typename TGraph>
  static
  ext::map<typename TGraph::node_type, ext::map<typename TGraph::node_type, typename TGraph::edge_type::weight_type>>
  run(const TGraph &graph);

// =====================================================================================================================

};

// =====================================================================================================================

template<typename TGraph>
ext::map<typename TGraph::node_type, ext::map<typename TGraph::node_type, typename TGraph::edge_type::weight_type>>
FloydWarshall::run(const TGraph &graph) {
  using weight_type = typename TGraph::edge_type::weight_type;
  using node_type = typename TGraph::node_type;

  ext::map<node_type, ext::map<node_type, weight_type>> dist;

  ext::set<node_type> nodes = graph.getNodes();

  // Init distances matrix
  for (auto &first : nodes) {
    dist.insert(ext::make_pair(first, ext::map<node_type, weight_type>()));
    for (auto &second: nodes) {
      if (first == second) {
        dist[first].insert(ext::make_pair(second, 0));
      } else {
        dist[first].insert(ext::make_pair(second, std::numeric_limits<weight_type>::max()));
      }
    }
  }

  for (auto &first : nodes) {
    for (auto &s_edge : graph.successorEdges(first)) {
      const node_type &s = common::SupportFunction::other(s_edge, first); // successor
      dist[first][s] = std::min(dist[s_edge.first][s_edge.second], s_edge.weight());
    }
  }

  // Run FloydWarshall
  for (auto &k : nodes) {
    for (auto &i : nodes) {
      if (dist[i][k] != std::numeric_limits<weight_type>::max()) { // Optimization
        for (auto &j : nodes) {
          if (dist[k][j] != std::numeric_limits<weight_type>::max()) { // Optimization
            dist[i][j] = std::min(dist[i][j], dist[i][k] + dist[k][j]);
          }
        }
      }
    }
  }

  // Check for negative cycle
  for (auto &n : nodes)
    if (dist[n][n] < 0)
      throw std::out_of_range("FloydWarshall: Detected negative weight cycle.");

  return dist;
}

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

} // namespace shortest_path

} // namespace graph