/*
    Copyright 2005-2010 Intel Corporation.  All Rights Reserved.

    This file is part of Threading Building Blocks.

    Threading Building Blocks is free software; you can redistribute it
    and/or modify it under the terms of the GNU General Public License
    version 2 as published by the Free Software Foundation.

    Threading Building Blocks is distributed in the hope that it will be
    useful, but WITHOUT ANY WARRANTY; without even the implied warranty
    of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Threading Building Blocks; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    As a special exception, you may use this file as part of a free software
    library without restriction.  Specifically, if other files instantiate
    templates or use macros or inline functions from this file, or you compile
    this file and link it with other files to produce an executable, this
    file does not by itself cause the resulting executable to be covered by
    the GNU General Public License.  This exception does not however
    invalidate any other reasons why the executable file might be covered by
    the GNU General Public License.
*/

#ifndef TBB_PREVIEW_CONCURRENT_PRIORITY_QUEUE
    #define TBB_PREVIEW_CONCURRENT_PRIORITY_QUEUE 1
#endif

#include "tbb/concurrent_priority_queue.h"
#include "tbb/atomic.h"
#include "harness.h"
#include <functional>
#include <vector>

#if _MSC_VER==1500 && !__INTEL_COMPILER
    // VS2008/VC9 seems to have an issue; limits pull in math.h
    #pragma warning( push )
    #pragma warning( disable: 4985 )
#endif
#include <climits>
#if _MSC_VER==1500 && !__INTEL_COMPILER
    #pragma warning( pop )
#endif

using namespace tbb;

const size_t MAX_ITER = 10000;
const int MAX_PRIO = 100000000;

tbb::atomic<unsigned int> counter;

class my_data_type {
public:
    int priority;
    char padding[tbb::internal::NFS_MaxLineSize - sizeof(int) % tbb::internal::NFS_MaxLineSize];
    my_data_type() {}
    my_data_type(int init_val) : priority(init_val) {}
    const my_data_type operator+(const my_data_type& other) const {
        return my_data_type(priority+other.priority);
    }
    bool operator==(const my_data_type& other) const {
        return this->priority == other.priority;
    }
};

const my_data_type DATA_MIN(INT_MIN);
const my_data_type DATA_MAX(INT_MAX);

class my_less {
public:
    bool operator()(const my_data_type d1, const my_data_type d2) const {
        return d1.priority<d2.priority;
    }
};

class my_throwing_type : public my_data_type {
public:
    static int throw_flag;
    my_throwing_type() : my_data_type() {}
    my_throwing_type(const my_throwing_type& src) : my_data_type(src) { 
        if (my_throwing_type::throw_flag) throw 42; 
        priority = src.priority;
    }
};

int my_throwing_type::throw_flag = 0;

typedef concurrent_priority_queue<my_throwing_type, my_less > cpq_ex_test_type;

template<typename T, typename C>
class FillBody : NoAssign {
    int nThread;
    T my_max, my_min;
    concurrent_priority_queue<T, C> *q;
    C less_than;
public:  
    FillBody(int nThread_, T max_, T min_, concurrent_priority_queue<T, C> *q_) : nThread(nThread_), my_max(max_), my_min(min_), q(q_) {}
    void operator()(const int threadID) const {
        T elem = my_min + T(threadID);
        for (size_t i=0; i<MAX_ITER; ++i) {
            // do some pushes
            q->push(elem);
            if (elem == my_max) elem = my_min;
            elem = elem + T(nThread);
        }
    }
};

template<typename T, typename C>
struct EmptyBody : NoAssign {
    int nThread;
    T my_max;
    concurrent_priority_queue<T, C> *q;
    C less_than;
public:
    EmptyBody(int nThread_, T max_, concurrent_priority_queue<T, C> *q_) : nThread(nThread_), my_max(max_), q(q_) {}
    void operator()(const int /*threadID*/) const {
        T elem(my_max), last;
        if (q->try_pop(last)) {
            ++counter;
            while(q->try_pop(elem)) {
                ASSERT(!less_than(last, elem), "FAILED pop/priority test in EmptyBody.");
                last = elem;
                elem = my_max;
                ++counter;
            }
        }
    }
};

