#pragma once

#include <vector>
#include <string>
#include <map>
#include <set>

#include "errors.h"
#include "types.h"
#include "../Distribution/DvmhDirective.h"
#include "../Distribution/Distribution.h"

struct DistrVariant;
struct ParallelDirective;
struct ParallelRegion;
class Statement;
struct FuncInfo;

namespace Distribution
{
    class Array;
}
namespace DIST = Distribution;

void getRealArrayRefs(DIST::Array* addTo, DIST::Array* curr, std::set<DIST::Array*>& realArrayRefs, const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls);
void getAllArrayRefs(DIST::Array* addTo, DIST::Array* curr, std::set<DIST::Array*>& realArrayRefs, const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls);

enum class LoopType { NONE, FOR, WHILE, IMPLICIT };

struct InductiveVariables
{
private:
    std::string mainVar;
    std::set<std::string> allVars;

public:
    InductiveVariables() { }

    explicit InductiveVariables(const std::string& mainVar, const std::set<std::string>& allVars) : mainVar(mainVar), allVars(allVars) { };

    std::string getMainVar() const { return mainVar; }
    std::set<std::string> getAllVars() const { return allVars; }

    void addVar(const std::string& var) { allVars.insert(var); }
    void addMainVar(const std::string& var) { mainVar = var; allVars.insert(var); }

    void replaceMainVar(const std::string& var) 
    {
        allVars.erase(mainVar);

        addMainVar(var);
    }
};

struct LoopGraph
{
private:
    std::vector<std::pair<DIST::Array*, DistrVariant*>> redistributeRules;
    LoopGraph* needToSwapWith;

    //for local directive creating in MPI mode
    DIST::GraphCSR<int, double, attrType> accessGraph;
    DIST::GraphCSR<int, double, attrType> reducedAccessGraph;    
    DataDirective dataDirectives;
    //

public:
    LoopGraph()
    {
        lineNumAfterLoop = lineNum = altLineNum = -1;
        perfectLoop = 0;
        hasGoto = false;
        hasPrints = false;
        hasUnknownArrayDep = false;
        hasUnknownScalarDep = false;
        hasUnknownArrayAssigns = false;
        hasNonRectangularBounds = false;
        hasIndirectAccess = false;
        withoutDistributedArrays = false;
        hasWritesToNonDistribute = false;
        hasUnknownDistributedMap = false;
        hasDifferentAlignRules = false;
        hasNonPureProcedures = false;
        hasDvmIntervals = false;
        hasStops = false;
        directive = NULL;
        oldDirective = NULL;
        directiveForLoop = NULL;
        region = NULL;
        needToSwapWith = NULL;
        countOfIters = 0;
        countOfIterNested = 1;
        loop = NULL;
        parent = NULL;
        userDvmDirective = NULL;
        startVal = endVal = stepVal = 0;
        calculatedCountOfIters = 0;
        executionTimeInSec = -1.0;
        inDvmhRegion = 0;
        loopType = LoopType::NONE;
        inCanonicalFrom = false;
        hasAccessToSubArray = false;
        hasSubstringRefs = false;
    }

    ~LoopGraph()
    {
        needToSwapWith = NULL;
        if (directive != NULL)
            delete directive;

        if (directiveForLoop != NULL)
            delete directiveForLoop;

        for (int i = 0; i < children.size(); ++i)
            delete children[i];

        calls.clear();
        readOpsArray.clear();
        readOps.clear();
        writeOps.clear();
        hasConflicts.clear();
        acrossOutAttribute.clear();
        accessGraph.ClearGraphCSR();
        reducedAccessGraph.ClearGraphCSR();
    }

    void setForSwap(LoopGraph* with) { needToSwapWith = with; }

    LoopGraph* getForSwap() const { return needToSwapWith; }

    void clearForSwap() 
    {
        for (auto& ch : children)
            ch->clearForSwap();
        needToSwapWith = NULL; 
    }

    bool hasLimitsToParallel() const
    {
        return hasUnknownArrayDep || hasUnknownScalarDep || hasGoto || hasPrints || (hasConflicts.size() != 0) || hasStops || hasNonPureProcedures ||
               hasUnknownArrayAssigns || hasNonRectangularBounds || hasIndirectAccess || hasWritesToNonDistribute || hasDifferentAlignRules || hasDvmIntervals ||
               !isFor() || lastprivateScalars.size() || hasAccessToSubArray || hasSubstringRefs;
    }
    
    bool hasLimitsToSplit() const
    {
        return hasGoto || hasStops || !isFor() || hasPrints;
    }

    bool hasLimitsToCombine() const
    {
        return hasGoto || hasStops || !isFor() || hasPrints || linesOfCycle.size();
    }

