## qpgraph package - this R code implements functions to learn qp-graphs from
##                   data, to estimate partial correlations, simulate undirected Gaussian
##                   graphical models and deal with microarray and genetic data in order
##                   to build network models of molecular regulation
##
## Copyright (c) 2008-2014 R. Castelo and A. Roverato, with contributions from Inma Tur.
## This package is open source and free software; you can redistribute it and/or
## modify it under the terms of the Artistic License 2.0
## as published at http://www.r-project.org/Licenses/Artistic-2.0
##
## Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT
## HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED
## WARRANTIES.  THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
## PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT
## PERMITTED BY YOUR LOCAL LAW.  UNLESS REQUIRED BY LAW, NO COPYRIGHT
## HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT,
## INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE
## OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## See the Artistic License 2.0 for more details.


## function: qpCItest
## purpose: perform a conditional independence test between two variables given
##          a conditioning set
## parameters: X - data where to perform the test
##             i - index or name of one of the two variables in X to test
##             j - index or name of the other variable in X to test
##             Q - indexes or names of the variables in X forming the conditioning set
##             I - indexes or names of the variables in X that are discrete
##             n - sample size (when data is directly the sample covariance matrix)
##             long.dim.are.variables - if TRUE it assumes that when the data is
##                                      a data frame or a matrix, the longer
##                                      dimension is the one defining the random
##                                      variables, if FALSE then random variables
##                                      are assumed to be at the columns
##             exact.test - employ an exact test when working with HMGMs
##             R.code.only - flag set to FALSE when using the C implementation
## return: a list with two members, the t-statistic value and the p-value
##         on rejecting the null hypothesis of independence

## ADD gLevels to either directly denote the number of genotype levels or trigger their
## automatic calculation

## X comes as a Biobase::ExpressionSet object
setMethod("qpCItest", signature(X="ExpressionSet"),
          function(X, i=1, j=2, Q=c(), exact.test=TRUE, use=c("complete.obs", "em"),
                   tol=0.01, R.code.only=FALSE) {

            use <- match.arg(use)

            p <- as.integer(nrow(X))
            h <- as.integer(ncol(pData(X)))
            n <- as.integer(ncol(X))
            fNames <- Biobase::featureNames(X)
            pNames <- colnames(Biobase::pData(X))
            Xsub <- matrix(0, nrow=n, ncol=length(c(i, j, Q)))
            colnames(Xsub) <- 1:length(c(i, j, Q))
            x <- Y <- I <- c()
            missingMask <- rep(FALSE, length(c(i, j, Q)))

            nam_i <- i
            if (is.character(i)) {
              if (is.na(match(i, fNames)) && is.na(match(i, pNames)))
                stop(sprintf("i=%s does not form part of the variable names of the data\n", i))
              if (!is.na(match(i, fNames))) ## then 'i' refers to an expression profile (cont.)
                x <- Biobase::exprs(X)[i, ]
              else ## then 'i' refers to a phenotypic variable (cont. or discrete)
                x <- Biobase::pData(X)[, i]
            } else {
              if (i <= p) ## then 'i' refers to an expression profile (cont.)
                x <- Biobase::exprs(X)[i, ]
              else ## then 'i' refers to a phenotypic variable (cont. or discrete)
                x <- Biobase::pData(X)[, i-p]
            }
            i <- 1L
            names(i) <- nam_i

            if (is.character(x) || is.factor(x) || is.logical(x)) {
              Xsub[, 1] <- as.numeric(factor(x, levels=unique(x)))
              I <- 1L
            } else {
              Xsub[, 1] <- as.numeric(x)
              Y <- 1L
            }
            missingMask[1] <- any(is.na(x))

            nam_j <- j
            if (is.character(j)) {
              if (is.na(match(j, fNames)) && is.na(match(j, pNames)))
                stop(sprintf("j=%s does not form part of the variable names of the data\n", j))
              if (!is.na(match(j, fNames))) ## then 'j' refers to an expression profile (cont.)
                x <- Biobase::exprs(X)[j, ]
              else ## then 'j' refers to a phenotypic variable (cont. or discrete)
                x <- Biobase::pData(X)[, j]
            } else {
              if (j <= p) ## then 'j' refers to an expression profile (cont.)
                x <- Biobase::exprs(X)[j, ]
              else ## then 'j' refers to a phenotypic variable (cont. or discrete)
                x <- Biobase::pData(X)[, j-p]
            }
            j <- 2L
            names(j) <- nam_j

            if (is.character(x) || is.factor(x) || is.logical(x)) {
              Xsub[, 2] <- as.numeric(factor(x, levels=unique(x)))
              I <- c(I, 2L)
            } else {
              Xsub[, 2] <- as.numeric(x)
              Y <- c(Y, 2L)
            }
            missingMask[2] <- any(is.na(x))

            nam_Q <- Q
            if (is.character(Q)) {
              Qe <- match(Q, fNames)
              if (any(!is.na(Qe))) {
                Xsub[, 3:(2+sum(!is.na(Qe)))] <- t(Biobase::exprs(X)[Qe[!is.na(Qe)], ])
                Y <- c(Y, 3:(2+sum(!is.na(Qe))))
              }
              if (any(is.na(Qe))) { ## then some variables in Q should refer to phenotypic vars.
                Qp <- match(Q[is.na(Qe)], pNames)
                if (any(is.na(Qp)))
                  stop(sprintf("Q={%s} do(es) not form part of the variable names of the data\n",
                       paste(Q[is.na(Qe)][is.na(Qp)], collapse=", ")))
                for (k in seq(along=Qp)) {
                  x <- Biobase::pData(X)[, Qp[k]]
                  idx <- 2L+sum(!is.na(Qe))+k

                  if (is.character(x) || is.factor(x)) {
                    Xsub[, idx] <- as.numeric(factor(x, levels=unique(x)))

                    I <- c(I, idx)
                  } else {
                    Xsub[, idx] <- as.numeric(x)
                    Y <- c(Y, idx)
                  }
                  missingMask[idx] <- any(is.na(x))
                }
              }
              Q <- 3:length(c(i, j, Q))
              names(Q) <- nam_Q
            } else if (!is.null(Q)) { ## if argument Q was empty, it should remain empty
              if (any(Q > p+h))
                stop(sprintf("Q={%s} is/are larger than the number of expression profiles (%d) and phenotypic variables (%d) together (%d+%d=%d)\n",
                             paste(Q[Q > p+h], collapse=", "), p, h, p, h, p+h))

              if (any(Q <= p)) { ## Q indices smaller or equal than p correspond to expression profiles
                Xsub[, 3:(2+sum(Q <= p))] <- Biobase::exprs(X)[Q[Q <= p], ]
                Y <- c(Y, 3:(2+sum(Q <= p)))
              }
              Qp <- which(Q > p) ## Q indices larger than p correspond to phenotypic variables
              for (k in seq(along=Qp)) {
                x <- Biobase::pData(X)[, Q[Qp[k]]-p]
                idx <- 2L+sum(Q <= p)+k

                if (is.character(x) || is.factor(x) || is.logical(x)) {
                  Xsub[, idx] <- as.numeric(factor(x, levels=unique(x)))
                  I <- c(I, idx)
                } else {
                  Xsub[, idx] <- as.numeric(x)
                  Y <- c(Y, idx)
                }
                missingMask[idx] <- any(is.na(x))
              }
              Q <- 3:length(c(i, j, Q))
              names(Q) <- nam_Q
            }

            rval <- NA
            rownames(Xsub) <- 1:nrow(Xsub)

            if (is.null(I)) {
              S <- qpCov(Xsub)
              rval <- .qpCItest(S, i, j, Q, R.code.only)
            } else {
              missingData <- any(missingMask)
              ssd <- mapX2ssd <- NULL
              if (!missingData) {
                ssd <- qpCov(Xsub[, Y, drop=FALSE], corrected=FALSE)
                mapX2ssd <- match(colnames(Xsub), colnames(ssd))
                names(mapX2ssd) <- colnames(Xsub)
              }

              nLevels <- rep(NA_integer_, times=ncol(Xsub))
              nLevels[I] <- apply(Xsub[, I, drop=FALSE], 2, function(x) nlevels(as.factor(x)))
              if (any(nLevels[I] == 1))
                stop(sprintf("Discrete variable %s has only one level", colnames(Xsub)[I[nLevels[I]==1]]))

              rval <- .qpCItestHMGM(Xsub, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                                              exact.test, use, tol, R.code.only)
              if (is.nan(rval$statistic))
                warning(paste(sprintf("CI test unavailable for i=%s, j=%s and Q={",
                                      i, j, paste(Q, collapse=", ")),
                                      "}. Try a smaller Q or increase n if you can\n"))
            }

            class(rval) <- "htest" ## this is kind of redundant but otherwise
                                   ## the object returned by the C function does
                                   ## not print by default as a 'htest' object

            return(rval)
          })

