
#ifndef PLS_INTERNAL_DATA_STRUCTURES_BOUNDED_TRADING_DEQUE_H_
#define PLS_INTERNAL_DATA_STRUCTURES_BOUNDED_TRADING_DEQUE_H_

#include <atomic>
#include <tuple>

#include "pls/internal/base/error_handling.h"
#include "pls/internal/base/system_details.h"

#include "pls/internal/data_structures/optional.h"
#include "pls/internal/data_structures/stamped_integer.h"

namespace pls {
namespace internal {
namespace data_structures {

template<typename TradedType>
class traded_field {
  static_assert(base::system_details::CACHE_LINE_SIZE >= 4,
                "Traded objects must not use their last address bits, as we use them for status flags."
                "As traded objects are usually cache aligned, we need big enough cache lines.");
  // TODO: Replace unsigned long with a portable sized integer
  //       (some systems might have different pointer sizes to long sizes).
  static constexpr unsigned long SHIFT = 0x2lu;
  static constexpr unsigned long TAG_BITS = 0x3lu;
  static constexpr unsigned long RELEVANT_BITS = ~TAG_BITS;
  static constexpr unsigned long EMPTY_TAG = 0x0lu;
  static constexpr unsigned long STAMP_TAG = 0x1lu;
  static constexpr unsigned long TRADE_TAG = 0x2lu;

 public:
  void fill_with_stamp(unsigned long stamp) {
    pointer_ = (void *) ((stamp << SHIFT) | STAMP_TAG);
  }
  unsigned long get_stamp() {
    PLS_ASSERT(is_filled_with_tag(), "Must only read out the tag when the traded field contains one.");
    return ((unsigned long) pointer_) >> SHIFT;
  }
  bool is_filled_with_tag() {
    return (((unsigned long) pointer_) & TAG_BITS) == STAMP_TAG;
  }

  void fill_with_trade_object(TradedType *trade_object) {
    PLS_ASSERT((((unsigned long) trade_object) & TAG_BITS) == 0,
               "Must only store aligned objects in this data structure (last bits are needed for tag bit)");
    pointer_ = reinterpret_cast<void *>(((unsigned long) trade_object) | TRADE_TAG);
  }
  TradedType *get_trade_object() {
    PLS_ASSERT(is_filled_with_object(), "Must only read out the object when the traded field contains one.");
    return reinterpret_cast<TradedType *>(((unsigned long) pointer_) & RELEVANT_BITS);
  }
  bool is_filled_with_object() {
    return (((unsigned long) pointer_) & TAG_BITS) == TRADE_TAG;
  }

  bool is_empty() {
    return (((unsigned long) pointer_) & TAG_BITS) == EMPTY_TAG;
  }

 private:
  void *pointer_{nullptr};
};

template<typename EntryType, typename TradedType>
class alignas(base::system_details::CACHE_LINE_SIZE) trading_deque_entry {
 public:
  /*
   * Fill the slot with its initial values, making it ready for being stolen.
   * Performs no synchronization/memory ordering constraints.
   *
   * Method is called to init a field on pushBot.
   */
  void fill_slots(EntryType *entry_item, unsigned long expected_stamp) {
    entry_slot_.store(entry_item, std::memory_order_relaxed);
    forwarding_stamp_.store(expected_stamp, std::memory_order_relaxed);

    // Relaxed is fine for this, as adding elements is synced over the bot pointer
    auto old = trade_slot_.load(std::memory_order_relaxed);
    old.fill_with_stamp(expected_stamp);
    trade_slot_.store(old, std::memory_order_relaxed);
  }

  /**
   * Tries to atomically read out the object traded in by thieves.
   * Either returns the traded in field (the slot was stolen) or no result (the slot is still owned locally).
   *
   * Method is used to pop a field on popBot.
   */
  optional<TradedType *> acquire_traded_type() {
    traded_field<TradedType> empty_field;
    traded_field<TradedType> old_field_value = trade_slot_.exchange(empty_field, std::memory_order_acq_rel);

    if (old_field_value.is_filled_with_tag()) {
      return optional<TradedType *>();
    } else {
      return optional<TradedType *>(old_field_value.get_trade_object());
    }
  }

  EntryType *get_object() {
    return entry_slot_;
  }

  bool is_empty() {
    return trade_slot_.load(std::memory_order_seq_cst).is_empty();
  }

  optional<EntryType *> trade_object(TradedType *offered_object, unsigned long &expected_stamp) {
    // Read our potential result
    EntryType *result = entry_slot_.load(std::memory_order_relaxed);
    unsigned long forwarding_stamp = forwarding_stamp_.load(std::memory_order_relaxed);

    // Try to get it by CAS with the expected field entry, giving up our offered_object for it
    traded_field<TradedType> expected_field;
    expected_field.fill_with_stamp(expected_stamp);
    traded_field<TradedType> offered_field;
    offered_field.fill_with_trade_object(offered_object);

    if (trade_slot_.compare_exchange_strong(expected_field, offered_field, std::memory_order_acq_rel)) {
      return optional<EntryType *>{result};
    } else {
      if (expected_field.is_empty()) {
        expected_stamp = forwarding_stamp;
      }
      return optional<EntryType *>{};
    }
  }

