## PART THAT DEALS WITH DIFFERENTIAL METHYLATION CALCULATIONS

##############################################################################
## S3 functions to be used in S4 stuff
##############################################################################

# wrapper function for SLIM, p.adjust, qvalue-package
p.adjusted <- function(pvals,method=c("SLIM","holm","hochberg","hommel",
                                      "bonferroni","BH","BY","fdr","none",
                                      "qvalue"),
                       n=length(pvals),fdr.level=NULL,pfdr=FALSE,STA=.1,Divi=10,
                       Pz=0.05,B=100,Bplot=FALSE){
  
  method <- match.arg(method)
  
  ## perform adjustment
  qvals=switch(method,
               # SLIM function
               SLIM={QValuesfun(pvals,
                                SLIMfunc(pvals,STA=STA,Divi=Divi,Pz=Pz,B=B,
                                         Bplot=Bplot)$pi0_Est)
               },
               # r-base/p-adjust functions
               holm={p.adjust(pvals,method=method,n)},
               hochberg={p.adjust(pvals,method=method,n)},
               hommel={p.adjust(pvals,method=method,n)},
               bonferroni={p.adjust(pvals,method=method,n)},
               BH={p.adjust(pvals,method=method,n)},
               BY={p.adjust(pvals,method=method,n)},
               fdr={p.adjust(pvals,method=method,n)},
               none={p.adjust(pvals,method=method,n)},
               # r-bioconductor/qvalue-package function
               qvalue={qvalue(pvals,fdr.level,pfdr)$qvalues}
  )
  
  return(qvals)
  
}

# SLIM
######################################
#####SLIM pi0 Estimation function
#####Copyright by Tsai Lab of UGA, US, and Hong-Qiang Wang, IIM, CAS, China
#####Reference: SLIM: A Sliding Linear Model for Estimating the Proportion of 
##### True Null Hypotheses in Datasets With Dependence Structures
#####usage:
#####inputs: 
#####rawp:p-values, required
#####STA:lambda1
#####Divi: the number of segments
#####Pz: maximum of p-values of alternative tests
#####B: the number of quantile points
#####Bplot: logic, if true, drawing lambda-gamma plot
#####outputs: 
#####pi0_Est: estimated value of pi0
#####selQuantile: the value of alpha determined 

#####################################


SLIMfunc<-function(rawp,STA=.1,Divi=10,Pz=0.05,B=100,Bplot=FALSE)
{
  
  
  ####################
  m <- length(rawp) 
  
  ########################
  alpha_mtx=NULL;#
  pi0s_est_COM=NULL;
  SzCluster_mtx=NULL;
  P_pi1_mtx=NULL;
  pi1_act_mtx=NULL;
  Dist_group=NULL;
  Num_mtx=NULL;
  Gamma=NULL;
  PI0=NULL;
  
  #############
  ##observed points
  lambda_ga=seq(0,1,0.001);
  gamma_ga=sapply(lambda_ga,f1,rawp=rawp);
  Gamma=c(Gamma,gamma_ga);
  alpha_mtx=c(alpha_mtx,gamma_ga[which(lambda_ga==0.05)]);
  
  
  ###esimation
  pi0_mtx=NULL;
  x.axis=NULL;
  y.axis=NULL;
  itv=(1-STA)/Divi;
  for (i in 1:Divi)##10 for uniform data
  {
    cutoff=STA+(i/Divi)*(1-STA);##10 for uniform data
    lambda=seq(cutoff-itv,cutoff,itv/10);
    gamma_mtx=sapply(lambda,f1,rawp=rawp);
    LModel=lm(gamma_mtx~lambda);
    pi0_mtx=c(pi0_mtx,coefficients(LModel)[2]);
  }
  
  
  ##################################
  ########searching
  N_COM=NULL;
  N_rawp=NULL;
  maxFDR_mtx=NULL;
  quapoint_mtx=NULL; 
  if (B<=1) B=100;
  quapoint_mtx=seq(0.01,0.99,1/B);
  for (k in 1:length(quapoint_mtx))
  {
    qua_point=quapoint_mtx[k];
    
    pi0_combLR=min(quantile(pi0_mtx,qua_point),1);#mean(pi0_mtx);#median();
    # qua_point=0.78 for desreasing distribution;
    ##0.4 for uniform or normal distribution;
    pi0_est=pi0_combLR;
    
    ###########Calculate independent index of raw p vlaues
    PI0=rbind(PI0,pi0_mtx);
    
    pi0s_est_COM=c(pi0s_est_COM,pi0_est);
    ##Condition1
    P_pi1=sort(rawp)[max(length(rawp)*(1-pi0_est),1)];##
    P_pi1_mtx=c(P_pi1_mtx,P_pi1);
    
    pi0=pi0_est;
    if (is.null(Pz)) Pz=0.05;
    maxFDR=Pz*pi0/(1-(1-Pz)*pi0);
    maxFDR_mtx=c(maxFDR_mtx,maxFDR);
    
    qvalues_combLR=QValuesfun(rawp,pi0);
    qvalue_cf=maxFDR;
    selected=which(qvalues_combLR<qvalue_cf);
    Sel_qvalues_combLR=selected;
    
    pi1_act_mtx=c(pi1_act_mtx,length(Sel_qvalues_combLR)/length(rawp));
    N_COM=c(N_COM,list(Sel_qvalues_combLR));
    Num_mtx=c(Num_mtx,length(Sel_qvalues_combLR));
  }
  length(N_COM)
  length(quapoint_mtx)
  
  ####doing judging
  ##by max FDR
  pi1s_est_COM=1-pi0s_est_COM;
  Diff=sum(rawp<=Pz)/length(rawp)-pi1_act_mtx;
  
  ###
  loc=which.min(abs(Diff));
  Diff.loc=Diff[loc];
  selQuantile=quapoint_mtx[loc];
  pi0_Est=min(1,pi0s_est_COM[loc]);
  maxFDR.Pz=Pz*pi0_Est/(1-(1-Pz)*pi0_Est);
  
  if(Bplot)
  {
    #windows();
    par(mfrow=c(1,2));
    hist(rawp,main="Histogram of p-value");
    gamma_ga=sapply(lambda_ga,f1,rawp=rawp);
    plot(lambda_ga,gamma_ga,type="l",
         main="Relationship of p- and q value",
         xlab=expression(lambda),ylab=expression(gamma),
         cex.lab=1.45,cex.axis=1.42)
    #par(xaxp=c(0,1,10));
    #axis(1);
    ##qvalues
    qValues=QValuesfun(rawp,pi0=pi0_Est);
    gammaq_ga=sapply(lambda_ga,f1,rawp=qValues);
    lines(lambda_ga,gammaq_ga,col="blue",lwd=2,lty="dashed")
    abline(v=Pz,col="black",lwd=2,lty="dotdash")
    abline(v=maxFDR.Pz,col="blue",lwd=2,lty="dotdash")
    text(0.75,0.6,labels=paste("L=",round(abs(Diff.loc),4),sep=""));
    leg=list(bquote("CPD of p-value"),bquote("CPD of q-value"),
             bquote("Pmax"==.(Pz)),bquote("FDRmax"==.(round(maxFDR.Pz,2))));
    legend("bottomright",legend=as.expression(leg),lwd=2,
           lty=c("solid","dashed","dotdash","dotdash"),
           col=c("black","blue","black","blue"));
  }
  
  return(list(pi0_Est=pi0_Est,selQuantile=selQuantile));
}