## X comes as a qtl::cross object
setMethod("qpCItest", signature(X="cross"),
          function(X, i=1, j=2, Q=c(), exact.test=TRUE, use=c("complete.obs", "em"),
                   tol=0.01, R.code.only=FALSE) {

            use <- match.arg(use)

            p <- as.integer(qtl::nphe(X))
            sByChr <- qtl::nmar(X)
            gLevels <- sum(unique(as.vector(as(X$geno[[1]]$data[, 1:min(sByChr[1], 1000)], "matrix"))) > 0, na.rm=TRUE)
            cumsum_sByChr <- c(0, cumsum(sByChr))
            s <- sum(sByChr)
            n <- as.integer(qtl::nind(X))
            pNames <- colnames(X$pheno)
            sNamesByChr <- lapply(X$geno, function(x) colnames(x$data))
            sNames <- unlist(sNamesByChr, use.names=FALSE)
            qtlNames <- c()
            nqtl <- 0 
            if ("qtlgeno" %in% names(X)) {
              qtlNames <- colnames(X$qtlgeno)
              nqtl <- length(qtlNames)
            }
            Xsub <- matrix(0, nrow=n, ncol=length(c(i, j, Q)))
            colnames(Xsub) <- 1:length(c(i, j, Q))
            nLevels <- rep(NA, length(c(i, j, Q)))
            missingMask <- rep(FALSE, length(c(i, j, Q)))
            x <- Y <- I <- c()

            nam_i <- i
            if (is.character(i)) {
              smt <- match(i, sNames)
              if (is.na(match(i, qtlNames)) && is.na(match(i, pNames)) && is.na(smt))
                stop(sprintf("i=%s does not form part of the variable names of the data\n", i))
              if (!is.na(match(i, pNames))) ## then 'i' refers to a phenotypic variable (cont. or discrete)
                x <- X$pheno[, i]
              else if (!is.na(match(i, qtlNames))) { ## then 'i' refers to a QTL genotype (discrete)
                x <- as(X$qtlgeno[, i], "numeric") ## assume genotypes come as 1, 2, ...
                nLevels[1] <- gLevels
              } else { ## then 'i' refers to a marker genotype (discrete)
                x <- as(X$geno[[sum(cumsum_sByChr < smt)]]$data[, i], "numeric") ## assume genotypes come as 1, 2, ...
                nLevels[1] <- gLevels
              }
            } else {
              if (i <= p) ## then 'i' refers to a phenotype
                x <- X$pheno[, i]
              else if (i <= p+s) ## then 'i' refers to a marker genotype (discrete)
                x <- as(X$geno[[sum(cumsum_sByChr < i-p)]]$data[, i-p], "numeric") ## assume genotypes come as 1, 2, ...
              else { ## then 'i' refers to a QTL (discrete)
                x <- as(X$qtlgeno[, i-p-s], "numeric") ## assume genotypes come as 1, 2, ...
                nLevels[1] <- gLevels
              }
            }
            i <- 1L
            names(i) <- nam_i

            if (!is.na(nLevels[1]) || is.character(x) || is.factor(x) || is.logical(x)) {
              x <- factor(x)
              if (is.na(nLevels[1]))
                nLevels[1] <- nlevels(x)

              I <- 1L
            } else
              Y <- 1L

            missingMask[1] <- any(is.na(x))
            Xsub[, 1] <- as.numeric(x)

            nam_j <- j
            if (is.character(j)) {
              smt <- match(j, sNames)
              if (is.na(match(j, qtlNames)) && is.na(match(j, pNames)) && is.na(smt))
                stop(sprintf("j=%s does not form part of the variable names of the data\n", j))
              if (!is.na(match(j, pNames))) ## then 'j' refers to a phenotypic variable (cont. or discrete)
                x <- X$pheno[, j]
              else if (!is.na(match(j, qtlNames))) { ## then 'i' refers to a QTL genotype (discrete)
                x <- as(X$qtlgeno[, j], "numeric") ## assume genotypes come as 1, 2, ...
                nLevels[2] <- gLevels
              } else { ## then 'j' refers to a marker genotype (discrete)
                x <- as(X$geno[[sum(cumsum_sByChr < smt)]]$data[, j], "numeric") ## assume genotypes come as 1, 2, ...
                nLevels[2] <- gLevels
              }
            } else {
              if (j <= p) ## then 'j' refers to a phenotype
                x <- X$pheno[, j]
              else if (j <= p+s) ## then 'j' refers to a marker genotype (discrete)
                x <- as(X$geno[[sum(cumsum_sByChr < j-p)]]$data[, j-p], "numeric") ## assume genotypes come as 1, 2, ...
              else { ## then 'j' refers to a QTL (discrete)
                x <- as(X$qtlgeno[, j-p-s], "numeric") ## assume genotypes come as 1, 2, ... 
                nLevels[2] <- gLevels
              }
            }
            j <- 2L
            names(j) <- nam_j

            if (!is.na(nLevels[2]) || is.character(x) || is.factor(x) || is.logical(x)) {
              x <- factor(x)
              if (is.na(nLevels[2]))
                nLevels[2] <- nlevels(x)
              I <- c(I, 2L)
            } else
              Y <- c(Y, 2L)

            missingMask[2] <- any(is.na(x))
            Xsub[, 2] <- as.numeric(x)

            nam_Q <- Q
            if (is.character(Q)) {
              Qp <- match(Q, pNames)
              Qs <- Qqtl <- c()
              if (any(is.na(Qp))) { ## then some variables in Q should refer to marker genotypes
                Qs <- match(Q[is.na(Qp)], sNames)
                if (any(is.na(Qs))) { ## then some variables in Q should refer to QTL genotypes
                  Qqtl <- match(Q[is.na(Qp)][is.na(Qs)], qtlNames)
                  if (any(is.na(Qqtl)))
                    stop(sprintf("Q={%s} do(es) not form part of the variable names of the data\n",
                                 paste(Q[is.na(Qp)][is.na(Qs)][is.na(Qqtl)], collapse=", ")))
                  Qs <- Qs[!is.na(Qs)]
                }
                Qp <- Qp[!is.na(Qp)]
              }
              for (k in seq(along=Qp)) {
                x <- X$pheno[, Qp[k]]
                idx <- 2L+k
                missingMask[idx] <- any(is.na(x))

                if (is.character(x) || is.factor(x) || is.logical(x)) {
                  x <- factor(x)
                  nLevels[idx] <- nlevels(x)
                  Xsub[, idx] <- as.numeric(x) ## transforming factor variable values into 1, 2, ...
                  I <- c(I, idx)
                } else {
                  Xsub[, idx] <- as.numeric(x)
                  Y <- c(Y, idx)
                }
              }
              for (k in seq(along=Qs)) {
                x <- as(X$geno[[sum(cumsum_sByChr < Qs[k])]]$data[, Qs[k]-cumsum_sByChr[sum(cumsum_sByChr < Qs[k])] ], "numeric") ## assume genotypes come as 1, 2, ...
                idx <- 2L+sum(!is.na(Qp))+k
                missingMask[idx] <- any(is.na(x))
                Xsub[, idx] <- x
                I <- c(I, idx)
                nLevels[idx] <- gLevels
              }
              for (k in seq(along=Qqtl)) {
                x <- as(X$qtlgeno[, Qqtl[k]], "numeric") ## assume genotypes come as 1, 2, ...
                idx <- 2L+sum(!is.na(Qp))+sum(!is.na(Qs))+k
                missingMask[idx] <- any(is.na(x))
                Xsub[, idx] <- x
                I <- c(I, idx)
                nLevels[idx] <- gLevels
              }
              Q <- 3:length(c(i, j, Q))
              names(Q) <- nam_Q
            } else if (!is.null(Q)) { ## if argument Q was empty, it should remain empty
              if (any(Q > p+s+nqtl))
                stop(sprintf("Q={%s} is/are larger than the number of phenotypic variables (%d), marker genotypes (%d) and QTL genotypes together (%d+%d+%d=%d)\n",
                             paste(Q[Q > p+s+nqtl], collapse=", "), p, s, nqtl, p, s, nqtl, p+s+nqtl))

              Qp <- which(Q <= p) ## Q smaller than p correspond to phenotypic variables
              for (k in seq(along=Qp)) {
                x <- X$pheno[, Q[Qp[k]]]
                idx <- 2L+k
                missingMask[idx] <- any(is.na(x))

                if (is.character(x) || is.factor(x)) {
                  x <- factor(x)
                  nLevels[idx] <- nlevels(x)
                  Xsub[, idx] <- as.numeric(x)
                  I <- c(I, idx)
                } else {
                  Xsub[, idx] <- as.numeric(x)
                  Y <- c(Y, idx)
                }
              }
              Qs <- which(Q > p && Q <= p+s) ## Q indices larger than p and smaller than p+s correspond to marker genotypes
              for (k in seq(along=Qs)) {
                x <- as(X$geno[[sum(cumsum_sByChr < Q[Qs[k]]-p)]]$data[, Q[Qs[k]]-p-cumsum_sByChr[sum(cumsum_sByChr < Q[Qs[k]]-p)] ], "numeric") ## assume genotypes come as 1, 2, ...

                idx <- 2L+sum(Q <= p)+k
                missingMask[idx] <- any(is.na(x))
                Xsub[, idx] <- x
                I <- c(I, idx)
                nLevels[idx] <- gLevels
              }
              Qqtl <- which(Q > p+s) ## Q indices larger than p+s correspond to QTL genotypes
              for (k in seq(along=Qqtl)) {
                x <- as(X$qtlgeno[, Q[Qqtl[k]]-p-s], "numeric") ## assume gentypes come as 1, 2, ...
                idx <- 2L+sum(Q <= p+s)+k
                missingMask[idx] <- any(is.na(x))
                Xsub[, idx] <- x
                I <- c(I, idx)
                nLevels[idx] <- gLevels
              }
              Q <- 3:length(c(i, j, Q))
              names(Q) <- nam_Q
            }

            rval <- NA
            rownames(Xsub) <- 1:nrow(Xsub)

            if (is.null(I)) {
              S <- qpCov(Xsub)
              rval <- .qpCItest(S, i, j, Q, R.code.only)
            } else {
              if (any(nLevels[I] == 1))
                stop(sprintf("Discrete variable %s has only one level", colnames(Xsub)[I[nLevels[I]==1]]))

              missingData <- any(missingMask)
              ssd <- mapX2ssd <- NULL
              if (!missingData) {
                ssd <- qpCov(Xsub[, Y, drop=FALSE], corrected=FALSE)
                mapX2ssd <- match(colnames(Xsub), colnames(ssd))
                names(mapX2ssd) <- colnames(Xsub)
              }

              rval <- .qpCItestHMGM(Xsub, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                                              exact.test, use, tol, R.code.only)
              if (is.nan(rval$statistic))
                warning(paste(sprintf("CI test unavailable for i=%s, j=%s and Q={",
                                      i, j, paste(Q, collapse=", ")),
                                      "}. Try a smaller Q or increase n if you can\n"))
            }

            class(rval) <- "htest" ## this is kind of redundant but otherwise
                                   ## the object returned by the C function does
                                   ## not print by default as an 'htest' object

            return(rval)
          })



