/** @file monte_carlo.cpp
 *
 *  @author Cory Alexander Balaton (coryab)
 *  @author Janita Ovidie Sandtrøen Willumsen (janitaws)
 *
 *  @version 1.0
 *
 *  @brief Implementation of the monte carlo functions
 *
 *  @bug No known bugs
 * */
#include "monte_carlo.hpp"

uint test_2x2_lattice(double tol, uint max_cycles)
{
    data_t data, tmp;
    size_t L = 2;
    size_t n_spins = L * L;
    double T = 1.;
    size_t cycles = 0;

    // Create random engine using the mersenne twister
    std::random_device rd;
    std::mt19937 engine(rd());

    IsingModel test(L, T);

    double E, M;

    std::function<bool(double, double)> is_close = [tol](double a, double b) {
        return std::abs(a - b) < tol;
    };

    // Loop through cycles
    while (cycles++ < max_cycles) {
        data += test.Metropolis(engine);
        tmp = data / (cycles * n_spins);
        // if (close(EPS_2, tmp.E)
        //&& close(MAG_2, tmp.M)
        //&& close(CV_2, (tmp.E2 - tmp.E * tmp.E) / (T * T))
        //&& close(X_2, (tmp.M2 - tmp.M_abs * tmp.M_abs) / T)) {
        // return cycles;
        //}
        if (is_close(EPS_2, tmp.E) && is_close(MAG_2, tmp.M)) {
            return cycles;
        }
    }
    std::cout << "hello" << std::endl;
    return 0;
}

void monte_carlo_progression(double T, uint L, uint cycles,
                             const std::string filename)
{
    // Set some variables
    data_t data, tmp;
    uint n_spins = L * L;

    // File stuff
    std::string directory = utils::dirname(filename);
    std::ofstream ofile;

    // Create random engine using the mersenne twister
    std::random_device rd;
    std::mt19937 engine(rd());

    IsingModel ising(L, T);

    // Create path and open file
    utils::mkpath(directory);
    ofile.open(filename);

    // Loop through cycles
    for (size_t i = 1; i <= cycles; i++) {
        data += ising.Metropolis(engine);
        tmp = data / (i * n_spins);
        ofile << i << ',' << tmp.E << ',' << tmp.E2 << ',' << tmp.M << ','
              << tmp.M2 << ',' << tmp.M_abs << '\n';
    }
    ofile.close();
}

void pd_estimate(double T, uint L, uint cycles, const std::string filename)
{
    // Set some variables
    data_t data;
    uint n_spins = L * L;

    // File stuff
    std::string directory = utils::dirname(filename);
    std::ofstream ofile;

    // Create random engine using the mersenne twister
    std::random_device rd;
    std::mt19937 engine(rd());

    IsingModel ising(L, T);

    // Create path and open file
    utils::mkpath(directory);
    ofile.open(filename);

    double E, M;

    // Figure out bin widths and such
}

// Code for seeing phase transitions.
void monte_carlo_serial(data_t &data, uint L, double T, uint cycles)
{
    IsingModel model(L, T);
    // Create random engine using the mersenne twister
    std::random_device rd;
    std::mt19937 engine(rd());

    for (size_t i = 0; i < BURN_IN_TIME; i++) {
        model.Metropolis(engine);
    }

    for (size_t i = 0; i < cycles; i++) {
        data += model.Metropolis(engine);
    }
    data /= cycles;
}

void monte_carlo_parallel(data_t &data, uint L, double T, uint cycles)
{
#pragma omp parallel
    {
        // Each thread creates an instance of IsingModel.
        IsingModel model(L, T);
        // Each thread creates an instance of the mersenne twister
        std::random_device rd;
        std::mt19937 engine(rd());

        data_t tmp;

        // Each thread runs the Metropolis algorithm before starting to collect
        // samples
        for (size_t i = 0; i < BURN_IN_TIME; i++) {
            model.Metropolis(engine);
        }

        // Now each thread work on one loop together, but using their own
        // instances of things, but the total of cycles add up.
        // static ensure that each thread gets the same amount of iterations
#pragma omp for schedule(static)
        for (size_t i = 0; i < cycles; i++) {
            tmp = tmp + model.Metropolis(engine);
        }

        // Combine all the data.
#pragma omp critical
        {
            data += tmp;
        }
    }

    data /= cycles;
}

void phase_transition(
    uint L, double start_T, double end_T, uint points_T,
    std::function<void(data_t &, uint, double, uint)> monte_carlo,
    std::string outfile)
{
    double dt_T = (end_T - start_T) / points_T;
    uint cycles = 10000;
    uint N = L * L;
    std::ofstream ofile;

    data_t data[points_T];

    for (size_t i = 0; i < points_T; i++) {
        monte_carlo(data[i], L, start_T + dt_T * i, cycles);
    }

    utils::mkpath(utils::dirname(outfile));
    ofile.open(outfile);

    double temp, CV, X;

    using utils::scientific_format;
    for (size_t i = 0; i < points_T; i++) {
        temp = start_T + dt_T * i;
        CV = (data[i].E2 - data[i].E * data[i].E) / (N * temp * temp);
        X = (data[i].M2 - data[i].M_abs * data[i].M_abs) / (N * temp);

        ofile << scientific_format(temp) << ','
              << scientific_format(data[i].E / N) << ','
              << scientific_format(data[i].M_abs / N) << ','
              << scientific_format(CV) << ',' << scientific_format(X) << '\n';
    }
    ofile.close();
}
