#ifndef INCLUDE_GUARD_ISING_MCMC_H
#define INCLUDE_GUARD_ISING_MCMC_H

#include "R_api.h"

/** Sample a single value from the Ising model via a Monte-Carlo Markov-Chain
 * method given the Ising model parameters in compact half-vectorized form.
 *
 *      f(x) = p0(params) exp(vech(x x')' params)
 *
 * @important requires to be wrapped between `GetRNGstate()` and `PutRNGState()`
 * calls. For example
 *
 * GetRNGstate()
 *  for (size_t sample = 0; sample < nr_samples; ++sample) {
 *      ising_mcmc_vech(warmup, dim, dim, params, &X[sample * dim]);
 *  }
 * PutRNGState()
 */
static inline void ising_mcmc_vech(
    const size_t warmup,
    const size_t dim, const double* params,
    int* X
) {
    // Initialize elements `X_i ~ Bernoulli(P(X_i = 1 | X_-i = 0))`
    for (size_t i = 0, I = 0; i < dim; I += dim - i++) {
        double invProb = 1.0 + exp(-params[I]);
        X[i] = unif_rand() * invProb < 1.0;
    }

    // Skip the first samples of the Markov-Chain (warmup)
    for (size_t skip = 0; skip < warmup; ++skip) {
        // For every component
        for (size_t i = 0; i < dim; ++i) {
            // Compute conditional probability `P(X_i = 1 | X_-i = x_-i)`
            double log_odds = 0.0;
            // Tracks position in half-vectorized storage
            size_t J = i;
            // Sum all log-odds for `j < i` (two way interactions)
            for (size_t j = 0; j < i; J += dim - ++j) {
                log_odds += X[j] ? params[J] : 0.0;
            }
            // Allways add log-odds for `i = j` (self-interaction)
            log_odds += params[J];
            // Continue adding log-odds for `j > i` (two way interactions)
            J++;
            for (size_t j = i + 1; j < dim; ++j, ++J) {
                log_odds += X[j] ? params[J] : 0.0;
            }
            // Update `i`th element `X_i ~ Bernoulli(P(X_i = 1 | X_-i = x_-i))`
            X[i] = unif_rand() * (1.0 + exp(-log_odds)) < 1.0;
        }
    }
}

// Thread save version of `ising_mcmc` (using thread save PRNG)
static inline void ising_mcmc_vech_thrd(
    const size_t warmup,
    const size_t dim, const double* params,
    int* X, rng_seed_t seed
) {
    // Initialize elements `X_i ~ Bernoulli(P(X_i = 1 | X_-i = 0))`
    for (size_t i = 0, I = 0; i < dim; I += dim - i++) {
        double invProb = 1.0 + exp(-params[I]);
        X[i] = unif_rand_thrd(seed) * invProb < 1.0;
    }

    // Skip the first samples of the Markov-Chain (warmup)
    for (size_t skip = 0; skip < warmup; ++skip) {
        // For every component
        for (size_t i = 0; i < dim; ++i) {
            // Compute conditional probability `P(X_i = 1 | X_-i = x_-i)`
            double log_odds = 0.0;
            // Tracks position in half-vectorized storage
            size_t J = i;
            // Sum all log-odds for `j < i` (two way interactions)
            for (size_t j = 0; j < i; J += dim - ++j) {
                log_odds += X[j] ? params[J] : 0.0;
            }
            // Allways add log-odds for `i = j` (self-interaction)
            log_odds += params[J];
            // Continue adding log-odds for `j > i` (two way interactions)
            J++;
            for (size_t j = i + 1; j < dim; ++j, ++J) {
                log_odds += X[j] ? params[J] : 0.0;
            }
            // Update `i`th element `X_i ~ Bernoulli(P(X_i = 1 | X_-i = x_-i))`
            X[i] = unif_rand() * (1.0 + exp(-log_odds)) < 1.0;
        }
    }
}