## X comes as a data frame
setMethod("qpCItest", signature(X="data.frame"),
          function(X, i=1, j=2, Q=c(), I=NULL, long.dim.are.variables=TRUE,
                   exact.test=TRUE, use=c("complete.obs", "em"), tol=0.01, R.code.only=FALSE) {

            use <- match.arg(use)

            X <- as.matrix(X)
            if (!is.double(X))
              stop("X should be double-precision real numbers\n")

            if (long.dim.are.variables &&
                sort(dim(X),decreasing=TRUE,index.return=TRUE)$ix[1] == 1)
              X <- t(X)

            if (is.null(colnames(X)))
              colnames(X) <- 1:ncol(X)

            nam_i <- names(i)
            if (is.character(i)) {
              if (is.na(match(i, colnames(X))))
                stop(sprintf("i=%s does not form part of the variable names of the data\n",i))
              i <- match(i,colnames(X))
            }
            i <- as.integer(i)
            names(i) <- nam_i

            nam_j <- names(j)
            if (is.character(j)) {
              if (is.na(match(j, colnames(X))))
                stop(sprintf("j=%s does not form part of the variable names of the data\n",j))
              j <- match(j,colnames(X))
            }
            j <- as.integer(j)
            names(j) <- nam_j

            nam_Q <- names(Q)
            if (is.character(Q)) {
              if (any(is.na(match(Q, colnames(X)))))
                stop(sprintf("%s in Q does not form part of the variable names of the data\n",
                     Q[is.na(match(Q, colnames(X)))]))
              Q <- match(Q, colnames(X))
            }
            Q <- as.integer(Q)
            names(Q) <- nam_Q

            if (is.character(I)) {
              if (any(is.na(match(I, colnames(X)))))
                stop(sprintf("%s in I does not form part of the variable names of the data\n",
                     I[is.na(match(I, colnames(X)))]))
              I <- match(I, colnames(X))
            }

            rval <- NA
            rownames(Xsub) <- 1:nrow(Xsub)

            if (is.null(I)) {
              S <- qpCov(X)
              n <- nrow(X)

              rval <- .qpCItest(S, i, j, Q, R.code.only)
            } else {
              if (!is.character(I) && !is.numeric(I) && !is.integer(I))
                stop("I should be either variables names or indices\n")

              Y <- colnames(X)
              if (is.character(I))
                Y <- setdiff(colnames(X), I)
              else
                Y <- (1:ncol(X))[-I]

              if (is.character(Y)) {
                if (any(is.na(match(Y, colnames(X)))))
                  stop(sprintf("Some variables in Y do not form part of the variable names in X\n",i))
                Y <- match(Y, colnames(X))
              }

              nLevels <- rep(NA_integer_, times=ncol(X))
              nLevels[I] <- apply(X[, I, drop=FALSE], 2, function(x) nlevels(as.factor(x)))
              if (any(nLevels[I] == 1))
                stop(sprintf("Discrete variable %s has only one level", colnames(X)[I[nLevels[I]==1]]))

              missingMask <- apply(X[, I, drop=FALSE], 2, function(x) any(is.na(x)))
              missingData <- any(missingMask)
              ssd <- mapX2ssd <- NULL
              if (!missingData) {
                ssd <- qpCov(X[, Y, drop=FALSE], corrected=FALSE)
                mapX2ssd <- match(colnames(X), colnames(ssd))
                names(mapX2ssd) <- colnames(X)
              }

              rval <- .qpCItestHMGM(X, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                                              exact.test, use, tol, R.code.only)
              if (is.nan(rval$statistic))
                warning(paste(sprintf("CI test unavailable for i=%s, j=%s and Q={",
                                      colnames(X)[i], colnames(X)[j]),
                              paste(colnames(X)[Q], collapse=", "),
                                    "}. Try a smaller Q or increase n if you can\n"))
            }

            class(rval) <- "htest" ## this is kind of redundant but otherwise
                                   ## the object returned by the C function does
                                   ## not print by default as a 'htest' object

            return(rval)
          })

          
## X comes as a matrix
setMethod("qpCItest", signature(X="matrix"),
          function(X, i=1, j=2, Q=c(), I=NULL,
                   long.dim.are.variables=TRUE, exact.test=TRUE,
                   use=c("complete.obs", "em"), tol=0.01, R.code.only=FALSE) {

            use <- match.arg(use)

            if (!is.double(X))
              stop("X should be double-precision real numbers\n")

            if (long.dim.are.variables &&
                sort(dim(X),decreasing=TRUE,index.return=TRUE)$ix[1] == 1)
              X <- t(X)

            if (is.null(colnames(X)))
              colnames(X) <- 1:ncol(X)

            nam_i <- i
            if (is.character(i)) {
              if (is.na(match(i, colnames(X))))
                stop(sprintf("i=%s does not form part of the variable names of the data\n",i))
              i <- match(i,colnames(X))
            }
            i <- as.integer(i)
            names(i) <- nam_i

            nam_j <- j
            if (is.character(j)) {
              if (is.na(match(j, colnames(X))))
                stop(sprintf("j=%s does not form part of the variable names of the data\n",j))
              j <- match(j,colnames(X))
            }
            j <- as.integer(j)
            names(j) <- nam_j

            nam_Q <- Q
            if (is.character(Q)) {
              if (any(is.na(match(Q, colnames(X)))))
                stop(sprintf("%s in Q does not form part of the variable names of the data\n",
                     Q[is.na(match(Q, colnames(X)))]))
              Q <- match(Q, colnames(X))
            }
            Q <- as.integer(Q)
            names(Q) <- nam_Q

            if (is.character(I)) {
              if (any(is.na(match(I, colnames(X)))))
                stop(sprintf("%s in I does not form part of the variable names of the data\n",
                     I[is.na(match(I, colnames(X)))]))
              I <- match(I, colnames(X))
            }

            rval <- NA
            rownames(X) <- 1:nrow(X)

            if (is.null(I)) {
              if (use == "em")
                stop("EM not implemented yet for missing values in continuous variables. Please set use=\"complete.obs\"\n")

              S <- qpCov(X)
              n <- nrow(X)

              rval <- .qpCItest(S, i, j, Q, R.code.only)
            } else {
              if (!is.character(I) && !is.numeric(I) && !is.integer(I))
                stop("argument I should contain either variables names or indices\n")

              Y <- colnames(X)
              if (is.character(I))
                Y <- setdiff(colnames(X), I)
              else
                Y <- (1:ncol(X))[-I]

              if (is.character(Y)) {
                if (any(is.na(match(Y, colnames(X)))))
                  stop(sprintf("Some variables in Y do not form part of the variable names in X\n",i))
                Y <- match(Y, colnames(X))
              }

              nLevels <- rep(NA_integer_, times=ncol(X))
              nLevels[I] <- apply(X[, I, drop=FALSE], 2, function(x) nlevels(as.factor(x)))
              if (any(nLevels[I] == 1))
                stop(sprintf("Discrete variable %s has only one level", colnames(X)[I[nLevels[I]==1]]))

              missingMask <- apply(X, 1, function(x) any(is.na(x)))
              missingData <- any(missingMask)
              ssd <- mapX2ssd <- NULL
              if (!missingData) {
                ssd <- qpCov(X[, Y, drop=FALSE], corrected=FALSE)
                ## mapX2ssd <- match(colnames(X), colnames(ssd))
                ## names(mapX2ssd) <- colnames(X)
                mapX2ssd <- rep(NA, ncol(X))
                mapX2ssd[Y] <- 1:length(Y)
              }

              rval <- .qpCItestHMGM(X, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                                              exact.test, use, tol, R.code.only)
              if (is.nan(rval$statistic))
                warning(paste(sprintf("CI test unavailable for i=%s, j=%s and Q={",
                                      colnames(X)[i], colnames(X)[j]),
                              paste(colnames(X)[Q], collapse=", "),
                                    "}. Try a smaller Q or increase n if you can\n"))
            }

          class(rval) <- "htest" ## this is kind of redundant but otherwise
                                 ## the object returned by the C function does
                                 ## not print by default as a 'htest' object

          return(rval)
        })

## X comes as an SsdMatrix (i.e., a covariance matrix calculated with qpCov())
setMethod("qpCItest", signature(X="SsdMatrix"),
          function(X, i=1, j=2, Q=c(), R.code.only=FALSE) {

            if (is.null(colnames(X)))
              colnames(X) <- 1:ncol(X)

            nam_i <- i
            if (is.character(i)) {
              if (is.na(match(i, colnames(X))))
                stop(sprintf("i=%s does not form part of the variable names of the data\n",i))
              i <- match(i,colnames(X))
            }
            i <- as.integer(i)
            names(i) <- nam_i

            nam_j <- j
            if (is.character(j)) {
              if (is.na(match(j, colnames(X))))
                stop(sprintf("j=%s does not form part of the variable names of the data\n",j))
              j <- match(j,colnames(X))
            }
            j <- as.integer(j)
            names(j) <- nam_j

            nam_Q <- Q
            if (is.character(Q)) {
              if (any(is.na(match(Q, colnames(X)))))
                stop(sprintf("%s in Q does not form part of the variable names of the data\n",
                     Q[is.na(match(Q, colnames(X)))]))
              Q <- match(Q, colnames(X))
            }
            Q <- as.integer(Q)
            names(Q) <- nam_Q

            rval <- NA

            if (is.null(rownames(X)))
              rownames(X) <- colnames(X)

            rval <- .qpCItest(X, i, j, Q, R.code.only)

            class(rval) <- "htest" ## this is kind of redundant but otherwise
                                   ## the object returned by the C function does
                                   ## not print by default as a 'htest' object

            return(rval)
          })

