#' Smallest unique protein groups
#'
#' @description For a given vector of protein group names, outputs the names of those protein groups for which none of its member proteins is present in a smaller protein group.
#' @param proteins A vector of characters or factors containing single proteins and/or protein groups (i.e. proteins separated by a separator symbol).
#' @param split The character string that is used to separate the indivudual protein names in each protein group.
#' @return A character vector containing the names of the protein groups for which none of its proteins is present in a smaller protein group.
#' @examples
#' data(pe)
#' smallestUniqueGroups(rowData(pe[["peptide"]])$Proteins)
#' @export
smallestUniqueGroups <- function(proteins,
    split = ";") {
    b <- strsplit(x = as.character(unique(proteins)), split = split, fixed = TRUE)

    included <- vector()

    j <- 1
    while (length(b) != 0) {
        included <- c(
            included,
            vapply(
                b[vapply(b, length, integer(1)) == j],
                function(x) paste(x, collapse = split),
                character(1)
            )
        )
        a <- unlist(b[vapply(b, length, integer(1)) == j])
        b <- b[vapply(b, length, integer(1)) > j]

        if (length(b) != 0) {
            sel <- vector()
            for (i in seq_len(length(b))) {
                sel[i] <- !any(b[[i]] %in% a)
            }
            b <- b[sel]
            j <- j + 1
        }
    }

    included <- unlist(included)
    return(included)
}

#' Make contrast matrix
#'
#' @description  Construct the contrast matrix corresponding to specified contrasts
#'               of a set of parameters.
#'
#' @param contrasts character vector specifying contrasts, i.e. the linear combination of the modelparameters that equals to zero.
#'
#' @param parameterNames character vector specifying the model parameters that are involved in the contrasts, e.g if we model data of
#'        three conditions using a factor condition with three levels a, b and c then our model will have 3 mean parameters named (Intercept),
#'        conditionb and conditionc. Hence the log2 fold change between  b and a is conditionb. Under the null hypothesis the log2 fold change
#'        equals 0. Which is to be encoded as "conditionb=0". If we would like to test for log2 fold change between condition c and b we assess if
#'        the log2 fold change conditionc-conditionb equals 0, encoded as "condtionb-conditionc=0".
#'
#' @examples
#' makeContrast(c("conditionb = 0"),
#'     parameterNames = c(
#'         "(Intercept)",
#'         "conditionb",
#'         "conditionc"
#'     )
#' )
#' makeContrast(c("conditionc=0"),
#'     parameterNames = c("conditionc")
#' )
#' makeContrast(c(
#'     "conditionb=0",
#'     "conditionc=0",
#'     "conditionc-conditionb=0"
#' ),
#' parameterNames = c(
#'     "conditionb",
#'     "conditionc"
#' )
#' )
#' @return A numeric contrast matrix with rownames that equal the model parameters that are involved in the contrasts
#'
#' @rdname makeContrast
#'
#' @export


makeContrast <- function(contrasts, parameterNames) {
    return(t(.chrlinfct2matrix(contrasts, parameterNames)$K))
}

##### None exported functions from multcomp package is included here to
##### During R and Bioc checks


