#ifdef _MSC_VER  //If the compiler is the Visual Studio Compiler
#define NOMINMAX //solves a problem with std::max on Windows
#pragma warning(disable: 4297) //remove throw warnings
#endif

#include "common/definitions.h"
#include <iostream>
#include <vector>

#include "common/SpinSystem.h"

const bool debug = false;

const char* get_version()
{
    return SPINTRUM_VERSION;
}

void print_software_info()
{
    std::cout<<"Spintrum version "<<get_version()<<". ";
    std::cout<<"This software is provided under the MPL license. "
               "For more information, contact Samer Afach: samer@afach.de."<<std::endl<<std::endl;
}

/**
 * A quantum mechanics operation (populate spins, tip spins, etc...) is a struct that contains a specific, predefined number of parameters.
 * Each operation has an Identifier, which is simply an integer. From this identifier, the code knows what parameters to seek
 * and how many parameters should be expected.
 * There are integer parameters, real parameters, and array of real parameters.
 */
struct SpinOperation
{
    long     OperationIdentifier;
    long     Params_int_count;     //number of integer parameters
    long*    Params_int;           //integer parameters
    long     Params_real_count;    //number of real parameters
    double*  Params_real;          //real parameters
    long     Params_array_count;   //number of arrays
    long*    Params_array_lengths; //lengths of suppled arrays
    char**   Params_array_names;   //names of the parameters of the arrays
    double** Params_array;         //array data
};
const static int OPERATION__THERMAL_POPULATE = 0;
const static int OPERATION__TIP_SPINS = 1;
const static int OPERATION__SET_HAMILTONIAN = 2;
const static int OPERATION__EVOLVE_TIME_INDEPENDENT = 3;
const static int OPERATION__INIT_TIME_INDEPENDENT_EVOLUTION = 4;
const static int OPERATION__EVOLVE_TIME_DEPENDENT = 5;

typedef double Real;

typedef SpinSystem<Real,std::complex<Real>,VEC,CX_VEC,MAT,CX_MAT> SpinSystemType;

struct SpinInfo
{
    CX_MAT jCouplings;
    CX_VEC gammas;
    G_MAT<int> multiplicities;
};

void devectorizeJCouplings(const long numOfSpins, const double* const jCouplingsInput, CX_MAT& jCouplingsOutput)
{
    if(jCouplingsInput == 0)
    {
        jCouplingsOutput.resize(0,0);
        return;
    }
    int i = 0; //element of given array
    int j = 0; //element of current row
    int row = 0;
    while (i < numOfSpins*(numOfSpins-1)/2)
    {
        if(j <= row)
        {
            j++;
            continue;
        }
        if(j >= numOfSpins)
        {
            j = 0;
            row++;
            continue;
        }
        jCouplingsOutput.at(row,j) = jCouplingsInput[i];
        j++;
        i++;
    }
}
extern "C" int InitializeSimulator(double* gammas, double* jCouplings, int* spinMultiplicities, int numOfSpins, bool doPrintOutput, void** SpinSystemObject)
{
    try
    {
        print_software_info();
        //    std::cout<<"Number of spins: "<<numOfSpins<<std::endl;
        SpinInfo spinInfo;
        spinInfo.gammas.resize(numOfSpins,1);
        spinInfo.multiplicities.resize(numOfSpins,1);
        spinInfo.jCouplings.resize(numOfSpins,numOfSpins);
        for(int i = 0; i < numOfSpins; i++)
        {
            //        std::cout<<gammas[i]<<"\t";
            spinInfo.gammas[i] = gammas[i];
        }
        //    std::cout<<spinInfo.gammas<<std::endl;
        if(spinMultiplicities == 0)
        {
            for(int i = 0; i < numOfSpins; i++)
                spinInfo.multiplicities[i] = 2;
        }
        else
        {
            for(int i = 0; i < numOfSpins; i++)
                spinInfo.multiplicities[i] = spinMultiplicities[i];
        }

        devectorizeJCouplings(numOfSpins, jCouplings, spinInfo.jCouplings);
        //    std::cout<<spinInfo.jCouplings<<std::endl;

        SpinSystemType *Spins = new SpinSystem<Real,std::complex<Real>,VEC,CX_VEC,MAT,CX_MAT>();
        Spins->print = doPrintOutput;
        Spins->init(spinInfo.gammas,spinInfo.jCouplings,spinInfo.multiplicities);
        *SpinSystemObject = static_cast<void*>(Spins);
        return 0;
    }
    catch(std::exception &ex)
    {
        std::cout<<"An exception was thrown while trying to initialize the spin system. Exception message is: " << ex.what() << std::endl;
        return -1;
    }
    catch(...)
    {
        std::cout<<"An unhandled exception was thrown while trying to initialize the spin system."<< std::endl;
        return -2;
    }
}

extern "C" int DenitializeSimulator(void* SpinSystemObject)
{
    try
    {
        delete (static_cast<SpinSystemType*>(SpinSystemObject));
        return 0;
    }
    catch(std::exception &ex)
    {
        std::cout<<"An exception was thrown while trying to delete the spin system from memory. Exception message is: " << ex.what() << std::endl;
        return -1;
    }
    catch(...)
    {
        std::cout<<"An unhandled exception was thrown while trying to delete the spin system from memory."<< std::endl;
        return -2;
    }
}