    void addConflictMessages(std::vector<Messages> *messages)
    {
        if (messages == NULL)
            return;

        const int line = altLineNum > 0 ? altLineNum : lineNum;
        if (hasUnknownArrayDep)
            messages->push_back(Messages(NOTE, line, R113, L"unknown array dependency prevents parallelization of this loop", 3006));

        if (hasUnknownScalarDep)
            messages->push_back(Messages(NOTE, line, R114, L"unknown scalar dependency prevents parallelization of this loop", 3006));

        if (hasGoto)
            messages->push_back(Messages(NOTE, line, R115, L"internal/external moves via GOTO or EXIT operations prevent parallelization of this loop", 3006));

        if (hasPrints)
            messages->push_back(Messages(NOTE, line, R116, L"IO operations prevent parallelization of this loop", 3006));

        if (hasStops)
            messages->push_back(Messages(NOTE, line, R117, L"stop operations prevent parallelization of this loop", 3006));

        if (hasConflicts.size() != 0)
            messages->push_back(Messages(NOTE, line, R118, L"conflict writes operations prevent parallelization of this loop", 3006));

        if (hasUnknownArrayAssigns)
            messages->push_back(Messages(NOTE, line, R119, L"unknown array reference for writes prevent parallelization of this loop", 3006));

        if (hasNonRectangularBounds)
            messages->push_back(Messages(NOTE, line, R144, L"non rectangular bounds prevent parallelization of this loop", 3006));

        if (hasIndirectAccess)
            messages->push_back(Messages(NOTE, line, R120, L"indirect access by distributed array prevents parallelization of this loop", 3006));

        if (hasWritesToNonDistribute)
            messages->push_back(Messages(NOTE, line, R121, L"writes to non distributed array prevents parallelization of this loop", 3006));

        if (hasDifferentAlignRules)
            messages->push_back(Messages(NOTE, line, R122, L"different aligns between writes to distributed array prevents parallelization of this loop", 3006));

        if (hasNonPureProcedures)
            messages->push_back(Messages(NOTE, line, R123, L"non pure procedures prevent parallelization of this loop", 3006));

        if (hasDvmIntervals)
            messages->push_back(Messages(NOTE, line, R145, L"DVM intervals prevent parallelization of this loop", 3006));

        if (!isFor() || !inCanonicalFrom)
            messages->push_back(Messages(NOTE, line, R178, L"This type of loop is not supported by the system", 3006));

        if (lastprivateScalars.size())
            messages->push_back(Messages(NOTE, line, R199, L"lastprivate scalar dependency prevents parallelization of this loop", 3006));
    }

    void setNewRedistributeRules(const std::vector<std::pair<DIST::Array*, DistrVariant*>> &newRedistributeRules)
    {
        // set to top and for all childs
        redistributeRules = newRedistributeRules;
        for (int i = 0; i < children.size(); ++i)
            children[i]->setNewRedistributeRules(newRedistributeRules);
    }

    DistrVariant* getRedistributeRule(const DIST::Array *arrayT) const
    {
        DistrVariant *retVal = NULL;

        for (int i = 0; i < redistributeRules.size(); ++i)
        {
            if (redistributeRules[i].first == arrayT)
            {
                retVal = redistributeRules[i].second;
                break;
            }
        }

        return retVal;
    }

    bool hasRedistribute() const { return redistributeRules.size(); }

    ParallelDirective* recalculateParallelDirective()
    {
        std::vector<ParallelDirective*> baseDirs(perfectLoop);
        LoopGraph *next = this;
        for (int z = 0; z < perfectLoop; ++z)
        {
            baseDirs[z] = next->directiveForLoop;
            if (z != perfectLoop - 1)
                next = next->children[0];
        }

        ParallelDirective *parDirective = baseDirs[0];
        for (int z = 1; z < baseDirs.size() && baseDirs[z]; ++z)
        {
            ParallelDirective *old = parDirective;
            parDirective = *parDirective + *baseDirs[z];

            if (z != 1)
                delete old;
        }

        oldDirective = directive;
        directive = parDirective;
        return directive;
    }

    void restoreDirective()
    {
        if (oldDirective)
        {
            delete directive;
            directive = oldDirective;
        }

        for (int i = 0; i < children.size(); ++i)
            children[i]->restoreDirective();
    }

    void setRegionToChilds()
    {
        for (auto &loop : children)
        {
            loop->region = region;
            loop->setRegionToChilds();
        }
    }

    void recalculatePerfect();

    void setWithOutDistrFlagToFalse()
    {
        for (auto &loop : children)
        {
            loop->withoutDistributedArrays = false;
            loop->setWithOutDistrFlagToFalse();
        }
    }

