#include <Rcpp.h>           // R to C++ binding library
#include <cmath>            // `std::exp`
#include <iostream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <bitset>           // mainly for debugging reasons

#include "types.h"          // MVBinary (Multivariate Binary Data)
#include "bit_utils.h"      // uint32_t, ... and the `bit*` functions
#include "int_utils.h"      // isqrt, ilog, ...

#include "threadPool.h"

/******************************************************************************/
/***                              Ising model                               ***/
/******************************************************************************/
/**
 * The Ising model (as a special case of the Multi-Variate Bernoulli) has its
 * probability mass function (pmf) for a `p` dim. binary vector `y` defined as
 *
 *      P(Y = y | theta) = p_0(theta) exp(T(y)' theta)
 *
 * with the parameter vector `theta` and a statistic `T` of `y`. The real valued
 * parameter vector `theta` is of dimension `p (p + 1) / 2` and the statistic
 * `T` has the same dimensions as the parameter vector given by
 *
 *      T(y) = vech(y y').
 *
 * TODO: continue
 */


// [[Rcpp::export(rng = false)]]
double ising_log_odds_sum(uint32_t y, const VechView& theta) {
    // Collects the result `T(theta)' y` (basically the sum of the log odds in
    // the parameter vector `theta` with a single or two way interaction in `y`
    double log_odds = 0.0;

    // Iterate over all bits in the event `y`
    for (; y; y &= y - 1) {
        // get LSB index
        int i = bitScanLS(y);
        // the single effect index in the parameter vector `theta`
        int base_index = (i * (2 * theta.dim() + 1 - i)) / 2;
        // add single effect log odds
        log_odds += theta[base_index];
        // For all (remaining) other effects add the two way interaction odds
        for (uint32_t b = y & (y - 1); b; b &= b - 1) {
            log_odds += theta[base_index + bitScanLS(b) - i];
        }

    }

    return log_odds;
}

//' Ising model scaling factor `p_0(theta)` for the Ising model
//'
// [[Rcpp::export(rng = false)]]
double ising_prob0(const VechView& theta) {
    // Value of the event `(1, ..., 1)`
    const uint32_t max_event = (static_cast<uint64_t>(1) << theta.dim()) - 1;
    // Accumulates `p_0(theta)^-1`
    double sum_0 = 1.0;

    // sum up all event (except `(0, .., 0)`, considured as initial value `1`)
    for (uint32_t a = 1; a <= max_event; ++a) {
        sum_0 += exp(ising_log_odds_sum(a, theta));
    }

    // scaling factor `p_0(theta) = sum_0^-1`
    return 1.0 / sum_0;
}

//' Ising model probabilities for every event, this returns a vector of size `2^p`
//' with indices corresponding to the events binary representation.
//'
//' Note: the R indexing leads to adding +1 to every index.
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_probs(const VechView& theta) {
    // setup probability vector
    Rcpp::NumericVector probs(1 << theta.dim());

    // Value of the event `(1, ..., 1)`
    const uint32_t max_event = (static_cast<uint64_t>(1) << theta.dim()) - 1;
    // Accumulates `p_0(theta)^-1`
    double sum_0 = 1.0;

    // set prob for the zero event to `1`, scaled later with all the other events
    probs[0] = 1.0;

    // sum up all event (except `(0, .., 0)`, considured as initial value `1`)
    for (uint32_t a = 1; a <= max_event; ++a) {
        // set and accumulate (unscaled) probabilites
        sum_0 += (probs[a] = exp(ising_log_odds_sum(a, theta)));
    }

    // finish scaling factor calculation `p_0(theta) = 1 / sum_0`
    double prob_0 = 1.0 / sum_0;

    // scale probabilites
    for (auto& prob : probs) {
        prob *= prob_0;
    }

    return probs;
}