extern "C" int SetGammasInSimulator(double* gammas, void* SpinSystemObject)
{
    try
    {
        CX_VEC gammasVec((static_cast<SpinSystemType*>(SpinSystemObject))->getNumOfSpins(),1);
        for(int i = 0; i < gammasVec.size(); i++)
        {
            gammasVec[i] = gammas[i];
        }
        (static_cast<SpinSystemType*>(SpinSystemObject))->setGammas(gammasVec);
        return 0;
    }
    catch(std::exception &ex)
    {
        std::cout<<"An exception was thrown while trying to delete the spin system from memory. Exception message is: " << ex.what() << std::endl;
        return -1;
    }
    catch(...)
    {
        std::cout<<"An unhandled exception was thrown while trying to delete the spin system from memory."<< std::endl;
        return -2;
    }
}

extern "C" int SetJCouplingsInSimulator(double* jCouplings, void* SpinSystemObject)
{
    try
    {
        CX_MAT jCouplingsMat((static_cast<SpinSystemType*>(SpinSystemObject))->getNumOfSpins(),(static_cast<SpinSystemType*>(SpinSystemObject))->getNumOfSpins());
        devectorizeJCouplings((static_cast<SpinSystemType*>(SpinSystemObject))->getNumOfSpins(), jCouplings, jCouplingsMat);
        (static_cast<SpinSystemType*>(SpinSystemObject))->setJCouplings(jCouplingsMat);
        return 0;
    }
    catch(std::exception &ex)
    {
        std::cout<<"An exception was thrown while trying to delete the spin system from memory. Exception message is: " << ex.what() << std::endl;
        return -1;
    }
    catch(...)
    {
        std::cout<<"An unhandled exception was thrown while trying to delete the spin system from memory."<< std::endl;
        return -2;
    }
}
void _do_operation_evolve_time_dependent(SpinOperation* op, SpinSystemType* SpinsPtr, double* outputArray, long &currentDataPoint)
{
    if(debug) std::cout<<"Printing received data:"<<std::endl;
    if(op->Params_array_count == 0)
    {
        throw std::runtime_error("There's no data provided for the time-dependent evolution.");
    }
    else
    {
        //make sure that all lengths are equal
        long initialLength = op->Params_array_lengths[0];
        for(long i = 1; i < op->Params_array_count; i++)
        {
            if(op->Params_array_lengths[i] != initialLength)
            {
                throw std::length_error("Data arrays in time-dependent evolution are expected all to be of the same length.");
            }
        }
    }
    std::vector<std::string> varsNames(static_cast<std::vector<std::string>::size_type>(op->Params_array_count));
    std::copy(op->Params_array_names,op->Params_array_names+op->Params_array_count,varsNames.begin()); //copy names to vector<string>

    CX_MAT evData(op->Params_array_lengths[0],op->Params_array_count);
    for(long i = 0; i < op->Params_array_count; i++)
    {
        std::copy(&op->Params_array[i][0],&op->Params_array[i][op->Params_array_lengths[i]],evData.begin()+evData.rows()*i);
    }
    std::set<char> dirString;
    if(op->Params_int[1]) dirString.insert('x');
    if(op->Params_int[2]) dirString.insert('y');
    if(op->Params_int[3]) dirString.insert('z');
    std::vector<Real> tr = SpinsPtr->evolveDensityMatrix(op->Params_real[0],varsNames,evData,static_cast<int>(op->Params_int[0]),dirString);
    std::copy(std::make_move_iterator(tr.begin()), std::make_move_iterator(tr.end()), outputArray + currentDataPoint);
    currentDataPoint += op->Params_array_lengths[0]*static_cast<long>(dirString.size());
}