template<typename T, typename C>
struct ReserveBody : NoAssign {
    int nThread;
    concurrent_priority_queue<T, C> *q;
public:
    ReserveBody(int nThread_, concurrent_priority_queue<T, C> *q_) : nThread(nThread_), q(q_) {}
    void operator()(const int threadID) const {
        T in_elem(threadID), out_elem;
        q->reserve(q->capacity()+1);
        for (size_t i=0; i<MAX_ITER; ++i) {
            q->push(in_elem);
            if (i%2) in_elem = in_elem + nThread;
            ASSERT(q->try_pop(out_elem), "FAILED simultaneous enqueue/dequeue test in ReserveBody.");
        }
    }
};

template <typename T, typename C>
class FloggerBody : NoAssign {
    int nThread;
    concurrent_priority_queue<T, C> *q;
public:
    FloggerBody(int nThread_, concurrent_priority_queue<T, C> *q_) : 
        nThread(nThread_), q(q_) {}  
    void operator()(const int threadID) const {
        T elem = T(threadID);
        for (size_t i=0; i<MAX_ITER; ++i) {
            q->push(elem);
            (void) q->try_pop(elem);
        }
    }
};

void TestConstructorsDestructorsAccessors() {
    std::vector<int> v;
    std::allocator<int> a;
    concurrent_priority_queue<int, std::less<int> > *q, *qo;
    concurrent_priority_queue<int, std::less<int>, std::allocator<int>  > *qi;

    // Test constructors/destructors
    REMARK("Testing default constructor.\n");
    q = new concurrent_priority_queue<int, std::less<int> >();
    REMARK("Default constructor complete.\n");
    ASSERT(q->size()==0, "FAILED size test.");
    ASSERT(q->capacity()==0, "FAILED capacity test.");
    ASSERT(q->empty(), "FAILED empty test.");
    REMARK("Testing destructor.\n");
    delete q;
    REMARK("Destruction complete.\n");

    REMARK("Testing capacity constructor.\n");
    q = new concurrent_priority_queue<int, std::less<int> >(42);
    REMARK("Capacity constructor complete.\n");
    ASSERT(q->size()==0, "FAILED size test.");
    ASSERT(q->capacity()==42, "FAILED capacity test.");
    ASSERT(q->empty(), "FAILED empty test.");
    REMARK("Testing destructor.\n");
    delete q;
    REMARK("Destruction complete.\n");

    REMARK("Testing allocator constructor.\n");
    qi = new concurrent_priority_queue<int, std::less<int>, std::allocator<int> >(a);
    REMARK("Allocator constructor complete.\n");
    ASSERT(qi->size()==0, "FAILED size test.");
    ASSERT(qi->capacity()==0, "FAILED allocator test.");
    ASSERT(qi->empty(), "FAILED empty test.");
    REMARK("Testing destructor.\n");
    delete qi;
    REMARK("Destruction complete.\n");

    REMARK("Testing capacity+allocator constructor.\n");
    qi = new concurrent_priority_queue<int, std::less<int>, std::allocator<int> >(42, a);
    REMARK("Capacity+allocator constructor complete.\n");
    ASSERT(qi->size()==0, "FAILED size test.");
    ASSERT(qi->capacity()==42, "FAILED capacity+allocator test.");
    ASSERT(qi->empty(), "FAILED empty test.");
    REMARK("Testing destructor.\n");
    delete qi;
    REMARK("Destruction complete.\n");

    REMARK("Testing iterator filler constructor.\n");
    for (int i=0; i<42; ++i)
        v.push_back(i);
    q = new concurrent_priority_queue<int, std::less<int> >(v.begin(), v.end());
    REMARK("Iterator filler constructor complete.\n");
    ASSERT(q->size()==42, "FAILED vector/size test.");
    ASSERT(q->capacity()==42, "FAILED vector/capacity test.");
    ASSERT(!q->empty(), "FAILED vector/empty test.");

    REMARK("Testing copy constructor.\n");
    qo = new concurrent_priority_queue<int, std::less<int> >(*q);
    REMARK("Copy constructor complete.\n");
    ASSERT(qo->size()==42, "FAILED vector/size test.");
    ASSERT(qo->capacity()==42, "FAILED vector/capacity test.");
    ASSERT(!qo->empty(), "FAILED vector/empty test.");
    REMARK("Testing destructor.\n");
    delete q;
    delete qo;
    REMARK("Destruction complete.\n");
}