####
f1<-function(cutoff,rawp){sum(rawp<cutoff)/length(rawp)};

###
QValuesfun<-function(rawp,pi0)
{
  order_rawp=sort(rawp);
  #order_rawp=sort(rawp,method="qu");
  qvalues=pi0*length(order_rawp)*order_rawp/c(1:length(order_rawp));
  temp=cummin(qvalues[seq(length(qvalues),1,-1)])
  qvalues=temp[seq(length(temp),1,-1)];
  qvalues=qvalues[order(order(rawp))]
}


# New calulateDiffMeth-part

logReg<-function(counts, vars, treatment, 
                 overdispersion=c("none","MN","shrinkMN"),
                 effect=c("wmean","mean","predicted"), parShrinkMN=list(), 
                 test=c("F","Chisq")){
  
  # correct counts and treatment factor for NAs in counts
  treatment<-ifelse(is.na(counts),NA,treatment)[1:length(treatment)]
  vars = vars[!is.na(treatment),] # update covariates if there are NAs
  treatment<-treatment[!is.na(treatment)]
  counts<-counts[!is.na(counts)] # update counts if there are NAs
  
  w=counts[1:(length(counts)/2)]+counts[((length(counts)/2)+1):length(counts)]
  y=counts[1:(length(counts)/2)]
  prop=y/w
  
  # get the model matrix from treatment and (optional) covariates
  vars <- as.data.frame(cbind(treatment,vars))
  
  # get formula from model matrix
  formula <-as.formula(paste("~ ", paste(colnames(vars), collapse= "+")))
  
  # if there are covariates other than treatment
  if(ncol(vars)>1){
    fmlaCov<-as.formula(paste("~ ", paste(colnames(vars)[-1], collapse= "+")))
    modelCov<-model.matrix(as.formula(fmlaCov),as.data.frame(vars[,-1,drop=FALSE]) )
    # this is only with covariates
    objCov=glm.fit(modelCov,prop,weights=w,family=binomial(link=logit))
  }
  
  # full model with all variables
  modelMat<-model.matrix( formula ,as.data.frame(vars) )
  obj=glm.fit(modelMat,prop,weights=w,family=binomial(link=logit))
  
  mu=fitted(obj)
  nprm=length(obj$coef) # number of parameters fitted
  
  #get dispersion
  overdispersion <- match.arg(overdispersion)
  phi=switch(overdispersion,
             none=1,
             MN={
               mu=fitted(obj)
               uresids <- (y-w*mu)/sqrt(mu*(w-w*mu)) # pearson residuals
               phi=sum( uresids^2 )/(length(w)-nprm) # correction factor  
               ifelse(phi>1,phi,1)
             },
             shrinkMN={
               mu=fitted(obj)
               uresids <- (y-w*mu)/sqrt(mu*(w-w*mu)) # pearson residuals
               phi=sum( uresids^2 )/(length(w)-nprm) # correction factor
               
               # change phi with parameters from parShrinkMN
               df.prior=parShrinkMN$df.prior
               var.prior=parShrinkMN$var.prior
               df.total=(length(w)-nprm)+df.prior
               phi=((length(w)-nprm)*phi + df.prior*var.prior)/df.total
               ifelse(phi>1,phi,1)
             })
  
  if(ncol(vars)>1){
    deviance <- objCov$deviance - obj$deviance
    if(deviance==0) warning("Full model does not deviate from Reduced model.")
    ddf=objCov$df.residual-obj$df.residual
  }else{
    deviance <- obj$null.deviance - obj$deviance
    ddf=obj$df.null-obj$df.residual # difference in degrees of freedom
  }
  
  test=match.arg(test)
  
  # do F-test when overdispersion >1 given
  test=ifelse(test=="F" & phi>1,"F","Chisq")
  
  p.value=switch(test,
                 F={
                   pf(deviance/phi, ddf, (length(w)-nprm), lower.tail = FALSE)     
                 },
                 Chisq={
                   pchisq(deviance/phi, 1, lower.tail = FALSE)
                 })
  if(is.na(p.value)) {
    warning("Replacing NaN with 1.")
    p.value <- 1
  }
  
  
  #calculate effect size
  effect <- match.arg(effect)
  meths=switch(effect,
               wmean={
                 #ms=tapply(y, as.factor(vars$treatment), sum )
                 ms=tapply(y,treatment,sum)
                 #ws=tapply(w, as.factor(vars$treatment), sum )
                 ws=tapply(w,treatment,sum)
                 ms/ws  
               },
               mean={
                 tapply(prop, treatment, FUN = mean)
               },
               predicted={
                 tapply(mu, treatment, FUN = mean)
               }
  )
  
  # calculate the difference
  # if more than two groups calculate is as abs(max difference between two groups)
  if(length(unique(treatment))>2){
    meth.diff=max(meths)-min(meths)
  }else{
    meth.diff=meths[2]-meths[1]
  }
  
  names(meths)=paste0("meth_",names(meths))
  c(meth.diff=100*meth.diff,p.value=p.value,q.value=p.value,100*meths)
}

# estimation of shrinkage parameters
estimateShrinkageMN<-function(cntlist,treatment,covariates,
                              sample.size=100000,mc.cores=1){
  
  message("Estimating shrinkage for scaling factor phi...")
  
  # get formula and construct model matrix
  vars <- as.data.frame(cbind(treatment,covariates))
  formula <-as.formula(paste("~ ", paste(colnames(vars), collapse= "+")))
  modelMat<-model.matrix( formula ,as.data.frame(vars) )
  
  # check number of samples
  sample.size <- ifelse(length(cntlist)<=100000,length(cntlist),sample.size)
  
  # calculate phis up to the first 100000 sites (stabilizing point)
  estimation=simplify2array(
    mclapply(cntlist[1:sample.size],estimatePhi,modelMat=modelMat,treatment=treatment,
             mc.cores=mc.cores))
  
  # for each phi, take the correct df (depending on number of model parameters)
  phis<-estimation[1,]
  df<-estimation[2,]
  
  # squeeze sample variances 
  shr=squeezeVar(phis,df)
  
  # output prior df and variances (to be used later as input for parShrinkMN)
  list(df.prior=shr$df.prior,var.prior=shr$var.prior)
}

