
#ifndef PLS_DATAFLOW_INTERNAL_GRAPH_H_
#define PLS_DATAFLOW_INTERNAL_GRAPH_H_

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

#include "buffer.h"
#include "in_port.h"
#include "out_port.h"

#include "pls/dataflow/inputs.h"
#include "pls/dataflow/outputs.h"

namespace pls {
namespace dataflow {
namespace internal {

struct graph_invocation {
  enum state { clear, running, finished };

  explicit graph_invocation(int num_outputs) : num_outputs_{num_outputs}, outputs_missing_{num_outputs} {};

  void reset() {
    state_ = clear;
    internal_call_ = false;
    previous_color_ = {};

    outputs_missing_ = num_outputs_;
  }

  const int num_outputs_;
  std::atomic<int> outputs_missing_;

  state state_{clear};

  bool internal_call_{false};
  token_color previous_color_{};
};

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

template<template<typename> class B, typename I0, typename ...I, typename O0, typename ...O>
class graph<B, pls::dataflow::inputs<I0, I...>, pls::dataflow::outputs<O0, O...>> {
 private:
  using self_type = graph<B, pls::dataflow::inputs<I0, I...>, pls::dataflow::outputs<O0, O...>>;
  struct in_port_cb {
    self_type &self_;

    explicit in_port_cb(self_type &self) : self_{self} {};

    template<int POS, typename T>
    void token_pushed(token<T> token) {
      self_.in_port_token_pushed<POS, T>(token);
    }
  };
  struct output_cb {
    const self_type &self_;

    explicit output_cb(self_type &self) : self_{self} {};

    template<int POS, typename T>
    void token_pushed(token<T> token) {
      self_.output_token_pushed<POS, T>(token);
    }
  };

  // Type-Defs used internally
  using my_type = graph<B, pls::dataflow::inputs<I0, I...>, pls::dataflow::outputs<O0, O...>>;

  using multi_in_port_type = multi_in_port<in_port_cb, 0, I0, I...>;
  using multi_out_port_type = multi_out_port<O0, O...>;

  using input_tuple = std::tuple<I0, I...>;
  using output_tuple = std::tuple<O0, O...>;

 public:
  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>();
  }

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

  template<int POS, typename T>
  void in_port_token_pushed(token<T> token) {
    auto my_clock = input_clock_++;

    auto index = token.color().get_index(buffer_size_);

    std::cout << "Token pushed at " << POS << " with value " << token.value() << std::endl;

//    std::get<POS>(inputs_[index]) = token;
//
//    auto remaining = --inputs_required_[index];
//    if (remaining == 0) {
//      std::cout << "All tokens at clock " << token.color().clock_ << std::endl;
//    }
  }

  template<int POS, typename T>
  void output_token_pushed(token<T> token) {

  }

  graph() : in_port_cb_{*this}, in_port_{in_port_cb_}, out_port_{}, invocations_{} {
    buffer_size_ = invocations_.capacity();
  }

 private:
  void reset_input(int i) {
    inputs_missing_[i] =
  }

  // Input/Output ports
  in_port_cb in_port_cb_;
  multi_in_port_type in_port_;
  multi_out_port_type out_port_;

  const int buffer_size_;

  // Clocks and state for execution
  std::atomic<unsigned int> input_clock_{0};
  std::atomic<unsigned int> output_clock_{0};

  B<input_tuple> input_buffer_;
  B<std::atomic<int>> inputs_missing_;

  B<graph_invocation> invocations_;
  B<output_tuple> output_buffer_;
};

}
}
}

#endif //PLS_DATAFLOW_INTERNAL_GRAPH_H_