.qpCItest <- function(S, i=1L, j=2L, Q=c(), R.code.only=FALSE) {

  if (!is(S, "SsdMatrix"))
    stop("internal function .qpCItest() expects an 'SsdMatrix' object as first argument\n");

  p <- (d <- dim(S))[1]
  if (p != d[2] || !isSymmetric(S))
    stop("S is not squared and symmetric. Is it really a covariance matrix?n")
  n <- S@n

  if (!is.integer(i) || !is.integer(j) || (!is.null(Q) && !is.integer(Q)))
    stop("i, j and Q should contain only integer values when calling .qpCItest()")

  if (!R.code.only) {
    return(.qpFastCItestStd(S, i, j, Q));
  }

  q       <- length(Q)
  Mmar    <- S[c(i, j, Q), c(i, j, Q)]
  S11     <- Mmar[1,1]
  S12     <- Mmar[1,-1]
  S21     <- Mmar[-1,1]
  S22     <- Mmar[-1,-1]
  S22inv  <- solve(S22)
  betahat <- as.numeric(S12 %*% S22inv[,1])
  df      <- c(df = n - q - 2)
  sigma   <- sqrt((S11 - S12 %*% S22inv %*% S21) * (n - 1) / df)
  se      <- as.numeric(sigma * sqrt(S22inv[1,1] / (n - 1)))
  t.value <- c(t = betahat / se)
  p.value <- c(two.sided = 2 * (1 - pt(abs(t.value), df)))
  est     <- c(beta = betahat)
  n.value <- c("partial regression coefficient" = 0)
  method  <- "Conditional independence test for continuous data using a t test for zero partial regression coefficient"

  RVAL <- list(statistic=t.value,
               parameter=df,
               p.value=p.value,
               estimate=est,
               null.value=n.value,
               alternative="two.sided",
               method=method,
               data.name=sprintf("%s and %s given {%s}", names(i), names(j), paste(names(Q), collapse=", ")))
  class(RVAL) <- "htest"

  RVAL
}

##
## calculate the ssd matrix using complete observations only
##
.ssdStatsCompleteObs <- function(X, I, Y, missingMask) {

  if (length(I) == 0) {
    ssd <- qpCov(X[!missingMask, Y, drop=FALSE], corrected=FALSE)
    return(ssd)
  }

  n <- nrow(X)
  n.co <- n - sum(missingMask)

  xtab <- tapply(1:n, as.data.frame(X[, I, drop=FALSE]))
  xtab[missingMask] <- -1 ## label missing observations
  xtab <- split(as.data.frame(X[, Y, drop=FALSE]), xtab)
  xtab <- xtab[as.integer(names(xtab)) > 0] ## remove missing observations
  xtab <- xtab[which(sapply(lapply(xtab, dim), "[", 1) > 0)]
  ## ni <- sapply(lapply(xtab, dim), "[", 1)
  ssd <- Reduce("+",
                lapply(as.list(1:length(xtab)),
                       function(i, x) qpCov(as.matrix(x[[i]]), corrected=FALSE)@ssd, xtab))
                        ##function(i, x, ni, n) (ni[i]-1)*cov(x[[i]]), xtab, ni, n))
  new("SsdMatrix", ssd=as(as(as(ssd, "dMatrix"), "symmetricMatrix"), "packedMatrix"), n=n.co)
}

##
## the following functions calculate the ssd matrix using the EM algorithm
##

## k(i) = y^T\Sigma^{-1}\mu(i) - 1/2 * [y^T\Sigma^{-1} y + \mu(i)^T \Sigma^{-1}\mu(i)] + log p(i)
Ki <- function(x, Ys, i, mapX2Y, Sigma, mu, p) {

  aux <- p[i]
  if (length(Ys) > 0) {
    Ys_Sigma <- mapX2Y[Ys]
    aux <- exp( t(x[ ,Ys]) %*% solve(Sigma[Ys_Sigma, Ys_Sigma]) %*% mu[i, Ys_Sigma] -
                (t(x[ ,Ys]) %*% solve(Sigma[Ys_Sigma, Ys_Sigma]) %*% x[ ,Ys] +
                 t(mu[i, Ys_Sigma]) %*% solve(Sigma[Ys_Sigma, Ys_Sigma]) %*% mu[i, Ys_Sigma])/2 + log(p[i]) )
  }

  aux
}

## calculate the probability of I=i for each observation with missing values
## pr(I=i' | (i_{obs}, y)^{(\nu)}) = exp k(i') / \sum_{s\in{\cal S}} exp k(s)
prob_i <- function(idxMissingObs, X, I, Is, Ys, k, level_i, levels_I, mapX2I, mapX2Y, p, mu, Sigma) {

  p_i <- vector(length=length(idxMissingObs))
  ## names(p_i) <- idxMissingObs

  for (i in 1:length(idxMissingObs)) {
    x <- X[idxMissingObs[i], ,drop=FALSE]
    ## I_obs <- which(!is.na(x[, I])) ## 2/4/13 I_obs should be on the X-scale and not on the I-scale
    I_obs <- intersect(which(!is.na(x)), I)

    ## if (length(I_obs) > 0 && !all(x[, intersect(Is, I_obs)] == level_i[intersect(Is, I_obs)])) {
    if (length(I_obs) > 0 && !all(x[, intersect(Is, I_obs)] == level_i[!is.na(match(Is, I_obs))])) {
      p_i[i] <- 0
    } else {
      ifelse(length(I_obs)==0,
             index_S <- 1:nrow(levels_I),
             index_S <- which(apply(levels_I[, mapX2I[I_obs], drop=FALSE], 1, function(l) {all(l == x[, I_obs])})))
      index_Si <- index_S[which(sapply(index_S, function(i) {all(levels_I[i, mapX2I[Is]] == level_i)}))]
      if (all(intersect(index_S, index_Si)==index_S)) {
        p_i[i] <- 1
      } else {
        K_den <- sum(sapply(index_S, function(i) {Ki(x, Ys, i, mapX2Y, Sigma, mu, p)}))
        ifelse (K_den == 0,
                p_i[i] <- 0,
                p_i[i] <- sum(sapply(index_Si, function(i) {Ki(x, Ys, i, mapX2Y, Sigma, mu, p)/K_den})))
      }
    }
  }

  p_i
}

## complete sufficient statistics: calculate sufficient statistics (En, Es and Ess) of those
## observations that do not have missing values
stat_com <- function(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs, Is, Ys, levels_Is) {
  Es_com <- Ess_com <- n_com <- c()

  if (length(idxCompleteObs) > 0) {
    xi_level <- lapply(1:nrow(levels_Is), function(k) names(which(apply(X[idxCompleteObs, Is, drop=FALSE], 1, function(x){all(x == levels_Is[k, ])}))))
    n_com <- sapply(xi_level, length)
  }

  if (length(Ys) > 0) {
    Es_com <- matrix(0, nrow=nrow(levels_Is), ncol=length(Ys))##, dimnames=list(1:nrow(levels_Is), Ys))
    Ess_com <- matrix(0, nrow=length(Ys), ncol=length(Ys))##, dimnames=list(Ys, Ys))
    if (length(idxCompleteObs) > 0) {
      aux <- which(n_com > 0)
      Es_com[aux, ] <- matrix(t(sapply(aux, function(i) colSums(X[xi_level[[i]], Ys, drop=FALSE]))), ncol=length(Ys))##, dimnames=list(aux, Ys))
      Ess_com <- Reduce("+" ,lapply(xi_level, function(i) t(as.matrix(X[i, Ys, drop=FALSE]))%*%as.matrix(X[i, Ys,drop=FALSE])))
    }
  }

  list(idxMissingObs=idxMissingObs, mapAllObs2MissingObs=mapAllObs2MissingObs,
       n_com=n_com, Es_com=Es_com, Ess_com=Ess_com)
}

## sufficient statistics from missing data: perform the E step for the observations with missing values.
stat_mis <- function(X, Is, Ys, levels_Is, stat, I, levels_I, mapX2I, Y, mapX2Y, p, mu, Sigma) {

  m <- vector(length=nrow(levels_I))
  h <- bar_y <- matrix(0, nrow=nrow(levels_I), ncol=length(Y))##, dimnames=list(1:nrow(levels_I), Y))
  K <- matrix(0, nrow=length(Y), ncol=length(Y))##, dimnames=list(Y, Y))
  n <- nrow(X)

  Ys_Sigma <- mapX2Y[Ys]

  ### Is != emptyset
  if (length(Is) > 0) {

    ### Pr(I=i | x_obs)
    p_i <- sapply(1:nrow(levels_Is),
                  function(k) prob_i(stat$idxMissingObs, X, I, Is, Ys, k,
                                     levels_Is[k, ], levels_I, mapX2I, mapX2Y, p, mu, Sigma))
    if (!is.matrix(p_i)) {
      p_i <- t(as.matrix(p_i))
      ## dimnames(p_i) <- list(stat$idxMissingObs, 1:nrow(levels_Is))
    }

    index <- lapply(1:nrow(levels_Is), function(k) which(apply(levels_I[, mapX2I[Is], drop=FALSE], 1, function(l){all(l == levels_Is[k, ])})))

    ### En
    E_n <- stat$n_com + colSums(p_i)

    ### m     # afegit
    for (k in 1:nrow(levels_Is)) {
      j <- index[[k]]
      m[j] <- rep(E_n[k], length(j))
    }

    ### Is != emptyset & Ys != emptyset
    if (length(Ys) > 0) {

      ### Es
      Es <- stat$Es_com + t(p_i) %*% X[stat$idxMissingObs, Ys, drop=FALSE]
      ## mapX2Ys <- rep(NA, ncol(X))
      ## mapX2Ys[Ys] <- 1:length(Ys)


      ### Ess
      Ess <- stat$Ess_com + Reduce("+", lapply(1:nrow(levels_Is),
                                               function(k, idxMissingObs, mapAllObs2MissingObs)
                                                 Reduce("+" , lapply(idxMissingObs,
                                                                     function(i, k, mapAllObs2MissingObs) {
                                                                       p_i[mapAllObs2MissingObs[i], k]*t(X[i, Ys, drop=FALSE]) %*% X[i, Ys, drop=FALSE]
                                                                     }, k, mapAllObs2MissingObs)
                                                       ),
                                               stat$idxMissingObs, stat$mapAllObs2MissingObs)
                                  )

      ### ssd
      ssd <- Ess - t(Es)%*%(Es/E_n)

      for (k in 1:nrow(levels_Is)) {
        j <- index[[k]]
        ### WATCH OUT: Es[k, Ys] REPLACED BY Es[k, ] SINCE LOOKS LIKE ALL Ys ARE USED ALONG THE COLUMNS FROM Es
        bar_y[j, Ys_Sigma] <- matrix(rep(Es[k, ]/E_n[k], length(j)), byrow=TRUE, ncol=length(Ys))##, dimnames=list(j, Ys))
      }
      h[ , Ys_Sigma] <- t(n*solve(ssd)%*%t(bar_y[ ,Ys_Sigma]))
      K[Ys_Sigma, Ys_Sigma] <- n*solve(ssd)
    } else {  ### Is != emptyset & Ys == emptyset
      ssd <- 1
    }

  } else {
    ### Is == emptyset & Ys == emptyset
    if (length(Ys)==0) {
      m <- 1
      ssd <- 1
    } else { ### Is == emptyset & Ys != emptyset
      m <- n
      ssd <- (n - 1)*cov(X[ , Ys, drop = FALSE])
      h[, Ys_Sigma] <- matrix(rep(n*solve(ssd) %*% colMeans(X[, Ys, drop=FALSE]), nrow(levels_I)), ncol=length(Ys), byrow=TRUE)
      K[Ys_Sigma, Ys_Sigma] <- n*solve(ssd)
    }
  }

  list(ssd=ssd, K=K, h=h, m=m, bar_y=bar_y)
}