estimatePhi<-function(counts,modelMat,treatment){
  
  # correct modelMat, counts and treatment factor for NAs in counts
  treatment<-treatment[!is.na(counts)[1:length(treatment)]]
  modelMat <- modelMat[!is.na(counts)[1:nrow(modelMat)],]
  counts<-counts[!is.na(counts)]
  
  n=counts[1:(length(counts)/2)]+counts[((length(counts)/2)+1):length(counts)]
  y=counts[1:(length(counts)/2)]
  prop=y/n
  
  glmfit=glm.fit(modelMat,prop,weights=n,family=binomial(link=logit))
  
  # fit glm
  mu <- fitted(glmfit)
  nprm=length(glmfit$coef) # number of parameters fitted
  
  # calculate and record results
  resids <- (y-n*mu)/sqrt(mu*(n-n*mu))

  # get phi correction coefficients 
  phi <- sum( resids^2 )/(length(n)-nprm)
  c(phi,(length(n)-nprm))
}

# A FASTER VERSION OF FISHERs EXACT
fast.fisher<-function (x, y = NULL, workspace = 2e+05, hybrid = FALSE, control = list(), 
                       or = 1, alternative = "two.sided", conf.int = TRUE, conf.level = 0.95, 
                       simulate.p.value = FALSE, B = 2000, cache=F) 
{
  if (nrow(x)!=2 | ncol(x)!=2) stop("Incorrect input format for fast.fisher")
  #if (cache) {
  #  key = paste(x,collapse="_")
  # cachedResult = hashTable[[key]]
  #  if (!is.null(cachedResult)) {
  #    return(cachedResult)
  #  }
  #}
  # ---- START: cut version of fisher.test ----
  DNAME <- deparse(substitute(x))
  METHOD <- "Fisher's Exact Test for Count Data"
  nr <- nrow(x)
  nc <- ncol(x)
  PVAL <- NULL
  if ((nr == 2) && (nc == 2)) {
    m <- sum(x[, 1])
    n <- sum(x[, 2])
    k <- sum(x[1, ])
    x <- x[1, 1]
    lo <- max(0, k - n)
    hi <- min(k, m)
    NVAL <- or
    names(NVAL) <- "odds ratio"
    support <- lo:hi
    logdc <- dhyper(support, m, n, k, log = TRUE)
    dnhyper <- function(ncp) {
      d <- logdc + log(ncp) * support
      d <- exp(d - max(d))
      d/sum(d)
    }
    mnhyper <- function(ncp) {
      if (ncp == 0) 
        return(lo)
      if (ncp == Inf) 
        return(hi)
      sum(support * dnhyper(ncp))
    }
    pnhyper <- function(q, ncp = 1, upper.tail = FALSE) {
      if (ncp == 1) {
        if (upper.tail) 
          return(phyper(x - 1, m, n, k, lower.tail = FALSE))
        else return(phyper(x, m, n, k))
      }
      if (ncp == 0) {
        if (upper.tail) 
          return(as.numeric(q <= lo))
        else return(as.numeric(q >= lo))
      }
      if (ncp == Inf) {
        if (upper.tail) 
          return(as.numeric(q <= hi))
        else return(as.numeric(q >= hi))
      }
      d <- dnhyper(ncp)
      if (upper.tail) 
        sum(d[support >= q])
      else sum(d[support <= q])
    }
    if (is.null(PVAL)) {
      PVAL <- switch(alternative, less = pnhyper(x, or), 
                     greater = pnhyper(x, or, upper.tail = TRUE), 
                     two.sided = {
                       if (or == 0) 
                         as.numeric(x == lo)
                       else if (or == Inf) 
                         as.numeric(x == hi)
                       else {
                         relErr <- 1 + 10^(-7)
                         d <- dnhyper(or)
                         sum(d[d <= d[x - lo + 1] * relErr])
                       }
                     })
      if (PVAL > 1)
        PVAL = floor(PVAL)
      RVAL <- list(p.value = PVAL)
    }
    mle <- function(x) {
      if (x == lo) 
        return(0)
      if (x == hi) 
        return(Inf)
      mu <- mnhyper(1)
      if (mu > x) 
        uniroot(function(t) mnhyper(t) - x, c(0, 1))$root
      else if (mu < x) 
        1/uniroot(function(t) mnhyper(1/t) - x, c(.Machine$double.eps, 
                                                  1))$root
      else 1
    }
    ESTIMATE <- mle(x)
    #names(ESTIMATE) <- "odds ratio"
    RVAL <- c(RVAL, estimate = ESTIMATE, null.value = NVAL)
  }
  RVAL <- c(RVAL, alternative = alternative, method = METHOD, data.name = DNAME)
  attr(RVAL, "class") <- "htest"
  # ---- END: cut version of fisher.test ----    
  #if (cache) hashTable[[key]] <<- RVAL # write to global variable
  return(RVAL)                                                                         
}


## suggested by @zellerivo https://github.com/al2na/methylKit/issues/96
midPval <- function (cntg_table) # very fast fisher
{
  q <- cntg_table[1, 1]
  m <- cntg_table[1, 1] + cntg_table[2, 1]
  n <- cntg_table[1, 2] + cntg_table[2, 2]
  k <- cntg_table[1, 1] + cntg_table[1, 2]
  pval_right <- phyper(q = q, m = m, n = n, k = k, lower.tail = FALSE) + 
    (0.5 * dhyper(q, m, n, k))
  pval_left <- phyper(q = q - 1, m = m, n = n, k = k, lower.tail = TRUE) + 
    (0.5 * dhyper(q, m, n, k))
  return(ifelse(test = pval_right > pval_left, yes = pval_left * 
                  2, no = pval_right * 2))
}


## checks before running .calculateDiffMeth
.checksCalculateDiffMeth <- function(treatment,covariates,Tcols){ 
  
  if(length(treatment) < 2 )
    stop("can not do differential methylation calculation with less ",
         "than two samples")
  
  if(length(unique(treatment)) < 2 )
    stop("can not do differential methylation calculation when there ",
         "is no control\n",
         "treatment option should have 0 and 1 designating treatment ",
         "and control samples")
  
  if(length(unique(treatment)) == 2 )
    message("two groups detected:\n ",
            "will calculate methylation difference as the difference of\n",
            sprintf("treatment (group: %s) - control (group: %s)",
                    levels(as.factor(treatment))[2],
                    levels(as.factor(treatment))[1]))
  
  if(length(unique(treatment)) > 2 )
    message("more than two groups detected:\n ",
            "will calculate methylation difference as the difference ",
            "of max(x) - min(x),\n ",
            "where x is vector of mean methylation per group per region,",
            "but \n the statistical test will remain the same.")
  
  #### check if covariates+intercept+treatment more than replicates 
  if(!is.null(covariates)){if(ncol(covariates)+2 >= length(Tcols)){
    stop("Too many covariates/too few replicates.")}}
  
}

