// JarnikPrim.hpp
//
//     Created on: 19. 03. 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

#pragma once

#include <alib/pair>
#include <alib/set>
#include <alib/map>

#include <common/SupportFunction.hpp>

namespace graph {

namespace spanning_tree {

class JarnikPrim {
// ---------------------------------------------------------------------------------------------------------------------
 public:

  template<typename TGraph, typename TNode>
  static
  TGraph
  findSpanningTree(const TGraph &graph, const TNode &start);

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

 private:

  template<typename TNode, typename TWeight>
  struct Data {
    ext::set<ext::pair<TWeight, TNode>> queue; // priority queue
    ext::map<TNode, TWeight> g; // distances (aka G score)
    ext::map<TNode, TNode> p; // parents
  };

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

  template<typename TNode, typename TWeight>
  inline static void init(JarnikPrim::Data<TNode, TWeight> &data, const TNode &start);

};

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

template<typename TGraph, typename TNode>
TGraph JarnikPrim::findSpanningTree(const TGraph &graph, const TNode &start) {
  using weight_type = typename TGraph::edge_type::weight_type;

  TGraph res;
  Data<TNode, weight_type> data;

  // Init search
  init(data, start);

  while (!data.queue.empty()) {
    TNode n = data.queue.begin()->second;
    data.queue.erase(data.queue.begin());

    // If not already in spanning tree add it
    auto search = data.p.find(n);
    if (search != data.p.end()) {
      res.addEdge(search->second, n, data.g.at(n));
    }

    for (const auto &s_edge: graph.successorEdges(n)) {
      const TNode &s = common::SupportFunction::other(s_edge, n); // successor

      // Calculate new G score
      weight_type gscore = s_edge.weight();

      // Search if the node s was already visited
      auto search_d = data.g.find(s);

      // If not or the distance can be improve do relaxation
      if (search_d == data.g.end() || search_d->second > gscore) {
        // Search if the node s is in OPEN
        auto search_q = data.queue.find(ext::make_pair(search_d->second, s));
        if (search_q != data.queue.end()) {
          // Erase node from priority queue
          data.queue.erase(search_q);
        }

        data.g[s] = gscore;
        data.p.insert_or_assign(s, n);
        data.queue.insert(ext::make_pair(data.g[s], s));
      }
    }
  }

  return res;
}

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

template<typename TNode, typename TWeight>
void JarnikPrim::init(JarnikPrim::Data<TNode, TWeight> &data, const TNode &start) {
  data.g[start] = 0;
  data.p.insert_or_assign(start, start);
  data.queue.insert(ext::make_pair(data.g[start], start));
}

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

} // namespace spanning_tree

} // namespace graph