## convergence: calculate convergence diagnostic of the EM algorithm by comparing
## the updated values of the moment parameteres to the moment parameters of the
## previous iteration
convergence <- function(Sigma_update, mu_update, m_update, Sigma, mu, m) {

  delta_m <- abs(m_update - m)/sqrt(m_update + 1)
  delta_mu <- abs(mu_update - mu)/sqrt(diag(Sigma_update))
  delta_Sigma <- abs(Sigma_update - Sigma)
  for (i in 1:ncol(Sigma)) {
    for (j in 1:nrow(Sigma)) {
      delta_Sigma[i,j] <- delta_Sigma[i,j]/sqrt(Sigma_update[i,i]*Sigma_update[j,j] + Sigma_update[i,j]^2)
    }
  }

  list(mdiff=max(max(delta_m), max(delta_mu), max(delta_Sigma)), m=m_update, mu=mu_update, Sigma=Sigma_update)
}

## calculate ssd matrices for H0 and H1 using the EM algorithm
.ssdStatsEM <- function(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs,
                        I, mapX2I, nLevels, Y, mapX2Y, i, j, Q, tol=0.01) {

  if (length(I) > 0) {
    levels_I <- expand.grid(sapply(nLevels[I], seq_len, simplify=FALSE))
    dimnames(levels_I) <- list(1:nrow(levels_I), I)
  } else {
    levels_I <- matrix(1, ncol=1, nrow=1, dimnames=list(1,1))
  }

  p0 <- rep(1/nrow(levels_I), nrow(levels_I))
  mu0 <- matrix(0, ncol=length(Y), nrow=nrow(levels_I))
  ## colnames(mu0) <- Y
  Sigma0 <- diag(length(Y))
  ## dimnames(Sigma0) <- list(Y, Y)

  p <- p0
  mu <- mu0
  Sigma <- Sigma0
  n <- nrow(X)

  I_j <- intersect(I, c(i, Q))
  Y_j <- intersect(Y, c(i, Q))
  levels_I_j <- comStat_j <- c()
  if (length(I_j) > 0) {
    levels_I_j <- as.matrix(unique(levels_I[, mapX2I[I_j], drop=FALSE]))
    ## colnames(levels_I_j) <- I_j
    comStat_j <- stat_com(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs,
                          Is=I_j, Ys=Y_j, levels_Is=levels_I_j)
    if (any(comStat_j$n_com == 0))
      stop("Some joint levels of the discrete variables involved lack observations (n(i) == 0).")

    ## cat("Es_com_j=\n")
    ## print(comStat_j$Es_com)
    ## cat("Ess_com_j=\n")
    ## print(comStat_j$Ess_com)
    ## cat("n_com_j=", comStat_j$n_com,"\n")
  }

  I_i <- intersect(I, c(j, Q))
  Y_i <- intersect(Y, c(j, Q))
  levels_I_i <- comStat_i <- c()

  if (length(I_i) > 0) {
    levels_I_i <- as.matrix(unique(levels_I[, mapX2I[I_i], drop=FALSE]))
    ## colnames(levels_I_i) <- I_i
    comStat_i <- stat_com(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs,
                          Is=I_i, Ys=Y_i, levels_Is=levels_I_i)
    if (any(comStat_i$n_com == 0))
      stop("Some joint levels of the discrete variables involved lack observations (n(i) == 0).")

    ## cat("Es_com_i=\n")
    ## print(comStat_i$Es_com)
    ## cat("Ess_com_i=\n")
    ## print(comStat_i$Ess_com)
    ## cat("n_com_i=", comStat_i$n_com,"\n")
  }

  I_ij <- intersect(I, Q)
  Y_ij <- intersect(Y, Q)
  levels_I_ij <- comStat_ij <- c()
  if (length(I_ij) > 0) {
    levels_I_ij <- as.matrix(unique(levels_I[, mapX2I[I_ij], drop=FALSE]))
    ## colnames(levels_I_ij) <- I_ij
    comStat_ij <- stat_com(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs,
                           Is=I_ij, Ys=Y_ij, levels_Is=levels_I_ij)
    if (any(comStat_ij$n_com == 0))
      stop("Some joint levels of the discrete variables involved lack observations (n(i) == 0).")

    ## cat("Es_com_ij=\n")
    ## print(comStat_ij$Es_com)
    ## cat("Ess_com_ij=\n")
    ## print(comStat_ij$Ess_com)
    ## cat("n_com_ij=", comStat_ij$n_com,"\n")
  }

  mdiff <- 1
  while (mdiff > tol) {
    sufstat_j <- stat_mis(X, I_j, Y_j, levels_I_j, comStat_j, I, levels_I, mapX2I, Y, mapX2Y, p, mu, Sigma)
    sufstat_i <- stat_mis(X, I_i, Y_i, levels_I_i, comStat_i, I, levels_I, mapX2I, Y, mapX2Y, p, mu, Sigma)
    sufstat_ij <- stat_mis(X, I_ij, Y_ij, levels_I_ij, comStat_ij, I, levels_I, mapX2I, Y, mapX2Y, p, mu, Sigma)

    K <- sufstat_j$K + sufstat_i$K - sufstat_ij$K
    h <- sufstat_j$h + sufstat_i$h - sufstat_ij$h
    m <- sufstat_j$m*sufstat_i$m/sufstat_ij$m

    if (all(K == matrix(0,ncol=length(Y), nrow=length(Y)))){
      Sigma_update <- 0
    } else {
      Sigma_update <- solve(K)
    }
    mu_update <- t(Sigma_update%*%t(h))
    conv <- convergence(Sigma_update=Sigma_update, mu_update=mu_update, m_update=m, Sigma=Sigma, mu=mu, m=p*n)
    mdiff <- conv$mdiff
    p <- conv$m/n
    mu <- conv$mu
    Sigma <- conv$Sigma
  }

  mdiff <- 1
  p <- p0
  mu <- mu0
  Sigma <- Sigma0
  comStat <- stat_com(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs, Is=I, Ys=Y, levels_Is=levels_I)

  while (mdiff > tol) {
    sufstat <- stat_mis(X, I, Y, levels_I, comStat, I, levels_I, mapX2I, Y, mapX2Y, p, mu, Sigma)

    ssd <- sufstat$ssd
    bar_y <- sufstat$bar_y
    m <- sufstat$m

    conv <- convergence(Sigma_update=ssd/n, mu_update=bar_y, m_update=m, Sigma=Sigma, mu=mu, m=p*n)
    mdiff <- conv$mdiff
    p <- conv$m/n
    mu <- conv$mu
    Sigma <- conv$Sigma
  }

  list(ssd_j=sufstat_j$ssd, ssd_i=sufstat_i$ssd, ssd_ij=sufstat_ij$ssd, ssd=sufstat$ssd)
}