.calculateDiffMeth <- function(subst, 
                               treatment, 
                               covariates=NULL,
                               overdispersion=c("none",
                                                "MN","shrinkMN"),
                               adjust=c("SLIM","holm","hochberg",
                                        "hommel", "bonferroni","BH",
                                        "BY","fdr", "none","qvalue"),
                               effect=c("wmean","mean","predicted"),
                               parShrinkMN=list(),
                               test=c("F","Chisq",
                                      "fast.fisher","midPval"),
                               mc.cores=1,
                               slim=TRUE,
                               weighted.mean=TRUE) {
  
  # add backwards compatibility with old parameters
  if(slim==FALSE) adjust="BH" else adjust=adjust
  if(weighted.mean==FALSE) effect="mean" else effect=effect
  
  vars <- covariates
  
  # get C and T cols from methylBase data.frame-part
  Tcols = seq(from = 7,to = ncol(subst), by=3)
  Ccols = Tcols-1
  
  cnt_matrix <- as.matrix(subst[, c(Ccols, Tcols)])
  # unique rows in matrix; output indices in cnt_matrix as well
  un_cnt <- mgcv::uniquecombs(cnt_matrix)
  index <- attr(un_cnt, "index")
  
  # message(sprintf(" lookup table reduced number of calculations by %.2f %%",
  #                 (1-nrow(un_cnt)/nrow(cnt_matrix))*100 ) )
  
  if( length(index) == 1 ) 
    un_cnt <- matrix(un_cnt,ncol = 4,byrow = TRUE)
  # split count matrix
  cntlist = split(un_cnt,
                  f = 1:nrow(un_cnt))
  
  test=match.arg(test)
  
  if(length(treatment)==2 ){
    
    # do fast.fisher by default
    if(test %in% c("F", "Chisq")) {
      
      default_pair_test = "fast.fisher" 
      
      message(sprintf(c("\n NOTE: performing '%s' instead of '%s' ",
                        "for two groups testing."),
                      default_pair_test, test))
      
      test = default_pair_test
    }
    
    fpval <- switch(test, 
                    fast.fisher={
                      unlist( mclapply( cntlist, 
                                        function(x) fast.fisher(
                                          matrix(as.numeric( x),
                                                 ncol=2,byrow=F),
                                          conf.int = F)$p.value,
                                        mc.cores=mc.cores,
                                        mc.preschedule = TRUE) )
                    },
                    midPval = {
                      unlist( mclapply( cntlist, 
                                        function(x) midPval(
                                          matrix(as.numeric( x),
                                                 ncol=2,byrow=F)),
                                        mc.cores=mc.cores,
                                        mc.preschedule = TRUE) )
                    } 
    )
    
    # map pvals back to whole set of cytosine positions
    fpval <- fpval[index]
    
    # set1 is the high, set2 is the low level of the group 
    set1.Cs=Ccols[treatment==levels(as.factor(treatment))[2]]
    set2.Cs=Ccols[treatment==levels(as.factor(treatment))[1]]
    
    # calculate mean methylation change
    mom.meth1    = 100*(subst[,set1.Cs]/subst[,set1.Cs-1]) # get % methylation
    mom.meth2    = 100*(subst[,set2.Cs]/subst[,set2.Cs-1])
    # get difference between percent methylations
    mom.mean.diff=mom.meth1-mom.meth2 
    x=data.frame(subst[,1:4],fpval,
                 p.adjusted(fpval,method=adjust),
                 meth.diff=mom.mean.diff,stringsAsFactors=FALSE)
    colnames(x)[5:7] <- c("pvalue","qvalue","meth.diff")
    
  }else{        
    
    # do fast.fisher by default
    if(test %in% c("fast.fisher","midPval")) {
      
      default_multi_test = "Chisq" 
      
      message(sprintf(c("\n NOTE: performing '%s' instead of '%s' ",
                        "for more than two groups testing."),
                      default_multi_test, test))
      test = default_multi_test
      
    }
    
    # get count matrix and make list
    cntlist=split(as.matrix(subst[,c(Ccols,Tcols)]),1:nrow(subst))
    
    # call estimate shrinkage before logReg
    if(overdispersion[1] == "shrinkMN"){
      parShrinkMN<-estimateShrinkageMN(cntlist,
                                       treatment=treatment,
                                       covariates=vars,
                                       sample.size=100000,
                                       mc.cores=mc.cores)
    }
    
    # get the result of tests
    tmp=simplify2array(
      mclapply(cntlist,logReg,vars,treatment=treatment,
               overdispersion=overdispersion,effect=effect,
               parShrinkMN=parShrinkMN,test=test,mc.cores=mc.cores))
    
    # return the data frame part of methylDiff
    tmp <- as.data.frame(t(tmp))
    x=data.frame(subst[,1:4],tmp$p.value,
                 p.adjusted(tmp$q.value,method=adjust),
                 meth.diff=tmp[,1],
                 stringsAsFactors=FALSE)
    colnames(x)[5:7] <- c("pvalue","qvalue","meth.diff")
  }
  
  return(x)
  
}


# end of S3 functions

##############################################################################
## S4 OBJECTS
##############################################################################


# methylDiff --------------------------------------------------------------

#' An S4 class that holds differential methylation information
#'
#' This class is designed to hold statistics and locations for differentially 
#' methylated regions/bases. It extends \code{\link{data.frame}} class.
#' \code{\link{calculateDiffMeth}} function returns an object 
#' with \code{methylDiff} class.
#'          
#' @section Slots:\describe{
#'    \item{\code{sample.ids}}{ids/names of samples in a vector}
#'    \item{\code{assembly}}{a name of genome assembly, such as :hg18,mm9, etc}
#'    \item{\code{context}}{numeric vector identifying which samples are which
#'    group }
#'    \item{\code{treatment}}{numeric vector identifying which samples are which
#'     group }
#'    \item{\code{destranded}}{logical denoting if methylation inormation is
#'     destranded or not}
#'    \item{\code{resolution}}{string either 'base' or 'region' defining the 
#'    resolution of methylation information}
#'    \item{\code{.Data}}{data.frame holding the locations and statistics}
#'
#' }
#' 
#' @section Details:
#' \code{methylDiff} class extends \code{\link{data.frame}} class therefore
#'  providing novice and experienced R users with a data structure that is 
#'  well known and ubiquitous in many R packages.
#' 
#' @section Subsetting:
#'  In the following code snippets, \code{x} is a \code{methylDiff} object.
#'  Subsetting by \code{x[i,]} will produce a new object if subsetting is done 
#'  on rows. Column subsetting is not directly allowed to prevent errors in the 
#'  downstream analysis. see ?methylKit[ .
#' 
#' @section Coercion:
#'   \code{methylDiff} object can be coerced to 
#'   \code{\link[GenomicRanges:GRanges-class]{GRanges}} object via \code{\link{as}} function.
#' 
#' @section Accessors: 
#' The following functions provides access to data slots of methylDiffDB:
#' - \code{\link{getData}}: get the data slot from the methylKit objects,
#' - \code{\link{getAssembly}}: get assembly of the genome,
#' - \code{\link{getContext}}: get the context of methylation
#' 
#' @examples
#' data(methylKit)
#' library(GenomicRanges)
#' my.gr=as(methylDiff.obj,"GRanges")
#' 
#' @name methylDiff-class
#' @aliases methylDiff
#' @rdname methylDiff-class
#' @export
#' @docType class
setClass("methylDiff",representation(
  sample.ids = "character", assembly = "character",context = "character",
  treatment="numeric",destranded="logical",resolution="character"),
  contains="data.frame")