/** Sample a single value from the Ising model via a Monte-Carlo Markov-Chain
 * method given the Ising model parameters in symmetric matrix form `A`.
 *
 *      f(x) = prob_0(A) exp(x' A x)
 *
 * The relation to the natural parameters `params` as used in the half-vectorized
 * version `ising_mcmc_vech()`
 *
 *      f(x) = prob_0(params) exp(vech(x x')' params)
 *
 * is illustrated via an example with `dim = 5`;
 *
 *          p0                             [ p0   p1/2 p2/2  p3/2  p4/2  ]
 *          p1 p5                          [ p1/2 p5   p6/2  p7/2  p8/2  ]
 * params = p2 p6 p9             =>    A = [ p2/2 p6/2 p9    p10/2 p11/2 ]
 *          p3 p7 p10 p12                  [ p3/2 p7/2 p10/2 p12   p13/2 ]
 *          p4 p8 p11 p13 p14              [ p4/2 p8/2 p11/2 p13/2 p14   ]
 *
 * or the other way arroung given the symmetric matrix form `A` converted to
 * the natural parameters
 *
 *     [ a00 a10 a20 a30 a40 ]                   a00
 *     [ a10 a11 a21 a31 a41 ]                   2*a10 a11
 * A = [ a20 a21 a22 a32 a42 ]    =>    params = 2*a20 2*a21 a22
 *     [ a30 a31 a32 a33 a43 ]                   2*a30 2*a31 2*a32 a33
 *     [ a40 a41 a42 a43 a44 ]                   2*a40 2*a41 2*a42 2*a43 a44
 *
 */
static inline void ising_mcmc_mat(
    const size_t warmup,
    const size_t dim, const size_t ldA, const double* A,
    int* X
) {
    // Initialize elements `X_i ~ Bernoulli(P(X_i = 1 | X_-i = 0))`
    for (size_t i = 0; i < dim; ++i) {
        X[i] = unif_rand() * (1.0 + exp(-A[i * (ldA + 1)])) < 1.0;
    }

    // Skip the first samples of the Markov-Chain (warmup)
    for (size_t skip = 0; skip < warmup; ++skip) {
        // For every component
        for (size_t i = 0; i < dim; ++i) {
            // Compute conditional probability `P(X_i = 1 | X_-i = x_-i)`
            double log_odds = 0.0;
            for (size_t j = 0; j < i; ++j) {
                log_odds += (2 * X[j]) * A[i * ldA + j];
            }
            log_odds += A[i * (ldA + 1)];
            for (size_t j = i + 1; j < dim; ++j) {
                log_odds += (2 * X[j]) * A[i * ldA + j];
            }
            // Update `i`th element `X_i ~ Bernoulli(P(X_i = 1 | X_-i = x_-i))`
            X[i] = unif_rand() * (1.0 + exp(-log_odds)) < 1.0;
        }
    }
}

// thread save version of `ising_mcmc_mat()` (using thread save PRNG)
static inline void ising_mcmc_mat_thrd(
    const size_t warmup,
    const size_t dim, const size_t ldA, const double* A,
    int* X, rng_seed_t seed
) {
    // Initialize elements `X_i ~ Bernoulli(P(X_i = 1 | X_-i = 0))`
    for (size_t i = 0; i < dim; ++i) {
        X[i] = unif_rand_thrd(seed) * (1.0 + exp(-A[i * (ldA + 1)])) < 1.0;
    }

    // Skip the first samples of the Markov-Chain (warmup)
    for (size_t skip = 0; skip < warmup; ++skip) {
        // For every component
        for (size_t i = 0; i < dim; ++i) {
            // Compute conditional probability `P(X_i = 1 | X_-i = x_-i)`
            double log_odds = 0.0;
            for (size_t j = 0; j < i; ++j) {
                log_odds += (2 * X[j]) * A[i * ldA + j];
            }
            log_odds += A[i * (ldA + 1)];
            for (size_t j = i + 1; j < dim; ++j) {
                log_odds += (2 * X[j]) * A[i * ldA + j];
            }
            // Update `i`th element `X_i ~ Bernoulli(P(X_i = 1 | X_-i = x_-i))`
            X[i] = unif_rand_thrd(seed) * (1.0 + exp(-log_odds)) < 1.0;
        }
    }
}


#endif /* INCLUDE_GUARD_ISING_MCMC_H */
