
#ifndef PLS_DATAFLOW_INTERNAL_MERGE_NODE_H_
#define PLS_DATAFLOW_INTERNAL_MERGE_NODE_H_

#include <atomic>

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

namespace pls {
namespace dataflow {
namespace internal {

template<typename I>
class merge_node : public node {
  // Our own type
  using self_type = merge_node<I>;

  // Input-Output port types
  using multi_in_port_type = multi_in_port<self_type, 0, I, I, bool>;
  using multi_out_port_type = multi_out_port<I>;

  // Input-Output tuples
  using input_tuple = std::tuple<token < I>, token<I>, token<bool>>;

  // Memory used for ONE active invocation of the dataflow-graph
  struct invocation_memory {
    std::atomic<unsigned int> inputs_missing_;
    input_tuple input_buffer_;
  };

  // Encodings for current input state (needs to fit into an atomic)
  static constexpr unsigned int IN_0_MISSING = 1u << 0u;
  static constexpr unsigned int IN_1_MISSING = 1u << 1u;
  static constexpr unsigned int COND_MISSING = 1u << 2u;
  static constexpr unsigned int INITIAL_STATE = IN_0_MISSING + IN_1_MISSING + COND_MISSING;

 public:
  explicit merge_node() : in_port_{this, this}, out_port_{} {}

  in_port<I> &true_in_port() {
    return in_port_.template get<0>();
  }

  in_port<I> &false_in_port() {
    return in_port_.template get<1>();
  }

  in_port<bool> &condition_in_port() {
    return in_port_.template get<2>();
  }

  out_port<I> &value_out_port() {
    return out_port_.template get<0>();
  }

  template<int POS, typename T>
  void token_pushed(token<T> token) {
    auto current_memory = get_invocation<invocation_memory>(token.invocation());

    std::get<POS>(current_memory->input_buffer_) = token;

    unsigned int remaining_inputs;
    if (POS == 0) {
      remaining_inputs = (current_memory->inputs_missing_).fetch_sub(IN_0_MISSING) - IN_0_MISSING;
    } else if (POS == 1) {
      remaining_inputs = (current_memory->inputs_missing_).fetch_sub(IN_1_MISSING) - IN_1_MISSING;
    } else {
      remaining_inputs = (current_memory->inputs_missing_).fetch_sub(COND_MISSING) - COND_MISSING;
    }

    if ((remaining_inputs & COND_MISSING) == 0) {
      if ((remaining_inputs & IN_0_MISSING) == 0) {
        auto &data = std::get<0>(current_memory->input_buffer_);
        value_out_port().push_token(data);
        current_memory->inputs_missing_ += IN_0_MISSING + COND_MISSING;
      } else if ((remaining_inputs & IN_1_MISSING) == 0) {
        auto &data = std::get<1>(current_memory->input_buffer_);
        value_out_port().push_token(data);
        current_memory->inputs_missing_ += IN_1_MISSING + COND_MISSING;
      }
    }
  }

  int num_successors() const override {
    return 1;
  }
  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_ = INITIAL_STATE;
  };
  void clean_up_instance_buffer(void *memory) const override {
    auto invocation = reinterpret_cast<invocation_memory *>(memory);
    invocation->~invocation_memory();
  }

 private:
  multi_in_port_type in_port_;
  multi_out_port_type out_port_;
};

}
}
}

#endif //PLS_DATAFLOW_INTERNAL_MERGE_NODE_H_
