#include "Jikan.h"
#include "JikanDepths.h"
#include "ToJSON.h"

#include <iostream>
// #include <map>
#include <string>

#ifdef INCLUDE_OPEN_MP
#include <omp.h>
#endif

using namespace std;

Jikan::Jikan()
{
    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        this->EventType["Synchronous events"] = set<string>();
        this->EventType["Asynchronous events"] = set<string>();

        this->JSONname = OUTPUT_NAME + string(".json");
        this->JSONdata = "";
        this->Error = "";
    #ifdef INCLUDE_OPEN_MP
    }
    #endif
};

#ifdef INCLUDE_MPI
    bool Jikan::ifWriteProc(MPI_Comm comm, int id)
    {
        bool if_write = false;
        int init_flag, fin_flag, iproc;

        MPI_Initialized(&init_flag);
        MPI_Finalized(&fin_flag);

        bool if_MPI = (init_flag) && (!fin_flag);
        if(if_MPI)
        {
            MPI_Comm_rank(comm, &iproc);
            if( iproc == id)
                if_write = true;
        }
        else
            if_write = true;


        return if_write;
    }
#endif

bool Jikan::ifWriteProc()
{
    #ifdef INCLUDE_MPI
        int init_flag, fin_flag;

        MPI_Initialized(&init_flag);
        MPI_Finalized(&fin_flag);

        int GlobalCommRank;

        if((!fin_flag) && init_flag)
        {
            MPI_Comm_rank(MPI_COMM_WORLD, &GlobalCommRank);
            if(GlobalCommRank == 0)
                return true;
            else
                return false;
        }
        else
            return true;
    #endif

    return true;
}

Jikan::~Jikan()
{
    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        this->Events.clear();
        
        unordered_map<string, set<string> >::iterator it;

        for(it = this->EventType.begin(); it!=this->EventType.end(); ++it)
            it->second.clear();

        this->EventType.clear();
    #ifdef INCLUDE_OPEN_MP
    }
    #endif
}

bool Jikan::ifContains(const string& Name)
{
    unordered_map<string, EventData>::iterator it;

    it = this->Events.find(Name);
    if (it == this->Events.end())
        return false;

    return true;
}

void Jikan::JikanStart(const string& name, const int& mode)
{
    bool ExistFlag, ifMPI, ifOpenMP;
    ExistFlag = this->ifContains(name);

    if(!ExistFlag)
    {
        #ifdef INCLUDE_OPEN_MP
        #pragma omp master
        {
        #endif
            (this->Events)[name] = EventData(name);
            (this->Events)[name].GetModeVals(mode);
        #ifdef INCLUDE_OPEN_MP
        }
        #endif
    }

    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        (this->Events)[name].ifStart = true;
        // printf("(this->Events)[name].ifStart = %d\n", (this->Events)[name].ifStart);
    #ifdef INCLUDE_OPEN_MP
    }
    #endif

    // printf("118 (this->Events)[name].ifStart = %d\n", (this->Events)[name].ifStart);
    // else
    // {
    //     bool ifStart = (this->Events)[name].ifStart;

    //     if(!ifStart)
    //         return;
    // }

    #ifdef INCLUDE_OPEN_MP
    #pragma omp barrier
    #endif

    ifMPI = (this->Events)[name].mode[0], ifOpenMP = (this->Events)[name].mode[1];

    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        if(!ExistFlag)
        {
            string EventTypeName;

            if(mode == TimerMode::NO_SYNC)
                EventTypeName = "Asynchronous events";
            else
                EventTypeName = "Synchronous events";

            this->EventType[EventTypeName].insert(name);
        }

        //--------------------------- MPI synchronization ---------------------------------------
        if(ifMPI)
            this->BarrierMPI();
        //---------------------------------------------------------------------------------------
    #ifdef INCLUDE_OPEN_MP
    }
    #endif

    //--------------------------- OpenMP synchronization --------------------------------
    if(ifOpenMP)
    {
        #ifdef INCLUDE_OPEN_MP
        #pragma omp barrier
        #endif
    }
    //-----------------------------------------------------------------------------------

    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        // printf("170 (this->Events)[name].ifStart = %d\n", (this->Events)[name].ifStart);
        this->StartEvent(name);
        // printf("(this->Events)[name].ifStart = %d\n", (this->Events)[name].ifStart);
     #ifdef INCLUDE_OPEN_MP
    }
    #endif

    // printf("177 (this->Events)[name].ifStart = %d\n", (this->Events)[name].ifStart);
}

void Jikan::JikanEnd(const string& name, const int cont_mode_t)
{    
    bool ExistFlag, ifStart;
    bool ifMPI, ifOpenMP, ifCUDA;

    ExistFlag = this->ifContains(name);
    ifStart = (this->Events)[name].ifStart;

    // printf("JikanEnd\n");

    if((!ExistFlag) || (!ifStart))
    {
        // printf("ExistFlag = %d, ifStart = %d\n", ExistFlag, ifStart);
        return;
    }

    ifMPI = (this->Events)[name].mode[0], ifOpenMP = (this->Events)[name].mode[1], ifCUDA = (this->Events)[name].mode[2];
    // printf("JikanEnd:: ifCUDA = %d\n", ifCUDA);
    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        if((ifCUDA) && (!ifMPI) && (!ifOpenMP))
        {    
            #ifdef INCLUDE_GPU_TIMER
                this->EndCUDASync(name);
                this->CUDAGetTime(name, cont_mode_t);
            #endif
        }
    #ifdef INCLUDE_OPEN_MP
    }
    #endif

    //--------------------------- OpenMP synchronization --------------------------------
    if(ifOpenMP)
    {
        #ifdef INCLUDE_OPEN_MP
        #pragma omp barrier
        #endif
    }
    //-----------------------------------------------------------------------------------

    //--------------------------- MPI synchronization ---------------------------------------
    #ifdef INCLUDE_OPEN_MP
    #pragma omp master
    {
    #endif
        if(ifMPI)
            this->BarrierMPI();

        this->EndEvent(name, cont_mode_t);
    #ifdef INCLUDE_OPEN_MP
    }
    #endif
    //---------------------------------------------------------------------------------------
    (this->Events)[name].ifStart = false;
}

