/*
    Copyright 2005-2014 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 __TIME_FRAMEWORK_H__
#define __TIME_FRAMEWORK_H__

#include <cstdlib>
#include <math.h>
#include <vector>
#include <string>
#include <sstream>
#include "tbb/tbb_stddef.h"
#include "tbb/task_scheduler_init.h"
#include "tbb/tick_count.h"
#define HARNESS_CUSTOM_MAIN 1
#include "../test/harness.h"
#include "../test/harness_barrier.h"
#define STATISTICS_INLINE
#include "statistics.h"

#ifndef ARG_TYPE
typedef intptr_t arg_t;
#else
typedef ARG_TYPE arg_t;
#endif

class Timer {
    tbb::tick_count tick;
public:
    Timer() { tick = tbb::tick_count::now(); }
    double get_time()  { return (tbb::tick_count::now() - tick).seconds(); }
    double diff_time(const Timer &newer) { return (newer.tick - tick).seconds(); }
    double mark_time() { tbb::tick_count t1(tbb::tick_count::now()), t2(tick); tick = t1; return (t1 - t2).seconds(); }
    double mark_time(const Timer &newer) { tbb::tick_count t(tick); tick = newer.tick; return (tick - t).seconds(); }
};

class TesterBase /*: public tbb::internal::no_copy*/ {
protected:
    friend class TestProcessor;
    friend class TestRunner;

    //! it is barrier for synchronizing between threads
    Harness::SpinBarrier *barrier;
    
    //! number of tests per this tester
    const int tests_count;
    
    //! number of threads to operate
    int threads_count;

    //! some value for tester
    arg_t value;

    //! tester name
    const char *tester_name;

    // avoid false sharing
    char pad[128 - sizeof(arg_t) - sizeof(int)*2 - sizeof(void*)*2 ];

public:
    //! init tester base. @arg ntests is number of embeded tests in this tester.
    TesterBase(int ntests)
        : barrier(NULL), tests_count(ntests)
    {}
    virtual ~TesterBase() {}

    //! internal function
    void base_init(arg_t v, int t, Harness::SpinBarrier &b) {
        threads_count = t;
        barrier = &b;
        value = v;
        init();
    }

    //! optionally override to init after value and threads count were set.
    virtual void init() { }

    //! Override to provide your names
    virtual std::string get_name(int testn) {
        return Format("test %d", testn);
    }

    //! optionally override to init test mode just before execution for a given thread number.
    virtual void test_prefix(int testn, int threadn) { }

    //! Override to provide main test's entry function returns a value to record
    virtual value_t test(int testn, int threadn) = 0;

    //! Type of aggregation from results of threads
    enum result_t {
        SUM, AVG, MIN, MAX
    };

    //! Override to change result type for the test. Return postfix for test name or 0 if result type is not needed.
    virtual const char *get_result_type(int /*testn*/, result_t type) const {
        return type == AVG ? "" : 0; // only average result by default
    }
};

/*****
a user's tester concept:

class tester: public TesterBase {
public:
    //! init tester with known amount of work
    tester() : TesterBase(<user-specified tests count>) { ... }

    //! run a test with sequental number @arg test_number for @arg thread.
    / *override* / value_t test(int test_number, int thread);
};

******/

template<typename Tester, int scale = 1>
class TimeTest : public Tester {
    /*override*/ value_t test(int testn, int threadn) {
        Timer timer;
        Tester::test(testn, threadn);
        return timer.get_time() * double(scale);
    }
};

template<typename Tester>
class NanosecPerValue : public Tester {
    /*override*/ value_t test(int testn, int threadn) {
        Timer timer;
        Tester::test(testn, threadn);
        // return time (ns) per value
        return timer.get_time()*1000000.0/double(Tester::value);
    }
};

template<typename Tester, int scale = 1>
class ValuePerSecond : public Tester {
    /*override*/ value_t test(int testn, int threadn) {
        Timer timer;
        Tester::test(testn, threadn);
        // return value per seconds/scale
        return double(Tester::value)/(timer.get_time()*scale);
    }
};

template<typename Tester, int scale = 1>
class NumberPerSecond : public Tester {
    /*override*/ value_t test(int testn, int threadn) {
        Timer timer;
        Tester::test(testn, threadn);
        // return a scale per seconds
        return double(scale)/timer.get_time();
    }
};

// operate with single tester
class TestRunner {
    friend class TestProcessor;
    friend struct RunArgsBody;
    TestRunner(const TestRunner &); // don't copy

    const char *tester_name;
    StatisticsCollector *stat;
    std::vector<std::vector<StatisticsCollector::TestCase> > keys;

public:
    TesterBase &tester;

    template<typename Test>
    TestRunner(const char *name, Test *test)
        : tester_name(name), tester(*static_cast<TesterBase*>(test))
    {
        test->tester_name = name;
    }
    
    ~TestRunner() { delete &tester; }

    void init(arg_t value, int threads, Harness::SpinBarrier &barrier, StatisticsCollector *s) {
        tester.base_init(value, threads, barrier);
        stat = s;
        keys.resize(tester.tests_count);
        for(int testn = 0; testn < tester.tests_count; testn++) {
            keys[testn].resize(threads);
            std::string test_name(tester.get_name(testn));
            for(int threadn = 0; threadn < threads; threadn++)
                keys[testn][threadn] = stat->SetTestCase(tester_name, test_name.c_str(), threadn);
        }
    }