##############################################################################
#### S4 FUNCTIONS #### 
##############################################################################

# calculateDiffMeth ----------------------------------------------------------

#' Calculate differential methylation statistics
#' 
#' The function calculates differential methylation statistics between two groups 
#' of samples. The function uses either logistic regression test
#' or Fisher's Exact test to calculate differential methylation. 
#' See the rest of the help page and 
#' references for detailed explanation on statistics.
#' 
#' @param .Object a \code{\link{methylBase}} or \code{\link{methylBaseDB}} object to calculate differential
#'  methylation                    
#' @param covariates a \code{\link{data.frame}} containing covariates, which should be 
#' included in the test.                   
#' @param overdispersion If set to "none"(default), no overdispersion correction 
#' will be attempted.
#'              If set to "MN", basic overdispersion correction, 
#'              proposed by McCullagh and Nelder (1989) will be applied.This
#'              correction applies a scaling parameter to variance estimated
#'              by the model.
#'              EXPERIMENTAL: If set to "shrinkMN", scaling parameter will be
#'              shrunk towards a common value  (not thoroughly tested as of yet).
#' @param adjust different methods to correct the p-values for multiple testing. 
#'              Default is "SLIM" from methylKit. For "qvalue" please see 
#'              \code{\link[qvalue]{qvalue}} 
#'              and for all other methods see \code{\link[stats]{p.adjust}}.
#' @param effect method to calculate the mean methylation different between groups 
#'              using read coverage as weights (default). When set to "mean", 
#'              the generic mean is applied
#'              and when set to "predicted", predicted means from the logistic
#'              regression model is used for calculating the effect.
#' @param parShrinkMN a list for squeezeVar(). (NOT IMPLEMENTED)
#' @param test the statistical test used to determine the methylation differences. 
#'              The Chisq-test is used by default for more than two groups, 
#'              while the F-test can be chosen if overdispersion control 
#'              is applied. If there is one sample per group 
#'              the Fisher's exact test will be applied using "fast.fisher", while 
#'              "midPval" can be choosen to boost calculation speed. 
#'              See details section for more information.  
#' @param mc.cores integer denoting how many cores should be used for parallel
#'              differential methylation calculations (can only be used in
#'              machines with multiple cores).
#' @param slim If set to FALSE, \code{adjust} will be set to "BH" (default 
#'              behaviour of earlier versions)
#' @param weighted.mean If set to FALSE, \code{effect} will be set to "mean" 
#'                      (default behaviour of earlier versions)  
#' @param chunk.size Number of rows to be taken as a chunk for processing the 
#'                    \code{methylBaseDB} objects (default: 1e6)
#' @param save.db A Logical to decide whether the resulting object should be 
#'                saved as flat file database or not, default: explained in 
#'                Details section.
#' @param ... optional Arguments used when save.db is TRUE
#'            
#'            \code{suffix}
#'                  A character string to append to the name of the output 
#'                  flat file database, 
#'                  only used if save.db is true, default actions:  
#'                  The default suffix is a 13-character random string appended 
#'                  to the fixed prefix \dQuote{methylDiff}, e.g. 
#'                  \dQuote{methylDiff_16d3047c1a254.txt.bgz}. 
#'                  
#'                  
#'            \code{dbdir} 
#'                  The directory where flat file database(s) should be stored, 
#'                  defaults
#'                  to getwd(), working directory for newly stored databases
#'                  and to same directory for already existing database
#'                  
#'           \code{dbtype}
#'                  The type of the flat file database, currently only option 
#'                  is "tabix"
#'                  (only used for newly stored databases)             
#'                    
#' 
#' @examples
#' 
#' data(methylKit)
#' 
#' # The Chisq-test will be applied when no overdispersion control is chosen.
#' my.diffMeth=calculateDiffMeth(methylBase.obj,covariates=NULL,
#'                               overdispersion=c("none"),
#'                               adjust=c("SLIM"))
#' 
#' # pool samples in each group
#' pooled.methylBase=pool(methylBase.obj,sample.ids=c("test","control"))
#'  
#' # After applying the pool() function, there is one sample in each group.
#' # The Fisher's exact test will be applied for differential methylation.
#' my.diffMeth2=calculateDiffMeth(pooled.methylBase,covariates=NULL,
#'                                overdispersion=c("none"),
#'                                adjust=c("SLIM"))
#'                                
#' # Covariates and overdispersion control:
#' # generate a methylBase object with age as a covariate
#' covariates=data.frame(age=c(30,80,30,80))
#' sim.methylBase<-dataSim(replicates=4,sites=1000,treatment=c(1,1,0,0),
#'                         covariates=covariates,
#'                         sample.ids=c("test1","test2","ctrl1","ctrl2"))
#' 
#' # Apply overdispersion correction and include covariates 
#' # in differential methylation calculations.
#' my.diffMeth3<-calculateDiffMeth(sim.methylBase,
#'                                 covariates=covariates,
#'                                 overdispersion="MN",test="Chisq",mc.cores=1)
#'                                
#' @return a \code{\link{methylDiff}} object containing the differential methylation 
#'                      statistics and locations for regions or bases
#' @section Details:
#' Covariates can be included in the analysis. The function will then try to 
#' separate the 
#' influence of the covariates from the treatment effect via a linear model.\cr
#' The Chisq-test is used per default only when no overdispersion correction is
#'  applied.
#' If overdispersion correction is applied, the function automatically switches 
#' to the 
#' F-test. The Chisq-test can be manually chosen in this case as well, but the 
#' F-test only 
#' works with overdispersion correction switched on. \cr
#' If there is one sample in each group, e.g. after applying the pooling samples, 
#' the Fisher's exact test will be applied for differential methylation. 
#' methyKit offers two implementations to perform this test, which yield 
#' slightly different results but differ much in computation time. 
#' "fast.fisher" is a cut down version `fisher.test()` that should produce 
#' the exact same results as the base implementation, while "midPval" 
#' will produce marginaly different p-values, but offers a large boost in 
#' calculation speed. 
#' 
#' The parameter \code{chunk.size} is only used when working with 
#' \code{methylBaseDB} objects, as they are read in chunk by chunk to enable 
#' processing large-sized objects which are stored as flat file database.
#' Per default the chunk.size is set to 1M rows, which should work for most systems. 
#' If you encounter memory problems or 
#' have a high amount of memory available feel free to adjust the \code{chunk.size}.
#' 
#' The parameter \code{save.db} is per default TRUE for 
#' methylDB objects as \code{methylBaseDB}, 
#' while being per default FALSE for \code{methylBase}. 
#' If you wish to save the result of an 
#' in-memory-calculation as flat file database or if the size of the database 
#' allows the calculation in-memory, 
#' then you might want to change the value of this parameter.
#' 
#' @references Altuna Akalin, Matthias Kormaksson, Sheng Li,
#'             Francine E. Garrett-Bakelman, Maria E. Figueroa, Ari Melnick, 
#'             Christopher E. Mason. (2012). 
#'             "methylKit: A comprehensive R package for the analysis 
#'             of genome-wide DNA methylation profiles." Genome Biology. 
#'             
#'             McCullagh and Nelder. (1989). Generalized Linear Models. Chapman
#'             and Hall. London New York.
#'             
#'             Barnard. (1989). On alleged gains in power from lower P-values.
#'             Statistics in Medicine. 
#'             Armitage and Berry. (1994) Statistical Methods in Medical Research (3rd
#'             edition). Blackwell.
#' @seealso \code{\link{pool}}, \code{\link{reorganize}}
#'          \code{\link{dataSim}}
#' 
#' @export
#' @docType methods
#' @aliases calculateDiffMeth,methylBase-method
#' @rdname calculateDiffMeth-methods