.chrlinfct2matrix <- function(ex, var) {
    if (!is.character(ex)) {
        stop("msqrob2:::.chrlinfct2matrix: argument ", sQuote(ex),
            " is not of type character",
            call. = FALSE
        )
    }
    if (!is.character(var)) {
        stop("msqrob2:::.chrlinfct2matrix: argument ", sQuote(var),
            " is not of type character",
            call. = FALSE
        )
    }
    K <- matrix(0, nrow = length(ex), ncol = length(var))
    colnames(K) <- var
    rownames(K) <- seq_along(ex)
    m <- rep(0, length(ex))
    for (i in seq_len(length(ex))) {
        expr <- parse(text = ex[i])
        if (length(expr[[1]]) != 3) {
            stop("msqrob2:::.chrlinfct2matrix: argument ", sQuote(ex[i]),
                " cannot be interpreted as expression",
                call. = FALSE
            )
        }
        tmp <- .expression2coef(expr, vars = var)
        if ("(Intercept)" %in% var) {
            tmp$names[tmp$names == "Intercept"] <- "(Intercept)"
        }
        if (!all(tmp$names %in% var)) {
            stop("msqrob2:::.chrlinfct2matrix: variable(s) ",
                paste(sQuote(tmp$names[!tmp$names %in% var]),
                    collapse = ", "
                ), " not found",
                call. = FALSE
            )
        }
        for (n in tmp$names) {
            K[i, var == n] <- tmp$coef[tmp$names ==
                n]
        }
        m[i] <- tmp$m
        if (i == 1) {
            alternative <- tmp$alternative
        }
        else {
            if (tmp$alternative != alternative) {
                stop("msqrob2:::.chrlinfct2matrix: mix of alternatives currently not implemented",
                    call. = FALSE
                )
            }
        }
        rownames(K)[i] <- paste0(tmp$lhs, collapse = "")
    }
    list(K = K, m = m, alternative = alternative)
}

#' @importFrom codetools walkCode makeCodeWalker
#' @importFrom methods is

