//
// Usage (bash):
// ~$ R -e "library(Rcpp); sourceCpp('runtime_tests_grad.cpp')"
//
// Usage (R):
// > library(Rcpp)
// > sourceCpp('runtime_tests_grad.cpp')
//

// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
#include <math.h>

// [[Rcpp::export]]
arma::mat arma_grad(const arma::mat& X,
                    const arma::mat& X_diff,
                    const arma::vec& Y,
                    const arma::vec& Y_rep,
                    const arma::mat& V,
                    const double h) {
    using namespace arma;

    uword n = X.n_rows;
    uword p = X.n_cols;

    // orthogonal projection matrix `Q = I - VV'` for dist computation
    mat Q = -(V * V.t());
    Q.diag() += 1;
    // calc pairwise distances as `D` with `D(i, j) = d_i(V, X_j)`
    vec D_vec = sum(square(X_diff * Q), 1);
    mat D = reshape(D_vec, n, n);
    // calc weights as `W` with `W(i, j) = w_i(V, X_j)`
    mat W = exp(D / (-2.0 * h));
    W = normalise(W, 1); // colomn-wise, 1-norm
    vec W_vec = vectorise(W);
    // centered weighted `Y` means
    vec y1 = W.t() * Y;
    vec y2 = W.t() * square(Y);
    // loss for each X_i, meaning `L(i) = L_n(V, X_i)`
    vec L = y2 - square(y1);
    // "global" loss
    double loss = mean(L);
    // `G = \nabla_V L_n(V)` a.k.a. gradient of `L` with respect to `V`
    vec scale = (repelem(L, n, 1) - square(Y_rep - repelem(y1, n, 1))) % W_vec % D_vec;
    mat X_diff_scale = X_diff.each_col() % scale;
    mat G = X_diff_scale.t() * X_diff * V;
    G *= -2.0 / (h * h * n);

    return G;
}

// ATTENTION: assumed `X` stores X_i's column wise, `X = cbind(X_1, X_2, ..., X_n)`
// [[Rcpp::export]]
arma::mat grad(const arma::mat& X,
               const arma::vec& Y,
               const arma::mat& V,
               const double h
) {
    using namespace arma;

    // get dimensions
    const uword n = X.n_cols;
    const uword p = X.n_rows;
    const uword q = V.n_cols;

    // compute orthogonal projection
    mat Q = -(V * V.t());
    Q.diag() += 1.0;

    // distance matrix `D(i, j) = d_i(V, X_j)`
    mat D(n, n, fill::zeros);
    // weights matrix `W(i, j) = w_i(V, X_j)`
    mat W(n, n, fill::ones); // exp(0) = 1

    double mvm = 0.0; // Matrix Vector Mult.
    double sos = 0.0; // Sum Of Squares
    // double wcn = 0.0; // Weights Column Norm
    for (uword j = 0; j + 1 < n; ++j) {
        for (uword i = j + 1; i < n; ++i) {
            sos = 0.0;
            for (uword k = 0; k < p; ++k) {
                mvm = 0.0;
                for (uword l = 0; l < p; ++l) {
                    mvm += Q(k, l) * (X(l, i) - X(l, j));
                }
                sos += mvm * mvm;
            }
            D(i, j) = D(j, i) = sos;
            W(i, j) = W(j, i) = std::exp(sos / (-2. * h));
        }
    }

    // column normalization of weights `W`
    double col_sum;
    for (uword j = 0; j < n; ++j) {
        col_sum = 0.0;
        for (uword i = 0; i < n; ++i) {
            col_sum += W(i, j);
        }
        for (uword i = 0; i < n; ++i) {
            W(i, j) /= col_sum;
        }
    }

    // weighted first, secend order responce means `y1`, `y2`
    // and component wise Loss `L(i) = L_n(V, X_i)`
    vec y1(n);
    vec y2(n);
    vec L(n);
    double tmp;
    double loss = 0.0;
    for (uword i = 0; i < n; ++i) {
        mvm = 0.0; // Matrix Vector Mult.
        sos = 0.0; // Sum Of Squared (weighted)
        for (uword k = 0; k < n; ++k) {
            mvm += (tmp = W(k, i) * Y(k));
            sos += tmp * Y(k); // W(k, i) * Y(k) * Y(k)
        }
        y1(i) = mvm;
        y2(i) = sos;
        loss += (L(i) = sos - (mvm * mvm)); // L_n(V, X_i) = y2(i) - y1(i)^2
    }
    loss /= n;

    mat S(n, n);
    for (uword k = 0; k < n; ++k) {
        for (uword l = 0; l < n; ++l) {
            tmp = Y(k) - y1(l);
            S(k, l) = (L(l) - (tmp * tmp)) * W(k, l) * D(k, l);
        }
    }

    // gradient
    mat G(p, q);
    double factor = -2. / (n * h * h);
    double gij;
    for (uword j = 0; j < q; ++j) {
        for (uword i = 0; i < p; ++i) {
            gij = 0.0;
            for (uword k = 0; k < n; ++k) {
                for (uword l = 0; l < n; ++l) {
                    mvm = 0.0;
                    for (uword m = 0; m < p; ++m) {
                        mvm += (X(m, l) - X(m, k)) * V(m, j);
                    }
                    // gij += (S(k, l) + S(l, k)) * (X(i, l) - X(i, k));
                    gij += S(k, l) * (X(i, l) - X(i, k)) * mvm;
                }
            }
            G(i, j) = factor * gij;
        }
    }

    return G;
}

