
#ifndef PLS_WORK_STEALING_DEQUE_IMPL_H_
#define PLS_WORK_STEALING_DEQUE_IMPL_H_

#include <utility>
#include <new>

namespace pls {
namespace internal {
namespace scheduling {
namespace data_structures {

template<typename Task>
work_stealing_deque_item *work_stealing_deque<Task>::item_at(deque_offset offset) {
  return reinterpret_cast<work_stealing_deque_item *>(stack_->memory_at_offset(offset));
}

template<typename Task>
deque_offset work_stealing_deque<Task>::current_stack_offset() {
  return stack_->save_offset();
}

template<typename Task>
template<typename T, typename ...ARGS>
T *work_stealing_deque<Task>::push_task(ARGS &&... args) {
  static_assert(std::is_same<Task, T>::value || std::is_base_of<Task, T>::value,
                "Must only push types of <Task> onto work_stealing_deque<Task>");

  // 'Union' type to push both the task and the deque entry as one part onto the stack
  using pair_t = std::pair<work_stealing_deque_item, T>;
  // Allocate space on stack
  auto new_pair = reinterpret_cast<pair_t *>(stack_->push_bytes<pair_t>());
  // Initialize memory on stack
  new((void *) &(new_pair->first)) work_stealing_deque_item();
  new((void *) &(new_pair->second)) T(std::forward<ARGS>(args)...);

  // Keep reference for later publishing
  last_pushed_task_ = &new_pair->second;

  // Item is not publicly visible until it is published
  return &(new_pair->second);
}

template<typename Task>
template<typename T, typename ...ARGS>
T *work_stealing_deque<Task>::push_object(ARGS &&... args) {
  // Simply add data to the stack, do not publish it in any way
  return stack_->push<T>(std::forward(args)...);
}

template<typename Task>
void *work_stealing_deque<Task>::push_bytes(size_t size) {
  // Simply add data to the stack, do not publish it in any way
  return stack_->push_bytes(size);
}

template<typename Task>
void work_stealing_deque<Task>::publish_last_task() {
  deque_offset local_tail = tail_;

  // Prepare current tail to point to correct next task
  auto tail_deque_item = item_at(local_tail);
  tail_deque_item->set_data(last_pushed_task_);
  tail_deque_item->set_next_item(current_stack_offset());
  tail_deque_item->set_previous_item(previous_tail_);
  previous_tail_ = local_tail;

  // Linearization point, task appears after this write
  deque_offset new_tail = current_stack_offset();
  tail_ = new_tail;
}

template<typename Task>
Task *work_stealing_deque<Task>::pop_local_task() {
  bool cas_fail_out;
  return pop_local_task(cas_fail_out);
}

template<typename Task>
Task *work_stealing_deque<Task>::pop_local_task(bool &cas_fail_out) {
  deque_offset local_tail = tail_;
  stamped_integer local_head = head_;

  if (local_tail <= local_head.value) {
    cas_fail_out = false;
    return nullptr; // EMPTY
  }

  work_stealing_deque_item *previous_tail_item = item_at(previous_tail_);
  deque_offset new_tail = previous_tail_;
  previous_tail_ = previous_tail_item->previous_item();

  // Publish our wish to set the tail back
  tail_ = new_tail;
  // Get the state of local head AFTER we published our wish
  local_head = head_; // Linearization point, outside knows list is empty

  if (local_head.value < new_tail) {
    cas_fail_out = false;
    return previous_tail_item->data<Task>(); // Success, enough distance to other threads
  }

  if (local_head.value == new_tail) {
    stamped_integer new_head = stamped_integer{local_head.stamp + 1, new_tail};
    // Try competing with consumers by updating the head's stamp value
    if (head_.compare_exchange_strong(local_head, new_head)) {
      cas_fail_out = false;
      return previous_tail_item->data<Task>(); // SUCCESS, we won the competition with other threads
    }
  }

  // Some other thread either won the competition or it already set the head further than we are
  // before we even tried to compete with it.
  // Reset the queue into an empty state => head_ = tail_
  tail_ = local_head.value; // ...we give up to the other winning thread

  cas_fail_out = false; // We failed the CAS race, but the queue is also empty for sure!
  return nullptr; // EMPTY, we lost the competition with other threads
}

template<typename Task>
Task *work_stealing_deque<Task>::pop_external_task() {
  bool cas_fail_out;
  return pop_external_task(cas_fail_out);
}

template<typename Task>
Task *work_stealing_deque<Task>::pop_external_task(bool &cas_fail_out) {
  stamped_integer local_head = head_;
  deque_offset local_tail = tail_;

  if (local_tail <= local_head.value) {
    cas_fail_out = false;
    return nullptr; // EMPTY
  }
  // Load info on current deque item.
  // In case we have a race with a new (aba) overwritten item at this position,
  // there has to be a competition over the tail -> the stamp increased and our next
  // operation will fail anyways!
  work_stealing_deque_item *head_deque_item = item_at(local_head.value);
  deque_offset next_item_offset = head_deque_item->next_item();
  Task *head_data_item = head_deque_item->data<Task>();

  // We try to set the head to this new position.
  // Possible outcomes:
  // 1) no one interrupted us, we win this competition
  // 2) other thread took the head, we lose to this
  // 3) owning thread removed tail, we lose to this
  stamped_integer new_head = stamped_integer{local_head.stamp + 1, next_item_offset};
  if (head_.compare_exchange_strong(local_head, new_head)) {
    cas_fail_out = false;
    return head_data_item; // SUCCESS, we won the competition
  }

  cas_fail_out = true;
  return nullptr; // EMPTY, we lost the competition
}

template<typename Task>
void work_stealing_deque<Task>::reset_offset(deque_offset offset) {
  stack_->reset_offset(offset);

  stamped_integer local_head = head_;
  deque_offset local_tail = tail_;
  if (offset < local_tail) {
    tail_ = offset;
    if (local_head.value >= local_tail) {
      head_ = stamped_integer{local_head.stamp + 1, offset};
    }
  }
}

template<typename Task>
deque_offset work_stealing_deque<Task>::save_offset() {
  return current_stack_offset();
}

}
}
}
}

#endif //PLS_WORK_STEALING_DEQUE_IMPL_H_