//' Computes the zero conditioned probabilities
//'      P(Y_i = 1 | Y_-i = 0)
//' and
//'      P(Y_i = 1, Y_j = 1 | Y_-i = 0)
//' from the natural parameters `theta` with matching indexing.
//'
//' This is the inverse function of `ising_theta_from_cond_prob`
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_cond_probs(const VechView& theta) {
    // setup result of the same size as theta
    Rcpp::NumericVector pi(theta.size());

    // set random variable dimension
    std::size_t p = theta.dim();

    // iterate all single effects
    for (std::size_t i = 0; i < p; ++i) {
        // compute probs for single effects
        std::size_t base_index = (i * (2 * p + 1 - i)) / 2;
        double exp_i = exp(theta[base_index]);
        // the probability `P(Y_i = 1 | Y_-i = 0)`
        pi[base_index] = exp_i / (1 + exp_i);
        // iterate over bigger indexed components interacting with the `i`th one
        for (std::size_t j = i + 1; j < p; ++j) {
            // again compute exp(theta_j)
            double exp_j = exp(theta[(j * (2 * p + 1 - j)) / 2]);
            // as well as the two way exponent
            double exp_ij = exp(theta[base_index + (j - i)]);
            // two way consitional probability `P(Y_i = Y_j = 1 | Y_-i,-j = 0)`
            pi[base_index + (j - i)] = (exp_i * exp_j * exp_ij)
                / (1.0 + exp_i + exp_j + exp_i * exp_j * exp_ij);
        }
    }

    return pi;
}

//' Computes the expectation of `Y` under the Ising model with natural parameter
//' `theta` given component wise by
//'
//'      E Y_i = P(Y_i = 1)
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_expectation(const VechView& theta) {
    const std::size_t p = theta.dim();  // dim of `Y`
    const uint32_t max_event = static_cast<uint32_t>(-1) >> (32 - p);
    double sum_0 = 1.0;                 // accumulates `p_0(theta)^-1`
    Rcpp::NumericVector mu(p, 0.0);     // `mu = E Y`

    // iterate all 2^p events (except the zero event)
    for (uint32_t y = 1; y <= max_event; ++y) {
        // dot product `vech(y y')' theta` for current event
        double dot = 0;
        // iterate all bits in `y`
        for (uint32_t a = y; a; a &= a - 1) {
            int i = bitScanLS(a);
            int base_index = (i * (2 * p + 1 - i)) / 2;
            // add single effects of `y`
            dot += theta[base_index];
            // iterate over all (higher indexed) interactions and add then
            for (uint32_t b = a & (a - 1); b; b &= b - 1) {
                dot += theta[base_index + bitScanLS(b) - i];
            }
        }
        // compute (unscaled) event `y` probability `exp(T(y)' theta)`
        double prob_y = exp(dot);
        sum_0 += prob_y;

        // add current (unscalled) probability to all components of set bits
        for (uint32_t a = y; a; a &= a - 1) {
            mu[bitScanLS(a)] += prob_y;
        }
    }

    // finalize `E[Y]` by scaling with `p_0 = sum_0^-1`
    double prob_0 = 1.0 / sum_0;
    for (auto& mui : mu) {
        mui *= prob_0;
    }

    return mu;
}