// ATTENTION: assumed `X` stores X_i's column wise, `X = cbind(X_1, X_2, ..., X_n)`
// [[Rcpp::export]]
arma::mat grad_p(const arma::mat& X_ref,
                 const arma::vec& Y_ref,
                 const arma::mat& V_ref,
                 const double h
) {
    using arma::uword;

    // get dimensions
    const uword n = X_ref.n_cols;
    const uword p = X_ref.n_rows;
    const uword q = V_ref.n_cols;

    const double* X = X_ref.memptr();
    const double* Y = Y_ref.memptr();
    const double* V = V_ref.memptr();

    // allocate memory for entire algorithm
    //              Q (p,p)   D+W+S (n,n)   y1+L (n)  G (p,q)
    uword memsize = (p * p) + (3 * n * n) + (2 * n) + (p * q);
    double* mem = static_cast<double*>(malloc(sizeof(double) * memsize));

    // assign pointer to memory associated memory area
    double* Q = mem;
    double* D = Q + (p * p);
    double* W = D + (n * n);
    double* S = W + (n * n);
    double* y1 = S + (n * n);
    double* L = y1 + n;
    double* G = L + n;

    // compute orthogonal projection
    double sum;
    // compute orthogonal projection `Q = I_p - V V'`
    for (uword j = 0; j < p; ++j) {
        for (uword i = j; i < p; ++i) {
            sum = 0.0;
            for (uword k = 0; k < q; ++k) {
                sum += V[k * p + i] * V[k * p + j];
            }
            if (i == j) {
                Q[j * p + i] = 1.0 - sum;
            } else {
                Q[j * p + i] = Q[i * p + j] = -sum;
            }
        }
    }

    // set `diag(D) = 0` and `diag(W) = 1`.
    for (uword i = 0; i < n * n; i += n + 1) {
        D[i] = 0.0;
        W[i] = 1.0;
    }
    // components (using symmetrie) of `D` and `W` (except `diag`)
    double mvm = 0.0; // Matrix Vector Mult.
    for (uword j = 0; j + 1 < n; ++j) {
        for (uword i = j + 1; i < n; ++i) {
            sum = 0.0;
            for (uword k = 0; k < p; ++k) {
                mvm = 0.0;
                for (uword l = 0; l < p; ++l) {
                    mvm += Q[l * p + k] * (X[i * p + l] - X[j * p + l]);
                }
                sum += mvm * mvm;
            }
            D[j * n + i] = D[i * n + j] = sum;
            W[j * n + i] = W[i * n + j] = std::exp(sum / (-2. * h));
        }
    }

    // column normalization of weights `W`
    for (uword j = 0; j < n; ++j) {
        sum = 0.0;
        for (uword i = 0; i < n; ++i) {
            sum += W[j * n + i];
        }
        for (uword i = 0; i < n; ++i) {
            W[j * n + i] /= sum;
        }
    }
    // weighted first, secend order responce means `y1`, `y2`
    // and component wise Loss `L(i) = L_n(V, X_i)`
    double tmp;
    double loss = 0.0;
    for (uword i = 0; i < n; ++i) {
        mvm = 0.0; // Matrix Vector Mult.
        sum = 0.0; // Sum Of (weighted) Squares
        for (uword k = 0; k < n; ++k) {
            mvm += (tmp = W[i * n + k] * Y[k]);
            sum += tmp * Y[k];
        }
        y1[i] = mvm;
        loss += (L[i] = sum - (mvm * mvm));
    }
    loss /= n;

    // scaling for gradient summation
    for (uword k = 0; k < n; ++k) {
        for (uword l = 0; l < n; ++l) {
            tmp = Y[k] - y1[l];
            S[l * n + k] = (L[l] - (tmp * tmp)) * W[l * n + k] * D[l * n + k];
        }
    }

    // gradient
    double factor = -2. / (n * h * h);
    for (uword j = 0; j < q; ++j) {
        for (uword i = 0; i < p; ++i) {
            sum = 0.0;
            for (uword k = 0; k < n; ++k) {
                for (uword l = 0; l < n; ++l) {
                    mvm = 0.0;
                    for (uword m = 0; m < p; ++m) {
                        mvm += (X[l * p + m] - X[k * p + m]) * V[j * p + m];
                    }
                    sum += S[l * n + k] * (X[l * p + i] - X[k * p + i]) * mvm;
                }
            }
            G[j * p + i] = factor * sum;
        }
    }

    // construct 'Armadillo' matrix from `G`s memory area
    arma::mat Grad(G, p, q);

    // free entire allocated memory block
    free(mem);

    return Grad;
}