.qpCItestHMGM <- function(X, I, nLevels, Y, ssdMat, mapX2ssdMat, i, j, Q,
                          exact.test=TRUE, use=c("complete.obs", "em"), tol=0.01,
                          R.code.only=FALSE ) {

  if (!is.null(ssdMat)) {
    p <- (d <- dim(ssdMat))[1]
    if (p != d[2] || !isSymmetric(ssdMat))
      stop("ssdMat is not squared and symmetric. Is it really an ssd matrix?\n")
    if (!is(ssdMat, "SsdMatrix"))
      stop(".qpCItestHMGM: the ssdMat argument should be an object of class SsdMatrix\n")
  }

  if (all(!is.na(match(c(i,j), I))))
    stop("i and j cannot be both discrete at the moment")

  if (!R.code.only) {
    return(.qpFastCItestHMGM(X, I, nLevels, Y, ssdMat, mapX2ssdMat,
                                       i, j, Q, exact.test, use, tol))
  }

  if (!is.na(match(j, I))) { ## if any of (i,j) is discrete, it should be i
    tmp <- i
    i <- j
    j <- tmp
  }
  ## cat ("\n", i, "ci", j,"\n")


  I <- intersect(I, c(i, Q))
  Y <- intersect(Y, c(i, j, Q))

  if (!is.na(match(i, I)))                   ## put the continuous i or j (if i is discrete)
    Y <- c(Y[match(j, Y)], Y[-match(j, Y)])  ## as first continuous variable in the margin
  else
    Y <- c(Y[match(i, Y)], Y[-match(i, Y)])

  ssd <- ssd_i <- ssd_j <- ssd_ij <- diag(2) 
  n_co <- n <- nrow(X)

  ## build logical mask of missing observations
  missingMask <- apply(X[, c(I, Y), drop=FALSE], 1, function(x) any(is.na(x)))
  missingData <- any(missingMask)

  rss0 <- NA
  if (!missingData || use == "complete.obs") { ## either no missing data or use complete obs
    n_co <- n - sum(missingMask)

    if (!missingData && !is.null(ssdMat) && length(I) == 0)
      ssd <- ssdMat[mapX2ssdMat[Y], mapX2ssdMat[Y], drop=FALSE]
    else
      ssd <- .ssdStatsCompleteObs(X, I, Y, missingMask)
    ## cat("ssd:\n")
    ## print(as.matrix(ssd))

    ## ssd_i = ssd_Gamma when i is discrete or ssd_{Gamma\i} when i is continuous
    if (!missingData && !is.null(ssdMat) && length(setdiff(I, i)) == 0)
      ssd_i <- ssdMat[mapX2ssdMat[setdiff(Y, i)], mapX2ssdMat[setdiff(Y, i)], drop=FALSE]
    else
      ssd_i <- .ssdStatsCompleteObs(X, setdiff(I, i), setdiff(Y, i), missingMask)
    ## cat("ssd_i:\n")
    ## print(as.matrix(ssd_i))

    if (length(setdiff(Y, j)) > 0) {
      ssd_j <- .ssdStatsCompleteObs(X, I, setdiff(Y, j), missingMask)
      ## cat("ssd_j:\n")
      ## print(ssd_j)
      if (length(setdiff(Y, c(i,j))) > 0) {
        if (!missingData && !is.null(ssdMat) && length(setdiff(I, i)) == 0)
          ssd_ij <- ssdMat[mapX2ssdMat[setdiff(Y, c(i, j))],
                           mapX2ssdMat[setdiff(Y, c(i, j))], drop=FALSE]
        else
          ssd_ij <- .ssdStatsCompleteObs(X, setdiff(I, i), setdiff(Y, c(i, j)), missingMask)
        ## cat("ssd_j:\n")
        ## print(ssd_ij)
      }
    }
    rss0 <- var(X[!missingMask, Y[1]])*(n_co-1)
  } else { ## missing data and should use the EM algorithm
    missingMask2 <- apply(X[, Y, drop=FALSE], 1, function(x) any(is.na(x)))
    if (length(I) == 0 || any(missingMask2))
      stop("EM not implemented yet for missing values in continuous variables. Please set use=\"complete.obs\"\n")

    mapX2Y <- rep(NA, ncol(X))
    mapX2Y[Y] <- 1:length(Y)
    mapX2I <- rep(NA, ncol(X))
    mapX2I[I] <- 1:length(I)

    idxMissingObs <- which(missingMask)
    idxCompleteObs <- setdiff(1:n, idxMissingObs)
    mapAllObs2MissingObs <- rep(NA, n)
    mapAllObs2MissingObs[idxMissingObs] <- 1:length(idxMissingObs)
    ssdMats <- .ssdStatsEM(X, idxCompleteObs, idxMissingObs, mapAllObs2MissingObs,
                                       I, mapX2I, nLevels, Y, mapX2Y, i, j, Q, tol)
    ssd <- as.matrix(ssdMats$ssd)
    ssd_i <- as.matrix(ssdMats$ssd_i)
    ssd_j <- as.matrix(ssdMats$ssd_j)
    ssd_ij <- as.matrix(ssdMats$ssd_ij)
    rss0 <- var(X[, Y[1]])*(n-1) ## assuming there is no missing data in Y
  }
 
  ## calculate residual sums of squares for estimating effect size as partial eta-squared
  ## assuming response continuous variable is on the first row and column
  ## REMINDER: this might need a flag to avoid doing this calculation for the sake of speed
  if (!is.na(match(i, I))) { ## if i is discrete, j is the response and RSS1 = SSD_i
    if (nrow(ssd_i) > 1)
      rss1 <- as.vector(ssd_i[1, 1] - ssd_i[1, -1] %*% solve(ssd_i[-1, -1]) %*% ssd_i[-1, 1])
    else
      rss1 <- as.matrix(ssd_i)[1, 1]
  } else {                   ## otherwise, when both i, j are continuous, i is the response and RSS1 = SSD_j
    if (nrow(ssd_j) > 1)
      rss1 <- as.vector(ssd_j[1, 1] - ssd_j[1, -1] %*% solve(ssd_j[-1, -1]) %*% ssd_j[-1, 1])
    else
      rss1 <- as.matrix(ssd_j)[1, 1]
  }

  if (nrow(ssd) > 1)
    rss2 <- as.vector(ssd[1, 1] - ssd[1, -1] %*% solve(ssd[-1, -1]) %*% ssd[-1, 1])
  else
    rss2 <- as.matrix(ssd)[1, 1]

  ssd <- determinant(ssd)         ## WATCH OUT! when using Matrix::determinant(..., logarithm=TRUE)
  ssd_i <- determinant(ssd_i)     ## $modulus is always 0, don't know why. since this is its default
  ssd_j <- determinant(ssd_j)     ## this argument is not being put explicitly in the call
  ssd_ij <- determinant(ssd_ij)   ## keep an eye in case the default ever changes
  final_sign <- ssd$sign * ssd_ij$sign * ssd_j$sign *ssd_i$sign

  lr <- parEta2hat <- NaN
  p.value <- df <- a <- b <- NA
  stat <- param <- n.value <- method <- alt <- NA

  zero_boundary <- log(.Machine$double.eps)
  if (ssd$modulus > zero_boundary && ssd_ij$modulus > zero_boundary &&
      ssd_j$modulus > zero_boundary && ssd_i$modulus > zero_boundary &&
      final_sign == 1) {
    mixedEdge <- sum(!is.na(match(c(i, j), I))) > 0
    nGamma <- length(Y)
    Delta <- I
    DeltaStar <- setdiff(I, c(i, j))
    llr <- ssd$modulus[1]+ssd_ij$modulus[1]-ssd_j$modulus[1]-ssd_i$modulus[1] ## log-likelihood ratio
    parEta2hat <- (rss1 - rss2) / rss0
    names(parEta2hat) <- "partial eta2"
    if (exact.test) {
      lr <- exp(llr)
      a <- (n_co-nGamma-prod(nLevels[Delta])+1)/2 ## by now missing data w/ complete.obs
      b <- ifelse(mixedEdge,
                  prod(nLevels[DeltaStar])*(nLevels[intersect(Delta, c(i,j))]-1)/2,
                  0.5)
      if (a > 0 && b > 0)
        p.value <- c(less = pbeta(q=lr, shape1=a, shape2=b, lower.tail=TRUE))
      else {
        p.value <- a <- b <- NA
        names(p.value) <- "less"
      }
      stat <- lr
      names(stat) <- "Lambda"
      param <- c(a=a, b=b, n=ifelse(use == "em", n, n_co))
      n.value <- c("Lambda" = 1)
      method  <- "Conditional independence test for homogeneous mixed data using an exact likelihood ratio test"
      alt <- "less"
    } else {
      lr <- -n_co * llr
      df <- 1
      if (mixedEdge)
        df <- prod(nLevels[DeltaStar])*(nLevels[intersect(Delta, c(i, j))]-1)
      stat <- lr
      names(stat) <- "-n log Lambda"
      param <- c(df=df, n=ifelse(use == "em", n, n_co))
      p.value <- c(greater = 1 - pchisq(lr, df=df, lower.tail=TRUE))
      n.value <- c("-n log Lambda" = 0)
      method  <- "Conditional independence test for homogeneous mixed data using an asymptotic likelihood ratio test"
      alt <- "greater"
    }
  }

  RVAL <- list(statistic=stat,
               parameter=param,
               p.value=if (use != "em") p.value else NA_real_, ## p-values currently not valid with EM
               estimate=parEta2hat,
               null.value=n.value,
               alternative=alt,
               method=method,
               data.name=sprintf("%s and %s given {%s}", names(i), names(j), paste(names(Q), collapse=", ")))
  class(RVAL) <- "htest"

  RVAL
}

setMethod("qpAllCItests", signature(X="matrix"),
          function(X, I=NULL, Q=NULL, pairup.i=NULL, pairup.j=NULL,
                   long.dim.are.variables=TRUE, exact.test=TRUE,
                   use=c("complete.obs", "em"), tol=0.01,
                   return.type=c("p.value", "statn", "all"), verbose=TRUE,
                   R.code.only=FALSE, clusterSize=1, estimateTime=FALSE,
                   nAdj2estimateTime=10) {

            use <- match.arg(use)
            return.type <-  match.arg(return.type)

            startTime <- c(user.self=0, sys.self=0, elapsed=0, user.child=0, sys.child=0)
            class(startTime) <- "proc_time"
            if (estimateTime)
              startTime <- proc.time()

            if (clusterSize > 1 && R.code.only)
              stop("Using a cluster (clusterSize > 1) only works with R.code.only=FALSE\n")

            if (clusterSize > 1 &&
               (!.qpIsPackageInstalled("snow") || !.qpIsPackageInstalled("Rmpi")))
              stop("Using a cluster (clusterSize > 1) requires first installing packages 'snow' and 'Rmpi'\n")

            if (long.dim.are.variables &&
                sort(dim(X),decreasing=TRUE,index.return=TRUE)$ix[1] == 1)
              X <- t(X)

            if (is.null(colnames(X))) 
              colnames(X) <- 1:ncol(X)

            .qpAllCItests(X, I, Q, pairup.i, pairup.j,
                          exact.test, use, tol, return.type,
                          verbose, R.code.only, clusterSize,
                          startTime, nAdj2estimateTime)
          })