setGeneric("calculateDiffMeth", function(.Object,covariates=NULL,
                                         overdispersion=c("none",
                                                          "MN","shrinkMN"),
                                         adjust=c("SLIM","holm","hochberg",
                                                  "hommel", "bonferroni","BH",
                                                  "BY","fdr", "none","qvalue"),
                                         effect=c("wmean","mean","predicted"),
                                         parShrinkMN=list(),
                                         test=c("F","Chisq",
                                                "fast.fisher","midPval"),
                                         mc.cores=1,
                                         slim=TRUE,
                                         weighted.mean=TRUE,
                                         chunk.size=1e6,
                                         save.db=FALSE,
                                         ...) {
           
  standardGeneric("calculateDiffMeth")
}
)


setMethod("calculateDiffMeth", "methylBase",
          function(.Object,covariates,overdispersion,
                   adjust,effect,parShrinkMN,
                   test,mc.cores,slim,weighted.mean,save.db=FALSE, ...){
            
            # check object before starting calculations
            .checksCalculateDiffMeth(treatment = .Object@treatment,
                                     covariates = covariates,
                                     Tcols = .Object@numTs.index)
            
            # extract data.frame from methylBase
            subst=S3Part(.Object,strictS3 = TRUE)        
            
            # run calculation
            x = .calculateDiffMeth(subst = subst, 
                               treatment = .Object@treatment, 
                               covariates = covariates, 
                               overdispersion = overdispersion, 
                               adjust = adjust, 
                               effect = effect,
                               parShrinkMN = parShrinkMN, 
                               test = test, 
                               slim = slim, 
                               weighted.mean = weighted.mean,
                               mc.cores = mc.cores)
      
            if(!save.db) {
              
              obj=new("methylDiff",x,sample.ids=.Object@sample.ids,
                      assembly=.Object@assembly,context=.Object@context,
                      destranded=.Object@destranded,treatment=.Object@treatment,
                      resolution=.Object@resolution)
              
              return(obj)
            
              } else {
              
                # catch additional args 
                args <- list(...)
                
                if( !( "dbdir" %in% names(args)) ){
                  dbdir <- .check.dbdir(getwd())
                } else { dbdir <- .check.dbdir(args$dbdir) }
                #                         if(!( "dbtype" %in% names(args) ) ){
                #                           dbtype <- "tabix"
                #                         } else { dbtype <- args$dbtype }
                if(!( "suffix" %in% names(args) ) ){
                  suffix <- NULL
                } else { 
                  suffix <- paste0("_",args$suffix)
                }
                
                # create methylDiffDB
                obj <- makeMethylDiffDB(df=x,dbpath=dbdir,dbtype="tabix",
                                 sample.ids=.Object@sample.ids,
                                 assembly=.Object@assembly,context=.Object@context,
                                 destranded=.Object@destranded,
                                 treatment=.Object@treatment,
                                 resolution=.Object@resolution,suffix=suffix )
                
                message(paste0("flatfile located at: ",obj@dbpath))
                
                return(obj)
            }
  
  }
)



##############################################################################
## ACESSOR FUNCTIONS FOR methylDiff OBJECT
##############################################################################


#' @rdname show-methods
#' @aliases show,methylDiff
setMethod("show", "methylDiff", function(object) {
  
  cat("methylDiff object with",nrow(object),"rows\n--------------\n")
  print(head(object))
  cat("--------------\n")
  cat("sample.ids:",object@sample.ids,"\n")
  cat("destranded",object@destranded,"\n")
  cat("assembly:",object@assembly,"\n")
  cat("context:", object@context,"\n")
  cat("treament:", object@treatment,"\n")
  cat("resolution:", object@resolution,"\n")
})

#' @rdname getContext-methods
#' @aliases getContext,methylDiff-method
setMethod("getContext", signature="methylDiff", definition=function(x) {
  return(x@context)
})


#' @rdname getAssembly-methods
#' @aliases getAssembly,methylDiff-method
setMethod("getAssembly", signature="methylDiff", definition=function(x) {
  return(x@assembly)
})


# a function for getting data part of methylDiff    
#' @rdname getData-methods
#' @aliases getData,methylDiff-method
setMethod(f="getData", signature="methylDiff", definition=function(x) {
  #return(as(x,"data.frame"))
  return(S3Part(x, strictS3 = TRUE))
}) 


#' @rdname getTreatment-methods
#' @aliases getTreatment,methylDiff-method
setMethod("getTreatment", signature = "methylDiff", function(x) {
  return(x@treatment)
})

#' @rdname getTreatment-methods
#' @aliases getTreatment,methylDiff-method
setReplaceMethod("getTreatment", signature = "methylDiff", function(x, value) {
  
  if(! ( length(x@treatment) == length(value) ) ){
    stop("The new treatment vector is not valid, check the length of input")
  } else {
    x@treatment <- value
    return(x)
  }
  
})


#' @rdname getSampleID-methods
#' @aliases getSampleID,methylDiff-method
setMethod("getSampleID", signature = "methylDiff", function(x) {
  return(x@sample.ids)
})

#' @rdname getSampleID-methods
#' @aliases getSampleID,methylDiff-method
setReplaceMethod("getSampleID", signature = "methylDiff", function(x, value) {
  
  if(! ( length(value) == length(x@sample.ids) ) ){
    stop("The vector of new sample ids is not valid, check the length of input")
  } else {
    x@sample.ids <- value
    return(x)
  }
  
})


##############################################################################
## CONVERTOR FUNCTIONS FOR methylDiff OBJECT
##############################################################################

setAs("methylDiff", "GRanges", function(from)
{
  
  GRanges(seqnames=as.character(from$chr),ranges=IRanges(start=from$start, 
                                                         end=from$end),
          strand=from$strand, 
          pvalue = from$pvalue,
          qvalue=from$qvalue,
          meth.diff=from$meth.diff
  )
  
})



### subset methylDiff


