
#ifndef PLS_INTERNAL_SCHEDULING_TASK_TRADING_DEQUE_H_
#define PLS_INTERNAL_SCHEDULING_TASK_TRADING_DEQUE_H_

#include <atomic>
#include <utility>
#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"

#include "pls/internal/scheduling/traded_cas_field.h"
#include "pls/internal/scheduling/task.h"

namespace pls {
namespace internal {
namespace scheduling {

using namespace data_structures;

struct trading_deque_entry {
  std::atomic<task *> traded_task_{nullptr};
  std::atomic<unsigned long> forwarding_stamp_{};
};

/**
 * A work stealing deque (single produces/consumer at the end, multiple consumers at the start).
 * A task object can only be acquired by stealing consumers (from the start),
 * when they also offer a task to trade in for it.
 *
 * The exchange of 'goods' (here tasks) happens atomically at a linearization point.
 * This means that the owning thread always gets a tasks for each and every task that was
 * successfully stolen.
 *
 * As each task is associated with memory this suffices to exchange memory blocks needed for execution.
 */
class external_trading_deque {

 public:
  external_trading_deque(trading_deque_entry *entries, size_t num_entries) :
      entries_{entries}, num_entries_{num_entries} {};
  void set_thread_id(unsigned id) {
    thread_id_ = id;
  }

  static optional<task *> peek_traded_object(task *target_task) {
    traded_cas_field current_cas = target_task->external_trading_deque_cas_.load();
    if (current_cas.is_filled_with_object()) {
      return optional<task *>{current_cas.get_trade_object()};
    } else {
      return optional<task *>{};
    }
  }

  static optional<task *> get_trade_object(task *target_task) {
    traded_cas_field current_cas = target_task->external_trading_deque_cas_.load();
    if (current_cas.is_filled_with_object()) {
      task *result = current_cas.get_trade_object();
      traded_cas_field empty_cas;
      if (target_task->external_trading_deque_cas_.compare_exchange_strong(current_cas, empty_cas)) {
        return optional<task *>{result};
      }
    }

    return optional<task *>{};
  }

  /**
   * Pushes a task on the bottom of the deque.
   * The task itself wil be filled with the unique, synchronizing cas word.
   *
   * @param published_task The task to publish on the bottom of the deque.
   */
  void push_bot(task *published_task) {
    auto expected_stamp = bot_internal_.stamp;
    auto &current_entry = entries_[bot_internal_.value];

    // Publish the prepared task in the deque.
    current_entry.forwarding_stamp_.store(expected_stamp, std::memory_order_relaxed);
    current_entry.traded_task_.store(published_task, std::memory_order_relaxed);

    // Field that all threads synchronize on.
    // This happens not in the deque itself, but in the published task.
    traded_cas_field sync_cas_field;
    sync_cas_field.fill_with_stamp(expected_stamp, thread_id_);
    published_task->external_trading_deque_cas_.store(sync_cas_field, std::memory_order_release);

    // Advance the bot pointer. Linearization point for making the task public.
    bot_internal_.stamp++;
    bot_internal_.value++;
    bot_.store(bot_internal_.value, std::memory_order_release);
  }

  void reset_bot_and_top() {
    bot_internal_.value = 0;
    bot_internal_.stamp++;

    bot_.store(0);
    top_.store({bot_internal_.stamp, 0});
  }

  void decrease_bot() {
    bot_internal_.value--;
    bot_.store(bot_internal_.value, std::memory_order_relaxed);
  }

  /**
   * Tries to pop the last task on the deque.
   *
   * @return optional<task*> holding the popped task if successful.
   */
  optional<task *> pop_bot() {
    if (bot_internal_.value == 0) {
      reset_bot_and_top();
      return optional<task *>{};
    }
    decrease_bot();

    auto &current_entry = entries_[bot_internal_.value];
    auto *popped_task = current_entry.traded_task_.load(std::memory_order_relaxed);
    auto expected_stamp = current_entry.forwarding_stamp_.load(std::memory_order_relaxed);

    // We know what value must be in the cas field if no other thread stole it.
    traded_cas_field expected_sync_cas_field;
    expected_sync_cas_field.fill_with_stamp(expected_stamp, thread_id_);
    traded_cas_field empty_cas_field;

    if (popped_task->external_trading_deque_cas_.compare_exchange_strong(expected_sync_cas_field,
                                                                         empty_cas_field,
                                                                         std::memory_order_acq_rel)) {
      return optional<task *>{popped_task};
    } else {
      reset_bot_and_top();
      return optional<task *>{};
    }
  }

  struct peek_result {
    peek_result(optional<task *> top_task, stamped_integer top_pointer) : top_task_{std::move(top_task)},
                                                                          top_pointer_{top_pointer} {};
    optional<task *> top_task_;
    stamped_integer top_pointer_;
  };

  /**
   * Peek at the current task on top of the deque.
   * This is required, as we need to look at the task to figure out what we trade in for it.
   * (Note: we could go without this by doing some tricks with top/bot pointers, but this
   * is simpler and also more flexible if the traded objects are not as trivial as currently).
   *
   * @return a peek result containing the optional top task (if present) and the current head pointer.
   */
  peek_result peek_top() {
    auto local_top = top_.load();
    auto local_bot = bot_.load();

    if (local_top.value < local_bot) {
      return peek_result{optional<task *>{entries_[local_top.value].traded_task_}, local_top};
    } else {
      return peek_result{optional<task *>{}, local_top};
    }
  }

  optional<task *> pop_top(task *offered_task, stamped_integer expected_top) {
    auto local_bot = bot_.load();
    if (expected_top.value >= local_bot) {
      return data_structures::optional<task *>{};
    }

    auto &target_entry = entries_[expected_top.value];

    // Read our potential result
    task *result = target_entry.traded_task_.load();
    unsigned long forwarding_stamp = target_entry.forwarding_stamp_.load();

    // Try to get it by CAS with the expected field entry, giving up our offered_task for it
    traded_cas_field expected_sync_cas_field;
    expected_sync_cas_field.fill_with_stamp(expected_top.stamp, thread_id_);

    traded_cas_field offered_field;
    offered_field.fill_with_trade_object(offered_task);

    if (result->external_trading_deque_cas_.compare_exchange_strong(expected_sync_cas_field, offered_field)) {
      // We got it, for sure move the top pointer forward.
      top_.compare_exchange_strong(expected_top, {expected_top.stamp + 1, expected_top.value + 1});
      // Return the stolen task
      return data_structures::optional<task *>{result};
    } else {
      // We did not get it...help forwarding the top pointer anyway.
      if (expected_top.stamp == forwarding_stamp) {
        // ...move the pointer forward if someone else put a valid trade object in there.
        top_.compare_exchange_strong(expected_top, {expected_top.stamp + 1, expected_top.value + 1});
      } else {
        // ...we failed because the top tag lags behind...try to fix it.
        // This means only updating the tag, as this location can still hold data we need.
        top_.compare_exchange_strong(expected_top, {forwarding_stamp, expected_top.value});
      }
      return data_structures::optional<task *>{};
    }
  }

 private:
  // info on this deque
  trading_deque_entry *const entries_;
  const size_t num_entries_;
  unsigned thread_id_;

  // fields for stealing/interacting
  stamped_integer bot_internal_{0, 0};

  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};

};

template<size_t SIZE>
class static_external_trading_deque {
 public:
  static_external_trading_deque() : items_{}, deque_{items_.data(), SIZE} {}

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

}
}
}

#endif //PLS_INTERNAL_SCHEDULING_TASK_TRADING_DEQUE_H_