//' Computes the covariance (second centered moment) of `Y` under the Ising model
//' with natural parameter `theta`.
//'
//'      cov(Y, Y) = E[Y Y'] - E[Y] E[Y]'
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericMatrix ising_cov(const VechView& theta) {
    const std::size_t p = theta.dim();  // dim of `Y`
    const uint32_t max_event = static_cast<uint32_t>(-1) >> (32 - p);
    double sum_0 = 1.0;                 // accumulates `p_0(theta)^-1`
    Rcpp::NumericMatrix cov(p, p);

    // iterate over all 2^p events (except the zero event)
    for (std::size_t y = 1; y <= max_event; ++y) {
        // dot product `vech(y y')' theta` for current event
        double dot = 0;
        // iterate all bits in `y`
        for (uint32_t a = y; a; a &= a - 1) {
            int i = bitScanLS(a);
            int base_index = (i * (2 * p + 1 - i)) / 2;
            // add single effects of `y`
            dot += theta[base_index];
            // iterate over all (higher indexed) interactions and add then
            for (uint32_t b = a & (a - 1); b; b &= b - 1) {
                dot += theta[base_index + bitScanLS(b) - i];
            }
        }
        // compute (unscaled) event `y` probability `exp(T(y)' theta)`
        double prob_y = exp(dot);
        sum_0 += prob_y;

        // sum E[Y, Y'] but still unscaled
        for (uint32_t a = y; a; a &= a - 1) {
            int i = bitScanLS(a);
            for (uint32_t b = y; b; b &= b - 1) {
                int j = bitScanLS(b);
                cov[i * p + j] += prob_y;
            }
        }
    }

    // finish computing `E[Y Y']` by scaling with `p_0 = sum_0^-1`
    double prob_0 = 1.0 / sum_0;
    for (auto& covij : cov) {
        covij *= prob_0;
    }

    // subtract outer product of expectation `-= E[Y] E[Y]'`
    const auto mu = ising_expectation(theta); // `mu = E[Y]`
    for (std::size_t j = 0; j < p; ++j) {
        for (std::size_t i = 0; i < p; ++i) {
            cov[j * p + i] -= mu[i] * mu[j];
        }
    }

    return cov;
}

//' Computes the single and two way effect marginal probabilities
//'
//'      P(Y_i = 1)
//' and
//'      P(Y_i Y_j = 1)
//'
//' In its vectorized form this function computes E[vech(Y Y')]
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_marginal_probs(const VechView& theta) {
    // Step 0: Setup (and validate) variables/parameters
    const std::size_t p = theta.dim();
    if (p != theta.dim()) {
        Rcpp::stop("Parameter dimension does not match data dimension");
    }
    const uint32_t max_event = static_cast<uint32_t>(-1) >> (32 - p);
    double sum_0 = 1.0;  // accumulates p_0(theta)^-1
    Rcpp::NumericVector score(theta.size(), 0.0); // grad l(theta)

    // Step 1: Calc `-n E[vech(Y Y')]` where the sum over `y` is the sum over
    // all possible events `y in {0, 1}^p`.
    for (uint32_t y = 1; y <= max_event; ++y) {
        // sum of `T(y)' theta` for current instance `y`
        double log_odds = 0;
        // iterate all bits in `y`
        for (uint32_t a = y; a; a &= a - 1) {
            int i = bitScanLS(a);
            int base_index = (i * (2 * p + 1 - i)) / 2;
            // add single effects of `y`
            log_odds += theta[base_index];
            // iterate over all (higher indexed) interactions and add then
            for (uint32_t b = a & (a - 1); b; b &= b - 1) {
                log_odds += theta[base_index + bitScanLS(b) - i];
            }
        }
        // compute (unscaled) event `y` probability `exp(T(y)' theta)`
        double prob_y = exp(log_odds);
        sum_0 += prob_y;

        // at this point we know the (unscaled) probability of the event `y`
        // which needs to be added to the gradient at the set bit positions
        // of `y` corresponding to single and interaction effects
        for (uint32_t a = y; a; a &= a - 1) {
            int i = bitScanLS(a);
            int base_index = (i * (2 * p + 1 - i)) / 2;
            score[base_index] += prob_y;
            for (uint32_t b = a & (a - 1); b; b &= b - 1) {
                score[base_index + bitScanLS(b) - i] += prob_y;
            }
        }
    }
    // finalize `-E[vech(Y Y')]` by scaling with `-p_0 = -sum_0^-1`
    double prob_0 = 1.0 / sum_0;
    for (auto& s : score) {
        s *= prob_0;
    }

    return score;
}