void TestAssignmentClearShrinkSwap() {
    std::vector<int> v;
    concurrent_priority_queue<int, std::less<int> > *q, *qo;
    int e;

    for (int i=0; i<42; ++i)
        v.push_back(i);
    q = new concurrent_priority_queue<int, std::less<int> >(v.begin(), v.end());
    qo = new concurrent_priority_queue<int, std::less<int> >();

    REMARK("Testing assignment (1).\n");
    *qo = *q; 
    REMARK("Assignment complete.\n");
    ASSERT(qo->size()==42, "FAILED assignment/size test.");
    ASSERT(qo->capacity()==42, "FAILED assignment/capacity test.");
    ASSERT(!qo->empty(), "FAILED assignment/empty test.");

    REMARK("Testing clear.\n");
    q->clear();
    REMARK("Clear complete.\n");
    ASSERT(q->size()==0, "FAILED clear/size test.");
    ASSERT(q->capacity()==42, "FAILED clear/capacity test.");
    ASSERT(q->empty(), "FAILED clear/empty test.");

    for (size_t i=0; i<5; ++i)
        (void) qo->try_pop(e);

    REMARK("Testing assignment (2).\n");
    *q = *qo;
    REMARK("Assignment complete.\n");
    ASSERT(q->size()==37, "FAILED assignment/size test.");
    ASSERT(q->capacity()==37, "FAILED assignment/capacity test.");
    ASSERT(!q->empty(), "FAILED assignment/empty test.");

    for (size_t i=0; i<5; ++i)
        (void) qo->try_pop(e);

    REMARK("Testing shrink_to_fit.\n");
    qo->shrink_to_fit();
    REMARK("Shrink_to_fit complete.\n");
    ASSERT(qo->size()==32, "FAILED shrink_to_fit/size test.");
    ASSERT(qo->capacity()==32, "FAILED shrink_to_fit/capacity test.");
    ASSERT(!qo->empty(), "FAILED shrink_to_fit/empty test.");

    REMARK("Testing swap.\n");
    q->swap(*qo);
    REMARK("Swap complete.\n");
    ASSERT(q->size()==32, "FAILED swap/size test.");
    ASSERT(q->capacity()==32, "FAILED swap/capacity test.");
    ASSERT(!q->empty(), "FAILED swap/empty test.");
    ASSERT(qo->size()==37, "FAILED swap_operand/size test.");
    ASSERT(qo->capacity()==37, "FAILED swap_operand/capacity test.");
    ASSERT(!qo->empty(), "FAILED swap_operand/empty test.");
    delete q;
    delete qo;
}

void TestSerialPushPop() {
    concurrent_priority_queue<int, std::less<int> > *q;
    int e=42, prev=INT_MAX;
    size_t count=0;

    q = new concurrent_priority_queue<int, std::less<int> >(MAX_ITER);
    REMARK("Testing serial push.\n");
    for (size_t i=0; i<MAX_ITER; ++i) {
        q->push(e);
        e = e*-1 + int(i);
    }
    REMARK("Pushing complete.\n");
    ASSERT(q->size()==MAX_ITER, "FAILED push/size test.");
    ASSERT(q->capacity()==MAX_ITER, "FAILED push/capacity test.");
    ASSERT(!q->empty(), "FAILED push/empty test.");
    
    REMARK("Testing serial pop.\n");
    while (!q->empty()) {
        ASSERT(q->try_pop(e), "FAILED pop test.");
        ASSERT(prev>=e, "FAILED pop/priority test.");
        prev = e;
        ++count;
        ASSERT(q->size()==MAX_ITER-count, "FAILED swap/size test.");
        ASSERT(q->capacity()==MAX_ITER, "FAILED swap/capacity test.");
        ASSERT(!q->empty() || count==MAX_ITER, "FAILED swap/empty test.");
    }
    REMARK("Popping complete.\n");
    delete q;
}