    void run_test(int threadn) {
        for(int testn = 0; testn < tester.tests_count; testn++) {
            tester.test_prefix(testn, threadn);
            tester.barrier->wait();                                 // <<<<<<<<<<<<<<<<< Barrier before running test mode
            value_t result = tester.test(testn, threadn);
            stat->AddRoundResult(keys[testn][threadn], result);
        }
    }

    void post_process(StatisticsCollector &report) {
        const int threads = tester.threads_count;
        for(int testn = 0; testn < tester.tests_count; testn++) {
            size_t coln = keys[testn][0].getResults().size()-1;
            value_t rsum = keys[testn][0].getResults()[coln];
            value_t rmin = rsum, rmax = rsum;
            for(int threadn = 1; threadn < threads; threadn++) {
                value_t result = keys[testn][threadn].getResults()[coln];
                rsum += result; // for both SUM or AVG
                if(rmin > result) rmin = result;
                if(rmax < result) rmax = result;
            }
            std::string test_name(tester.get_name(testn));
            const char *rname = tester.get_result_type(testn, TesterBase::SUM);
            if( rname ) {
                report.SetTestCase(tester_name, (test_name+rname).c_str(), threads);
                report.AddRoundResult(rsum);
            }
            rname = tester.get_result_type(testn, TesterBase::MIN);
            if( rname ) {
                report.SetTestCase(tester_name, (test_name+rname).c_str(), threads);
                report.AddRoundResult(rmin);
            }
            rname = tester.get_result_type(testn, TesterBase::AVG);
            if( rname ) {
                report.SetTestCase(tester_name, (test_name+rname).c_str(), threads);
                report.AddRoundResult(rsum / threads);
            }
            rname = tester.get_result_type(testn, TesterBase::MAX);
            if( rname ) {
                report.SetTestCase(tester_name, (test_name+rname).c_str(), threads);
                report.AddRoundResult(rmax);
            }
        }
    }
};

struct RunArgsBody {
    const vector<TestRunner*> &run_list;
    RunArgsBody(const vector<TestRunner*> &a) : run_list(a) { }
#ifndef __TBB_parallel_for_H
    void operator()(int thread) const {
#else
    void operator()(const tbb::blocked_range<int> &r) const {
        ASSERT( r.begin() + 1 == r.end(), 0);
        int thread = r.begin();
#endif
        for(size_t i = 0; i < run_list.size(); i++)
            run_list[i]->run_test(thread);
    }
};

//! Main test processor.
/** Override or use like this:
 class MyTestCollection : public TestProcessor {
    void factory(arg_t value, int threads) {
        process( value, threads,
            run("my1", new tester<my1>() ),
            run("my2", new tester<my2>() ),
        end );
        if(value == threads)
            stat->Print();
    }
};
*/

class TestProcessor {
    friend class TesterBase;

    // <threads, collector>
    typedef std::map<int, StatisticsCollector *> statistics_collection;
    statistics_collection stat_by_threads;

protected:
    // Members
    const char *collection_name;
    // current stat
    StatisticsCollector *stat;
    // token
    size_t end;

public:
    StatisticsCollector report;

    // token of tests list
    template<typename Test>
    TestRunner *run(const char *name, Test *test) {
        return new TestRunner(name, test);
    }

    // iteration processing
    void process(arg_t value, int threads, ...) {
        // prepare items
        stat = stat_by_threads[threads];
        if(!stat) {
            stat_by_threads[threads] = stat = new StatisticsCollector((collection_name + Format("@%d", threads)).c_str(), StatisticsCollector::ByAlg);
            stat->SetTitle("Detailed log of %s running with %d threads.", collection_name, threads);
        }
        Harness::SpinBarrier barrier(threads);
        // init args
        va_list args; va_start(args, threads);
        vector<TestRunner*> run_list; run_list.reserve(16);
        while(true) {
            TestRunner *item = va_arg(args, TestRunner*);
            if( !item ) break;
            item->init(value, threads, barrier, stat);
            run_list.push_back(item);
        }
        va_end(args);
        std::ostringstream buf;
        buf << value;
        const size_t round_number = stat->GetRoundsCount();
        stat->SetRoundTitle(round_number, buf.str().c_str());
        report.SetRoundTitle(round_number, buf.str().c_str());
        // run them
#ifndef __TBB_parallel_for_H
        NativeParallelFor(threads, RunArgsBody(run_list));
#else
        tbb::parallel_for(tbb::blocked_range<int>(0,threads,1), RunArgsBody(run_list));
#endif
        // destroy args
        for(size_t i = 0; i < run_list.size(); i++) {
            run_list[i]->post_process(report);
            delete run_list[i];
        }
    }

public:
    TestProcessor(const char *name, StatisticsCollector::Sorting sort_by = StatisticsCollector::ByAlg)
        : collection_name(name), stat(NULL), end(0), report(collection_name, sort_by)
    { }

    ~TestProcessor() {
        for(statistics_collection::iterator i = stat_by_threads.begin(); i != stat_by_threads.end(); i++)
            delete i->second;
    }
};

#endif// __TIME_FRAMEWORK_H__