//' Natural parameters from the sufficient conditional probability statistic `pi`
//'
//' Computes the natural parameters `theta` of the Ising model from zero
//' conditioned probabilites for single and two way effects.
//'
//' This is the inverse function of `ising_cond_prob_from_theta`
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_theta_from_cond_prob(const VechView& pi) {
    // initialize natural parameters vector theta
    Rcpp::NumericVector theta(pi.size());

    // check if given probabilities are in the ragne [0, 1]
    if (std::any_of(pi.begin(), pi.end(), [](const double prob) {
        return (prob < 0.0) || (1.0 < prob);
    })) {
        Rcpp::stop("`pi` must contain only elements in the range [0, 1]");
    }

    // get random variable dimension
    std::size_t p = pi.dim();

    // iterate all single effects
    for (std::size_t i = 0; i < p; ++i) {
        // compute single effect theta_i
        std::size_t base_index = (i * (2 * p + 1 - i)) / 2;
        theta[base_index] = log(pi[base_index] / (1.0 - pi[base_index]));
        // iterate all higher indexed interactions with the currecnt effect
        for (std::size_t j = i + 1; j < p; ++j) {
            theta[base_index + (j - i)] = log(
                ((1 - pi(i) * pi(j)) * pi(i, j)) / (pi(i) * pi(j) * (1 - pi(i, j)))
            );
        }
    }

    return theta;
}

//' Computes the log-likelihood at natural parameters `theta` of the Ising model
//' given a set of observations `Y`
//'
//'      l(theta) = log(p_0(theta)) + n^-1 sum_i vech(y_i y_i')' theta
//'
// [[Rcpp::export(rng = false)]]
double ising_log_likelihood(const VechView& theta, const MVBinary& Y) {
    // sum the log odds `sum_i vech(y_i y_i')' theta`
    double sum = 0.0;
    for (const uint32_t y : Y) {
        sum += ising_log_odds_sum(y, theta);
    }

    // add scaling factor `log(p_0(theta))`
    return log(ising_prob0(theta)) + (sum / static_cast<double>(Y.size()));
}

//' Computes the Score of the Ising model, this is the gradiend of the (mean)
//' log-likelihood with respect to the natural parameters
//'
//'      grad l(theta) = -E[vech(Y Y')] + n^-1 sum_i vech(y_i y_i')
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_score(const VechView& theta, const MVBinary& Y) {
    const std::size_t p = theta.dim();

    // Step 1: compute -E[vech(Y Y')] (data independent part)
    auto score = ising_marginal_probs(theta);
    for (auto& s : score) {
        s *= -1.0;
    }

    // Step 2: Add data dependend part `mean_i vech(y_i y_i')`
    const double n_inv = 1.0 / static_cast<double>(Y.size());
    for (const uint32_t y : Y) {
        // start by iterating the single effects in `y` (LSB)
        for (uint32_t a = y; a; a &= a - 1) {
            int i = bitScanLS(a);
            int base_index = (i * (2 * p + 1 - i)) / 2;
            // add single effects
            score[base_index] += n_inv;
            // and the two way interaction effects
            for (uint32_t b = a & (a - 1); b; b &= b - 1) {
                score[base_index + bitScanLS(b) - i] += n_inv;
            }
        }
    }

    return score;
}

/**
 * Overload of `ising_score` for a single observation `y`
 */
Rcpp::NumericVector ising_score(const VechView& theta,
    const uint32_t y, const std::size_t p
) {
    // Step 1: compute -E[vech(Y Y')] (data independent part)
    auto score = ising_marginal_probs(theta);
    for (auto& s : score) {
        s *= -1.0;
    }

    // Step 2: Add data dependend part `vech(y y')`
    // start by iterating the single effects in `y` (LSB)
    for (uint32_t a = y; a; a &= a - 1) {
        int i = bitScanLS(a);
        int base_index = (i * (2 * p + 1 - i)) / 2;
        // add single effects
        score[base_index] += 1.0;
        // and the two way interaction effects
        for (uint32_t b = a & (a - 1); b; b &= b - 1) {
            score[base_index + bitScanLS(b) - i] += 1.0;
        }
    }

    return score;
}

/**
 * Computes the log-likelihood at natural parameters `theta` of the Ising model
 * given a set of observations `Y`
 *
 *      l(alpha) = n^-1 sum_i (log(p_0(alpha, x_i)) + (x_i' alpha y_i)^2)
 *               = n^-1 sum_i (log(p_0(theta_alpha(x_i))) + vech(y_i y_i')'theta_alpha(x_i))
 */