    void propagateUserDvmDir()
    {
        for (auto &loop : children)
        {
            if (loop->userDvmDirective == NULL)
                loop->userDvmDirective = userDvmDirective;
            loop->propagateUserDvmDir();
        }
    }

    void propagateDvmhRegion(const int flag)
    {
        for (auto& loop : children)
        {
            loop->inDvmhRegion = flag;
            loop->propagateDvmhRegion(flag);
        }
    }

    std::string genLoopArrayName(const std::string &funcName) const
    {
        return funcName + "_loop_" + std::to_string(lineNum);
    }

    std::set<DIST::Array*> getAllArraysInLoop()
    {
        std::set<DIST::Array*> retVal(readOpsArray);
        for (auto &elem : writeOps)
            retVal.insert(elem.first);
        return retVal;
    }

    void removeNonDistrArrays()
    {
        std::set<DIST::Array*> newUsedArrays;        
        for (auto &elem : usedArrays)
            if (elem->GetDistributeFlagVal() == DIST::DISTR)
                newUsedArrays.insert(elem);
        usedArrays = newUsedArrays;

        std::set<DIST::Array*> newUsedArraysW;        
        for (auto &elem : usedArraysWrite)
            if (elem->GetDistributeFlagVal() == DIST::DISTR)
                newUsedArraysW.insert(elem);
        usedArraysWrite = newUsedArraysW;
        
        readOpsArray.clear();
        readOps.clear();
        writeOps.clear();
        hasConflicts.clear();

        for (auto &ch : children)
            ch->removeNonDistrArrays();
    }

    void removeGraphData()
    {
        accessGraph.ClearGraphCSR();
        reducedAccessGraph.ClearGraphCSR();

        for (auto& ch : children)
            ch->removeGraphData();
    }

    void clearUserDirectives();    

    bool isArrayTemplatesTheSame(DIST::Array*& sameTemplate, const uint64_t regId, const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls)
    {
        if (sharedMemoryParallelization != 0)
            return true;

        std::set<DIST::Array*> usedForRegAccess;
        for (auto& array : writeOps)
        {
            std::set<DIST::Array*> realArrayRefs;
            getRealArrayRefs(array.first, array.first, realArrayRefs, arrayLinksByFuncCalls);
            for (auto& realArr : realArrayRefs)
                usedForRegAccess.insert(realArr);
        }

        //read operations can be REMOTE_ACCESS
        /*for (auto& array : readOps)
        {
            std::set<DIST::Array*> realArrayRefs;
            getRealArrayRefs(array.first, array.first, realArrayRefs, arrayLinksByFuncCalls);
            for (auto& realArr : realArrayRefs)
                usedForRegAccess.insert(realArr);
        }*/

        std::set<DIST::Array*> usedTemplates;
        for (auto& array : usedArrays)
        {
            std::set<DIST::Array*> realArrayRefs;
            getRealArrayRefs(array, array, realArrayRefs, arrayLinksByFuncCalls);
            for (auto& realArr : realArrayRefs)
            {
                if (usedForRegAccess.find(realArr) == usedForRegAccess.end())
                    continue;

                auto templ = realArr->GetTemplateArray(regId);
                //TODO: what about NULL?
                if (templ)
                    usedTemplates.insert(templ);
            }
        }

        if (usedArrays.size())
        {
            if (usedTemplates.size() == 0 || usedTemplates.size() > 1)
                return false;
            else
                sameTemplate = *usedTemplates.begin();
        }
        return true;
    }

    bool hasParallelLoopsInChList();

    DIST::GraphCSR<int, double, attrType>& getGraphToModify() { return accessGraph; }
    const DIST::GraphCSR<int, double, attrType>& getGraph() const { return accessGraph; }

    DataDirective& getDataDirToModify() { return dataDirectives; }
    const DataDirective& getDataDir() const { return dataDirectives; }

    void reduceAccessGraph();

    void createVirtualTemplateLinks(const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls, std::map<std::string, std::vector<Messages>>& SPF_messages, bool isMpiProgram = false);

    bool hasParalleDirectiveBefore();

    void analyzeParallelDirs();

    void* getRealStat(const char* file) const;

    bool isFor() const { return loopType == LoopType::FOR; }

    bool isWhile() const { return loopType == LoopType::WHILE; }

    bool isImplicit() const { return loopType == LoopType::IMPLICIT; }

    std::string loopSymbol() const { return loopSymbols.getMainVar(); }

public:
    int lineNum;
    int altLineNum;
    int lineNumAfterLoop;
    std::string fileName;
    int perfectLoop;
    int countOfIters; // calculated total with nested loops
    double countOfIterNested;
    double executionTimeInSec;
    int calculatedCountOfIters; // calculated for current loop

    int startVal, endVal, stepVal;
    std::tuple<Expression*, Expression*, Expression*> startEndStepVals;