#' @aliases select,methylDiff-method
#' @rdname select-methods
setMethod("select", "methylDiff",
          function(x, i)
          {
            if( max(i) > nrow(x)  )
              stop("subscript contains out-of-bounds indices")

            new("methylDiff",getData(x)[i,],
                sample.ids = x@sample.ids,
                assembly = x@assembly,
                context = x@context,
                treatment=x@treatment,
                destranded=x@destranded,
                resolution=x@resolution)
          }
)

#' @rdname extract-methods
#' @aliases [,methylDiff,ANY,ANY,ANY-method
#' @aliases extract,methylDiff,ANY-method
setMethod("[","methylDiff", 
          function(x,i,j){
            #cat(missing(i),"\n",missing(j),"\n",missing(drop))
            if(!missing(j)){
              stop(paste("subsetting on columns is not allowed for",class(x),
                         "object\nif you want to do extraction on the data part", 
                         "of the object use getData() first"),
                   call. = FALSE)
            }
            select(x,i)
          }
)

#' @aliases selectByOverlap,methylDiff-method
#' @rdname selectByOverlap-methods
setMethod("selectByOverlap", c("methylDiff","GRanges"),
          .selectByOverlap
)


#' get differentially methylated regions/bases based on cutoffs 
#' 
#' The function subsets a \code{\link{methylDiff}} or \code{\link{methylDiffDB}} 
#' object in order to get 
#' differentially methylated bases/regions
#' satisfying thresholds.
#' 
#' @param .Object  a \code{\link{methylDiff}} or \code{\link{methylDiffDB}} object
#' @param difference  cutoff for absolute value of methylation percentage change 
#'                    between test and control (default:25)
#' @param qvalue  cutoff for qvalue of differential methylation statistic 
#'                (default:0.01) 
#' @param type  one of the "hyper","hypo" or "all" strings. Specifies what type 
#'              of differentially menthylated bases/regions should be returned.
#'              For retrieving Hyper-methylated regions/bases type="hyper", 
#'              for hypo-methylated type="hypo" (default:"all") 
#' @param chunk.size Number of rows to be taken as a chunk for processing the 
#'                   \code{methylDiffDB} objects (default: 1e6)
#' @param save.db A Logical to decide whether the resulting object should be 
#'                saved as flat file database or not, default: see Details  
#' @param ... optional Arguments used when save.db is TRUE
#'            
#'            \code{suffix}
#'                  A character string to append to the name of the output flat 
#'                  file database, 
#'                  only used if save.db is true, default actions: append 
#'                  \dQuote{_type} to current filename 
#'                  if database already exists or generate new file with 
#'                  filename \dQuote{methylDiff_type}
#'                  
#'            \code{dbdir} 
#'                  The directory where flat file database(s) should be stored, 
#'                  defaults
#'                  to getwd(), working directory for newly stored databases
#'                  and to same directory for already existing database
#'                  
#'            \code{dbtype}
#'                 The type of the flat file database, currently only 
#'                  option is "tabix"
#'                  (only used for newly stored databases)
#' 
#' @return a methylDiff or methylDiffDB object containing the differential 
#' methylated locations satisfying the criteria 
#' 
#' @examples
#' 
#' data(methylKit)
#' 
#' # get differentially methylated bases/regions with specific cutoffs
#' all.diff=getMethylDiff(methylDiff.obj,difference=25,qvalue=0.01,type="all")
#' 
#' # get hyper-methylated
#' hyper=getMethylDiff(methylDiff.obj,difference=25,qvalue=0.01,type="hyper")
#' 
#' # get hypo-methylated
#' hypo=getMethylDiff(methylDiff.obj,difference=25,qvalue=0.01,type="hypo")
#' 
#' @section Details:
#' The parameter \code{chunk.size} is only used when working with 
#' \code{methylDiffDB} objects, 
#' as they are read in chunk by chunk to enable processing large-sized objects 
#' which are stored as flat file database.
#' Per default the chunk.size is set to 1M rows, which should work for most 
#' systems. If you encounter memory problems or 
#' have a high amount of memory available feel free to adjust the \code{chunk.size}.
#' 
#' The parameter \code{save.db} is per default TRUE for methylDB objects as 
#' \code{methylDiffDB}, 
#' while being per default FALSE for \code{methylDiff}. If you wish to save 
#' the result of an 
#' in-memory-calculation as flat file database or if the size of the database 
#' allows the calculation in-memory, 
#' then you might want to change the value of this parameter.
#'
#' @export
#' @docType methods
#' @rdname getMethylDiff-methods
setGeneric(name="getMethylDiff", def=function(.Object,difference=25,qvalue=0.01,
                                               type="all",chunk.size=1e6,
                                               save.db=FALSE,...) 
  standardGeneric("getMethylDiff"))

#' @aliases getMethylDiff,methylDiff-method
#' @rdname getMethylDiff-methods
setMethod(f="getMethylDiff", signature="methylDiff", 
          definition=function(.Object,difference,qvalue,type,save.db=FALSE,...){
            
  if(!save.db) {
  
    if(type=="all"){
      idx <- which((.Object$qvalue < qvalue) & (abs(.Object$meth.diff) > difference))
    }else if(type=="hyper"){
      idx <- which(.Object$qvalue<qvalue & (.Object$meth.diff) > difference)
    }else if(type=="hypo"){
      idx <- which(.Object$qvalue<qvalue & (.Object$meth.diff) < -1*difference)
    }else{
      stop("Wrong 'type' argument supplied for the function, it can be ",
           "'hypo', 'hyper' or 'all' ")
    }
    
    new.obj=new("methylDiff",.Object[idx,],
                sample.ids=.Object@sample.ids,assembly=.Object@assembly,
                context=.Object@context,
                treatment=.Object@treatment,destranded=.Object@destranded,
                resolution=.Object@resolution) 
    return(new.obj)
  
  } else {
    
    # catch additional args 
    args <- list(...)
    
    if( !( "dbdir" %in% names(args)) ){
      dbdir <- .check.dbdir(getwd())
    } else { dbdir <- .check.dbdir(args$dbdir) }
    #                         if(!( "dbtype" %in% names(args) ) ){
    #                           dbtype <- "tabix"
    #                         } else { dbtype <- args$dbtype }
    if(!( "suffix" %in% names(args) ) ){
      suffix <- paste0("_",type)
    } else { 
      suffix <- paste0("_",args$suffix)
    }
    
    # create methylDiffDB
    if(type=="all"){
      idx <- which(.Object$qvalue<qvalue & abs(.Object$meth.diff) > difference)
    }else if(type=="hyper"){
      idx <- which(.Object$qvalue<qvalue & (.Object$meth.diff) > difference)
    }else if(type=="hypo"){
      idx <- which(.Object$qvalue<qvalue & (.Object$meth.diff) < -1*difference)
    }else{
      stop("Wrong 'type' argument supplied for the function, it can be ",
           "'hypo', 'hyper' or 'all' ")
    }
    
    new.obj= makeMethylDiffDB(df=.Object[idx,],
                              dbpath=dbdir,dbtype="tabix",sample.ids=.Object@sample.ids,
                              assembly=.Object@assembly,context=.Object@context,
                              destranded=.Object@destranded,treatment=.Object@treatment,
                              resolution=.Object@resolution,suffix=suffix )
    return(new.obj) 
  }
}) 