// [[Rcpp::export(rng = false)]]
double ising_conditional_log_likelihood(const Rcpp::NumericMatrix& alpha,
    const Rcpp::NumericMatrix& X, const MVBinary& Y
) {
    // get probem dimensions
    const std::size_t p = alpha.nrow();
    const std::size_t q = alpha.ncol();

    // check parameter dimensions
    if (X.nrow() != Y.size() || X.ncol() != p || Y.dim() != q) {
        Rcpp::stop("Parameter dimension missmatch");
    }

    // natural parameter for the conditional Ising model
    //      `theta_alpha(x) = vech((2 1_q 1_q' - I_q) o Theta_alpha(x))`
    // with `o` denoting the Hadamart product.
    VechView theta(q);
    // temp inbetween variables
    Rcpp::NumericVector z(q);

    // sum over all observations
    double ll = 0.0;
    for (std::size_t i = 0; i < Y.size(); ++i) {
        // get i'th observation (response `y` and predictor `x`)
        const uint32_t y = Y[i];
        const auto x = X.row(i);

        // compute natural parameter from `alpha` with current predictor `x`
        // First `z = alpha' x_i` and `s = y_i' alpha' x_i`
        double s = 0.0;
        for (std::size_t j = 0; j < q; ++j) {
            z[j] = 0.0;
            for (std::size_t k = 0; k < p; ++k) {
                z[j] += x[k] * alpha[j * p + k];
            }
            s += static_cast<bool>(y & (1 << j)) * z[j];
        }
        // and then `vech` of the outer product `z z'` with off diagonal
        // elements multiplied by `2`. (See: `Theta` to `theta` relation)
        // Explicitly (`theta = theta_alpha(x)`):
        //      `theta = vech((2 1_q 1_q' - I_q) o (alpha' x x' alpha))`
        // where `o` is the Hadamard pdoruct.
        for (std::size_t j = 0; j < q; ++j) {
            theta(j, j) = z[j] * z[j];
            for (std::size_t k = j + 1; k < q; ++k) {
                theta(j, k) = 2.0 * z[j] * z[k];
            }
        }

        // add to log-lilelihood sum
        ll += log(ising_prob0(theta)) + s * s;
    }

    return ll / static_cast<double>(Y.size());
}

/**
 * Comutes the Score for the conditional Ising model
 *
 *      P(Y = y | X = x) = p_0(alpha) exp(y' Theta_alpha(x) y)
 *
 * with the parameter relation
 *
 *      Theta_alpha(x) = alpha' x x' alpha.
 *
 * The computed Score has the form
 *
 *      grad l(alpha) =
 *          2 n^-1 sum_i x_i x_i' alpha (-E[vec(Y Y') | X = x_i] + vec(y_i y_i'))
 */
