
#ifndef PLS_DATAFLOW_INTERNAL_FUNCTION_NODE_H_
#define PLS_DATAFLOW_INTERNAL_FUNCTION_NODE_H_

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

#include "in_port.h"
#include "out_port.h"
#include "node.h"

#include "inputs.h"
#include "outputs.h"

#include "pls/internal/helpers/seqence.h"
#include "pls/pls.h"

namespace pls {
namespace dataflow {
namespace internal {

using namespace pls::internal::helpers;

// Forward Decl
template<typename INS, typename OUTS>
class graph;

template<typename INS, typename OUTS, typename F>
class function_node {};

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

  // Input-Output port types
  using multi_in_port_type = multi_in_port<self_type, 0, I0, I...>;
  using multi_out_port_type = multi_out_port<O0, O...>;

  // Input-Output value tuples
  using input_tuple = std::tuple<token<I0>, token<I>...>;
  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 {
    std::atomic<int> inputs_missing_;
    input_tuple input_buffer_;
  };

 public:
  explicit function_node(F function) : in_port_{this, this}, out_port_{}, function_{function} {}

  template<int POS>
  using in_port_at = typename multi_in_port_type::template in_port_type_at<POS>;
  template<int POS>
  using out_port_at = typename multi_out_port_type::template out_port_type_at<POS>;

  template<int POS>
  in_port_at<POS> &in_port() {
    return in_port_.template get<POS>();
  }

  multi_in_port_type &in_ports() {
    return in_port_;
  }

  template<int POS>
  out_port_at<POS> &out_port() {
    return out_port_.template get<POS>();
  }

  multi_out_port_type &out_ports() {
    return out_port_;
  }

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

  template<typename ...IS>
  void operator>>(graph<inputs<IS...>, outputs<O0, O...>> &graph);

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

  int num_successors() const override {
    return num_out_ports;
  }
  node *successor_at(int pos) const override {
    return out_port_.next_node_at(pos);
  }

  bool is_fully_connected() const override {
    return in_port_.fully_connected() && out_port_.fully_connected();
  }

  int instance_buffer_size() const override {
    return sizeof(invocation_memory);
  }
  void init_instance_buffer(void *memory) const override {
    auto invocation = new(memory) invocation_memory{};
    invocation->inputs_missing_ = num_in_ports;
  };

 private:
  multi_in_port_type in_port_;
  multi_out_port_type out_port_;

  F function_;

  //////////////////////////////////////////////////////////////////
  // Helpers for actually calling the work lambda
  //////////////////////////////////////////////////////////////////
  void execute_function(invocation_memory *invocation_memory, invocation_info invocation_info);

  template<int N, typename ...OT>
  struct propagate_output;

  template<int ...IS, int ...OS>
  void execute_function_internal(input_tuple &inputs, sequence<IS...>,
                                 output_tuple &outputs, sequence<OS...>,
                                 invocation_info invocation_info);
};

}
}
}
#include "function_node_impl.h"

#endif //PLS_DATAFLOW_INTERNAL_NODE_H_