/*** R

suppressMessages(library(microbenchmark))

cat("Start timing:\n")
time.start <- Sys.time()

rStiefl <- function(p, q) {
    return(qr.Q(qr(matrix(rnorm(p * q, 0, 1), p, q))))
}

## compare runtimes
n <- 200L
p <- 12L
q <- p - 2L
X <- matrix(rnorm(n * p), n, p)
Xt <- t(X)
X_diff <- kronecker(rep(1, n), X) - kronecker(X, rep(1, n))
Y <- rnorm(n)
Y_rep <- kronecker(rep(1, n), Y) # repmat(Y, n, 1)
h <- 1. / 4.;
V <- rStiefl(p, q)

# A <- arma_grad(X, X_diff, Y, Y_rep, V, h)
# G1 <- grad(Xt, Y, V, h)
# G2 <- grad_p(Xt, Y, V, h)
# 
# print(round(A[1:6, 1:6], 3))
# print(round(G1[1:6, 1:6], 3))
# print(round(G2[1:6, 1:6], 3))
# print(round(abs(A - G1), 9))
# print(round(abs(A - G2), 9))
# 
# q()


comp <- function (A, B, tol = sqrt(.Machine$double.eps)) {
    max(abs(A - B)) < tol
}
comp.all <- function (res) {
    if (length(res) < 2) {
        return(TRUE)
    }
    res.one = res[[1]]
    for (i in 2:length(res)) {
        if (!comp(res.one, res[[i]])) {
            return(FALSE)
        }
    }
    return(TRUE)
}
counter <- 0
setup.tests <- function () {
    if ((counter %% 3) == 0) {
        X <<- matrix(rnorm(n * p), n, p)
        Xt <<- t(X)
        X_diff <<- kronecker(rep(1, n), X) - kronecker(X, rep(1, n))
        Y <<- rnorm(n)
        Y_rep <<- kronecker(rep(1, n), Y) # arma::repmat(Y, n, 1)
        h <<- 1. / 4.;
        V <<- rStiefl(p, q)
    }
    counter <<- counter + 1
}
(mbm <- microbenchmark(
    arma = arma_grad(X, X_diff, Y, Y_rep, V, h),
    grad = grad(Xt, Y, V, h),
    grad_p = grad_p(Xt, Y, V, h),
    check = comp.all,
    setup = setup.tests(),
    times = 100L
))

cat("Total time:", format(Sys.time() - time.start), '\n')

boxplot(mbm, las = 2, xlab = NULL)

*/