// [[Rcpp::export(rng = false)]]
Rcpp::NumericVector ising_conditional_score(const Rcpp::NumericMatrix& alpha,
    const Rcpp::NumericMatrix& X, const MVBinary& Y
) {
    // get probem dimensions
    const std::size_t p = alpha.nrow();
    const std::size_t q = alpha.ncol();

    // check parameter dimensions
    if (X.nrow() != Y.size() || X.ncol() != p || Y.dim() != q) {
        Rcpp::stop("Parameter dimension missmatch");
    }

    // setup the Score (default zero initialized)
    Rcpp::NumericMatrix score(p, q);
    // natural parameter for the conditional Ising model
    //      `theta_alpha(x) = vech((2 1_q 1_q' - I_q) o Theta_alpha(x))`
    // with `o` denoting the Hadamart product.
    VechView theta(q);
    // temp inbetween variables
    Rcpp::NumericVector z(q);

    // for each observation (iter observation index i = 0, ..., n - 1)
    for (std::size_t i = 0; i < Y.size(); ++i) {
        // get i'th observation (response `y` and predictor `x`)
        const uint32_t y = Y[i];
        const auto x = X.row(i);

        // compute natural parameter from `alpha` with current predictor `x`
        // First `z = alpha' x`
        for (std::size_t j = 0; j < q; ++j) {
            z[j] = 0.0;
            for (std::size_t k = 0; k < p; ++k) {
                z[j] += x[k] * alpha[j * p + k];
            }
        }
        // and then `vech` of the outer product `z z'` with off diagonal
        // elements multiplied by `2`. (See: `Theta` to `theta` relation)
        // Explicitly (`theta = theta_alpha(x)`):
        //      `theta = vech((2 1_q 1_q' - I_q) o (alpha' x x' alpha))`
        // where `o` is the Hadamard pdoruct.
        for (std::size_t j = 0; j < q; ++j) {
            theta(j, j) = z[j] * z[j];
            for (std::size_t k = j + 1; k < q; ++k) {
                theta(j, k) = 2.0 * z[j] * z[k];
            }
        }

        // With `theta_alpha(x)` compute the classic Ising model Score at
        // `theta = theta_alpha(x)` for a single observation `y`.
        //      `S = grad_theta l`
        const auto S = ising_score(theta, y, q);

        // convert classic ising model Score to conditional Ising model Score
        // by using:
        //      `grad_alpha l = 2 x x' alpha vec^-1(D_q grad_theta l)`
        // where `D_q` is the duplication matrix.
        // For each column of `grad_alpha l`
        for (std::size_t k = 0; k < q; ++k) {
            // compute the `k`th vector matrix product `(z' S)_k` component
            double zS_k = 0.0;
            for (std::size_t l = 0; l < q; ++l) {
                zS_k += z[l] * S[theta.index(std::min(k, l), std::max(k, l))];
            }
            // and now add the `k`th column `x (z' S)_k` to the Score
            for (std::size_t j = 0; j < p; ++j) {
                score[k * p + j] += x[j] * zS_k;
            }
        }
    }

    return (2.0 / static_cast<double>(X.nrow())) * score;
}

// TODO: Drop or rewrite to test/fix issue with R/Rcpp multithreading issue,
//       Apparently I can NOT use Rcpp in conjunction with multiple threads
//       as the garbage collector stack gets out of sync.
// SEE: https://stackoverflow.com/questions/42119609/how-to-handle-mismatching-in-calling-convention-in-r-and-c-using-rcpp
//
// NOTE: The below algorithm implements the _old_ `theta` to `eta` relation
//
// // [[Rcpp::export(rng = false)]]
// Rcpp::NumericVector ising_conditional_score_mt(const Rcpp::NumericMatrix& alpha,
//     const Rcpp::NumericMatrix& X, const MVBinary& Y
// ) {
//     // get probem dimensions
//     const std::size_t p = alpha.nrow();
//     const std::size_t q = alpha.ncol();

//     // check parameter dimensions
//     if (X.nrow() != Y.size() || X.ncol() != p || Y.dim() != q) {
//         Rcpp::stop("Parameter dimension missmatch");
//     }

//     // setup the Score (default zero initialized)
//     Rcpp::NumericMatrix score(p, q);

//     // setup a thread pool to perform per observation computations in parallel
//     ThreadPool pool;

//     // for each observation setup a task to compute the Score this observation
//     // Score and then add them up
//     for (std::size_t i = 0; i < Y.size(); ++i) {
//         // push task to the thread pool, they are executed as soon as possible
//         pool.push([&alpha, &X, &Y, i, p, q, /*out*/ &score]() {

//             // get i'th observation (response `y` and predictor `x`)
//             const uint32_t y = Y[i];
//             const auto x = X.row(i);

//             // natural parameter for the conditional Ising model
//             //      `theta_alpha(x) = vech((2 1_q 1_q' - I_q) o Theta_alpha(x))`
//             // with `o` denoting the Hadamart product.
//             VechView theta(q);
//             // temp inbetween variables
//             Rcpp::NumericVector z(q);