template <typename T, typename C>
void TestParallelPushPop(int nThreads, T t_max, T t_min, C /*compare*/) {
    size_t qsize;

    concurrent_priority_queue<T, C> *q = new concurrent_priority_queue<T, C>(0);
    FillBody<T, C> filler(nThreads, t_max, t_min, q);
    EmptyBody<T, C> emptier(nThreads, t_max, q);
    counter = 0;
    REMARK("Testing parallel push.\n");
    NativeParallelFor(nThreads, filler);
    REMARK("Pushing complete.\n");
    qsize = q->size();
    ASSERT(q->size()==nThreads*MAX_ITER, "FAILED push/size test.");
    ASSERT(q->capacity()>=qsize, "FAILED push/capacity test.");
    ASSERT(!q->empty(), "FAILED push/empty test.");
    
    REMARK("Testing parallel pop.\n");
    NativeParallelFor(nThreads, emptier);
    REMARK("Popping complete.\n");
    ASSERT(counter==qsize, "FAILED pop/size test.");
    ASSERT(q->size()==0, "FAILED pop/empty test.");

    q->clear();
    q->shrink_to_fit();
    delete(q);
}

void TestPushPopReserve(int nThreads) {
    concurrent_priority_queue<int, std::less<int> > *q = new concurrent_priority_queue<int, std::less<int> >(0);
    counter = 0;
    REMARK("Testing reserve.\n");
    NativeParallelFor(nThreads, ReserveBody<int, std::less<int> >(nThreads, q));
    REMARK("Reserve complete.\n");
    ASSERT(q->size()==0, "FAILED reserve/size test.");
    ASSERT(q->empty(), "FAILED reserve/empty test.");
    q->clear();
    q->shrink_to_fit();
    delete q;
}

void TestExceptions() {
    const size_t TOO_LARGE_SZ = 1000000000;
    my_throwing_type elem;

    REMARK("Testing basic constructor exceptions.\n");
    // Allocate empty queue should not throw no matter the type
    try {
        my_throwing_type::throw_flag = 1;
        cpq_ex_test_type q;
    } catch(...) {
        REMARK("FAILED: allocating empty queue should not throw exception.\n");
    }
    // Allocate small queue should not throw for reasonably sized type
    try {
        my_throwing_type::throw_flag = 1;
        cpq_ex_test_type q(42);
    } catch(...) {
        REMARK("FAILED: allocating small queue should not throw exception.\n");
    }
    // Allocate a queue with too large initial size
    try {
        my_throwing_type::throw_flag = 0;
        cpq_ex_test_type q(TOO_LARGE_SZ);
        REMARK("FAILED: Huge queue did not throw exception.\n");
    } catch(...) {}

    cpq_ex_test_type *pq;
    try {
        my_throwing_type::throw_flag = 0;
        pq = NULL;
        pq = new cpq_ex_test_type(TOO_LARGE_SZ);
        REMARK("FAILED: Huge queue did not throw exception.\n");
        delete pq;
    } catch(...) {
        ASSERT(!pq, "FAILED: pq should not be touched when constructor throws.\n");
    }
    REMARK("Basic constructor exceptions testing complete.\n");
    REMARK("Testing copy constructor exceptions.\n");
    my_throwing_type::throw_flag = 0;
    cpq_ex_test_type src_q(42);
    elem.priority = 42;
    for (size_t i=0; i<42; ++i) src_q.push(elem);
    try {
        my_throwing_type::throw_flag = 1;
        cpq_ex_test_type q(src_q);
        REMARK("FAILED: Copy construct did not throw exception.\n");
    } catch(...) {}
    try {
        my_throwing_type::throw_flag = 1;
        pq = NULL;
        pq = new concurrent_priority_queue<my_throwing_type, my_less >(src_q);
        REMARK("FAILED: Copy construct did not throw exception.\n");
        delete pq;
    } catch(...) {
        ASSERT(!pq, "FAILED: pq should not be touched when constructor throws.\n");
    }
    REMARK("Copy constructor exceptions testing complete.\n");
    REMARK("Testing assignment exceptions.\n");
    // Assignment is copy-swap, so it should be exception safe
    my_throwing_type::throw_flag = 0;
    cpq_ex_test_type assign_q(24);
    try {
        my_throwing_type::throw_flag = 1;
        assign_q = src_q;
        REMARK("FAILED: Assign did not throw exception.\n");
    } catch(...) {
        ASSERT(assign_q.empty(), "FAILED: assign_q should be empty.\n");
        ASSERT(assign_q.capacity()==24, "FAILED: assign_q should have no newly allocated memory.\n");
    }
    REMARK("Assignment exceptions testing complete.\n");
    REMARK("Testing reserve exceptions.\n");
    try {
        my_throwing_type::throw_flag = 1;
        src_q.reserve(50);
        REMARK("FAILED: Reserve did not throw exception.\n");
    } catch(...) {
        ASSERT(!src_q.empty(), "FAILED: src_q should not be empty.\n");
        ASSERT(src_q.capacity()==42, "FAILED: src_q should be have no new allocated memory.\n");
        ASSERT(src_q.size()==42, "FAILED: src_q should have same size.\n");
        ASSERT(src_q.try_pop(elem), "FAILED: src_q is not functional.\n");
    }
    REMARK("Reserve exceptions testing complete.\n");
    REMARK("Testing push exceptions.\n");
    my_throwing_type::throw_flag = 0;
    pq = new cpq_ex_test_type(3);
    try {
        pq->push(elem);
        pq->push(elem);
        pq->push(elem);
    } catch(...) {
        REMARK("FAILED: Push should not throw exception... yet.\n");
    }
    try { // should crash on reserve copy 
        my_throwing_type::throw_flag = 1;
        pq->push(elem);
        REMARK("FAILED: Push did not throw exception.\n");
    } catch(...) {
        ASSERT(!pq->empty(), "FAILED: pq should not be empty.\n");
        ASSERT(pq->capacity()==3, "FAILED: pq should be have no new allocated memory.\n");
        ASSERT(pq->size()==3, "FAILED: pq should be only three elements.\n");
        ASSERT(pq->try_pop(elem), "FAILED: pq is not functional.\n");
    }
    try { // should crash on push copy of element, reserve not needed
        my_throwing_type::throw_flag = 1;
        pq->push(elem);
        REMARK("FAILED: Push did not throw exception.\n");
    } catch(...) {
        ASSERT(!pq->empty(), "FAILED: pq should not be empty.\n");
        ASSERT(pq->capacity()==3, "FAILED: pq should be have no new allocated memory.\n");
        ASSERT(pq->size()==2, "FAILED: pq should be only two elements.\n");
        ASSERT(pq->try_pop(elem), "FAILED: pq is not functional.\n");
    }
    delete pq;
    REMARK("Push exceptions testing complete.\n");
}