long ExecuteOperations(void* SpinSystemObject, void** ops, int numOfOperations, double* output, bool debug)
{
    if(debug)
    {
        std::cout<<"Number of operations: "<<numOfOperations<<std::endl;
    }

    SpinSystemType* SpinsPtr = static_cast<SpinSystemType*>(SpinSystemObject);
    long currentDataPoint = 0; //for a simulation with many operations that consecutively fill the output, this keeps track of the current point
    for(int i = 0; i < numOfOperations; i++) {
        void** currOperationBytePosition = (static_cast<void**>(ops));
        long OperationIdentifier = *(static_cast<long*>(currOperationBytePosition[i]));
        if(debug) std::cout<<"Identifier: "<<OperationIdentifier<<std::endl;
        if(OperationIdentifier == OPERATION__THERMAL_POPULATE) {
            SpinOperation* op;
            op = static_cast<SpinOperation*>(currOperationBytePosition[i]);
            SpinsPtr->populate_thermal(op->Params_real[0],op->Params_real[1],op->Params_real[2],op->Params_real[3]);
        }
        else if (OperationIdentifier == OPERATION__TIP_SPINS) {
            SpinOperation* op;
            op = static_cast<SpinOperation*>(currOperationBytePosition[i]);
            SpinsPtr->tipSpins(static_cast<int>(op->Params_int[0]),op->Params_real[0]);
        }
        else if (OperationIdentifier == OPERATION__SET_HAMILTONIAN) {
            SpinOperation* op;
            op = static_cast<SpinOperation*>(currOperationBytePosition[i]);
            SpinsPtr->calculateHamiltonians(op->Params_real[0],op->Params_real[1],op->Params_real[2]);
        }
        else if (OperationIdentifier == OPERATION__EVOLVE_TIME_INDEPENDENT) {
            SpinOperation* op;
            op = static_cast<SpinOperation*>(currOperationBytePosition[i]);
            std::vector<Real> trVals = SpinsPtr->solveMagnetization_parallel(op->Params_int[0],static_cast<int>(op->Params_int[1]));
            std::copy(trVals.begin(), trVals.end(), output + currentDataPoint);
            currentDataPoint += op->Params_int[0];
        }
        else if (OperationIdentifier == OPERATION__INIT_TIME_INDEPENDENT_EVOLUTION) {
            SpinOperation* op;
            op = static_cast<SpinOperation*>(currOperationBytePosition[i]);
            SpinsPtr->initTimeIndependentEvolution(op->Params_real[0],static_cast<int>(op->Params_int[0]));
        }
        else if (OperationIdentifier == OPERATION__EVOLVE_TIME_DEPENDENT) {
            _do_operation_evolve_time_dependent(static_cast<SpinOperation*>(currOperationBytePosition[i]), SpinsPtr, output, currentDataPoint);
        }
        else {
            std::cout << "Error: An unknown operation was requested... Exiting." << std::endl;
            exit(1);
        }
    }
    return currentDataPoint;
}

extern "C" long CreateSignalFromInitializedSimulator(void* SpinSystemObject, void** ops, int numOfOperations, long numOfPoints, double* output)
{
    try {
        if(debug)
        {
            std::cout<<"Number of operations: "<<numOfOperations<<std::endl;
            std::cout<<"Number of points: "<<numOfPoints<<std::endl;
        }

        long processedNumOfPoints = ExecuteOperations(SpinSystemObject,ops,numOfOperations,output,debug);
        if (debug) std::cout<<processedNumOfPoints<<std::endl;
        return processedNumOfPoints;
    }
    catch(std::exception& ex) {
        std::cerr << "An exception was thrown by Spintrum. It says: " << ex.what() << std::endl;
        return 0;
    }
    catch(...) {
        std::cerr << "An unknown exception was thrown by Spintrum. " << std::endl;
        return 0;
    }
}
void _fill_spin_info(int numOfSpins, double* gammas, double* jCouplings, int* spinMultiplicities, SpinInfo& spinInfo)
{
    spinInfo.gammas.resize(numOfSpins,1);
    spinInfo.multiplicities.resize(numOfSpins,1);
    spinInfo.jCouplings.resize(numOfSpins,numOfSpins);
    for(int i = 0; i < numOfSpins; i++) {
        spinInfo.gammas[i] = gammas[i];
    }
    if(spinMultiplicities == 0) {
        for(int i = 0; i < numOfSpins; i++)
            spinInfo.multiplicities[i] = 2;
    }
    else {
        for(int i = 0; i < numOfSpins; i++)
            spinInfo.multiplicities[i] = spinMultiplicities[i];
    }
    if(debug) std::cout<<"Gammas:"<<std::endl;
    if(debug) std::cout<<spinInfo.gammas<<std::endl;
    devectorizeJCouplings(numOfSpins, jCouplings, spinInfo.jCouplings);
    if(debug) std::cout<<"J-couplings:"<<std::endl;
    if(debug) std::cout<<spinInfo.jCouplings<<std::endl;
}

extern "C" long SimulateSignal(void** ops, double* gammas, double* jCouplings, int* spinMultiplicities, int numOfSpins, int numOfOperations, long numOfPoints, double* output)
{
    try {
        print_software_info();
        if(debug) {
            std::cout<<"Number of operations: "<<numOfOperations<<std::endl;
            std::cout<<"Number of points: "<<numOfPoints<<std::endl;
            std::cout<<"Number of spins: "<<numOfSpins<<std::endl;
        }
        SpinInfo spinInfo;
        _fill_spin_info(numOfSpins, gammas, jCouplings, spinMultiplicities, spinInfo);
        SpinSystem<Real,std::complex<Real>,VEC,CX_VEC,MAT,CX_MAT > Spins(spinInfo.gammas,spinInfo.jCouplings,spinInfo.multiplicities);
        long processedNumOfPoints = ExecuteOperations(static_cast<void*>(&Spins),ops,numOfOperations,output,debug);
        if (debug) std::cout<<processedNumOfPoints<<std::endl;
        return processedNumOfPoints;
    }
    catch(std::exception& ex) {
        std::cerr << "An exception was thrown by Spintrum. It says: " << ex.what() << std::endl;
        return 0;
    }
    catch(...) {
        std::cerr << "An unknown exception was thrown by Spintrum. " << std::endl;
        return 0;
    }
}