//             // compute natural parameter from `alpha` with current predictor `x`
//             // First `z = alpha' x`
//             for (std::size_t j = 0; j < q; ++j) {
//                 z[j] = 0.0;
//                 for (std::size_t k = 0; k < p; ++k) {
//                     z[j] += x[k] * alpha[j * p + k];
//                 }
//             }
//             // and then `vech` of the outer product `z z'` with off diagonal
//             // elements multiplied by `2`. (See: `Theta` to `theta` relation)
//             // Explicitly (`theta = theta_alpha(x)`):
//             //      `theta = vech((2 1_q 1_q' - I_q) o (alpha' x x' alpha))`
//             // where `o` is the Hadamard pdoruct.
//             for (std::size_t j = 0; j < q; ++j) {
//                 theta(j, j) = z[j] * z[j];
//                 for (std::size_t k = j + 1; k < q; ++k) {
//                     theta(j, k) = 2.0 * z[j] * z[k];
//                 }
//             }

//             // With `theta_alpha(x)` compute the classic Ising model Score at
//             // `theta = theta_alpha(x)` for a single observation `y`.
//             //      `S = grad_theta l`
//             const auto S = ising_score(theta, y, q);

//             // convert classic ising model Score to conditional Ising model Score
//             // by using:
//             //      `grad_alpha l = 2 x x' alpha vec^-1(D_q grad_theta l)`
//             // where `D_q` is the duplication matrix.
//             // For each column of `grad_alpha l`
//             for (std::size_t k = 0; k < q; ++k) {
//                 // compute the `k`th vector matrix product `(z' S)_k` component
//                 double zS_k = 0.0;
//                 for (std::size_t l = 0; l < q; ++l) {
//                     zS_k += z[l] * S[theta.index(std::min(k, l), std::max(k, l))];
//                 }
//                 // and now add the `k`th column `x (z' S)_k` to the Score
//                 // Add to output score, `+=` should be atomic (TODO: check this!!!)
//                 for (std::size_t j = 0; j < p; ++j) {
//                     score[k * p + j] += x[j] * zS_k;
//                 }
//             }
//         });
//     }

//     return (2.0 / static_cast<double>(X.nrow())) * score;
// }


//' Fisher information for the natural parameters `theta` under the Ising model.
//'
//'      I(theta) = -E(H l(theta) | theta) = Cov(vech(Y Y'), vech(Y Y') | theta)
//'
//' where `H l(theta)` is the Hessian of the log-likelihood `l(theta)` defined as
//'
//'      l(theta) = n^-1 prod_i P(Y = y_i | theta)
//'               = log(p_0(theta)) - mean_i exp(vech(y_i y_i')' theta)
//'
//' Note that the Fisher information does _not_ depend on data.
//'
// [[Rcpp::export(rng = false)]]
Rcpp::NumericMatrix ising_fisher_info(const VechView& theta) {

    // Step 0: Setup (and validate) variables/parameters
    const std::size_t p = theta.dim();
    const std::size_t pp = p * (p + 1) / 2;
    const uint32_t max_event = static_cast<uint32_t>(-1) >> (32 - p);
    double sum_0 = 1.0;  // accumulates p_0(theta)^-1
    // Initialize result covariance matrix (zero initialized)
    Rcpp::NumericMatrix cov(pp, pp);

    // Compute (unscaled) lower triag part of `E(vech(Y Y') vech(Y Y')' | theta)`
    for (uint32_t y = 1; y <= max_event; ++y) {
        // Compute (unscaled) probability `P(Y = y | theta)`
        const double prob = exp(ising_log_odds_sum(y, theta));
        // and add to scaling factor accumulator
        sum_0 += prob;

        // Iterate Fisher information column indices (set bits in vech(y y'))
        // Those are the LSB lower triag indices of the `a, b` LSBs
        for (uint32_t a = y; a; a &= a - 1) {
            const std::size_t i = bitScanLS(a);
            const std::size_t ti = (i * (2 * p + 1 - i)) / 2;

            for (uint32_t b = a; b; b &= b - 1) {
                // Fisher info column index
                const std::size_t col = ti + (bitScanLS(b) - i);

                // Fill (lower triangular) Fisher information row positions
                // by computing the remaining (higher indexed) of vech(y y')
                // via the LSB bits of `c, d`
                // TODO: Slight inaccuracy with doing a bit to much work
                for (uint32_t c = a; c; c &= c - 1) {
                    const std::size_t j = bitScanLS(c);
                    const std::size_t tj = (j * (2 * p + 1 - j)) / 2;

                    for (uint32_t d = c; d; d &= d - 1) {
                        // Fisher info row index (lower triag part)
                        const std::size_t row = tj + (bitScanLS(d) - j);

                        // add (unscaled) probability for current event effects
                        cov[row + pp * col] += prob;
                    }
                }
            }
        }
    }

    // finish scaling factor
    const double p_0 = 1.0 / sum_0;

    // Scale and mirrow lower triag part of `E(vech(Y Y') vech(Y Y')' | theta)`
    for (std::size_t col = 0; col < pp; col++) {
        for (std::size_t row = col; row < pp; ++row) {
            cov[col + pp * row] = (cov[row + pp * col] *= p_0);
        }
    }

    // Subtract outer product of the expectation
    // `cov -= E(vech(Y Y') | theta) E(vech(Y Y') | theta)'`
    auto score = ising_marginal_probs(theta);
    std::transform(cov.begin(), cov.end(),
        Rcpp::outer(score, score, std::multiplies<double>()).begin(),
        cov.begin(), std::minus<double>());

    return cov;
}