.expression2coef <- function(ex, vars, debug = FALSE) {
    m.rhs <- .rhs(ex)
    m.lhs <- .lhs(ex)
    symcoef <- new.env(parent = emptyenv())
    get_coef_attr <- function(x) {
        if (is.symbol(x)) {
            get0(as.character(x), envir = symcoef, ifnotfound = NULL)
        } else {
            attr(x, "coef")
        }
    }
    set_coef_attr <- function(x, val) {
        if (is.symbol(x)) {
            if (is.null(x)) {
                rm(as.character(x), envir = symcoef)
            } else {
                assign(as.character(x), val, envir = symcoef)
            }
        }
        else {
            attr(x, "coef") <- val
        }
        x
    }
    set_coef_attr(m.lhs, 1)
    if (debug) {
        message(".expression2coef", ": lhs is ", sQuote(paste0(deparse(m.lhs),
            collapse = ""
        )))
        message(".expression2coef", ": rhs is ", sQuote(paste0(deparse(m.rhs),
            collapse = ""
        )))
    }
    effects <- walkCode(m.lhs, makeCodeWalker(handler = function(v,
    w) {
        if (debug) {
            w$trace("handler", v, w)
        }
        switch(v,
            `-` = w$sub,
            `+` = w$add,
            `*` = w$mul,
            `/` = w$div,
            `(` = w$exp,
            `:` = w$ita,
            w$eval
        )
    }, is.effect <- function(x) {
        as.character(x) %in% vars
    }, eval = function(v, w) {
        if (debug) {
            w$trace("eval", v, w)
        }
        parms <- c()
        for (e in as.list(v)[-1]) {
            coef <- 1
            if (debug) {
                message(
                    "eval", ": walking ", sQuote(e), " with coef = ",
                    coef
                )
            }
            parms <- c(parms, p <- walkCode(
                w$setCoef(e, coef),
                w
            ))
            if (is.effect(p)) {
                w$fatal(
                    "eval", "within ", sQuote(deparse(v)),
                    ", the term ", sQuote(p), " ", "must not denote an effect. Apart from that, ",
                    "the term must evaluate to a real valued constant"
                )
            }
        }
        cparms <- c()
        for (e in parms) {
            cparms <- c(cparms, parse(text = paste(e, "*", w$getCoef(e))))
        }
        if (debug) {
            dumped <- lapply(cparms, function(x, w) {
                paste(
                    x,
                    "with coef =", w$getCoef(x)
                )
            }, w)
            message("eval", ": cparms = ", w$enum(dumped))
        }
        res <- try(do.call(as.character(v[[1]]), as.list(cparms)),
            silent = TRUE
        )
        if (is(res, "try-error")) {
            w$fatal(
                "eval", "the evaluation of the expression ",
                sQuote(deparse(v)), " ", "failed with ", dQuote(attr(
                    res,
                    "condition"
                )$message)
            )
        }
        if (length(res) != 1 || !is.numeric(res) || !is.finite(res)) {
            w$fatal(
                "eval", "the expression ", sQuote(deparse(v)),
                " ", "did not evaluate to a real valued constant. ",
                "Result is ", sQuote(res)
            )
        }
        res <- w$setCoef(res * w$getCoef(v), 1)
        if (debug) {
            dumped <- lapply(res, function(x, w) {
                paste(
                    x, "with coef =",
                    w$getCoef(x)
                )
            }, w)
            message("eval", ": res = ", w$enum(dumped))
        }
        res
    }, call = function(v, w) {
        if (debug) {
            w$trace("call", v, w)
        }
        w$fatal(
            "call", "there is probably a syntax error within subexpression",
            sQuote(deparse(v))
        )
    }, sub = function(v, w) {
        if (debug) {
            w$trace("sub", v, w)
        }
        uminus <- length(as.list(v)) == 2
        minuend <- as.list(v)[2]
        subtrahend <- as.list(v)[3]
        if (debug) {
            message(
                "sub", ": minuend is ", sQuote(minuend),
                ", coef = ", w$getCoef(minuend)
            )
            message(
                "sub", ": subtrahend is ", sQuote(subtrahend),
                ", coef = ", w$getCoef(subtrahend)
            )
            message("sub", ": uminus is ", uminus)
        }
        exp.coef <- ifelse(uminus, -w$getCoef(v), w$getCoef(v))
        res <- c()
        for (e in minuend) {
            if (debug) {
                message(
                    "sub", ": walking minuend ", sQuote(e),
                    ", coef = ", exp.coef
                )
            }
            res <- c(res, walkCode(w$setCoef(e, exp.coef), w))
        }
        if (!uminus) {
            for (e in subtrahend) {
                if (debug) {
                    message(
                        "sub", ": walking subtrahend ", sQuote(e),
                        ", coef = ", -exp.coef
                    )
                }
                res <- c(res, walkCode(
                    w$setCoef(e, -exp.coef),
                    w
                ))
            }
        }
        sum <- 0
        symbols <- c()
        for (e in res) {
            if (is.numeric(e)) {
                sum <- sum + e
            } else {
                symbols <- c(symbols, e)
            }
        }
        if (length(dups <- symbols[duplicated(symbols)])) {
            w$fatal(
                "sub", "multiple occurence of ", w$enum(dups),
                " ", "found within expression ", sQuote(deparse(v))
            )
        }
        if (length(symbols) == 0) {
            return(w$setCoef(sum, 1))
        }
        if (sum) {
            w$fatal(
                "sub", "forming a difference between a constant and ",
                "an effect as in ", sQuote(deparse(v)), " ",
                "is not supported"
            )
        }
        symbols
    }, ita = function(v, w) {
        if (debug) {
            w$trace("ita", v, w)
        }
        tmp <- deparse(v)
        prefix <- gsub("^([+-]*)(.*)", "\\1", tmp)
        name <- gsub("^([+-]*)(.*)", "\\2", tmp)
        sign <- 1
        if (prefix != "") {
            for (x in base::unlist(base::strsplit(prefix, ""))) {
                sign <- sign * switch(x,
                    `-` = -1,
                    `+` = 1,
                    w$fatal(
                        "ita",
                        "strange character ", sQuote(x), " seen in ",
                        sQuote(deparse(v))
                    )
                )
            }
        }
        res <- w$setCoef(as.name(name), sign * w$getCoef(v))
        if (debug) {
            dumped <- lapply(res, function(x, w) {
                paste(
                    x, "with coef =",
                    w$getCoef(x)
                )
            }, w)
            message("ita", ": res = ", w$enum(dumped))
        }
        res
    }, exp = function(v, w) {
        if (debug) {
            w$trace("exp", v, w)
        }
        res <- c()
        for (e in as.list(v)[-1]) {
            res <- c(res, walkCode(w$setCoef(e, 1), w))
        }
        symbols <- c()
        for (e in res) {
            symbols <- c(symbols, w$setCoef(e, w$getCoef(e) *
                w$getCoef(v)))
        }
        if (debug) {
            dumped <- lapply(symbols, function(x, w) {
                paste(
                    x,
                    "with coef =", w$getCoef(x)
                )
            }, w)
            message("exp", ": res = ", w$enum(dumped))
        }
        symbols
    }, add = function(v, w) {
        if (debug) {
            w$trace("add", v, w)
        }
        res <- c()
        for (e in as.list(v)[-1]) {
            res <- c(res, walkCode(w$setCoef(e, 1), w))
        }
        symbols <- c()
        sum <- 0
        for (e in res) {
            if (is.numeric(e)) {
                sum <- sum + e
            } else {
                symbols <- c(symbols, e)
            }
        }
        if (length(symbols) == 0) {
            return(w$setCoef(sum * w$getCoef(v), 1))
        }
        if (length(dups <- symbols[duplicated(symbols)]) != 0) {
            w$fatal(
                "add", "multiple occurence of ", w$enum(dups),
                " ", "within subexpression ", sQuote(deparse(v))
            )
        }
        if (sum) {
            w$fatal(
                "add", "adding up a constant and an effect ",
                "as in ", sQuote(deparse(v)), " is not supported"
            )
        }
        res <- c()
        for (e in symbols) {
            res <- c(res, w$setCoef(e, w$getCoef(e) * w$getCoef(v)))
        }
        res
    }, mul = function(v, w) {
        if (debug) {
            w$trace("mul", v, w)
        }
        res <- c()
        for (e in as.list(v)[-1]) {
            res <- c(res, walkCode(w$setCoef(e, 1), w))
        }
        product <- 1
        symbols <- c()
        for (r in res) {
            if (is.numeric(r)) {
                product <- product * r
            } else {
                symbols <- c(symbols, r)
            }
        }
        if (product == 0 && length(symbols)) {
            w$fatal(
                "mul", "The constant part of the expression ",
                sQuote(deparse(v)), " ", "evaluates to zero. This would zero out the effect(s) ",
                sQuote(symbols)
            )
        }
        product <- product * w$getCoef(v)
        if (length(symbols) == 0) {
            return(w$setCoef(product, 1))
        }
        if (length(symbols) > 1 && all(unlist(lapply(res, is.symbol)))) {
            w$fatal(
                "mul", "the multiplication of effects ",
                w$enum(symbols), " ", "as in ", sQuote(deparse(v)),
                " is not supported"
            )
        }
        res <- c()
        for (s in symbols) {
            res <- c(res, w$setCoef(s, w$getCoef(s) * product))
        }
        if (debug) {
            dumped <- lapply(res, function(x, w) {
                paste(
                    x, "with coef =",
                    w$getCoef(x)
                )
            }, w)
            message("mul", ": res = ", w$enum(dumped))
        }
        res
    }, div = function(v, w) {
        if (debug) {
            w$trace("div", v, w)
        }
        if ((lv <- length(v)) != 3) {
            w$fatal(
                "div", "internal error: length of language object ",
                sQuote(v), " ", "is not 3, but ", lv, ". Please file a bug report"
            )
        }
        dividend <- c()
        for (e in as.list(v)[2]) {
            dividend <- c(dividend, walkCode(
                w$setCoef(e, 1),
                w
            ))
        }
        divisor <- c()
        for (e in as.list(v)[3]) {
            divisor <- c(divisor, walkCode(w$setCoef(e, 1), w))
        }
        if (length(divisor) != 1) {
            w$fatal(
                "div", "can't divide by ", sQuote(divisor),
                " in ", sQuote(deparse(v))
            )
        }
        if (any(unlist(lapply(divisor, is.effect)))) {
            w$fatal(
                "div", "cant't divide by effect ", sQuote(divisor),
                " in ", sQuote(deparse(v))
            )
        }
        if (any(unlist(lapply(divisor, is.symbol)))) {
            w$fatal(
                "div", "cant't divide by symbol ", sQuote(divisor),
                " in ", sQuote(deparse(v))
            )
        }
        divisor <- as.numeric(divisor)
        if (!is.finite(divisor) || divisor == 0) {
            w$fatal(
                "div", "can't divide by ", sQuote(divisor),
                " in ", sQuote(deparse(v))
            )
        }
        res <- c()
        for (s in dividend) {
            if (is.numeric(s)) {
                res <- c(res, s * w$getCoef(v) / divisor)
            }
            else {
                res <- c(res, w$setCoef(s, w$getCoef(s) * w$getCoef(v) / divisor))
            }
        }
        if (debug) {
            message("div", ": dividend = ", w$enum(dividend))
            message("div", ": divisor = ", w$enum(divisor))
            message("div", ": res = ", w$enum(res))
        }
        res
    }, leaf = function(e, w) {
        if (debug) {
            w$trace("leaf", e, w)
        }
        if (is.numeric(e)) {
            return(w$setCoef(e * w$getCoef(e), 1))
        }
        e
    }, getCoef = function(e) {
        a <- get_coef_attr(e)
        ifelse(is.null(a), 1, a)
    }, setCoef = function(e, coef) {
        set_coef_attr(e, coef)
    }, enum = function(x) {
        paste0("'", x, "'", collapse = ", ")
    }, fatal = function(name, ...) {
        stop(
            "msqrob2:::.expression2coef::walkCode::",
            name, ": ", ...,
            call. = FALSE
        )
    }, trace = function(fn, v, w) {
        message(
            fn, ": v = ", sQuote(v), ", mode = ", mode(v),
            ", typeof = ", typeof(v), ", length = ", length(v),
            ", coef   = ", w$getCoef(v)
        )
    }))
    if (any(idx <- is.numeric(effects))) {
        stop("msqrob2:::.expression2coef: The lhs expression ",
            sQuote(deparse(m.lhs)), " ", "contains a numeric offset term evaluating to ",
            paste0(effects[idx], collapse = ", "), ". ", "This is either an internal flaw or a misspecification from your part. ",
            "If so, please pull these offsets to the right-hand side of the equation",
            call. = FALSE
        )
    }
    effect.names <- c()
    effect.coefs <- c()
    for (effect in c(effects)) {
        effect.names <- c(effect.names, as.character(effect))
        effect.coefs <- c(effect.coefs, get_coef_attr(effect))
    }
    list(
        coef = effect.coefs, names = effect.names, m = m.rhs,
        alternative = .side(ex), lhs = deparse(m.lhs, width.cutoff = 500)
    )
}


