
#ifndef PLS_DATAFLOW_INTERNAL_INPUTS_H_
#define PLS_DATAFLOW_INTERNAL_INPUTS_H_

#include <tuple>
#include <array>

#include "input.h"
#include "outputs.h"

namespace pls {
namespace dataflow {
namespace internal {

template<int P, typename CB, typename ...I>
class inputs : push_token_cb {
 public:
  using raw_types = std::tuple<I...>;
  using values_type = std::tuple<input<P, I>...>;

  template<int N>
  using raw_type_at = typename std::tuple_element<N, raw_types>::type;
  template<int N>
  using value_type_at = typename std::tuple_element<N, values_type>::type;
  static constexpr unsigned int num_values = std::tuple_size<values_type>::value;

 private:
  values_type values_;
  std::array<std::atomic<unsigned int>, P> required_inputs_;
  CB cb_;

 public:
  explicit inputs(CB cb) : cb_{cb} {
    for (int i = 0; i < P; i++) {
      required_inputs_[i] = num_values;
    }
    init_cb<0, I...>::call(this);
  }
  inputs(inputs &&other) = delete;
  inputs(const inputs &other) = delete;
  inputs &operator=(inputs &&other) = delete;
  inputs &operator=(const inputs &other) = delete;

  void token_pushed(int pos, token_color color) override {
    auto index = color.get_index(P);
    int current_required = --required_inputs_[index];

    cb_.one_input(pos, color);

    if (current_required == 0) {
      cb_.all_inputs(color);
      required_inputs_[index] = num_values;
    }
  }

  template<int N>
  typename std::tuple_element<N, values_type>::type &get() {
    return std::get<N>(values_);
  }

  raw_types get_outputs(int p) {
    return fill_output<0, I...>::call(*this, p);
  }
  template<int POS, typename ...TAIL>
  struct fill_output {
    static std::tuple<> call(inputs<P, CB, I...> &self, int p) {
      return {};
    }
  };
  template<int POS, typename HEAD, typename ...TAIL>
  struct fill_output<POS, HEAD, TAIL...> {
    static std::tuple<HEAD, TAIL...> call(inputs<P, CB, I...> &self, int p) {
      std::tuple<decltype(self.get<POS>().token_at(p).value())> our_tuple{self.get<POS>().token_at(p).value()};
      return std::tuple_cat(our_tuple, fill_output<POS + 1, TAIL...>::call(self, p));
    }
  };

  void push_to_outputs(outputs<P, I...> &outputs, int input_index, token_color new_color) {
    push_to_outputs_rec<0, I...>::call(this, &outputs, input_index, new_color);
  }
  template<int POS, typename ...TAIL>
  struct push_to_outputs_rec {
    static void call(inputs<P, CB, I...> *inputs, outputs<P, I...> *outputs, int input_index, token_color new_color) {}
  };
  template<int POS, typename HEAD, typename ...TAIL>
  struct push_to_outputs_rec<POS, HEAD, TAIL...> {
    static void call(inputs<P, CB, I...> *inputs, outputs<P, I...> *outputs, int input_index, token_color new_color) {
      auto token = inputs->get<POS>().token_at(input_index);
      outputs->template get<POS>().push_token({token.value(), new_color});
      push_to_outputs_rec<POS + 1, TAIL...>::call(inputs, outputs, input_index, new_color);
    }
  };

// TODO: Change CB code using proper templating to save method calls during execution...
  template<int POS, typename ...TAIL>
  struct init_cb {
    static void call(inputs<P, CB, I...> *inputs) {}
  };
  template<int POS, typename HEAD, typename ...TAIL>
  struct init_cb<POS, HEAD, TAIL...> {
    static void call(inputs<P, CB, I...> *inputs) {
      inputs->get<POS>().set_cb(inputs, POS);
      init_cb<POS + 1, TAIL...>::call(inputs);
    }
  };
};

}
}
}

#endif //PLS_DATAFLOW_INTERNAL_INPUTS_H_