//' Samples from the Ising model given the natural parameters `theta`
//'
// [[Rcpp::export(rng = true)]]
MVBinary ising_sample(const std::size_t n, const VechView& theta,
    const std::size_t warmup = 1000
) {
    const std::size_t p = theta.dim();
    const std::size_t max_event = static_cast<uint32_t>(-1) >> (32 - p);

    // for small dimensions we use Rcpp::sample
    if (p < 5) {
        // setup complete probability vector for every possible event
        const auto probs = ising_probs(theta);

        // sample from all possible events given probabilities for each
        const auto events = Rcpp::sample(1 << p, n, true, probs, false);

        return MVBinary(events.begin(), events.end(), p);
    }
    // Else: "non trivial implementation" case: Gibbs sampling

    // RNG for continuous uniform `U[0, 1]`
    auto runif = Rcpp::stats::UnifGenerator(0, 1);

    // conditional probability `P(Y_i = 1 | Y_-i = y_-i) = exp_i / (1 + exp_i)`
    // where `exp_i = exp(theta_i + sum_{j != i} y_j theta_{i,j})`
    auto icond_prob = [&theta](const std::size_t i, const uint32_t y) {
        double log_odds = theta(i);
        for (uint32_t a = y & ~(static_cast<uint32_t>(1) << i); a; a &= a - 1) {
            const std::size_t j = bitScanLS(a);
            log_odds += theta(std::min(i, j), std::max(i, j));
        }
        return 1.0 / (1.0 + exp(-log_odds));
    };

    // Reserve result data set
    MVBinary events(n, p);

    // repeat untill `n` samples are drawn
    while (events.size() < n) {
        // Initialize sample as vector of independent bernoulli draws with
        // component wise probability `P(Y_i = 1 | Y_-i = 0)`
        uint32_t y = 0;
        for (std::size_t i = 0; i < p; ++i) {
            y |= static_cast<uint32_t>((runif() * (1.0 + exp(-theta(i)))) < 1.0) << i;
        }

        // repeated cyclic updating
        for (std::size_t rep = 0; rep < warmup; ++rep) {
            for (std::size_t i = 0; i < p; ++i) {
                // event with the `i`th component `1` and all others `0`
                const uint32_t ei = static_cast<uint32_t>(1) << i;
                // sample `i`th bit from `Bernoulli(P(Y_i = 1 | Y_-i = y_-i))`
                y = (y & ~ei) | (ei * (runif() < icond_prob(i, y)));
            }
        }

        // add generated sample to result set
        events.push_back(y);
    }

    return events;
}