.rhs <- function(ex) {
    if (length(ex) != 1) {
        stop("msqrob2:::.rhs: expression is not of length 1",
            call. = FALSE
        )
    }
    if (length(ex[[1]][[3]]) == 2) {
        return(-ex[[1]][[3]][[2]])
    }
    rhs <- ex[[1]][[3]]
    if (!.is_num(rhs) || length(rhs) > 1) {
        stop("msqrob2:::.rhs: right hand side of expression ",
            sQuote(ex), " is not a scalar numeric",
            call. = FALSE
        )
    }
    return(rhs)
}

.lhs <- function(ex) {
    if (length(ex) != 1) {
        stop("msqrob2:::.lhs: expression is not of length 1",
            call. = FALSE
        )
    }
    if (length(ex[[1]]) != 3) {
        stop("msqrob2:::.lhs: expression ", sQuote(ex), " does not contain a left and right hand side",
            call. = FALSE
        )
    }
    return(ex[[1]][[2]])
}

.is_num <- function(x) {
    if (length(x) == 1) {
          return(is.numeric(x))
      }
    if (length(x) == 2) {
          return(is.name(x[[1]]) && is.numeric(x[[2]]))
      }
    return(FALSE)
}

.side <- function(ex) {
    side <- .as.char(ex[[1]][[1]])
    if (!(side %in% c("<=", ">=", "==", "="))) {
        stop("msqrob2:::side: does not contain ", sQuote("<=, >=, =="),
            call. = FALSE
        )
    }
    alternative <- switch(side,
        `<=` = "greater",
        `>=` = "less",
        `==` = "two.sided",
        `=` = "two.sided"
    )
    return(alternative)
}