 private:
  std::atomic<EntryType *> entry_slot_{nullptr};
  std::atomic<unsigned long> forwarding_stamp_{};
  std::atomic<traded_field<TradedType>> trade_slot_{};
};

/**
 * A work stealing deque (single produces/consumer at the end, multiple consumers at the start).
 * A pointer to an OfferedType object can only be acquired by stealing consumers (from the start),
 * when they also offer a pointer to a TradeType object.
 *
 * The exchange of 'goods' (OfferedType and TradedType) happens atomically at a linearization point.
 * This means that the owning thread always gets a TradedType for each and every OfferedType that was
 * successfully stolen.
 *
 * The owner of the deque must pop ALL elements, even the stolen ones (to get the traded goods instead).
 *
 * @tparam EntryType The type of objects stored in the deque
 * @tparam TradedType The type of objects traded in for acquiring a deque element.
 */
template<typename EntryType, typename TradedType>
class bounded_trading_deque {
  using deque_entry = trading_deque_entry<EntryType, TradedType>;

 public:
  bounded_trading_deque(deque_entry *entries, size_t num_entries) :
      entries_{entries}, num_entries_{num_entries} {};

  void push_bot(EntryType *offered_object) {
    auto expected_stamp = bot_internal_.stamp;
    auto &current_entry = entries_[bot_internal_.value];

    current_entry.fill_slots(offered_object, expected_stamp);
    bot_internal_.stamp++;
    bot_internal_.value++;

    bot_.store(bot_internal_.value, std::memory_order_release);
  }

  struct pop_result {
    explicit pop_result(optional<EntryType *> entry, optional<TradedType *> traded) : entry_{entry},
                                                                                      traded_{traded} {};
    pop_result() : entry_{}, traded_{} {};

    optional<EntryType *> entry_;
    optional<TradedType *> traded_;
  };
  pop_result pop_bot() {
    if (bot_internal_.value == 0) {
      return pop_result{}; // Empty, nothing to return...
    }

    // Go one step back
    bot_internal_.value--;

    auto &current_entry = entries_[bot_internal_.value];
    optional<TradedType *> traded_object = current_entry.acquire_traded_type();
    optional<EntryType *> queue_entry;
    if (traded_object) {
      // We do not return an entry, but the traded object
      queue_entry = optional<EntryType *>{};
    } else {
      // We still got it locally, grab the object
      queue_entry = optional<EntryType *>{current_entry.get_object()};
    }

    bot_.store(bot_internal_.value, std::memory_order_relaxed);
    if (bot_internal_.value == 0) {
      bot_internal_.stamp++;
      top_.store({bot_internal_.stamp, 0}, std::memory_order_release);
    }

    return pop_result{queue_entry, traded_object};
  }

  std::tuple<optional<EntryType *>, stamped_integer> peek_top() {
    auto local_top = top_.load();
    auto local_bot = bot_.load();
    if (local_top.value >= local_bot) {
      return std::make_tuple(optional<EntryType *>{}, local_top);
    } else {
      return std::make_tuple(optional<EntryType *>{entries_[local_top.value].get_object()}, local_top);
    }
  }

  optional<EntryType *> pop_top(TradedType *trade_offer) {
    auto local_top = top_.load();
    return pop_top(trade_offer, local_top);
  }

  optional<EntryType *> pop_top(TradedType *trade_offer, stamped_integer local_top) {
    auto local_bot = bot_.load();
    if (local_top.value >= local_bot) {
      return optional<EntryType *>{};
    }

    unsigned long expected_top_stamp = local_top.stamp;
    optional<EntryType *> entry = entries_[local_top.value].trade_object(trade_offer, expected_top_stamp);
    if (entry) {
      // We got it, for sure move the top pointer forward.
      top_.compare_exchange_strong(local_top, {local_top.stamp + 1, local_top.value + 1});
    } else {
      // We did not get it....
      if (entries_[local_top.value].is_empty()) {
        // ...update the top stamp, so the next call can get it (we still make system progress, as the owner
        // must have popped off the element)
        top_.compare_exchange_strong(local_top, {expected_top_stamp, local_top.value});
      } else {
        // ...move the pointer forward if someone else put a valid trade object in there.
        top_.compare_exchange_strong(local_top, {local_top.stamp + 1, local_top.value + 1});
      }
    }

    return entry;
  }

 private:
  deque_entry *entries_;
  size_t num_entries_;

  alignas(base::system_details::CACHE_LINE_SIZE) std::atomic<stamped_integer> top_{{0, 0}};
  alignas(base::system_details::CACHE_LINE_SIZE) std::atomic<size_t> bot_{0};

  stamped_integer bot_internal_{0, 0};
};

template<typename EntryType, typename TradedType, size_t SIZE>
class static_bounded_trading_deque {
 public:
  static_bounded_trading_deque() : items_{}, deque_{items_.data(), SIZE} {}

  bounded_trading_deque<EntryType, TradedType> &get_deque() { return deque_; }
 private:
  std::array<trading_deque_entry<EntryType, TradedType>, SIZE> items_;
  bounded_trading_deque<EntryType, TradedType> deque_;
};

}
}
}

#endif //PLS_INTERNAL_DATA_STRUCTURES_BOUNDED_TRADING_DEQUE_H_