void Jikan::GenerateTypedOutputData(const string& EventType, string& EventTypeString)
{
    const int n = this->EventType[EventType].size();
    EventTypeString = JSON::StartTypedBlock(EventType);

    if(n == 0)
    {
        EventTypeString += "\n\t}";
        return;
    }

    string Row;
    string Name;
    set<string>::iterator it;
    set<string>::iterator end_m = this->EventType[EventType].end();
    --end_m;

    for (it = this->EventType[EventType].begin(); it != end_m; ++it)
    {
        Name = *it;
        JSON::GenerateRow(this->Events[Name], Row, JSON::SeparateRows);
        EventTypeString += Row;
    }

    Name = *end_m;
    JSON::GenerateRow(this->Events[Name], Row, JSON::FinishRows);
    EventTypeString += Row;
}

void Jikan::GenerateOutputData()
{
    unordered_map<string, set<string> >::iterator it;
    unordered_map<string, set<string> >::iterator end_m; // = this->EventType.end();
    // end_m--;

    string Name;
    string Out = "{ ";
    string TypedOutput;

    for(it = this->EventType.begin(); it!=this->EventType.end(); ++it)
    {    
        if(next(it) != this->EventType.end())
        {
            Name = it->first;
            TypedOutput = "";
            this->GenerateTypedOutputData(Name, TypedOutput);
            Out += TypedOutput + ",\n";
        }
        else
            end_m = it;
    }

    Name = end_m->first;
    TypedOutput = "";
    this->GenerateTypedOutputData(Name, TypedOutput);
    Out += TypedOutput + "\n}";

    this->JSONdata = Out;

    bool OutputRes = JSON::WriteOutput(this->JSONname, this->JSONdata);

    if(OutputRes != true)
    {
        this->Error = JSON::Error;
    }
}

void Jikan::SetDumpFilename(const string& name)
{
    this->JSONname = name;
}

void Jikan::StartEvent(const string& name)
{
    bool ifMPI = (this->Events)[name].mode[0], ifOpenMP = (this->Events)[name].mode[1], ifCUDA = (this->Events)[name].mode[2];

    if((ifCUDA))
    {
        #ifdef INCLUDE_GPU_TIMER
            // printf("StartCUDASync\n");
            this->StartCUDASync(name);
        #endif
    }

    #ifdef INCLUDE_OPEN_MP
        else if( (!ifMPI) && (ifOpenMP) )
            (this->Events)[name].double_start = omp_get_wtime();
    #endif

    else
        (this->Events)[name].start = chrono::steady_clock::now();
}

#define btoa(x) ((x)?"true":"false")

void Jikan::EndEvent(const string& name, const int cont_mode_t)
{
    bool ifMPI = (this->Events)[name].mode[0], ifOpenMP = (this->Events)[name].mode[1], ifCUDA = (this->Events)[name].mode[2];

    // printf("%s %s %s\n", btoa(ifMPI), btoa(ifOpenMP), btoa(ifCUDA));

    if(ifCUDA)
    {
        #ifdef INCLUDE_GPU_TIMER
            this->EndCUDASync(name);
        #endif
    }

    #ifdef INCLUDE_OPEN_MP
        else if( (!ifMPI) && (ifOpenMP))
        {
            double end = omp_get_wtime();
            double main_elapsed = end - (this->Events)[name].double_start;

            (this->Events)[name].elapsed_time += main_elapsed;
            (this->Events)[name].ContiniousTime += main_elapsed;

            if(cont_mode_t == 0)
            {
                (this->Events)[name].count ++;
            }

            #ifdef SAVE_TIME_SERIES
                if(cont_mode_t == 0)
                { 
                    (this->Events)[name].time_series.push_back((this->Events)[name].ContiniousTime);
                    (this->Events)[name].ContiniousTime = 0.0;
                }
            #endif
        }
    #endif

    else
    {
        chrono_time_type end = chrono::steady_clock::now();
        chrono_time_type start = (this->Events)[name].start;

        double main_elapsed = chrono::duration_cast<chrono::nanoseconds>(end - start).count() * 1e-9;
	//printf("%e\n", main_elapsed);
        (this->Events)[name].elapsed_time += main_elapsed;
        (this->Events)[name].ContiniousTime += main_elapsed;

        if(cont_mode_t == 0)
        {
            (this->Events)[name].count ++;
        }

        #ifdef SAVE_TIME_SERIES
        if(cont_mode_t == 0)
        { 
            (this->Events)[name].time_series.push_back((this->Events)[name].ContiniousTime);
            (this->Events)[name].ContiniousTime = 0.0;
        }
        #endif
    }
}

void Jikan::BarrierMPI()
{
#ifdef INCLUDE_MPI
    int init_flag, fin_flag;

    MPI_Initialized(&init_flag);
    MPI_Finalized(&fin_flag);

    if((!fin_flag) && init_flag)
        MPI_Barrier(MPI_COMM_WORLD);
#endif
}

class Jikan Timer;