.qpAllCItests <- function(X, I=NULL, Q=NULL, pairup.i=NULL, pairup.j=NULL,
                          exact.test=TRUE, use=c("complete.obs", "em"), tol=0.01,
                          return.type=c("p.value", "statn", "all"), verbose=TRUE,
                          R.code.only=FALSE, clusterSize=1, startTime, nAdj2estimateTime=10) {

  cl <- NULL

  if (use == "em" && !R.code.only)
    stop("use=\"em\" does not work yet with R.code.only=FALSE\n")
 
  if (is.numeric(clusterSize) || is.integer(clusterSize)) {
    if (clusterSize > 1) {
      ## copying ShortRead's strategy, 'get()' are to quieten R CMD check, and for no other reason
      ## makeCl <- get("makeCluster", mode="function")
      ## clSetupRNG <- get("clusterSetupRNG", mode="function")
      ## clEvalQ <- get("clusterEvalQ", mode="function")
      ## clExport <- get("clusterExport", mode="function")
      ## clApply <- get("clusterApply", mode="function")
      ## stopCl <- get("stopCluster", mode="function")
      ## clCall <- get("clusterCall", mode="function")
      ## clOpt <- get("getClusterOption", mode="function")

      if (startTime["elapsed"] == 0)
        message("Testing conditional independences using a ", snow::getClusterOption("type"),
                " cluster of ", clusterSize, " nodes\n")
      else
        message("Estimating time of testing conditional independences using a ", snow::getClusterOption("type"),
                " cluster of ", clusterSize, " nodes\n")

      ## REPLACEMENT OF PACKAGE rlecuyer
      ## cl <- makeCl(clusterSize, snowlib=system.file(package="qpgraph"))
      ## clSetupRNG(cl)
      cl <- parallel::makeCluster(clusterSize, type="MPI", snowlib=system.file(package="qpgraph"))
      parallel::clusterSetRNGStream(cl)
      res <- parallel::clusterEvalQ(cl, require(qpgraph, quietly=TRUE))
      if (!all(unlist(res))) {
        parallel::stopCluster(cl)
        stop("The package 'qpgraph' could not be loaded in some of the nodes of the cluster")
      }
      assign("clusterSize", clusterSize, envir=.GlobalEnv)
      parallel::clusterExport(cl, list("clusterSize"))
      rm("clusterSize", envir=.GlobalEnv)
      parallel::clusterApply(cl, 1:clusterSize, function(x) assign("clusterRank", x, envir=.GlobalEnv))
    }
  } else {
    if (!is.na(match("cluster", class(clusterSize))))
      cl <- clusterSize
    else
      stop("Unknown class for argument clusterSize:", class(clusterSize))
  }

  ## X the matrix of data with columns as r.v. and rows as multivariate observations

  var.names <- colnames(X)
  n.var <- ncol(X)
  N <- nrow(X)

  ## check that the q parameter has proper values

  q <- length(Q)

  if (q > n.var - 2)
    stop(paste("q=",q," > p-2=", n.var-2))

  if (q < 0)
    stop(paste("q=",q," < 0"))

  if (q > N - 3)
    stop(paste("q=",q," > n-3=",N-3))

  ## check whether there are discrete variables and whether they're properly set

  nLevels <- rep(NA_integer_, times=ncol(X))
  Y <- NULL
  if (!is.null(I) && length(I) > 0) {
    if (is.character(I)) {
      if (any(is.na(match(I, var.names))))
        stop("Some variables in I do not form part of the variable names of the data in X\n")
      I <- match(I, var.names)
    } else {
      if (any(is.na(match(I, 1:n.var))))
        stop("Some variables in I do not form part of the variables of the data in X\n")
    }
    nLevels[I] <- apply(X[, I, drop=FALSE], 2, function(x) nlevels(as.factor(x)))
    if (any(nLevels[I] == 1))
      stop(sprintf("Discrete variable %s has only one level", colnames(X)[I[nLevels[I]==1]]))

    Y <- (1:n.var)[-I]
  }

  if ((!is.null(pairup.i) && is.null(pairup.j)) ||
      (is.null(pairup.i) && !is.null(pairup.j)))
    stop("pairup.i and pairup.j should both either be set to NULL or contain subsets of variables\n")

  if (is.null(pairup.i) || length(pairup.i) == 0)
    pairup.i <- 1:n.var
  else {
    if (is.character(pairup.i)) {
      if (any(is.na(match(pairup.i, var.names))))
        stop("Some variables in pairup.i do not form part of the variable names of the data in X\n")
      pairup.i <- match(pairup.i, var.names)
    }
  }

  if (is.null(pairup.j) || length(pairup.j) == 0) {
    pairup.j <- 1:n.var
    if (!is.null(I)) { ## by now, interactions between discrete variables are not considered
        pairup.j <- (1:n.var)[-I]
    }
  } else {
    if (is.character(pairup.j)) {
      if (any(is.na(match(pairup.j, var.names))))
        stop("Some variables in pairup.j do not form part of the variable names of the data in X\n")
      pairup.j <- match(pairup.j, var.names)
    }
  }

  if (!is.null(Q) && length(Q) > 0) {
    if (is.character(Q)) {
      if (any(is.na(match(Q, var.names))))
        stop("Some variables in Q do not form part of the variable names of the data\n")
      Q <- match(Q, var.names)
    } else {
      if (any(is.na(match(Q, 1:n.var))))
        stop("Some variables in Q do not form part of the variables of the data\n")
    }

    ## variables in Q are removed from the pairs for which CI tests are performed
    pairup.i <- setdiff(pairup.i, Q)
    pairup.j <- setdiff(pairup.j, Q)

    if (length(pairup.i) == 0 || length(pairup.j) == 0)
      stop("Cannot condition on Q. Either Q is too large or there are too few variable pairs to test.")
  }

  ## pair the two sets pairup.i and pairup.j without pairing the same variable
  l.pairup.i <- length(pairup.i)
  l.pairup.j <- length(pairup.j)
  l.int <- length(intersect(pairup.i, pairup.j))
  l.pairup.i.noint <- l.pairup.i - l.int
  l.pairup.j.noint <- l.pairup.j - l.int
  n.adj <- l.int * l.pairup.j.noint + l.int * l.pairup.i.noint +
           l.pairup.i.noint * l.pairup.j.noint + l.int * (l.int - 1) / 2

  pairup.ij.int <- intersect(pairup.i, pairup.j)
  pairup.i.noint <- setdiff(pairup.i, pairup.ij.int)
  pairup.j.noint <- setdiff(pairup.j, pairup.ij.int)

  pvstatn <- list(p.value=NA, statistic=NA, n=NA)

  if (!R.code.only) {
    elapsedTime <- 0
    if (startTime["elapsed"] > 0) {
      elapsedTime <- (proc.time() - startTime)["elapsed"]
      startTime <- proc.time()
    }

    if (is.null(cl)) { ## single-processor execution

      cit <- .qpFastAllCItests(X, I, Y, Q, pairup.i.noint,
                               pairup.j.noint, pairup.ij.int,
                               exact.test, use, tol, return.type, verbose,
                               startTime["elapsed"], nAdj2estimateTime)

      if (startTime["elapsed"] == 0) {
        if (return.type == "all" || return.type == "p.value") {
          pvstatn$p.value <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                                 Dimnames=list(var.names, var.names), x=cit$p.value)
        }
        if (return.type == "all" || return.type == "statn") {
          pvstatn$statistic <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                                   Dimnames=list(var.names, var.names), x=cit$statistic)
          pvstatn$n <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                           Dimnames=list(var.names, var.names), x=as.numeric(cit$n))
        }
      } else ## completion time estimation comes directly as a vector
        pvstatn <- cit

    } else {           ## use a cluster !
      ## clCall <- get("clusterCall", mode="function")
      valIdx <- list()
      if (verbose && startTime["elapsed"] == 0) { ## no cluster progress-call when only estimating time
        valIdx <- clPrCall(cl, .qpFastAllCItestsPar, n.adj, X, I, Y, Q,
                           pairup.i.noint, pairup.j.noint, pairup.ij.int,
                           exact.test, use, tol, return.type, verbose, FALSE, nAdj2estimateTime)
      } else {
        valIdx <- parallel::clusterCall(cl, .qpFastAllCItestsPar, X, I, Y, Q,
                                        pairup.i.noint, pairup.j.noint, pairup.ij.int,
                                        exact.test, use, tol, return.type, verbose, startTime["elapsed"] > 0,
                                        nAdj2estimateTime)
      }

      if (startTime["elapsed"] > 0) {
        ## the following calculation makes important part of the estimation of the time
        ## it assumes that the estimated time per processor is stored on the first position of 'p.value'
        ## and uses the median of the times estimated for each processor to try to be robust against
        ## fluctuations on the execution time taken in some processors
        elapsedTime <- elapsedTime + median(sapply(valIdx, function(x) x$p.value[1]))
        startTime <- proc.time()
      }

      if (is.numeric(clusterSize) || is.integer(clusterSize))
        parallel::stopCluster(cl)

        if (return.type == "all" || return.type == "p.value") {
          pvstatn$p.value <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                                 Dimnames=list(var.names, var.names),
                                 x=rep(as.double(NA), n.var*(n.var-1)/2+n.var)) 
          pvstatn$p.value@x[do.call("c", lapply(valIdx, function(x) x$idx))] <-
            do.call("c", lapply(valIdx, function(x) x$p.value))
        }

        if (return.type == "all" || return.type == "statn") {
          pvstatn$statistic <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                                   Dimnames=list(var.names, var.names),
                                   x=rep(as.double(NA), n.var*(n.var-1)/2+n.var)) 
          pvstatn$statistic@x[do.call("c", lapply(valIdx, function(x) x$idx))] <-
            do.call("c", lapply(valIdx, function(x) x$statistic))

          pvstatn$n <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                           Dimnames=list(var.names, var.names),
                           x=rep(as.double(NA), n.var*(n.var-1)/2+n.var)) 
          pvstatn$n@x[do.call("c", lapply(valIdx, function(x) x$idx))] <-
            do.call("c", lapply(valIdx, function(x) as.numeric(x$n)))
        }

      if (startTime["elapsed"] > 0) {
        elapsedTime <- elapsedTime + (proc.time() - startTime)["elapsed"]
        d <- as.vector(floor(elapsedTime / (24*3600)))
        h <- as.vector(floor((elapsedTime-d*24*3600)/3600))
        m <- as.vector(floor((elapsedTime-d*24*3600-h*3600)/60))
        s <- as.vector(ceiling(elapsedTime-d*24*3600-h*3600-m*60))
        pvstatn <- c(days=d, hours=h, minutes=m, seconds=s)
      }
    }

    return(pvstatn)
  }

  missingMask <- apply(X[, , drop=FALSE], 1, function(x) any(is.na(x)))
  missingData <- any(is.na(missingMask))

  S <- ssd <- mapX2ssd <- NULL
  if (!missingData) {
    if (!is.null(I)) {  ## calculate the uncorrected sum of squares and deviations
      ssd <- qpCov(X[, Y, drop=FALSE], corrected=FALSE)
      mapX2ssd <- match(var.names, colnames(ssd))
      ## names(mapX2ssd) <- colnames(X) ## is this necessary?
    } else             ## calculate sample covariance matrix
      S <- qpCov(X)
  }

  ## return an efficiently stored symmetric matrix
  if (return.type == "all" || return.type == "p.value") {
    pvstatn$p.value <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                           Dimnames=list(var.names, var.names),
                           x=rep(as.double(NA), n.var*(n.var-1)/2+n.var))
  }

  if (return.type == "all" || return.type == "statn") {
    pvstatn$statistic <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                             Dimnames=list(var.names, var.names),
                             x=rep(as.double(NA), n.var*(n.var-1)/2+n.var))
    pvstatn$n <- new("dspMatrix", Dim=as.integer(c(n.var, n.var)),
                     Dimnames=list(var.names, var.names),
                     x=rep(as.double(NA), n.var*(n.var-1)/2+n.var))
  }

  elapsedTime <- 0
  if (startTime["elapsed"] > 0) {
    elapsedTime <- (proc.time() - startTime)["elapsed"]
    startTime <- proc.time()
  }

  ppct <- -1
  k <- 0
  pb <- NULL
  if (verbose && elapsedTime == 0)
    pb <- txtProgressBar(style=3)

  cit <- NA ### build return object depending on return.type CONTINUE HERE !!!!

  ## intersection variables against ij-exclusive variables
  for (i in pairup.ij.int) {
    for (j in c(pairup.i.noint,pairup.j.noint)) {

      if (is.null(I)) {
        Xsub <- X[, c(i, j, Q), drop=FALSE]
        S <- qpCov(Xsub)
        cit <- .qpCItest(S, 1L, 2L, 2L+seq(along=Q), R.code.only=TRUE)
      } else
        cit <- .qpCItestHMGM(X, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                                       exact.test, use, tol, R.code.only=TRUE)

      if (return.type == "all" || return.type == "p.value")
        pvstatn$p.value[j,i] <- pvstatn$p.value[i,j] <- cit$p.value
      if (return.type == "all" || return.type == "statn") {
        pvstatn$statistic[j,i] <- pvstatn$statistic[i,j] <- cit$statistic
        pvstatn$n[j,i] <- pvstatn$n[i,j] <- cit$param["n"]
      }

      k <- k + 1
      if (elapsedTime > 0 && k == nAdj2estimateTime)
        break;
      pct <- floor((k * 100) / n.adj)
      if (pct != ppct && verbose && elapsedTime == 0) {
        setTxtProgressBar(pb, pct/100)
        ppct <- pct
      }
    }
    if (elapsedTime > 0 && k == nAdj2estimateTime)
      break;
  }

  ## i-exclusive variables against j-exclusive variables
  if (elapsedTime == 0 || k < nAdj2estimateTime) {
    for (i in pairup.i.noint) {
      for (j in pairup.j.noint) {

        if (is.null(I)) {
          Xsub <- X[, c(i, j, Q), drop=FALSE]
          S <- qpCov(Xsub)
          cit <- .qpCItest(S, 1L, 2L, 2L+seq(along=Q), R.code.only=TRUE)
        } else
          cit <- .qpCItestHMGM(X, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                                         exact.test, use, tol, R.code.only=TRUE)

        if (return.type == "all" || return.type == "p.value")
          pvstatn$p.value[j,i] <- pvstatn$p.value[i,j] <- cit$p.value
        if (return.type == "all" || return.type == "statn") {
          pvstatn$statistic[j,i] <- pvstatn$statistic[i,j] <- cit$statistic
          pvstatn$n[j,i] <- pvstatn$n[i,j] <- cit$param["n"]
        }

        k <- k + 1
        if (elapsedTime > 0 && k == nAdj2estimateTime)
          break;
        pct <- floor((k * 100) / n.adj)
        if (pct != ppct && verbose && elapsedTime == 0) {
          setTxtProgressBar(pb, pct/100)
          ppct <- pct
        }
      }
      if (elapsedTime > 0 && k == nAdj2estimateTime)
        break;
    }
  }

  ## intersection variables against themselves (avoiding pairing of the same)
  if (elapsedTime == 0 || k < nAdj2estimateTime) {
    for (i in seq(along=pairup.ij.int[-1])) {
      i2 <- pairup.ij.int[i]

      for (j in (i+1):l.int) {
        j2 <- pairup.ij.int[j]

        if (is.null(I)) {
          Xsub <- X[, c(i, j, Q), drop=FALSE]
          S <- qpCov(Xsub)
          cit <- .qpCItest(S, 1L, 2L, 2L+seq(along=Q), R.code.only=TRUE)
        } else
          cit <- .qpCItestHMGM(X, I, nLevels, Y, ssd, mapX2ssd, i2, j2, Q,
                                         exact.test, use, tol, R.code.only=TRUE)

        if (return.type == "all" || return.type == "p.value")
          pvstatn$p.value[j2,i2] <- pvstatn$p.value[i2,j2] <- cit$p.value
        if (return.type == "all" || return.type == "statn") {
          pvstatn$statistic[j2,i2] <- pvstatn$statistic[i2,j2] <- cit$statistic
          pvstatn$n[j2,i2] <- pvstatn$n[i2,j2] <- cit$param["n"]
        }

        k <- k + 1
        if (elapsedTime > 0 && k == nAdj2estimateTime)
          break;
        pct <- floor((k * 100) / n.adj)
        if (pct != ppct && verbose && elapsedTime == 0) {
          setTxtProgressBar(pb, pct/100)
          ppct <- pct
        }
      }
      if (elapsedTime > 0 && k == nAdj2estimateTime)
        break;
    }
  }

  if (verbose && elapsedTime == 0) {
    close(pb)
  }

  if (elapsedTime > 0) {
    elapsedTime <- elapsedTime + ((proc.time()-startTime)["elapsed"]/k) * n.adj
    startTime <- proc.time()
  }

  ## this is necessary till we find out how to properly assign values in a dspMatrix
  ## nrrMatrix <- as(as(as(nrrMatrix, "dMatrix"), "symmetricMatrix"), "packedMatrix")

  if (elapsedTime > 0) {
    elapsedTime <- elapsedTime + (proc.time()-startTime)["elapsed"]
    d <- as.vector(floor(elapsedTime / (24*3600)))
    h <- as.vector(floor((elapsedTime-d*24*3600)/3600))
    m <- as.vector(floor((elapsedTime-d*24*3600-h*3600)/60))
    s <- as.vector(ceiling(elapsedTime-d*24*3600-h*3600-m*60))
    pvstatn <- c(days=d, hours=h, minutes=m, seconds=s)
  }

  return(pvstatn)
}


