
#ifndef PLS_DATAFLOW_INTERNAL_GRAPH_H_
#define PLS_DATAFLOW_INTERNAL_GRAPH_H_

#include <atomic>
#include <tuple>
#include <iostream>

#include "build_state.h"
#include "node.h"
#include "function_node.h"
#include "in_port.h"
#include "out_port.h"
#include "inputs.h"
#include "outputs.h"

#include "pls/pls.h"

namespace pls {
namespace dataflow {
namespace internal {

template<typename INS, typename OUTS>
class graph {};

template<typename I0, typename ...I, typename O0, typename ...O>
class graph<inputs<I0, I...>, outputs<O0, O...>> : public node {
  template<typename INS, typename OUTS, typename FUNC>
  friend
  class function_node;
 private:
  // Our own type
  using self_type = graph<inputs<I0, I...>, outputs<O0, O...>>;

  // Input-Output port types (internal)
  using inputs_type = multi_out_port<I0, I...>;
  using outputs_type = multi_in_port<self_type, 0, O0, O...>;

  // Input-Output value tuples
  using value_input_tuple = std::tuple<I0, I...>;
  using input_tuple = std::tuple<token<I0>, token<I>...>;

  using value_output_tuple = std::tuple<O0, O...>;
  using output_tuple = std::tuple<token<O0>, token<O>...>;

  static constexpr int num_in_ports = std::tuple_size<input_tuple>();
  static constexpr int num_out_ports = std::tuple_size<output_tuple>();

  // Memory used for ONE active invocation of the dataflow-graph
  struct invocation_memory {
    value_output_tuple *output_buffer_;
  };

 public:
  template<int POS>
  using input_at = typename inputs_type::template out_port_type_at<POS>;
  template<int POS>
  using output_at = typename outputs_type::template in_port_type_at<POS>;

  template<int POS>
  input_at<POS> &input() {
    return inputs_.template get<POS>();
  }

  inputs_type &input_ports() {
    return inputs_;
  }

  template<int POS>
  output_at<POS> &output() {
    return outputs_.template get<POS>();
  }

  outputs_type &output_ports() {
    return outputs_;
  }

  template<typename ...OS, typename FUNC>
  function_node<inputs<I0, I...>, outputs<OS...>, FUNC>
  &operator>>(function_node<inputs<I0, I...>, outputs<OS...>, FUNC> &other_node);

  void wait_for_all() {
    pls::scheduler::wait_for_all();
  }

  template<int POS, typename T>
  void token_pushed(token<T> token);

  graph() : inputs_{}, outputs_{this, this} {}

  void build();

  void run(value_input_tuple input, value_output_tuple *output) {
    PLS_ASSERT(build_state_ == build_state::built, "Must build graph before running it!")
    const auto lambda = [=]() {
      pls::scheduler::spawn_child<run_graph_task>(this, input, output);
    };
    pls::scheduler::spawn_child<lambda_task_by_value<decltype(lambda)>>(lambda);
  }

  int num_successors() const override {
    return 0;
  }
  node *successor_at(int) const override {
    PLS_ERROR("A graph instance has no direct successor!")
  }

  bool is_fully_connected() const override {
    return inputs_.fully_connected() && outputs_.fully_connected();
  }

  int instance_buffer_size() const override {
    return sizeof(invocation_memory);
  }
  void init_instance_buffer(void *memory) const override {
    new(memory) invocation_memory{};
  };
  void clean_up_instance_buffer(void *memory) const override {
    auto invocation = reinterpret_cast<invocation_memory *>(memory);
    invocation->~invocation_memory();
  }

 private:
  inputs_type inputs_;
  outputs_type outputs_;

  // Information about building the graph
  node *node_list_start_{nullptr};
  node *node_list_current_{nullptr};
  int num_nodes_{0};

  // Internals required for building and running
  void build_recursive(node *node);
  void add_node(node *new_node);

  template<int N, typename ...IT>
  struct feed_inputs;
  class run_graph_task;
};

}
}
}
#include "graph_impl.h"

#endif //PLS_DATAFLOW_INTERNAL_GRAPH_H_