    InductiveVariables loopSymbols;
    std::pair<Expression*, Expression*> startEndExpr;

    bool hasGoto;
    std::vector<int> linesOfInternalGoTo;
    std::vector<int> linesOfExternalGoTo;
    std::vector<int> linesOfCycle;

    bool hasPrints;
    std::set<int> linesOfIO;

    bool hasStops;
    std::set<int> linesOfStop;

    bool hasUnknownScalarDep;
    std::vector<int> linesOfScalarDep;
    
    bool hasUnknownArrayDep;

    bool hasUnknownArrayAssigns;
 
    bool hasNonRectangularBounds;

    bool hasIndirectAccess;

    bool hasWritesToNonDistribute;

    bool withoutDistributedArrays;

    bool hasUnknownDistributedMap;

    bool hasDifferentAlignRules;

    bool hasNonPureProcedures;
    
    bool hasDvmIntervals;
    // make sense only for NODIST regime
    bool hasAccessToSubArray;

    bool hasSubstringRefs;

    LoopType loopType;

    bool inCanonicalFrom;
    

    std::vector<LoopGraph*> children;
    std::vector<LoopGraph*> funcChildren;
    LoopGraph *parent;
    std::vector<LoopGraph*> funcParents;

    // PAIR<FUNC_NAME, LINE>
    std::vector<std::pair<std::string, int>> calls;
    
    std::set<std::string> lastprivateScalars;
    std::set<std::string> privateScalars;

    std::map<DIST::Array*, std::vector<ArrayOp>> readOpsForLoop;
    std::map<DIST::Array*, std::vector<ArrayOp>> writeOpsForLoop;

    // agregated read and write operations by arrays
    std::set<DIST::Array*> readOpsArray;
    std::map<DIST::Array*, std::pair<std::vector<ArrayOp>, std::vector<bool>>> readOps;
    std::map<DIST::Array*, std::vector<ArrayOp>> writeOps;
    std::map<DIST::Array*, std::vector<ArrayOp>> remoteRegularReads;
    std::map<DIST::Array*, bool> hasConflicts;
    
    std::set<DIST::Array*> acrossOutAttribute;

    ParallelDirective *directive;        // united directive for nested loops
    ParallelDirective *oldDirective;     // save old directive for reverse
    ParallelDirective *directiveForLoop; // part of directive for loop
    Statement *userDvmDirective;         // user's DVM PARALLEL directive

    ParallelRegion *region;

    Statement *loop;

    std::set<DIST::Array*> usedArrays;// without NON DIST
    std::set<DIST::Array*> usedArraysAll;
    std::set<DIST::Array*> usedArraysWrite; // without NON DIST
    std::set<DIST::Array*> usedArraysWriteAll;

    int inDvmhRegion; // 0 -unknown, -1 - no, 1 - yes
};

void processLoopInformationForFunction(std::map<LoopGraph*, std::map<DIST::Array*, ArrayInfo*>>& loopInfo);
void addToDistributionGraph(const std::map<LoopGraph*, std::map<DIST::Array*, ArrayInfo*>>& loopInfo, const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls);
void selectFreeLoopsForParallelization(const std::vector<LoopGraph*>& loops, const std::string& funcName, bool isDistribute, const std::vector<ParallelRegion*>& regions, std::vector<Messages>& messagesForFile);

int printLoopGraph(const char* fileName, const std::map<std::string, std::vector<LoopGraph*>>& loopGraph, bool withRegs = false);
void checkCountOfIter(std::map<std::string, std::vector<LoopGraph*>>& loopGraph, const std::map<std::string, std::vector<FuncInfo*>>& allFuncInfo, std::map<std::string, std::vector<Messages>>& SPF_messages);

void createMapLoopGraph(const std::vector<LoopGraph*>& loops, std::map<int, LoopGraph*>& mapGraph);
void updateLoopIoAndStopsByFuncCalls(std::map<std::string, std::vector<LoopGraph*>>& loopGraph, const std::map<std::string, std::vector<FuncInfo*>>& allFuncInfo);
void checkArraysMapping(const std::map<std::string, std::vector<LoopGraph*>>& loopGraph, std::map<std::string, std::vector<Messages>>& SPF_messages, const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls);
void filterArrayInCSRGraph(std::map<std::string, std::vector<LoopGraph*>>& loopGraph, std::map<std::string, std::vector<FuncInfo*>>& allFuncs, ParallelRegion* reg, const std::map<DIST::Array*, std::set<DIST::Array*>>& arrayLinksByFuncCalls, std::map<std::string, std::vector<Messages>>& messages);
void swapLoopsForParallel(std::map<std::string, std::vector<LoopGraph*>>& loopGraph, std::map<std::string, std::vector<Messages>>& SPF_messages, const int rev);