.qpFastCItestStd <- function(S, i, j, Q) {
  return(.Call("qp_fast_ci_test_std", S@ssd@x, nrow(S), as.integer(S@n), i, j, Q))
}

.qpFastCItestOpt <- function(S, n, i, j, Q) {
  return(.Call("qp_fast_ci_test_opt", S@ssd@x, nrow(S), as.integer(S@n), i, j, Q))
}

.qpFastCItestHMGM <- function(X, I, nLevels, Y, ssd, mapX2ssd, i, j, Q,
                              exact.test, use, tol) {
  x <- NULL
  if (!is.null(ssd)) {
    if (!is(ssd, "SsdMatrix"))
      stop(".qpFastCItestHMGM: the ssd argument should be an object of class SsdMatrix\n")
    x <- ssd@ssd@x
  }

  return(.Call("qp_fast_ci_test_hmgm", X, I, nLevels, Y, x, as.integer(mapX2ssd),
               i, j, Q, as.integer(exact.test),
               as.integer(factor(use, levels=c("complete.obs", "em"))), tol))
}

.qpFastAllCItests <- function(X, I, Y, Q, pairup.i.noint, pairup.j.noint,
                              pairup.ij.int, exact.test, use, tol, return.type,
                              verbose, startTime, nAdj2estimateTime) {
  nLevels <- apply(X[, I, drop=FALSE], 2, function(x) nlevels(as.factor(x)))
  return(.Call("qp_fast_all_ci_tests", X, as.integer(I), as.integer(nLevels),
                                      as.integer(Y), as.integer(Q),
                                      as.integer(pairup.i.noint), as.integer(pairup.j.noint),
                                      as.integer(pairup.ij.int), as.integer(exact.test),
                                      as.integer(factor(use, levels=c("complete.obs", "em"))), tol,
                                      as.integer(factor(return.type, levels=c("p.value", "statn", "all"))),
                                      as.integer(verbose), as.double(startTime),
                                      as.integer(nAdj2estimateTime), .GlobalEnv))
}

.qpFastAllCItestsPar <- function(X, I, Y, Q, pairup.i.noint, pairup.j.noint,
                                 pairup.ij.int, exact.test, use, tol, return.type,
                                 verbose, estimateTime, nAdj2estimateTime) {
  ## clOpt <- get("getClusterOption", mode="function")
  myMaster <- snow::getClusterOption("masterNode")

  startTime <- 0
  if (estimateTime)
    startTime <- proc.time()["elapsed"]

  nLevels <- apply(X[, I, drop=FALSE], 2, function(x) nlevels(as.factor(x)))

  ## clusterRank and clusterSize should have been defined by the master node
  return(.Call("qp_fast_all_ci_tests_par", X, as.integer(I), as.integer(nLevels),
                                           as.integer(Y), as.integer(Q),
                                           as.integer(pairup.i.noint), as.integer(pairup.j.noint),
                                           as.integer(pairup.ij.int), as.integer(exact.test),
                                           as.integer(factor(use, levels=c("complete.obs", "em"))), tol,
                                           as.integer(factor(return.type, levels=c("p.value", "statn", "all"))),
                                           as.integer(verbose), as.double(startTime),
                                           as.integer(nAdj2estimateTime), as.integer(get("clusterRank")),
                                           as.integer(get("clusterSize")), myMaster, .GlobalEnv))
}