##############################################################################
## PLOTTING FUNCTIONS FOR methylDiff OBJECT
##############################################################################

#' Get and plot the number of hyper/hypo methylated regions/bases per chromosome
#'
#' This function gets number of  hyper/hypo methylated regions/bases from 
#' \code{\link{methylDiff}} object. 
#' It can also plot percentages of differentially methylated bases per chromosome.
#'
#' @param x a \code{\link{methylDiff}}  object
#' @param plot TRUE|FALSE. If TRUE horizontal barplots for proportion of
#'             hypo/hyper methylated bases/regions
#' @param qvalue.cutoff  cutoff for q-value
#' @param meth.cutoff cutoff for percent methylation difference
#' @param exclude names of chromosomes to be excluded from plot
#' @param ... extra graphical parameters to be passed to \code{\link{barplot}} 
#'                  function
#' @param keep.empty.chrom keep chromosome in list / plot, even if it contains
#'   no hyper/hypo sites
#' 
#' @return plots a piechart or a barplot for percentage of the target 
#' features overlapping with annotation
#' 
#' @examples
#' 
#' data(methylKit)
#'  
#' # get a list of differentially methylated bases/regions per chromosome and overall
#' diffMethPerChr(methylDiff.obj, plot=FALSE,qvalue.cutoff=0.01, 
#'                meth.cutoff=25,exclude=NULL)
#'
#' @export
#' @docType methods
#' @rdname diffMethPerChr-methods
setGeneric("diffMethPerChr", def=function(x,
                                          plot=TRUE,
                                          qvalue.cutoff=0.01, 
                                          meth.cutoff=25,
                                          exclude=NULL,
                                          keep.empty.chrom = FALSE,
                                          ...) 
  standardGeneric("diffMethPerChr"))


.diffMethPerChr <- function(data,qvalue.cutoff, meth.cutoff, 
                            keep.empty.chrom = FALSE){
  
  data <- as.data.table(data)
  .setMethylDBNames(data,"methylDiffDB")

  ## silence R CMD Check Note   
  . = type = qvalue = meth.diff = NULL 
  
  
  data[, type := "none" ]
  data[qvalue < qvalue.cutoff & meth.diff >= meth.cutoff, type := "hyper" ]
  data[qvalue < qvalue.cutoff & meth.diff <= -meth.cutoff, type := "hypo" ]
  
  ## summarize number of hype and hyper
  number = perc = .N = NULL
  temp.hyper.hypo <- data[,
                          .(number = .N) ,
                          by = list(chr, type)][,
                                                .(type,
                                                  number ,
                                                  perc = 100 * number / sum(number)),
                                                by = chr][type != "none"]
  
  empty.chrom <- setdiff(unique(data$chr), temp.hyper.hypo$chr)
  if (length(empty.chrom) > 0 & keep.empty.chrom) {
    ## create empty result if no hyper or hypo found
    temp.hyper.hypo <- rbind(temp.hyper.hypo,
                             data.table(
                               chr = empty.chrom,
                               type = c("hypo", "hyper"),
                               number = 0,
                               perc = 0
                             ))
  }
  
  ## merge hyper hypo per chromosome
  dmc.hyper.hypo <- merge(
    temp.hyper.hypo[type == "hyper",.(chr,number,perc)],
    temp.hyper.hypo[type == "hypo",.(chr,number,perc)],
    by = "chr",
    all = TRUE
  )
  
  ## replace NA with 0
  dmc.hyper.hypo[is.na(dmc.hyper.hypo)] <- 0
  
  names(dmc.hyper.hypo)=c("chr","number.of.hypermethylated",
                          "percentage.of.hypermethylated",
                          "number.of.hypomethylated",
                          "percentage.of.hypomethylated")
  
  dmc.hyper.hypo$chr <-  as.factor(dmc.hyper.hypo$chr)
  
  return(as.data.frame(dmc.hyper.hypo))
}

.plotDiffMethPerChr <- function(dmc.hyper.hypo,exclude,qvalue.cutoff,meth.cutoff,...) {
  
  if(!is.null(exclude)) {
    dmc.hyper.hypo = dmc.hyper.hypo[!dmc.hyper.hypo$chr %in% exclude, ]
  }
  
  barplot( 
    t(as.matrix(data.frame(hyper=dmc.hyper.hypo[,3],
                           hypo=dmc.hyper.hypo[,5],
                           row.names=dmc.hyper.hypo[,1]) ))
    ,las=2,horiz=TRUE,col=c("magenta","aquamarine4"),
    main=paste("% of hyper & hypo methylated regions per chromosome",sep=""),
    xlab="% (percentage)",...)
  
  mtext(side=3,paste("qvalue<",qvalue.cutoff,
                     " & methylation diff. >=",meth.cutoff,
                     " %",sep="") )
  legend("topright",
         legend=c("hyper","hypo"),
         fill=c("magenta","aquamarine4"))
}

#' @aliases diffMethPerChr,methylDiff-method
#' @rdname  diffMethPerChr-methods
setMethod("diffMethPerChr", signature(x = "methylDiff"),
          function(x,plot,qvalue.cutoff, meth.cutoff,exclude,keep.empty.chrom,...){
            x=getData(x)
            
            # get per chrom percentages of hypo/ hyper
            dmc.hyper.hypo <- .diffMethPerChr(
              data = x,
              qvalue.cutoff = qvalue.cutoff,
              meth.cutoff = meth.cutoff,
              keep.empty.chrom = keep.empty.chrom
            )
            
            # order the chromosomes
            dmc.hyper.hypo=dmc.hyper.hypo[order(as.numeric(sub("chr","",dmc.hyper.hypo$chr))),] 
            
            # get global percentages of hypo/ hyper
            dmc.hyper = 100 * sum(dmc.hyper.hypo$number.of.hypermethylated) / nrow(x)
            dmc.hypo = 100 * sum(dmc.hyper.hypo$number.of.hypomethylated) / nrow(x)
            
            all.hyper.hypo = data.frame(
              number.of.hypermethylated = sum(dmc.hyper.hypo$number.of.hypermethylated),
              percentage.of.hypermethylated = dmc.hyper,
              number.of.hypomethylated = sum(dmc.hyper.hypo$number.of.hypomethylated),
              percentage.of.hypomethylated = dmc.hypo
            )
            
            if (all(dmc.hyper.hypo$chr %in% exclude)) {
              warning("Cannot plot figure, excluded all available chromosomes.")
              plot <- FALSE
            }
            
            if(plot){
              
              .plotDiffMethPerChr(dmc.hyper.hypo,exclude,qvalue.cutoff,meth.cutoff,...)
              
            } else {
              
              return(list(diffMeth.per.chr = dmc.hyper.hypo,
                          diffMeth.all = all.hyper.hypo))
              
            }
            
          })