template <typename T, typename C>
void TestFlogger(int nThreads, T /*max*/, C /*compare*/) {
    REMARK("Testing queue flogger.\n");
    concurrent_priority_queue<T, C> *q = new concurrent_priority_queue<T, C> (0);
    NativeParallelFor(nThreads, FloggerBody<T, C >(nThreads, q));
    ASSERT(q->empty(), "FAILED flogger/empty test.");
    ASSERT(!q->size(), "FAILED flogger/size test.");
    REMARK("Flogging complete.\n");
    delete q;
}

void TestCpqOnNThreads(int nThreads) {
    std::less<int> int_compare;
    my_less data_compare;

    TestConstructorsDestructorsAccessors();
    TestAssignmentClearShrinkSwap();
    TestSerialPushPop(); 

    TestParallelPushPop(nThreads, INT_MAX, INT_MIN, int_compare);
    TestParallelPushPop(nThreads, (unsigned char)CHAR_MAX, (unsigned char)CHAR_MIN, int_compare);
    TestParallelPushPop(nThreads, DATA_MAX, DATA_MIN, data_compare);

    TestFlogger(nThreads, INT_MAX, int_compare);
    TestFlogger(nThreads, (unsigned char)CHAR_MAX, int_compare);
    TestFlogger(nThreads, DATA_MAX, data_compare);

    TestPushPopReserve(nThreads);
#if TBB_USE_EXCEPTIONS && !__TBB_THROW_ACROSS_MODULE_BOUNDARY_BROKEN
    TestExceptions();
#else
    REPORT("Known issue: exception handling tests are skipped.\n");
#endif
}

int TestMain() {
    if (MinThread < 1)
        MinThread = 1;
    for (int p = MinThread; p <= MaxThread; ++p) {
        REMARK("Testing on %d threads.\n", p);
        TestCpqOnNThreads(p);
    }
    return Harness::Done;
}