.as.char <- function(ex) {
    if (length(ex) == 1) {
          return(as.character(ex))
      }
    if (length(ex) == 3 && ex[[1]] == ":") {
          return(paste(.as.char(ex[[2]]), ":", .as.char(ex[[3]]),
              sep = ""
          ))
      }
    stop(
        "msqrob2:::.as.char: Failed to convert expression ",
        ex, " to character"
    )
}


## functions to generate parameter names based on a formula, 
## column data, variable name in the formula and logical indicating
## if msqrob models are fitted with or without ridge regression.
.getParamNames <- function(formula, coldata, var, ridge) {
  params <- lme4::nobars(formula) |>
    model.matrix(, data = coldata) |>
    colnames()
  params <- params[grepl(var, params)]
  if (length(params) == 0)
    stop(sQuote(var), " not found as fixed effect in formula.")
  if (any(grepl("[:]", params)))
    stop("You cannot automatically create pairwise contrasts for a variable involved in an interaction.")
  if (ridge) params <- paste0("ridge", params)
  unique(params)
}



#' Construct all contrasts for all pairwise comparisons between all levels of a factor
#'
#' @description  Construct all contrasts for all pairwise comparisons between all levels of a factor.
#'
#'
#' @param formula Model formula. The model is built based on the covariates in the 'coldata' object.
#' @param coldata data.frame or DFrame with information on the design. 
#' @param var object of type 'character' with the name of the factor in the formula for which the
#'        pairwise comparisons will be made. 
#' @param ridge logical indicating if the msqrob models are fitted with or without ridge regression. 
#'        The default is 'ridge = FALSE'
#' @param nullHypothesis object of type character that specifies the value of the contrast under the null
#'        hypothesis. The default is ' = 0'.
#'        
#' @return Vector of type character with nulhypotheses for all contrasts.
#'
#' @examples
#' # Load example data
#' # The data are a Feature object containing
#' # a SummarizedExperiment named "peptide" with MaxQuant peptide intensities
#' # The data are a subset of spike-in the human-ecoli study
#' # The variable condition in the colData of the Feature object
#' # contains information on the spike in condition a-e (from low to high)
#' data(pe)
#' 
#' # Model with intercept (conditiona is reference class)
#' createPairwiseContrasts(~ condition, colData(pe), "condition")
#' 
#' # Model without intercept
#' createPairwiseContrasts(~ -1 + condition, colData(pe), "condition")
#' 
#' @importFrom assertthat assert_that
#' @importFrom stats terms
#' @importFrom utils combn
#' @export
createPairwiseContrasts <- function(formula, coldata, var, ridge = FALSE, nullHypothesis = " = 0") {
  assertthat::assert_that(is(formula, "formula"), msg = "'formula' should be of the 'formula' class")
  assertthat::assert_that(is(coldata, "data.frame") | is(coldata, "DFrame"), msg = "'coldata' should be of class 'data.frame' or 'DFrame'")
  assertthat::assert_that(!is.numeric(coldata[[var]]), msg = "variable 'var' is not a factor")
  assertthat::assert_that(nlevels(as.factor(as.character(coldata[[var]]))) == nlevels(as.factor(coldata[[var]])), 
                          msg = "No data is available for one or more factor levels of variable 'var'")
  params <- .getParamNames(formula, coldata, var, ridge)
  if (length(params) > 1) {
    out <- combn(params, 2)
    out <- paste0(out[2, ], " - ", out[1, ])
    hasIntercept <- attr(terms(formula), "intercept")
    isFirst <- labels(terms(formula))[[1]] == var
  if (nlevels(as.factor(as.character(coldata[[var]])))>length(params))
    out <- c(params, out)} else
      out <- params
  paste0(out, nullHypothesis)
}
