#include <sstream>
#include <time.h>
#include <sys/time.h>
#include <iomanip>

#include <boost/filesystem/path.hpp>
#include <SpatialDomains/MeshGraph2D.h>
#include <MultiRegions/DisContField2D.h>

#ifdef NEKTAR_USING_CHUD
#include <CHUD/CHUD.h> 
//#define SHARK
#endif 

using namespace Nektar;

std::string PortablePath(const boost::filesystem::path& path);

int main(int argc, char *argv[])
{
    MultiRegions::DisContField2DSharedPtr Exp,Fce,Sol;
    int     i, nq,  coordim;
    Array<OneD,NekDouble>  fce,sol; 
    Array<OneD,NekDouble>  xc0,xc1,xc2; 
    NekDouble  lambda;
    vector<string> vFilenames;

    if(argc < 6)//< allows to parse "verbose" option
    {
        fprintf(stderr,"Usage: TimingHDGHelmSolve2D Type MeshSize NumModes OptimisationLevel\n");
        fprintf(stderr,"    where: - Type is one of the following:\n");
        fprintf(stderr,"                  1: Regular  Quadrilaterals \n");
        fprintf(stderr,"                  2: Deformed Quadrilaterals (may not be supported) \n");
        fprintf(stderr,"                  3: Regular  Triangles \n");
        fprintf(stderr,"    where: - MeshSize is 1/h \n");
        fprintf(stderr,"    where: - NumModes is the number of 1D modes of the expansion \n");
        fprintf(stderr,"    where: - OptimisationLevel is one of the following:\n");
        fprintf(stderr,"                  0: Use elemental sum-factorisation evaluation \n");
        fprintf(stderr,"                  2: Use elemental matrix evaluation using blockmatrices \n");
        fprintf(stderr,"                  3: Use global matrix evaluation \n");
        fprintf(stderr,"                  4: Use optimal evaluation (this option requires optimisation-files being set-up) \n");
        fprintf(stderr,"    where: - LinSysSolver is one of the following:\n");
        fprintf(stderr,"                  0: Use DirectStaticCond Solver \n");
        fprintf(stderr,"                  1: Use DirectMultilevelStaticCond Solver\n");
        fprintf(stderr,"                  2: Use IterativeStaticCond Solver \n");
        fprintf(stderr,"                  3: Use IterativeMultilevelStaticCond Solver\n");
        exit(1);
    }

    boost::filesystem::path basePath(BASE_PATH);

    int Type        = atoi(argv[1]);
    std::string TypeStr;
    int MeshSize    = atoi(argv[2]);
    int NumModes    = atoi(argv[3]);
    int optLevel    = atoi(argv[4]);
    std::string optLevelStr;
    int SolverType  = atoi(argv[5]);
    std::string SolverTypeStr;

    //----------------------------------------------
    // Retrieve the necessary input files
    stringstream MeshFileName;
    stringstream MeshFileDirectory;
    stringstream BCfileName;
    stringstream ExpansionsFileName;
    stringstream GlobOptFileName;

    switch(Type)
    {
        case 1:
            {
                MeshFileDirectory << "RegularQuadMeshes";
                MeshFileName << "UnitSquare_RegularQuadMesh_h_1_" << MeshSize << ".xml";
                TypeStr = "RQuad";
            }
            break;
        case 2:
            {
                MeshFileDirectory << "DeformedQuadMeshes";
                MeshFileName << "UnitSquare_DeformedQuadMesh_h_1_" << MeshSize << ".xml";
                TypeStr = "DQuad";
            }
            break;
        case 3:
            {
                MeshFileDirectory << "RegularTriMeshes";
                MeshFileName << "UnitSquare_RegularTriMesh_h_1_" << MeshSize << ".xml";
                TypeStr = "RTri";
            }
            break;
        default:
            {
                cerr << "Type should be equal to one of the following values: "<< endl;
                cerr << "  1: Regular Quads" << endl;
                cerr << "  2: Deformed Quads" << endl;
                cerr << "  3: Regular Tris" << endl;
                exit(1);
            }
    }

    BCfileName << "UnitSquare_DirichletBoundaryConditions.xml";
    ExpansionsFileName << "NektarExpansionsNummodes" << NumModes << ".xml";

    switch(optLevel)
    {
        case 0:
            {
                GlobOptFileName << "NoGlobalMat.xml";
                optLevelStr = "SumFac";
            }
            break;
        case 2:
            {
                GlobOptFileName << "DoBlockMat.xml";
                optLevelStr = "BlkMat";
            }
            break;
        case 3:
            {
                GlobOptFileName << "DoGlobalMat.xml";
                optLevelStr = "GlbMat";
            }
            break;
        case 4:
            {
                ASSERTL0(false,"Optimisation level not set up");            
            }
            break;
        default:
            {
                ASSERTL0(false,"Unrecognised optimisation level");
            }
    }


    boost::filesystem::path MeshFilePath = basePath / 
        boost::filesystem::path("InputFiles") /
        boost::filesystem::path("Geometry") /
        boost::filesystem::path(MeshFileDirectory.str()) /  
        boost::filesystem::path(MeshFileName.str());
    vFilenames.push_back(PortablePath(MeshFilePath));

    boost::filesystem::path BCfilePath = basePath / 
        boost::filesystem::path("InputFiles") /
        boost::filesystem::path("Conditions") /
        boost::filesystem::path(BCfileName.str());
    vFilenames.push_back(PortablePath(BCfilePath));

    boost::filesystem::path ExpansionsFilePath = basePath / 
        boost::filesystem::path("InputFiles") /
        boost::filesystem::path("Expansions") /
        boost::filesystem::path(ExpansionsFileName.str());
    vFilenames.push_back(PortablePath(ExpansionsFilePath));

    boost::filesystem::path GlobOptFilePath = basePath / 
        boost::filesystem::path("InputFiles") /
        boost::filesystem::path("Optimisation") /
        boost::filesystem::path(GlobOptFileName.str());
    vFilenames.push_back(PortablePath(GlobOptFilePath));

    //----------------------------------------------

    LibUtilities::SessionReaderSharedPtr vSession
        = LibUtilities::SessionReader::CreateInstance(argc, argv, vFilenames);

    switch(SolverType)
    {
        case 0:
            {
                vSession->SetSolverInfo("GlobalSysSoln", "DirectStaticCond");
                SolverTypeStr = "DSC";
            }
            break;
        case 1:
            {
                vSession->SetSolverInfo("GlobalSysSoln", "DirectMultiLevelStaticCond");
                SolverTypeStr = "DMSC";
            }
            break;
        case 2:
            {
                vSession->SetSolverInfo("GlobalSysSoln", "IterativeStaticCond");
                SolverTypeStr = "ISC";
            }
            break;
        case 3:
            {
                vSession->SetSolverInfo("GlobalSysSoln", "IterativeMultiLevelStaticCond");
                SolverTypeStr = "IMSC";
            }
            break;
        default:
            {
                ASSERTL0(false,"Unrecognised system solver");
            }
    }

    //----------------------------------------------
    // Read in mesh from input file
    SpatialDomains::MeshGraphSharedPtr graph2D = MemoryManager<SpatialDomains::MeshGraph2D>::AllocateSharedPtr(vSession);
    //----------------------------------------------

    //----------------------------------------------
    // Print summary of solution details
    lambda = vSession->GetParameter("Lambda");
    //----------------------------------------------

    //----------------------------------------------
    // Define Expansion 
    Exp = MemoryManager<MultiRegions::DisContField2D>::
        AllocateSharedPtr(vSession,graph2D,vSession->GetVariable(0));
    //----------------------------------------------
    int NumElements = Exp->GetExpSize();

    //----------------------------------------------
    // Set up coordinates of mesh for Forcing function evaluation
    coordim = Exp->GetCoordim(0);
    nq      = Exp->GetTotPoints();

    xc0 = Array<OneD,NekDouble>(nq,0.0);
    xc1 = Array<OneD,NekDouble>(nq,0.0);
    xc2 = Array<OneD,NekDouble>(nq,0.0);

    switch(coordim)
    {
        case 1:
            Exp->GetCoords(xc0);
            break;
        case 2:
            Exp->GetCoords(xc0,xc1);
            break;
        case 3:
            Exp->GetCoords(xc0,xc1,xc2);
            break;
    }
    //----------------------------------------------

    //----------------------------------------------
    // Define forcing function for first variable defined in file 
    fce = Array<OneD,NekDouble>(nq);
    LibUtilities::EquationSharedPtr ffunc = vSession->GetFunction("Forcing",0);
    ffunc->Evaluate(xc0,xc1,xc2,fce);
    //----------------------------------------------

    //----------------------------------------------
    // Setup expansion containing the  forcing function
    Fce = MemoryManager<MultiRegions::DisContField2D>::AllocateSharedPtr(*Exp);
    Fce->SetPhys(fce);
    //----------------------------------------------

    //----------------------------------------------
    // Helmholtz solution taking physical forcing
    FlagList flags;
    StdRegions::ConstFactorMap factors;
    factors[StdRegions::eFactorLambda] = lambda;
    factors[StdRegions::eFactorTau] = 1.0;
    Exp->HelmSolve(Fce->GetPhys(), Exp->UpdateCoeffs(),flags,factors);
    //----------------------------------------------

    //----------------------------------------------
    // Backward Transform Solution to get solved values at 
    Exp->BwdTrans(Exp->GetCoeffs(), Exp->UpdatePhys());
    //----------------------------------------------

    //----------------------------------------------
    // See if there is an exact solution, if so 
    // evaluate and plot errors
    LibUtilities::EquationSharedPtr ex_sol = vSession->GetFunction("ExactSolution",0);

    //----------------------------------------------
    // evaluate exact solution 
    sol = Array<OneD,NekDouble>(nq);
    ex_sol->Evaluate(xc0,xc1,xc2,sol);

    //--------------------------------------------
    // Calculate L_inf error 
    NekDouble L2Error    = Exp->L2  (Exp->GetPhys(), sol);
    NekDouble LinfError  = Exp->Linf(Exp->GetPhys(), sol); 
    
    //////////////////////////////////////////////////////////////////////////////////////
    // Alternative error computation (finer sampling) 
    //////////////////////////////////////////////////////////////////////////////////////
    
    const LibUtilities::PointsKey PkeyT1(30,LibUtilities::eGaussLobattoLegendre);
    const LibUtilities::PointsKey PkeyT2(30,LibUtilities::eGaussRadauMAlpha1Beta0);
    const LibUtilities::PointsKey PkeyQ1(30,LibUtilities::eGaussLobattoLegendre);
    const LibUtilities::PointsKey PkeyQ2(30,LibUtilities::eGaussLobattoLegendre);
    const LibUtilities::BasisKey  BkeyT1(LibUtilities::eModified_A,NumModes,PkeyT1);
    const LibUtilities::BasisKey  BkeyT2(LibUtilities::eModified_B,NumModes,PkeyT2);
    const LibUtilities::BasisKey  BkeyQ1(LibUtilities::eModified_A,NumModes,PkeyQ1);
    const LibUtilities::BasisKey  BkeyQ2(LibUtilities::eModified_A,NumModes,PkeyQ2);


    MultiRegions::ExpList2DSharedPtr ErrorExp = 
        MemoryManager<MultiRegions::ExpList2D>::AllocateSharedPtr(vSession,BkeyT1,BkeyT2,BkeyQ1,BkeyQ2,graph2D);

    int ErrorCoordim = ErrorExp->GetCoordim(0);
    int ErrorNq      = ErrorExp->GetTotPoints();

    Array<OneD,NekDouble> ErrorXc0(ErrorNq,0.0);
    Array<OneD,NekDouble> ErrorXc1(ErrorNq,0.0);
    Array<OneD,NekDouble> ErrorXc2(ErrorNq,0.0);

    switch(ErrorCoordim)
    {
        case 1:
            ErrorExp->GetCoords(ErrorXc0);
            break;
        case 2:
            ErrorExp->GetCoords(ErrorXc0,ErrorXc1);
            break;
        case 3:
            ErrorExp->GetCoords(ErrorXc0,ErrorXc1,ErrorXc2);
            break;
    }


    // evaluate exact solution 
    Array<OneD,NekDouble> ErrorSol(ErrorNq);
    ex_sol->Evaluate(ErrorXc0,ErrorXc1,ErrorXc2,ErrorSol);

    // calcualte spectral/hp approximation on the quad points of this new
    // expansion basis
    ErrorExp->BwdTrans_IterPerExp(Exp->GetCoeffs(),ErrorExp->UpdatePhys());

    NekDouble L2ErrorBis    = ErrorExp->L2  (ErrorExp->GetPhys(), ErrorSol);
    NekDouble LinfErrorBis  = ErrorExp->Linf(ErrorExp->GetPhys(), ErrorSol); 
    
    //////////////////////////////////////////////////////////////////////////////////////
    // postprocessing and error computation
    //////////////////////////////////////////////////////////////////////////////////////

    int num_points = NumModes + 1;
    //Triangle
    const LibUtilities::PointsKey ppPkeyT1(num_points+1,LibUtilities::eGaussLobattoLegendre);
    const LibUtilities::PointsKey ppPkeyT2(num_points,LibUtilities::eGaussRadauMAlpha1Beta0);//need to doublecheck this one
    LibUtilities::BasisKeyVector  BkeyT;
    BkeyT.push_back(LibUtilities::BasisKey(LibUtilities::eModified_A, NumModes+1, ppPkeyT1));
    BkeyT.push_back(LibUtilities::BasisKey(LibUtilities::eModified_B, NumModes+1, ppPkeyT2));
    //Quadrilateral
    const LibUtilities::PointsKey ppPkeyQ(num_points+1,LibUtilities::eGaussLobattoLegendre);
    LibUtilities::BasisKeyVector  BkeyQ;
    BkeyQ.push_back(LibUtilities::BasisKey(LibUtilities::eModified_A, NumModes+1, ppPkeyQ));
    BkeyQ.push_back(LibUtilities::BasisKey(LibUtilities::eModified_A, NumModes+1, ppPkeyQ));

    graph2D->SetBasisKey(LibUtilities::eTriangle, BkeyT);
    graph2D->SetBasisKey(LibUtilities::eQuadrilateral, BkeyQ);

    MultiRegions::DisContField2DSharedPtr PostProc = 
        MemoryManager<MultiRegions::DisContField2D>::AllocateSharedPtr(vSession,graph2D,vSession->GetVariable(0));

    int ppCoordim = PostProc->GetCoordim(0);
    int ppNq      = PostProc->GetTotPoints();

    Array<OneD,NekDouble> ppXc0(ppNq,0.0);
    Array<OneD,NekDouble> ppXc1(ppNq,0.0);
    Array<OneD,NekDouble> ppXc2(ppNq,0.0);

    switch(ppCoordim)
    {
        case 1:
            PostProc->GetCoords(ppXc0);
            break;
        case 2:
            PostProc->GetCoords(ppXc0,ppXc1);
            break;
        case 3:
            PostProc->GetCoords(ppXc0,ppXc1,ppXc2);
            break;
    }


    // evaluate exact solution 
    Array<OneD,NekDouble> ppSol(ppNq);
    ex_sol->Evaluate(ppXc0,ppXc1,ppXc2,ppSol);

    // calcualte spectral/hp approximation on the quad points of this new
    // expansion basis
    std::vector<LibUtilities::FieldDefinitionsSharedPtr> FieldDef 
        = Exp->GetFieldDefinitions();
    std::vector<std::vector<NekDouble> > FieldData(FieldDef.size());
    std::string fieldstr = "u";

    for(i = 0; i < FieldDef.size(); ++i)
    {
        FieldDef[i]->m_fields.push_back(fieldstr);
        Exp->AppendFieldData(FieldDef[i], FieldData[i]);
        PostProc->ExtractDataToCoeffs(FieldDef[i],FieldData[i],fieldstr,PostProc->UpdateCoeffs());
    }

    // Interpolation of trace 
    std::vector<LibUtilities::FieldDefinitionsSharedPtr> TraceDef 
        = Exp->GetTrace()->GetFieldDefinitions();
    std::vector<std::vector<NekDouble> > TraceData(TraceDef.size());
    for(i = 0; i < TraceDef.size(); ++i)
    {
        TraceDef[i]->m_fields.push_back(fieldstr);
        Exp->GetTrace()->AppendFieldData(TraceDef[i], TraceData[i]);
        PostProc->GetTrace()->ExtractDataToCoeffs(TraceDef[i],TraceData[i],fieldstr,PostProc->GetTrace()->UpdateCoeffs());
    }

    PostProc->BwdTrans_IterPerExp(PostProc->GetCoeffs(),PostProc->UpdatePhys());

    PostProc->EvaluateHDGPostProcessing(PostProc->UpdateCoeffs());
    PostProc->BwdTrans_IterPerExp(PostProc->GetCoeffs(),PostProc->UpdatePhys());

    NekDouble L2ErrorPostProc = PostProc->L2(PostProc->GetPhys(),ppSol);
    NekDouble LinfErrorPostProc = PostProc->Linf(PostProc->GetPhys(),ppSol); 

    //////////////////////////////////////////////////////////////////////////////////////
    // Timing section 
    //////////////////////////////////////////////////////////////////////////////////////
    //--------------------------------------------     
#if 0
    cout << "L infinity error: " << LinfErrorBis << endl;
    cout << "L 2 error:        " << L2ErrorBis   << endl;
#endif 
    //----------------------------------------------       

    //----------------------------------------------
    // Do the timings
    timeval timer1, timer2;
    NekDouble time1, time2;
    NekDouble exeTime;

    // We first do a single run in order to estimate the number of calls 
    // we are going to make
    gettimeofday(&timer1, NULL);
    Exp->HelmSolve(Fce->GetPhys(), Exp->UpdateCoeffs(),flags,factors);
    Exp->BwdTrans (Exp->GetCoeffs(),Exp->UpdatePhys());
    gettimeofday(&timer2, NULL);
    time1 = timer1.tv_sec*1000000.0+(timer1.tv_usec);
    time2 = timer2.tv_sec*1000000.0+(timer2.tv_usec);
    exeTime = (time2-time1);

    int NumCalls = (int) ceil(1.0e6/exeTime);
    if(NumCalls < 1)
    {
        NumCalls = 1;
    }

#ifdef SHARK
    NumCalls *= 20;

    chudInitialize();
    chudSetErrorLogFile(stderr);
    chudUmarkPID(getpid(), TRUE);    
    chudAcquireRemoteAccess();
    chudStartRemotePerfMonitor("TimingHDGHelmSolve2D");
#endif

    gettimeofday(&timer1, NULL);
    for(i = 0; i < NumCalls; ++i)
    {
        Exp->HelmSolve(Fce->GetPhys(), Exp->UpdateCoeffs(),flags,factors);
        Exp->BwdTrans (Exp->GetCoeffs(),Exp->UpdatePhys());
    }
    gettimeofday(&timer2, NULL);

#ifdef SHARK
    chudStopRemotePerfMonitor();
    chudReleaseRemoteAccess();
    chudCleanup();
#endif


    time1 = timer1.tv_sec*1000000.0+(timer1.tv_usec);
    time2 = timer2.tv_sec*1000000.0+(timer2.tv_usec);
    exeTime = (time2-time1);

    int nLocCoeffs     = Exp->GetTraceMap()->GetNumLocalCoeffs();
    int nGlobCoeffs    = Exp->GetTraceMap()->GetNumGlobalCoeffs();
    int nLocBndCoeffs  = Exp->GetTraceMap()->GetNumLocalBndCoeffs();
    int nGlobBndCoeffs = Exp->GetTraceMap()->GetNumGlobalBndCoeffs();
    int nLocDirCoeffs  = Exp->GetTraceMap()->GetNumLocalDirBndCoeffs();
    int nGlobDirCoeffs = Exp->GetTraceMap()->GetNumGlobalDirBndCoeffs();
    int nGlobBandwidth = Exp->GetTraceMap()->GetBndSystemBandWidth();
    int nGlobBndRank = nGlobBndCoeffs - nGlobDirCoeffs;
    int nnz = 0;

    ofstream outfile("TimingHDGHelmSolve2D.dat");
    outfile.precision(0);
    outfile << setw(10) << SolverTypeStr << " ";
    outfile << setw(10) << optLevelStr << " ";
    outfile << setw(10) << TypeStr << " ";
    outfile << setw(10) << NumElements << " ";
    outfile << setw(10) << NumModes << " ";
    outfile << setw(10) << fixed << noshowpoint << exeTime << " ";
    outfile << setw(10) << NumCalls << " ";
    outfile << setw(10) << fixed << noshowpoint << ((NekDouble) (exeTime/((NekDouble)NumCalls))) << " ";
    outfile.precision(7);
    outfile << setw(15) << scientific << noshowpoint << L2Error << " ";
    outfile << setw(15) << scientific << noshowpoint << L2ErrorPostProc << " ";
    outfile << setw(15) << scientific << noshowpoint << L2ErrorBis << " ";
    outfile << setw(15) << scientific << noshowpoint << LinfError << " ";
    outfile << setw(15) << scientific << noshowpoint << LinfErrorPostProc << " ";
    outfile << setw(15) << scientific << noshowpoint << LinfErrorBis << " ";
    outfile << setw(10) << nLocCoeffs  << " ";
    outfile << setw(10) << nGlobCoeffs << " ";
    outfile << setw(10) << nLocBndCoeffs  << " ";
    outfile << setw(10) << nGlobBndCoeffs << " ";
    outfile << setw(10) << nLocDirCoeffs  << " ";
    outfile << setw(10) << nGlobDirCoeffs << " ";
    outfile << setw(10) << nGlobBndRank << " ";
    outfile << setw(10) << nGlobBandwidth << " ";
    outfile << setw(10) << nnz << " ";
    outfile << endl;

    outfile.close();
    //----------------------------------------------



    return 0;
}

std::string PortablePath(const boost::filesystem::path& path)
{
    boost::filesystem::path temp = path;
#if BOOST_VERSION > 104200
    temp.make_preferred();
    return temp.string();
#else
    return temp.file_string();
#endif

}
