## |
## |  *Group sequential design*
## |
## |  This file is part of the R package rpact:
## |  Confirmatory Adaptive Clinical Trial Design and Analysis
## |
## |  Author: Gernot Wassmer, PhD, and Friedrich Pahlke, PhD
## |  Licensed under "GNU Lesser General Public License" version 3
## |  License text can be found here: https://www.r-project.org/Licenses/LGPL-3
## |
## |  RPACT company website: https://www.rpact.com
## |  rpact package website: https://www.rpact.org
## |
## |  Contact us for information about our services: info@rpact.com
## |

#' @include f_core_constants.R
NULL

.getGroupSequentialProbabilities <- function(decisionMatrix, informationRates) {
    return(.getGroupSequentialProbabilitiesCpp(decisionMatrix, informationRates))
}

#' @title
#' Get Group Sequential Probabilities
#'
#' @description
#' Calculates probabilities in the group sequential setting.
#'
#' @param decisionMatrix A matrix with either 2 or 4 rows and kMax = length(informationRates) columns, see details.
#' @inheritParams param_informationRates
#'
#' @details
#' Given a sequence of information rates (fixing the correlation structure), and
#' decisionMatrix with either 2 or 4 rows and kMax = length(informationRates) columns,
#' this function calculates a probability matrix containing, for two rows, the probabilities:\cr
#' P(Z_1 < l_1), P(l_1 < Z_1 < u_1, Z_2 < l_2),..., P(l_kMax-1 < Z_kMax-1 < u_kMax-1, Z_kMax < l_l_kMax)\cr
#' P(Z_1 < u_1), P(l_1 < Z_1 < u_1, Z_2 < u_2),..., P(l_kMax-1 < Z_kMax-1 < u_kMax-1, Z_kMax < u_l_kMax)\cr
#' P(Z_1 < Inf), P(l_1 < Z_1 < u_1, Z_2 < Inf),..., P(l_kMax-1 < Z_kMax-1 < u_kMax-1, Z_kMax < Inf)\cr
#' with continuation matrix\cr
#' l_1,...,l_kMax\cr
#' u_1,...,u_kMax\cr
#' That is, the output matrix of the function provides per stage (column) the cumulative probabilities
#' for values specified in decisionMatrix and Inf, and reaching the stage, i.e., the test
#' statistics is in the continuation region for the preceding stages.
#' For 4 rows, the continuation region contains of two regions and the probability matrix is
#' obtained analogously (cf., Wassmer and Brannath, 2025).
#'
#' @family design functions
#'
#' @template examples_get_group_sequential_probabilities
#'
#' @return Returns a numeric matrix containing the probabilities described in the details section.
#'
#' @export
#'
getGroupSequentialProbabilities <- function(decisionMatrix, informationRates) {
    .assertAreValidInformationRates(informationRates)
    .assertIsValidDecisionMatrix(decisionMatrix, length(informationRates))

    return(.getGroupSequentialProbabilities(decisionMatrix = decisionMatrix, informationRates = informationRates))
}

.validateGroupSequentialProbabilityResultsMulti <- function(...) {
    args <- list(...)
    for (variableName in names(args)) {
        if (!.validateGroupSequentialProbabilityResults(results = args[[variableName]], variableName)) {
            return(invisible())
        }
    }
}

.validateGroupSequentialProbabilityResults <- function(results, variableName) {
    numberOfNAs <- sum(is.na(results))
    if (numberOfNAs == 0) {
        return(TRUE)
    }

    warning(sprintf(
        paste0(
            C_EXCEPTION_TYPE_RUNTIME_ISSUE,
            "in .getGroupSequentialProbabilities(): ",
            "variable '%s' contains %s NA's (%.1f%%)"
        ),
        variableName, numberOfNAs, 100 * numberOfNAs / length(results)
    ), call. = FALSE)
    return(FALSE)
}

.validateTypeOfDesign <- function(design, userDefinedInterimStops) {
    .assertDesignParameterExists(design, "typeOfDesign", C_DEFAULT_TYPE_OF_DESIGN)

    design$.setParameterType("userAlphaSpending", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("userBetaSpending", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("deltaWT", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("deltaPT1", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("deltaPT0", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("optimizationCriterion", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("gammaA", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("gammaB", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("typeBetaSpending", C_PARAM_NOT_APPLICABLE)
    design$.setParameterType("constantBoundsHP", C_PARAM_NOT_APPLICABLE)

    if (!(design$typeOfDesign %in% .getDesignTypes())) {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "type of design (", design$typeOfDesign, ") must be one of the following: ", .printDesignTypes(),
            call. = FALSE
        )
    }

    if (design$typeOfDesign == C_TYPE_OF_DESIGN_WT) {
        .assertDesignParameterExists(design, "deltaWT", NA_real_)
        .assertIsSingleNumber(design$deltaWT, "deltaWT", naAllowed = FALSE)
        .showParameterOutOfValidatedBoundsMessage(design$deltaWT, "deltaWT", lowerBound = -0.5, upperBound = 1)
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_PT) {
        .assertDesignParameterExists(design, "deltaPT1", NA_real_)
        .assertIsSingleNumber(design$deltaPT1, "deltaPT1", naAllowed = FALSE)
        .showParameterOutOfValidatedBoundsMessage(design$deltaPT1, "deltaPT1", lowerBound = -0.5, upperBound = 1)
        .assertDesignParameterExists(design, "deltaPT0", NA_real_)
        .assertIsSingleNumber(design$deltaPT0, "deltaPT0", naAllowed = FALSE)
        .showParameterOutOfValidatedBoundsMessage(design$deltaPT0, "deltaPT0", lowerBound = -0.5, upperBound = 1)
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_WT_OPTIMUM) {
        .assertDesignParameterExists(design, "optimizationCriterion", C_OPTIMIZATION_CRITERION_DEFAULT)
        if (!.isOptimizationCriterion(design$optimizationCriterion)) {
            stop(
                C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
                "optimization criterion must be one of the following: ", .printOptimizationCriterion(),
                call. = FALSE
            )
        }
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_HP) {
        .assertDesignParameterExists(design, "constantBoundsHP", C_CONST_BOUND_HP_DEFAULT)
        .assertIsSingleNumber(design$constantBoundsHP, "constantBoundsHP")
        .showParameterOutOfValidatedBoundsMessage(design$constantBoundsHP, "constantBoundsHP", lowerBound = 2)
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_AS_KD) {
        .assertDesignParameterExists(design, "gammaA", NA_real_)
        .assertIsSingleNumber(design$gammaA, "gammaA", naAllowed = FALSE)
        .showParameterOutOfValidatedBoundsMessage(design$gammaA, "gammaA",
            lowerBound = 0.4, upperBound = 8,
            spendingFunctionName = "Kim & DeMets alpha spending"
        )
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_AS_HSD) {
        .assertDesignParameterExists(design, "gammaA", NA_real_)
        .assertIsSingleNumber(design$gammaA, "gammaA", naAllowed = FALSE)
        .showParameterOutOfValidatedBoundsMessage(design$gammaA, "gammaA",
            lowerBound = -10, upperBound = 5,
            spendingFunctionName = "Hwang, Shih & DeCani alpha spending"
        )
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_AS_USER) {
        if (!userDefinedInterimStops) {
            .validateUserAlphaSpending(design)
            design$.setParameterType("userAlphaSpending", C_PARAM_USER_DEFINED)
        } else {
            design$.setParameterType("userAlphaSpending", C_PARAM_GENERATED)
            design$.setParameterType("typeOfDesign", C_PARAM_DERIVED)
        }
    }

    if (.isUndefinedArgument(design$alpha)) {
        design$alpha <- C_ALPHA_DEFAULT
        design$.setParameterType("alpha", C_PARAM_DEFAULT_VALUE)
    }

    if (design$typeOfDesign == C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY) {
        .assertIsValidAlpha(design$alpha)
        design$.setParameterType("userAlphaSpending", C_PARAM_DEFAULT_VALUE)
    }

    if ((.isBetaSpendingDesignType(design$typeBetaSpending) || !.isAlphaSpendingDesignType(design$typeOfDesign)) &&
            (design$informationRates[length(design$informationRates)] != 1)) {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "For specified design, last information rate should be equal 1",
            call. = FALSE
        )
    }

    if (.isAlphaSpendingDesignType(design$typeOfDesign)) {
        .assertDesignParameterExists(design, "typeBetaSpending", C_TYPE_OF_DESIGN_BS_NONE)

        if (!.isBetaSpendingDesignType(design$typeBetaSpending, noneIncluded = TRUE)) {
            stop(
                C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
                "type of beta spending must be one of the following: ", .printBetaSpendingDesignTypes(),
                call. = FALSE
            )
        }

        if (design$typeBetaSpending == C_TYPE_OF_DESIGN_BS_KD) {
            .assertDesignParameterExists(design, "gammaB", NA_real_)
            .assertIsSingleNumber(design$gammaB, "gammaB", naAllowed = FALSE)
            .showParameterOutOfValidatedBoundsMessage(design$gammaB, "gammaB",
                lowerBound = 0.4, upperBound = 8,
                spendingFunctionName = "Kim & DeMets beta spending", c(-0.4, 8)
            )
        }

        if (design$typeBetaSpending == C_TYPE_OF_DESIGN_BS_HSD) {
            .assertDesignParameterExists(design, "gammaB", NA_real_)
            .assertIsSingleNumber(design$gammaB, "gammaB", naAllowed = FALSE)
            .showParameterOutOfValidatedBoundsMessage(design$gammaB, "gammaB",
                lowerBound = -10, upperBound = 5,
                spendingFunctionName = "Hwang, Shih & DeCani beta spending"
            )
        }

        if (design$typeBetaSpending == C_TYPE_OF_DESIGN_BS_USER) {
            if (!userDefinedInterimStops) {
                .validateUserBetaSpending(design)
                design$.setParameterType("userBetaSpending", C_PARAM_USER_DEFINED)
            } else {
                design$.setParameterType("userBetaSpending", C_PARAM_GENERATED)
                design$.setParameterType("typeBetaSpending", C_PARAM_DERIVED)
            }
        }
    } else {
        if (.designParameterExists(design, "typeBetaSpending") && design$typeBetaSpending != C_TYPE_OF_DESIGN_BS_NONE) {
            warning("'typeBetaSpending' (", design$typeBetaSpending, ") will be ignored ",
                "because 'typeOfDesign' (", design$typeOfDesign, ") is not an alpha spending design",
                call. = FALSE
            )
            design$typeBetaSpending <- C_TYPE_OF_DESIGN_BS_NONE
            design$.setParameterType("typeBetaSpending", C_PARAM_DEFAULT_VALUE)
        }

        if (.designParameterExists(design, "userBetaSpending")) {
            userBetaSpending <- NA_real_
            warning("'userBetaSpending' (", .arrayToString(design$userBetaSpending), ") will be ignored ",
                "because 'typeOfDesign' (", design$typeOfDesign, ") is not an alpha spending design",
                call. = FALSE
            )
        }
    }

    if (.isUndefinedArgument(design$beta)) {
        design$beta <- C_BETA_DEFAULT
        design$.setParameterType("beta", C_PARAM_DEFAULT_VALUE)
    }

    return(invisible(design))
}

.validateBaseParameters <- function(design, twoSidedWarningForDefaultValues = TRUE) {
    if (.isDefinedArgument(design$kMax)) {
        .assertDesignParameterExists(design, "kMax", C_KMAX_DEFAULT)
        .assertIsValidKMax(design$kMax)

        if (.isDefinedArgument(design$informationRates)) {
            .assertAreValidInformationRates(design$informationRates, design$kMax)
        }

        if (.isDefinedArgument(design$futilityBounds)) {
            .assertAreValidFutilityBounds(design$futilityBounds, design$kMax)
        }
    }

    .assertDesignParameterExists(design, "sided", C_SIDED_DEFAULT)
    .assertIsValidSidedParameter(design$sided)

    .setKmaxBasedOnAlphaSpendingDefintion(design)

    design$informationRates <- .getValidatedInformationRates(design)
    design$futilityBounds <- .getValidatedFutilityBounds(design,
        twoSidedWarningForDefaultValues = twoSidedWarningForDefaultValues
    )

    .assertDesignParameterExists(design, "tolerance", C_DESIGN_TOLERANCE_DEFAULT)
    if (design$tolerance < 1e-10 || design$tolerance > 1e-03) {
        stop(
            C_EXCEPTION_TYPE_ARGUMENT_OUT_OF_BOUNDS,
            "'tolerance' (", design$tolerance, ") out of bounds [1e-10; 1e-03]",
            call. = FALSE
        )
    }

    return(invisible(design))
}

.createDesign <- function(...,
        designClass,
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = C_SIDED_DEFAULT,
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        typeOfDesign = C_DEFAULT_TYPE_OF_DESIGN,
        deltaWT = NA_real_,
        deltaPT1 = NA_real_,
        deltaPT0 = NA_real_,
        optimizationCriterion = C_OPTIMIZATION_CRITERION_DEFAULT,
        gammaA = NA_real_,
        typeBetaSpending = C_TYPE_OF_DESIGN_BS_NONE,
        userAlphaSpending = NA_real_,
        userBetaSpending = NA_real_,
        gammaB = NA_real_,
        bindingFutility = C_BINDING_FUTILITY_DEFAULT,
        directionUpper = NA,
        constantBoundsHP = C_CONST_BOUND_HP_DEFAULT,
        twoSidedPower = NA,
        betaAdjustment = NA,
        delayedInformation = NA_real_,
        tolerance = C_DESIGN_TOLERANCE_DEFAULT) {
    .assertIsSingleInteger(kMax, "kMax", naAllowed = TRUE, validateType = FALSE)
    .assertIsSingleCharacter(typeOfDesign, "typeOfDesign")

    if (typeOfDesign == C_TYPE_OF_DESIGN_AS_USER && !anyNA(userAlphaSpending)) {
        if (!is.na(kMax) && kMax != length(userAlphaSpending)) {
            stop(sprintf(
                paste0(
                    C_EXCEPTION_TYPE_CONFLICTING_ARGUMENTS,
                    "length of 'userAlphaSpending' (%s) must be equal to 'kMax' (%s)"
                ),
                length(userAlphaSpending), kMax
            ))
        }
        kMax <- length(userAlphaSpending)
        if (kMax > 1 && all(userAlphaSpending[1:(kMax - 1)] == 0)) {
            message("Changed type of design to ", sQuote(C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY))
            typeOfDesign <- C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY
        }
    }

    .assertIsSingleLogical(bindingFutility, "bindingFutility")
    .assertIsSingleLogical(directionUpper, "directionUpper", naAllowed = TRUE)
    .assertIsNumericVector(delayedInformation, "delayedInformation", naAllowed = TRUE)
    .assertIsInClosedInterval(delayedInformation, "delayedInformation", lower = 0, upper = NULL, naAllowed = TRUE)

    if (designClass == C_CLASS_NAME_TRIAL_DESIGN_INVERSE_NORMAL) {
        design <- TrialDesignInverseNormal$new(
            kMax = kMax,
            bindingFutility = bindingFutility,
            directionUpper = directionUpper,
            delayedInformation = delayedInformation
        )
    } else if (designClass == C_CLASS_NAME_TRIAL_DESIGN_GROUP_SEQUENTIAL) {
        design <- TrialDesignGroupSequential$new(
            kMax = kMax,
            bindingFutility = bindingFutility,
            directionUpper = directionUpper,
            delayedInformation = delayedInformation
        )
    } else {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'designClass' ('", designClass, "') must be '", C_CLASS_NAME_TRIAL_DESIGN_INVERSE_NORMAL, "' or ",
            "'", C_CLASS_NAME_TRIAL_DESIGN_GROUP_SEQUENTIAL, "'",
            call. = FALSE
        )
    }

    .assertIsSingleInteger(sided, "sided", naAllowed = FALSE, validateType = FALSE)
    if (!is.integer(sided) && sided %in% c(1, 2)) {
        sided <- as.integer(sided)
    }

    .assertIsSingleCharacter(optimizationCriterion, "optimizationCriterion")
    .assertIsSingleCharacter(typeBetaSpending, "typeBetaSpending")
    .assertIsSingleLogical(twoSidedPower, "twoSidedPower", naAllowed = TRUE)
    .assertIsSingleLogical(betaAdjustment, "betaAdjustment", naAllowed = TRUE)
    .assertIsValidAlphaAndBeta(alpha = alpha, beta = beta, naAllowed = TRUE)
    .assertIsSingleNumber(deltaWT, "deltaWT", naAllowed = TRUE)
    .assertIsSingleNumber(deltaPT1, "deltaPT1", naAllowed = TRUE)
    .assertIsSingleNumber(deltaPT0, "deltaPT0", naAllowed = TRUE)
    .assertIsSingleNumber(gammaA, "gammaA", naAllowed = TRUE)
    .assertIsSingleNumber(gammaB, "gammaB", naAllowed = TRUE)
    .assertIsNumericVector(futilityBounds, "futilityBounds", naAllowed = TRUE)
    .assertIsNumericVector(informationRates, "informationRates", naAllowed = TRUE)
    .assertIsNumericVector(userAlphaSpending, "userAlphaSpending", naAllowed = TRUE)
    .assertIsNumericVector(userBetaSpending, "userBetaSpending", naAllowed = TRUE)

    design$alpha <- alpha
    design$beta <- beta
    design$sided <- sided
    design$typeOfDesign <- typeOfDesign
    design$deltaWT <- deltaWT
    design$deltaPT1 <- deltaPT1
    design$deltaPT0 <- deltaPT0
    design$gammaA <- gammaA
    design$gammaB <- gammaB
    design$optimizationCriterion <- optimizationCriterion
    design$typeBetaSpending <- typeBetaSpending
    design$futilityBounds <- futilityBounds
    design$informationRates <- informationRates
    design$userAlphaSpending <- userAlphaSpending
    design$userBetaSpending <- userBetaSpending
    design$bindingFutility <- bindingFutility
    design$delayedInformation <- delayedInformation

    if (!all(is.na(delayedInformation)) && any(delayedInformation > 0)) {
        design$.setParameterType("delayedInformation", C_PARAM_USER_DEFINED)
    }

    design$.setParameterType("directionUpper", ifelse(!is.na(directionUpper),
        C_PARAM_USER_DEFINED, C_PARAM_NOT_APPLICABLE
    ))

    if (design$typeOfDesign != C_TYPE_OF_DESIGN_WT_OPTIMUM &&
            optimizationCriterion != C_OPTIMIZATION_CRITERION_DEFAULT) {
        warning(
            "'optimizationCriterion' (", optimizationCriterion,
            ") will be ignored because it is only applicable for ",
            "'typeOfDesign' = \"", C_TYPE_OF_DESIGN_WT_OPTIMUM, "\"",
            call. = FALSE
        )
    }
    if (design$typeOfDesign == C_TYPE_OF_DESIGN_HP) {
        .assertIsSingleNumber(constantBoundsHP, "constantBoundsHP")
        .assertIsInClosedInterval(constantBoundsHP, "constantBoundsHP", lower = 2, upper = NULL)
        design$constantBoundsHP <- constantBoundsHP
    } else if (constantBoundsHP != C_CONST_BOUND_HP_DEFAULT) {
        warning(
            "'constantBoundsHP' (", constantBoundsHP,
            ") will be ignored because it is only applicable for ",
            "'typeOfDesign' = \"", C_TYPE_OF_DESIGN_HP, "\"",
            call. = FALSE
        )
    }
    if (design$sided == 1) {
        design$twoSidedPower <- C_TWO_SIDED_POWER_DEFAULT
        design$.setParameterType("twoSidedPower", C_PARAM_NOT_APPLICABLE)
        if (!is.na(twoSidedPower)) {
            warning("'twoSidedPower' (", twoSidedPower, ") will be ignored because it is ",
                "only applicable for two-sided testing",
                call. = FALSE
            )
        }
    } else {
        if (is.na(twoSidedPower)) {
            design$twoSidedPower <- C_TWO_SIDED_POWER_DEFAULT
            design$.setParameterType("twoSidedPower", C_PARAM_DEFAULT_VALUE)
        } else {
            design$twoSidedPower <- twoSidedPower
            design$.setParameterType("twoSidedPower", ifelse(
                twoSidedPower == C_TWO_SIDED_POWER_DEFAULT,
                C_PARAM_DEFAULT_VALUE,
                C_PARAM_USER_DEFINED
            ))
        }
    }
    if (design$sided == 2 && grepl("^bs", design$typeBetaSpending)) {
        if (is.na(betaAdjustment)) {
            design$betaAdjustment <- TRUE
            design$.setParameterType("betaAdjustment", C_PARAM_DEFAULT_VALUE)
        } else {
            design$betaAdjustment <- betaAdjustment
            design$.setParameterType("betaAdjustment", ifelse(
                betaAdjustment, C_PARAM_DEFAULT_VALUE, C_PARAM_USER_DEFINED
            ))
        }
    } else if (!is.na(betaAdjustment)) {
        warning(
            "'betaAdjustment' (", betaAdjustment,
            ") will be ignored because it is only applicable ",
            "for two-sided beta-spending designs",
            call. = FALSE
        )
    }

    design$tolerance <- tolerance

    return(design)
}

.getDesignGroupSequentialKMax1 <- function(design) {
    design$criticalValues <- .getOneMinusQNorm(design$alpha / design$sided)
    design$alphaSpent[1] <- design$alpha
    design$.setParameterType("typeOfDesign", C_PARAM_NOT_APPLICABLE)
    if (!identical(design$typeOfDesign, C_DEFAULT_TYPE_OF_DESIGN)) {
        warning("'typeOfDesign' (", design$typeOfDesign, ") will be ignored ",
            "because design is fixed",
            call. = FALSE
        )
    }
    return(invisible(design))
}

#
# Wang and Tsiatis design
#
.getDesignGroupSequentialWangAndTsiatis <- function(design) {
    if (design$typeOfDesign == C_TYPE_OF_DESIGN_P) {
        design$criticalValues <- .getDesignGroupSequentialPocockCpp(
            design$kMax,
            design$alpha,
            design$sided,
            design$informationRates,
            design$bindingFutility,
            design$futilityBounds,
            design$tolerance
        )
    } else if (design$typeOfDesign == C_TYPE_OF_DESIGN_OF) {
        design$criticalValues <- .getDesignGroupSequentialOBrienAndFlemingCpp(
            design$kMax,
            design$alpha,
            design$sided,
            design$informationRates,
            design$bindingFutility,
            design$futilityBounds,
            design$tolerance
        )
    } else {
        design$criticalValues <- .getDesignGroupSequentialDeltaWTCpp(
            design$kMax,
            design$alpha,
            design$sided,
            design$informationRates,
            design$bindingFutility,
            design$futilityBounds,
            design$tolerance,
            design$deltaWT
        )
    }

    .calculateAlphaSpent(design)
    return(invisible(design))
}

.getDesignGroupSequentialPampallonaTsiatis <- function(design) {
    cppResult <- .getDesignGroupSequentialPampallonaTsiatisCpp(
        design$tolerance, design$beta, design$alpha, design$kMax,
        design$deltaPT0, design$deltaPT1, design$informationRates,
        design$sided, design$bindingFutility
    )
    futilityBounds <- cppResult$futilityBounds
    criticalValues <- cppResult$criticalValues
    probs <- cppResult$probs

    if (design$sided == 1) {
        design$betaSpent <- cumsum(probs[1, ])
        design$power <- cumsum(probs[3, ] - probs[2, ])
    } else {
        design$betaSpent <- cumsum(probs[3, ] - probs[2, ])
        if (design$twoSidedPower) {
            design$power <- cumsum(probs[5, ] - probs[4, ] + probs[1, ])
        } else {
            design$power <- cumsum(probs[5, ] - probs[4, ])
        }
    }
    design$.setParameterType("betaSpent", C_PARAM_GENERATED)
    design$.setParameterType("power", C_PARAM_GENERATED)

    design$futilityBounds <- futilityBounds[1:(design$kMax - 1)]
    design$criticalValues <- criticalValues
    design$.setParameterType("futilityBounds", C_PARAM_GENERATED)
    design$.setParameterType("criticalValues", C_PARAM_GENERATED)

    .calculateAlphaSpent(design)

    design$futilityBounds[design$futilityBounds == 0] <- NA_real_

    .assertIsValidBetaSpent(design)

    return(invisible(design))
}

.calculateAlphaSpent <- function(design) {
    criticalValues <- .getCriticalValues(design)
    if (design$sided == 2) {
        if (design$bindingFutility) {
            futilityBounds <- design$futilityBounds
            futilityBounds[is.na(futilityBounds)] <- 0
            decisionMatrix <- matrix(c(
                -criticalValues, -futilityBounds, 0,
                futilityBounds, 0, criticalValues
            ), nrow = 4, byrow = TRUE)
        } else {
            decisionMatrix <- matrix(c(
                -criticalValues,
                criticalValues
            ), nrow = 2, byrow = TRUE)
        }
    } else {
        if (design$bindingFutility) {
            decisionMatrix <- matrix(c(
                design$futilityBounds, C_FUTILITY_BOUNDS_DEFAULT,
                criticalValues
            ), nrow = 2, byrow = TRUE)
        } else {
            decisionMatrix <- matrix(c(
                rep(C_FUTILITY_BOUNDS_DEFAULT, design$kMax),
                criticalValues
            ), nrow = 2, byrow = TRUE)
        }
    }

    tryCatch(
        {
            probs <- .getGroupSequentialProbabilities(decisionMatrix, design$informationRates)
            if (design$sided == 1) {
                design$alphaSpent <- cumsum(probs[3, ] - probs[2, ])
            } else if (nrow(decisionMatrix) == 2) {
                design$alphaSpent <- cumsum(probs[3, ] - probs[2, ] + probs[1, ])
            } else {
                design$alphaSpent <- cumsum(probs[5, ] - probs[4, ] + probs[1, ])
            }
            if (!is.na(design$alphaSpent[design$kMax])) {
                design$alphaSpent[design$kMax] <- floor(design$alphaSpent[design$kMax] * 1e8) / 1e8
            }
            design$.setParameterType("alphaSpent", C_PARAM_GENERATED)
        },
        error = function(e) {
            warning("Failed to calculate 'alphaSpent': ", e, call. = FALSE)
        }
    )
}

#'
#' Haybittle & Peto design
#'
#' @noRd
#'
.getDesignGroupSequentialHaybittleAndPeto <- function(design) {
    scale <- .getOneDimensionalRoot(
        function(scale) {
            criticalValues <- c(rep(design$constantBoundsHP, design$kMax - 1), scale)
            if (design$sided == 2) {
                decisionMatrix <- matrix(c(-criticalValues, criticalValues), nrow = 2, byrow = TRUE)
                probs <- .getGroupSequentialProbabilities(decisionMatrix, design$informationRates)
                return(sum(probs[3, ] - probs[2, ] + probs[1, ]) - design$alpha)
            } else {
                if (design$bindingFutility) {
                    decisionMatrix <- matrix(c(
                        design$futilityBounds, C_FUTILITY_BOUNDS_DEFAULT,
                        criticalValues
                    ), nrow = 2, byrow = TRUE)
                } else {
                    decisionMatrix <- matrix(c(
                        rep(C_FUTILITY_BOUNDS_DEFAULT, design$kMax),
                        criticalValues
                    ), nrow = 2, byrow = TRUE)
                }
                probs <- .getGroupSequentialProbabilities(decisionMatrix, design$informationRates)
                return(sum(probs[3, ] - probs[2, ]) - design$alpha)
            }
        },
        lower = 0, upper = 8, tolerance = design$tolerance,
        callingFunctionInformation = ".getDesignGroupSequentialHaybittleAndPeto"
    )

    design$criticalValues <- c(rep(design$constantBoundsHP, design$kMax - 1), scale)

    .calculateAlphaSpent(design)

    return(invisible(design))
}

.getOptimumDesign <- function(deltaWT, design) {
    scale <- .getOneDimensionalRoot(
        function(scale) {
            criticalValues <- scale * design$informationRates^(deltaWT - 0.5)
            if (design$sided == 2) {
                decisionMatrix <- (matrix(c(-criticalValues, criticalValues), nrow = 2, byrow = TRUE))
                probs <- .getGroupSequentialProbabilities(decisionMatrix, design$informationRates)
                return(sum(probs[3, ] - probs[2, ] + probs[1, ]) - design$alpha)
            } else {
                if (design$bindingFutility) {
                    decisionMatrix <- matrix(c(
                        design$futilityBounds, C_FUTILITY_BOUNDS_DEFAULT,
                        criticalValues
                    ), nrow = 2, byrow = TRUE)
                } else {
                    decisionMatrix <- matrix(c(
                        rep(C_FUTILITY_BOUNDS_DEFAULT, design$kMax),
                        criticalValues
                    ), nrow = 2, byrow = TRUE)
                }
                probs <- .getGroupSequentialProbabilities(decisionMatrix, design$informationRates)
                return(sum(probs[3, ] - probs[2, ]) - design$alpha)
            }
        },
        lower = 0, upper = 5, tolerance = design$tolerance, callingFunctionInformation = ".getOptimumDesign"
    )

    design$criticalValues <- scale * design$informationRates^(deltaWT - 0.5)
    designCharacteristics <- .getDesignCharacteristics(design = design)

    y <- NA_integer_
    if (design$optimizationCriterion == C_OPTIMIZATION_CRITERION_ASNH1) {
        y <- designCharacteristics$averageSampleNumber1
    }
    if (design$optimizationCriterion == C_OPTIMIZATION_CRITERION_ASNIFH1) {
        y <- designCharacteristics$inflationFactor + designCharacteristics$averageSampleNumber1
    }
    if (design$optimizationCriterion == C_OPTIMIZATION_CRITERION_ASN_SUM) {
        y <- designCharacteristics$averageSampleNumber0 +
            designCharacteristics$averageSampleNumber01 + designCharacteristics$averageSampleNumber1
    }
    return(y)
}

#'
#' Optimum design within Wang and Tsiatis class
#'
#' @noRd
#'
.getDesignGroupSequentialWangAndTsiatisOptimum <- function(design) {
    .assertDesignParameterExists(design, "optimizationCriterion", C_OPTIMIZATION_CRITERION_DEFAULT)
    .assertIsOptimizationCriterion(design$optimizationCriterion)

    optimumDesign <- stats::optimize(.getOptimumDesign,
        design = design,
        interval = c(0, 1), tol = 0.0001
    )

    design$deltaWT <- round(optimumDesign$minimum, 3)
    design$.setParameterType("deltaWT", C_PARAM_GENERATED)

    # Recalculation of design characteristics with rounded design$deltaWT
    design$criticalValues <- .getDesignGroupSequentialDeltaWTCpp(
        design$kMax,
        design$alpha,
        design$sided,
        design$informationRates,
        design$bindingFutility,
        design$futilityBounds,
        design$tolerance,
        design$deltaWT
    )

    designCharacteristics <- .getDesignCharacteristics(design = design)
    design$power <- designCharacteristics$power
    design$.setParameterType("power", C_PARAM_GENERATED)

    .calculateAlphaSpent(design)

    return(invisible(design))
}

#'
#' Alpha spending approaches
#'
#' @noRd
#'
.getDesignGroupSequentialAlphaSpending <- function(design, userFunctionCallEnabled) {
    design$criticalValues <- .getDesignGroupSequentialAlphaSpendingCpp(
        design$kMax,
        design$alpha,
        design$gammaA,
        design$typeOfDesign,
        design$sided,
        design$informationRates,
        design$bindingFutility,
        design$futilityBounds,
        design$tolerance
    )
    .calculateAlphaSpent(design)
    return(.getDesignGroupSequentialBetaSpendingApproaches(design, userFunctionCallEnabled))
}

#'
#' User defined alpha spending approach
#'
#' @noRd
#'
.getDesignGroupSequentialUserDefinedAlphaSpending <- function(design, userFunctionCallEnabled) {
    design$criticalValues <- rep(NA_real_, design$kMax)
    if (design$typeOfDesign == C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY) {
        design$userAlphaSpending <- rep(0, design$kMax)
        design$userAlphaSpending[design$kMax] <- design$alpha
    }
    if (design$typeOfDesign == C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY && !design$bindingFutility) {
        design$criticalValues[1:(design$kMax - 1)] <- C_QNORM_THRESHOLD
        design$criticalValues[design$kMax] <- .getOneMinusQNorm(design$alpha / design$sided)
    } else {
        design$criticalValues <- .getDesignGroupSequentialUserDefinedAlphaSpendingCpp(
            design$kMax, design$userAlphaSpending, design$sided,
            design$informationRates, design$bindingFutility, design$futilityBounds, design$tolerance
        )
        if (design$typeOfDesign == C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY) {
            design$criticalValues[1:(design$kMax - 1)] <- C_QNORM_THRESHOLD
        }
    }

    .calculateAlphaSpent(design)

    return(invisible(.getDesignGroupSequentialBetaSpendingApproaches(design, userFunctionCallEnabled)))
}

#'
#' Only for alpha spending approaches
#'
#' @noRd
#'
.getDesignGroupSequentialBetaSpendingApproaches <- function(design, userFunctionCallEnabled) {
    # beta spending approaches (additional to alpha spending)!
    if (.isBetaSpendingDesignType(design$typeBetaSpending,
            userDefinedBetaSpendingIncluded = FALSE, noneIncluded = FALSE
        )) {
        .getDesignGroupSequentialBetaSpending(design, userFunctionCallEnabled)
    }

    # User defined beta spending
    if (design$typeBetaSpending == C_TYPE_OF_DESIGN_BS_USER) {
        .getDesignGroupSequentialUserDefinedBetaSpending(design)
    }

    return(invisible(design))
}

.fillVec <- function(vec, n) {
    return(c(vec, rep(NA_real_, n - length(vec))))
}

#'
#' Beta spending approaches (additional to alpha spending)
#' Find shift with beta spending such that last critical values coincide
#'
#' @noRd
#'
.getDesignGroupSequentialBetaSpending <- function(design, userFunctionCallEnabled) {
    cppResult <- .getDesignGroupSequentialBetaSpendingCpp(
        .getCriticalValues(design),
        design$kMax,
        design$userAlphaSpending,
        design$userBetaSpending,
        design$informationRates,
        design$bindingFutility,
        design$tolerance,
        design$typeOfDesign,
        design$typeBetaSpending,
        design$gammaA,
        design$gammaB,
        design$alpha,
        design$beta,
        design$sided,
        design$betaAdjustment,
        design$twoSidedPower
    )

    design$futilityBounds <- cppResult$futilityBounds
    design$criticalValues <- cppResult$criticalValues
    design$betaSpent <- cppResult$betaSpent
    design$power <- cppResult$power

    if (design$sided == 2) {
        .calculateAlphaSpent(design)
    }

    design$.setParameterType("betaSpent", C_PARAM_GENERATED)
    design$.setParameterType("power", C_PARAM_GENERATED)
    design$.setParameterType("futilityBounds", C_PARAM_GENERATED)

    return(invisible(design))
}

#'
#' User defined beta spending
#'
#' Find shift with beta spending such that last critical values coincide
#'
#' @noRd
#'
.getDesignGroupSequentialUserDefinedBetaSpending <- function(design) {
    if (design$typeBetaSpending != C_TYPE_OF_DESIGN_BS_USER) {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'typeBetaSpending' ('", design$typeBetaSpending, "') must be '", C_TYPE_OF_DESIGN_BS_USER, "'",
            call. = FALSE
        )
    }

    cppResult <- .getDesignGroupSequentialUserDefinedBetaSpendingCpp(
        .getCriticalValues(design),
        design$kMax,
        design$userAlphaSpending,
        design$userBetaSpending,
        design$sided,
        design$informationRates,
        design$bindingFutility,
        design$tolerance,
        design$typeOfDesign,
        design$gammaA,
        design$alpha,
        design$betaAdjustment,
        design$twoSidedPower
    )

    design$futilityBounds <- cppResult$futilityBounds
    design$criticalValues <- cppResult$criticalValues
    design$betaSpent <- cppResult$betaSpent
    design$power <- cppResult$power

    if (design$sided == 2) {
        .calculateAlphaSpent(design)
    }

    design$.setParameterType("betaSpent", C_PARAM_GENERATED)
    design$.setParameterType("power", C_PARAM_GENERATED)
    design$.setParameterType("futilityBounds", C_PARAM_GENERATED)
    return(invisible(design))
}

#'
#' Calculate stopping, rejection and futility probabilities for delayed response design
#'
#' @noRd
#'
.calculateDecisionProbabilities <- function(sqrtShift,
        informationRates,
        delayedInformation,
        contRegionUpper,
        contRegionLower,
        decisionCriticalValues) {
    kMax <- length(informationRates)
    power <- numeric(kMax)
    futilityProbabilities <- numeric(kMax - 1)
    stoppingProbabilities <- numeric(kMax - 1)
    rejectionProbabilities <- numeric(kMax)
    contRegionLower <- c(contRegionLower, decisionCriticalValues[kMax])

    for (stage in 1:kMax) {
        if (!is.na(delayedInformation[stage]) && delayedInformation[stage] > 0) {
            # information rate vector in case of recruitment stop at 'stage'
            informationRatesUponDelay <- c(
                informationRates[1:stage],
                informationRates[stage] + delayedInformation[stage]
            )
            if (stage == 1) {
                probs1 <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionUpper[stage] - sqrtShift * sqrt(informationRatesUponDelay[1]),
                            decisionCriticalValues[stage] - sqrtShift * sqrt(informationRatesUponDelay[2]),
                            C_UPPER_BOUNDS_DEFAULT, C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRatesUponDelay
                )
                probs2 <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            -C_UPPER_BOUNDS_DEFAULT, decisionCriticalValues[stage] - sqrtShift * sqrt(informationRatesUponDelay[2]),
                            contRegionLower[stage] - sqrtShift * sqrt(informationRatesUponDelay[1]), C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRatesUponDelay
                )
                rejectionProbabilities[stage] <- probs1[2, stage + 1] - probs1[1, stage + 1] +
                    probs2[2, stage + 1] - probs2[1, stage + 1]
                power[stage] <- rejectionProbabilities[stage]
                futilityProbabilities[stage] <- probs2[2, stage]
                stoppingProbabilities[stage] <- probs2[2, stage] + 1 - probs1[1, stage]
            } else if (stage < kMax) {
                probs1 <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionLower[1:(stage - 1)] - sqrtShift * sqrt(informationRatesUponDelay[1:(stage - 1)]),
                            contRegionUpper[stage] - sqrtShift * sqrt(informationRatesUponDelay[stage]),
                            decisionCriticalValues[stage] - sqrtShift * sqrt(informationRatesUponDelay[stage + 1]),
                            contRegionUpper[1:(stage - 1)] - sqrtShift * sqrt(informationRatesUponDelay[1:(stage - 1)]),
                            C_UPPER_BOUNDS_DEFAULT, C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRatesUponDelay
                )
                probs2 <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionLower[1:(stage - 1)] - sqrtShift *
                                sqrt(informationRatesUponDelay[1:(stage - 1)]), -C_UPPER_BOUNDS_DEFAULT,
                            decisionCriticalValues[stage] - sqrtShift * sqrt(informationRatesUponDelay[stage + 1]),
                            contRegionUpper[1:(stage - 1)] - sqrtShift * sqrt(informationRatesUponDelay[1:(stage - 1)]),
                            contRegionLower[stage] - sqrtShift * sqrt(informationRatesUponDelay[stage]), C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRatesUponDelay
                )
                rejectionProbabilities[stage] <- probs1[2, stage + 1] - probs1[1, stage + 1] +
                    probs2[2, stage + 1] - probs2[1, stage + 1]
                power[stage] <- sum(rejectionProbabilities[1:stage])
                futilityProbabilities[stage] <- probs2[2, stage]
                stoppingProbabilities[stage] <- probs2[2, stage] + probs1[2, stage] - probs1[1, stage]
            } else {
                probs <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionLower[1:(stage - 1)] - sqrtShift * sqrt(informationRates[1:(stage - 1)]),
                            decisionCriticalValues[stage] - sqrtShift * sqrt(informationRates[stage]),
                            contRegionUpper[1:(stage - 1)] - sqrtShift * sqrt(informationRates[1:(stage - 1)]),
                            C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRates
                )
                rejectionProbabilities[stage] <- probs[2, stage] - probs[1, stage]
                power[stage] <- sum(rejectionProbabilities[1:stage])
            }
        } else {
            if (stage == 1) {
                probs <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionLower[stage] - sqrtShift * sqrt(informationRates[stage]),
                            contRegionUpper[stage] - sqrtShift * sqrt(informationRates[stage])
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRates[1]
                )
            } else {
                probs <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionLower[1:(stage - 1)] - sqrtShift * sqrt(informationRates[1:(stage - 1)]),
                            contRegionLower[stage] - sqrtShift * sqrt(informationRates[stage]),
                            contRegionUpper[1:(stage - 1)] - sqrtShift * sqrt(informationRates[1:(stage - 1)]),
                            contRegionUpper[stage] - sqrtShift * sqrt(informationRates[stage])
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRates[1:stage]
                )
            }
            rejectionProbabilities[stage] <- probs[3, stage] - probs[2, stage]
            if (stage < kMax) {
                futilityProbabilities[stage] <- probs[1, stage]
                stoppingProbabilities[stage] <- futilityProbabilities[stage] + rejectionProbabilities[stage]
            }
            power[stage] <- sum(rejectionProbabilities[1:stage])
        }
    }

    return(list(
        probs = probs,
        power = power,
        rejectionProbabilities = rejectionProbabilities,
        futilityProbabilities = futilityProbabilities,
        stoppingProbabilities = stoppingProbabilities
    ))
}

#'
#' @title
#' Get Design Inverse Normal
#'
#' @description
#' Provides adjusted boundaries and defines a group sequential design for its use in
#' the inverse normal combination test.
#'
#' @inheritParams getDesignGroupSequential
#'
#' @template details_group_sequential_design
#'
#' @template return_object_trial_design
#' @template how_to_get_help_for_generics
#'
#' @family design functions
#'
#' @template examples_get_design_inverse_normal
#'
#' @export
#'
getDesignInverseNormal <- function(...,
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = 1L, # C_SIDED_DEFAULT
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        typeOfDesign = c(
            "OF",
            "P",
            "WT",
            "PT",
            "HP",
            "WToptimum",
            "asP",
            "asOF",
            "asKD",
            "asHSD",
            "asUser",
            "noEarlyEfficacy"
        ), # C_DEFAULT_TYPE_OF_DESIGN,
        deltaWT = NA_real_,
        deltaPT1 = NA_real_,
        deltaPT0 = NA_real_,
        optimizationCriterion = c(
            "ASNH1",
            "ASNIFH1",
            "ASNsum"
        ), # C_OPTIMIZATION_CRITERION_DEFAULT
        gammaA = NA_real_,
        typeBetaSpending = c(
            "none",
            "bsP",
            "bsOF",
            "bsKD",
            "bsHSD",
            "bsUser"
        ), # C_TYPE_OF_DESIGN_BS_NONE
        userAlphaSpending = NA_real_,
        userBetaSpending = NA_real_,
        efficacyStops = NA,
        futilityStops = NA,
        gammaB = NA_real_,
        bindingFutility = NA,
        futilityBoundsScale = c(
            "zValue",
            "pValue",
            "reverseCondPower",
            "condPowerAtObserved",
            "predictivePower"
        ),
        directionUpper = NA,
        betaAdjustment = NA,
        constantBoundsHP = 3, # C_CONST_BOUND_HP_DEFAULT,
        twoSidedPower = NA,
        tolerance = 1e-08 # C_DESIGN_TOLERANCE_DEFAULT
        ) {
    .warnInCaseOfUnknownArguments(functionName = "getDesignInverseNormal", ...)

    typeBetaSpending <- .matchArgument(typeBetaSpending, C_TYPE_OF_DESIGN_BS_NONE)
    futilityBoundsScale <- match.arg(futilityBoundsScale)

    .assertIsLogicalVector(efficacyStops, "efficacyStops", naAllowed = TRUE)
    .assertIsLogicalVector(futilityStops, "futilityStops", naAllowed = TRUE)

    designClass <- C_CLASS_NAME_TRIAL_DESIGN_INVERSE_NORMAL

    design <- NULL
    if (!all(is.na(futilityBounds)) && .futilityBoundsCalculationRequiresDesign(futilityBoundsScale)) {
        design <- .getDesignGroupSequentialOrWithInterimStops(
            designClass = designClass,
            kMax = length(futilityBounds) + 1,
            alpha = alpha,
            beta = beta,
            sided = sided,
            informationRates = informationRates,
            futilityBounds = NA_real_,
            typeOfDesign = typeOfDesign,
            deltaWT = deltaWT,
            deltaPT1 = deltaPT1,
            deltaPT0 = deltaPT0,
            optimizationCriterion = optimizationCriterion,
            gammaA = gammaA,
            typeBetaSpending = typeBetaSpending,
            userAlphaSpending = userAlphaSpending,
            userBetaSpending = userBetaSpending,
            gammaB = gammaB,
            bindingFutility = NA,
            directionUpper = directionUpper,
            betaAdjustment = betaAdjustment,
            constantBoundsHP = constantBoundsHP,
            twoSidedPower = twoSidedPower,
            delayedInformation = NA_real_,
            tolerance = tolerance,
            efficacyStops = efficacyStops,
            futilityStops = futilityStops
        )
    }

    futilityBounds <- .getFutilityBoundsFromArgs(
        futilityBounds = futilityBounds,
        futilityBoundsScale = futilityBoundsScale,
        functionName = "getDesignGroupSequential",
        design = design,
        ...
    )

    return(.getDesignGroupSequentialOrWithInterimStops(
        designClass = designClass,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = typeOfDesign,
        deltaWT = deltaWT,
        deltaPT1 = deltaPT1,
        deltaPT0 = deltaPT0,
        optimizationCriterion = optimizationCriterion,
        gammaA = gammaA,
        typeBetaSpending = typeBetaSpending,
        userAlphaSpending = userAlphaSpending,
        userBetaSpending = userBetaSpending,
        gammaB = gammaB,
        bindingFutility = bindingFutility,
        directionUpper = directionUpper,
        betaAdjustment = betaAdjustment,
        constantBoundsHP = constantBoundsHP,
        twoSidedPower = twoSidedPower,
        delayedInformation = NA_real_,
        tolerance = tolerance,
        efficacyStops = efficacyStops,
        futilityStops = futilityStops
    ))
}

.getDesignGroupSequentialOrWithInterimStops <- function(...,
        designClass,
        kMax,
        alpha,
        beta,
        sided,
        informationRates,
        futilityBounds,
        typeOfDesign,
        deltaWT,
        deltaPT1,
        deltaPT0,
        optimizationCriterion,
        gammaA,
        typeBetaSpending,
        userAlphaSpending,
        userBetaSpending,
        gammaB,
        bindingFutility,
        directionUpper,
        betaAdjustment,
        constantBoundsHP,
        twoSidedPower,
        delayedInformation,
        tolerance,
        efficacyStops,
        futilityStops) {
    if (!all(is.na(efficacyStops)) || !all(is.na(futilityStops))) {
        return(.getDesignWithInterimStops(
            designClass = designClass,
            kMax = kMax,
            alpha = alpha,
            beta = beta,
            sided = sided,
            informationRates = informationRates,
            futilityBounds = futilityBounds,
            typeOfDesign = typeOfDesign,
            gammaA = gammaA,
            typeBetaSpending = typeBetaSpending,
            efficacyStops = efficacyStops,
            futilityStops = futilityStops,
            gammaB = gammaB,
            bindingFutility = bindingFutility,
            directionUpper = directionUpper,
            betaAdjustment = betaAdjustment,
            twoSidedPower = twoSidedPower,
            tolerance = tolerance
        ))
    }

    return(.getDesignGroupSequential(
        designClass = designClass,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = typeOfDesign,
        deltaWT = deltaWT,
        deltaPT1 = deltaPT1,
        deltaPT0 = deltaPT0,
        optimizationCriterion = optimizationCriterion,
        gammaA = gammaA,
        typeBetaSpending = typeBetaSpending,
        userAlphaSpending = userAlphaSpending,
        userBetaSpending = userBetaSpending,
        gammaB = gammaB,
        bindingFutility = bindingFutility,
        directionUpper = directionUpper,
        betaAdjustment = betaAdjustment,
        constantBoundsHP = constantBoundsHP,
        twoSidedPower = twoSidedPower,
        delayedInformation = delayedInformation,
        tolerance = tolerance,
        userFunctionCallEnabled = TRUE
    ))
}

.getDesignInverseNormal <- function(...,
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = C_SIDED_DEFAULT,
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        typeOfDesign = C_DEFAULT_TYPE_OF_DESIGN,
        deltaWT = NA_real_,
        deltaPT1 = NA_real_,
        deltaPT0 = NA_real_,
        optimizationCriterion = C_OPTIMIZATION_CRITERION_DEFAULT,
        gammaA = NA_real_,
        typeBetaSpending = C_TYPE_OF_DESIGN_BS_NONE,
        userAlphaSpending = NA_real_,
        userBetaSpending = NA_real_,
        gammaB = NA_real_,
        bindingFutility = NA,
        betaAdjustment = NA,
        constantBoundsHP = C_CONST_BOUND_HP_DEFAULT,
        twoSidedPower = NA,
        tolerance = C_DESIGN_TOLERANCE_DEFAULT) {
    .warnInCaseOfUnknownArguments(functionName = "getDesignInverseNormal", ...)

    return(.getDesignGroupSequential(
        designClass = C_CLASS_NAME_TRIAL_DESIGN_INVERSE_NORMAL,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = typeOfDesign,
        deltaWT = deltaWT,
        deltaPT1 = deltaPT1,
        deltaPT0 = deltaPT0,
        optimizationCriterion = optimizationCriterion,
        gammaA = gammaA,
        typeBetaSpending = typeBetaSpending,
        userAlphaSpending = userAlphaSpending,
        userBetaSpending = userBetaSpending,
        gammaB = gammaB,
        bindingFutility = bindingFutility,
        betaAdjustment = betaAdjustment,
        constantBoundsHP = constantBoundsHP,
        twoSidedPower = twoSidedPower,
        tolerance = tolerance,
        userFunctionCallEnabled = FALSE
    ))
}

.assertAllArgumentsHaveDefaultValues <- function(designFun, defaultValues) {
    argNames <- methods::formalArgs(designFun)
    argNames <- argNames[argNames != "..."]
    missingArgNames <- argNames[!(argNames %in% names(defaultValues))]
    if (length(missingArgNames) > 0) {
        stop(
            C_EXCEPTION_TYPE_RUNTIME_ISSUE,
            ".getDesignGroupSequentialDefaultValues() does return the arguments ",
            .arrayToString(missingArgNames, encapsulate = TRUE)
        )
    }
}

.getDesignGroupSequentialDefaultValues <- function(designFun = NULL) {
    defaultValues <- list(
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = C_SIDED_DEFAULT,
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        futilityBoundsScale = "zValue",
        typeOfDesign = C_DEFAULT_TYPE_OF_DESIGN,
        deltaWT = NA_real_,
        deltaPT1 = NA_real_,
        deltaPT0 = NA_real_,
        optimizationCriterion = C_OPTIMIZATION_CRITERION_DEFAULT,
        gammaA = NA_real_,
        typeBetaSpending = C_TYPE_OF_DESIGN_BS_NONE,
        userAlphaSpending = NA_real_,
        userBetaSpending = NA_real_,
        gammaB = NA_real_,
        bindingFutility = NA,
        directionUpper = NA,
        betaAdjustment = NA,
        constantBoundsHP = C_CONST_BOUND_HP_DEFAULT,
        twoSidedPower = NA,
        delayedInformation = NA_real_,
        tolerance = C_DESIGN_TOLERANCE_DEFAULT,
        efficacyStops = NA,
        futilityStops = NA
    )
    if (is.null(designFun)) {
        return(defaultValues)
    }

    .assertAllArgumentsHaveDefaultValues(designFun, defaultValues)
    return(defaultValues)
}

.getDesignInverseNormalDefaultValues <- function() {
    return(.getDesignGroupSequentialDefaultValues())
}

.getDesignWithInterimStops <- function(...,
        designClass,
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = C_SIDED_DEFAULT,
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        typeOfDesign = C_DEFAULT_TYPE_OF_DESIGN,
        gammaA = NA_real_,
        typeBetaSpending = C_TYPE_OF_DESIGN_BS_NONE,
        efficacyStops = NA,
        futilityStops = NA,
        gammaB = NA_real_,
        bindingFutility = NA,
        directionUpper = NA,
        betaAdjustment = NA,
        twoSidedPower = NA,
        tolerance = 1e-08) {
    validTypesOfDesign <- c("asOF", "asP", "asKD", "asHSD")
    if (!(typeOfDesign %in% validTypesOfDesign)) {
        stop(C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'typeOfDesign' ('", typeOfDesign, "') must be one of the following: ",
            .arrayToString(validTypesOfDesign, encapsulate = TRUE),
            call. = FALSE
        )
    }

    coreDesign <- .getDesignGroupSequential(
        designClass = designClass,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = typeOfDesign,
        gammaA = gammaA,
        typeBetaSpending = typeBetaSpending,
        gammaB = gammaB,
        bindingFutility = bindingFutility,
        directionUpper = directionUpper,
        betaAdjustment = betaAdjustment,
        twoSidedPower = twoSidedPower,
        tolerance = tolerance,
        userFunctionCallEnabled = TRUE
    )

    kMax <- coreDesign$kMax
    betaSpendingEnabled <- !identical(typeBetaSpending, C_TYPE_OF_DESIGN_BS_NONE)

    if (length(efficacyStops) == 1 && is.na(efficacyStops)) {
        efficacyStops <- rep(TRUE, kMax - 1)
    }

    if (length(efficacyStops) != kMax - 1) {
        stop(C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'efficacyStops' (", .arrayToString(efficacyStops,
                vectorLookAndFeelEnabled = FALSE
            ),
            ") must have length kMax - 1 (", kMax - 1, ")",
            call. = FALSE
        )
    }

    if (betaSpendingEnabled && length(futilityStops) == 1 && is.na(futilityStops)) {
        futilityStops <- rep(TRUE, kMax - 1)
    }

    if (betaSpendingEnabled && length(futilityStops) != kMax - 1) {
        stop(C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'futilityStops' (", .arrayToString(futilityStops,
                vectorLookAndFeelEnabled = FALSE
            ),
            ") must have length kMax - 1 (", kMax - 1, ")",
            call. = FALSE
        )
    }

    efficacyStops[is.na(efficacyStops)] <- TRUE
    futilityStops[is.na(futilityStops)] <- TRUE

    if (all(efficacyStops) && all(futilityStops)) {
        return(coreDesign)
    }

    newAlphaSpending <- coreDesign$alphaSpent
    for (k in 1:(kMax - 1)) {
        if (!efficacyStops[k]) {
            newAlphaSpending[k] <- ifelse(k == 1, 0, newAlphaSpending[k - 1])
        }
    }

    args <- list(
        designClass = designClass,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = coreDesign$sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = "asUser",
        userAlphaSpending = newAlphaSpending,
        directionUpper = directionUpper,
        twoSidedPower = twoSidedPower,
        userFunctionCallEnabled = TRUE,
        userDefinedInterimStops = TRUE
    )

    if (betaSpendingEnabled) {
        newBetaSpending <- coreDesign$betaSpent
        for (k in 1:(kMax - 1)) {
            if (!futilityStops[k]) {
                newBetaSpending[k] <- ifelse(k == 1, 0, newBetaSpending[k - 1])
            }
        }
        args$typeBetaSpending <- "bsUser"
        args$userBetaSpending <- newBetaSpending
    }

    newDesign <- do.call(.getDesignGroupSequential, args)
    newDesign$criticalValues[which(!efficacyStops)] <- Inf
    newDesign$futilityBounds[which(!futilityStops)] <- -Inf

    newDesign$efficacyStops <- efficacyStops
    newDesign$.setParameterType("efficacyStops", C_PARAM_USER_DEFINED)

    if (betaSpendingEnabled) {
        if (coreDesign$sided == 2) {
            newDesign$futilityBounds[which(!futilityStops)] <- 0
        }
        newDesign$futilityStops <- futilityStops
        newDesign$.setParameterType("futilityStops", C_PARAM_USER_DEFINED)
    }

    if (max(abs(newDesign$userAlphaSpending - newDesign$alphaSpent), na.rm = TRUE) <= 1e-07) {
        newDesign$.setParameterType("userAlphaSpending", C_PARAM_NOT_APPLICABLE)
    }

    if (betaSpendingEnabled && max(abs(newDesign$userBetaSpending - newDesign$betaSpent), na.rm = TRUE) <= 1e-07) {
        newDesign$.setParameterType("userBetaSpending", C_PARAM_NOT_APPLICABLE)
    }

    noEarlyEfficacyEnabled <-
        identical(newDesign$typeOfDesign, C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY) &&
            !identical(typeOfDesign, C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY)
    for (paramName in coreDesign$.getUserDefinedParameters()) {
        if (noEarlyEfficacyEnabled && identical(paramName, "typeOfDesign")) {
            next
        }

        newDesign[[paramName]] <- coreDesign[[paramName]]
        newDesign$.setParameterType(paramName, C_PARAM_USER_DEFINED)
    }

    indices <- which(
        !is.na(newDesign$stageLevels) &
            newDesign$stageLevels < 1e-07 &
            !is.na(newDesign$criticalValues) &
            is.infinite(newDesign$criticalValues)
    )
    if (length(indices) > 0) {
        newDesign$stageLevels[indices] <- 0
    }

    return(newDesign)
}

#'
#' @param userFunctionCallEnabled if \code{TRUE}, additional parameter validation methods will be called.
#'
#' @noRd
#'
.getDesignGroupSequential <- function(...,
        designClass = C_CLASS_NAME_TRIAL_DESIGN_GROUP_SEQUENTIAL,
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = C_SIDED_DEFAULT,
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        typeOfDesign = C_DEFAULT_TYPE_OF_DESIGN,
        deltaWT = NA_real_,
        deltaPT1 = NA_real_,
        deltaPT0 = NA_real_,
        optimizationCriterion = C_OPTIMIZATION_CRITERION_DEFAULT,
        gammaA = NA_real_,
        typeBetaSpending = C_TYPE_OF_DESIGN_BS_NONE,
        userAlphaSpending = NA_real_,
        userBetaSpending = NA_real_,
        gammaB = NA_real_,
        bindingFutility = NA,
        directionUpper = NA,
        constantBoundsHP = C_CONST_BOUND_HP_DEFAULT,
        twoSidedPower = NA,
        betaAdjustment = NA,
        delayedInformation = NA_real_,
        tolerance = C_DESIGN_TOLERANCE_DEFAULT,
        userFunctionCallEnabled = FALSE,
        userDefinedInterimStops = FALSE) {
    typeOfDesign <- .matchArgument(typeOfDesign, C_DEFAULT_TYPE_OF_DESIGN)
    optimizationCriterion <- .matchArgument(optimizationCriterion, C_OPTIMIZATION_CRITERION_DEFAULT)
    typeBetaSpending <- .matchArgument(typeBetaSpending, C_TYPE_OF_DESIGN_BS_NONE)

    if (.isDefinedArgument(kMax, argumentExistsValidationEnabled = userFunctionCallEnabled)) {
        .assertIsValidKMax(kMax, showWarnings = TRUE)
        if (!is.integer(kMax)) {
            kMax <- as.integer(kMax)
        }
    } else if (.isDefinedArgument(informationRates, argumentExistsValidationEnabled = userFunctionCallEnabled) &&
            length(informationRates) > 10) {
        warning("The usage of 'kMax' (", length(informationRates), ") > 10 is not validated", call. = FALSE)
    }

    if (is.na(bindingFutility)) {
        bindingFutility <- C_BINDING_FUTILITY_DEFAULT
    } else if (userFunctionCallEnabled &&
            !identical(typeOfDesign, C_TYPE_OF_DESIGN_PT) &&
            !grepl("^bs", typeBetaSpending)) {
        if (!is.na(kMax) && kMax == 1) {
            warning("'bindingFutility' (", bindingFutility, ") will be ignored ",
                "because kMax = 1",
                call. = FALSE
            )
        } else if (anyNA(futilityBounds)) {
            warning("'bindingFutility' (", bindingFutility, ") will be ignored ",
                "because 'futilityBounds' is not defined",
                call. = FALSE
            )
        } else if (all(futilityBounds == C_FUTILITY_BOUNDS_DEFAULT, na.rm = TRUE)) {
            warning("'bindingFutility' (", bindingFutility, ") will be ignored ",
                "because 'futilityBounds' (", .arrayToString(futilityBounds), ") ",
                "is set to default values",
                call. = FALSE
            )
        }
    }

    futilityBounds <- .applyDirectionOfAlternative(futilityBounds,
        directionUpper,
        type = "negateIfLower", phase = "design", syncLength = TRUE
    )

    design <- .createDesign(
        designClass = designClass,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = typeOfDesign,
        deltaWT = deltaWT,
        deltaPT1 = deltaPT1,
        deltaPT0 = deltaPT0,
        optimizationCriterion = optimizationCriterion,
        gammaA = gammaA,
        typeBetaSpending = typeBetaSpending,
        userAlphaSpending = userAlphaSpending,
        userBetaSpending = userBetaSpending,
        gammaB = gammaB,
        bindingFutility = bindingFutility,
        directionUpper = directionUpper,
        constantBoundsHP = constantBoundsHP,
        twoSidedPower = twoSidedPower,
        betaAdjustment = betaAdjustment,
        delayedInformation = delayedInformation,
        tolerance = tolerance
    )

    if (userFunctionCallEnabled) {
        .validateBaseParameters(design, twoSidedWarningForDefaultValues = FALSE)
        .validateTypeOfDesign(design, userDefinedInterimStops = userDefinedInterimStops)

        .assertIsValidTolerance(tolerance)
        .assertDesignParameterExists(design, "alpha", C_ALPHA_DEFAULT)
        .assertDesignParameterExists(design, "beta", C_BETA_DEFAULT)
        .assertDesignParameterExists(design, "sided", C_SIDED_DEFAULT)
        if (!userDefinedInterimStops) {
            .assertDesignParameterExists(design, "typeOfDesign", C_DEFAULT_TYPE_OF_DESIGN)
        }
        .assertDesignParameterExists(design, "bindingFutility", C_BINDING_FUTILITY_DEFAULT)
        .assertDesignParameterExists(design, "tolerance", C_DESIGN_TOLERANCE_DEFAULT)

        if (typeOfDesign != C_TYPE_OF_DESIGN_PT) {
            if (!is.na(deltaPT1)) {
                warning("'deltaPT1' (", deltaPT1, ") will be ignored", call. = FALSE)
            }
            if (!is.na(deltaPT0)) {
                warning("'deltaPT0' (", deltaPT0, ") will be ignored", call. = FALSE)
            }
        }
        if (typeOfDesign != C_TYPE_OF_DESIGN_WT && !is.na(deltaWT)) {
            warning("'deltaWT' (", deltaWT, ") will be ignored", call. = FALSE)
        }
        if (typeOfDesign != C_TYPE_OF_DESIGN_AS_KD &&
                typeOfDesign != C_TYPE_OF_DESIGN_AS_HSD && !is.na(gammaA)) {
            warning("'gammaA' (", gammaA, ") will be ignored", call. = FALSE)
        }
        if (typeBetaSpending != C_TYPE_OF_DESIGN_BS_KD &&
                typeBetaSpending != C_TYPE_OF_DESIGN_BS_HSD && !is.na(gammaB)) {
            warning("'gammaB' (", gammaB, ") will be ignored", call. = FALSE)
        }
        if (typeBetaSpending != C_TYPE_OF_DESIGN_BS_USER && !all(is.na(userBetaSpending))) {
            warning("'userBetaSpending' (", .arrayToString(userBetaSpending), ") ",
                "will be ignored",
                call. = FALSE
            )
        }
        if (!(typeOfDesign %in% c(C_TYPE_OF_DESIGN_AS_USER)) &&
                !all(is.na(userAlphaSpending))) {
            warning("'userAlphaSpending' (", .arrayToString(userAlphaSpending), ") ",
                "will be ignored",
                call. = FALSE
            )
        }
    }

    if (design$sided == 2 && design$bindingFutility && design$typeOfDesign != C_TYPE_OF_DESIGN_PT &&
            !.isBetaSpendingDesignType(design$typeBetaSpending)) {
        warning("'bindingFutility' will be ignored because ",
            "the test is defined as two-sided",
            call. = FALSE
        )
        design$bindingFutility <- FALSE
    }

    if (design$sided == 1 && design$twoSidedPower) {
        warning("'twoSidedPower' will be ignored because ",
            "the test is defined as one-sided",
            call. = FALSE
        )
        design$twoSidedPower <- FALSE
    }

    if (userFunctionCallEnabled) {
        .validateAlphaAndBeta(design)
    }

    design$alphaSpent <- rep(NA_real_, design$kMax)
    design$betaSpent <- rep(NA_real_, design$kMax)
    design$power <- rep(NA_real_, design$kMax)

    if (userFunctionCallEnabled) {
        design$.setParameterType("betaSpent", C_PARAM_NOT_APPLICABLE)
        design$.setParameterType("power", C_PARAM_NOT_APPLICABLE)
        design$.setParameterType("alphaSpent", C_PARAM_NOT_APPLICABLE)
        design$.setParameterType("criticalValues", C_PARAM_GENERATED)
    }

    if (design$kMax == 1) {
        .getDesignGroupSequentialKMax1(design)
    } else {
        # Wang and Tsiatis design
        if (design$typeOfDesign == C_TYPE_OF_DESIGN_WT ||
                design$typeOfDesign == C_TYPE_OF_DESIGN_P ||
                design$typeOfDesign == C_TYPE_OF_DESIGN_OF) {
            .getDesignGroupSequentialWangAndTsiatis(design)
        }

        # Pampallona & Tsiatis design
        else if (design$typeOfDesign == C_TYPE_OF_DESIGN_PT) {
            .getDesignGroupSequentialPampallonaTsiatis(design)
        }

        # Haybittle & Peto design
        else if (design$typeOfDesign == C_TYPE_OF_DESIGN_HP) {
            .getDesignGroupSequentialHaybittleAndPeto(design)
        }

        # Optimum design within Wang and Tsiatis class
        else if (design$typeOfDesign == C_TYPE_OF_DESIGN_WT_OPTIMUM) {
            .getDesignGroupSequentialWangAndTsiatisOptimum(design)
        }

        # alpha spending approaches
        else if (.isAlphaSpendingDesignType(design$typeOfDesign, userDefinedAlphaSpendingIncluded = FALSE)) {
            .getDesignGroupSequentialAlphaSpending(design, userFunctionCallEnabled)
        }

        # user defined alpha spending approach
        else if (design$typeOfDesign %in% c(C_TYPE_OF_DESIGN_AS_USER, C_TYPE_OF_DESIGN_NO_EARLY_EFFICACY)) {
            .getDesignGroupSequentialUserDefinedAlphaSpending(design, userFunctionCallEnabled)
        } else {
            stop(C_EXCEPTION_TYPE_RUNTIME_ISSUE, "no calculation routine defined for ", design$typeOfDesign)
        }
    }

    design$stageLevels <- 1 - stats::pnorm(.getCriticalValues(design))
    design$.setParameterType("stageLevels", C_PARAM_GENERATED)

    if (design$kMax == 1) {
        design$.setParameterType("futilityBounds", C_PARAM_NOT_APPLICABLE)
    }

    if (.hasApplicableFutilityBounds(design)) {
        if (length(design$futilityBounds) == 0 || all(design$futilityBounds == C_FUTILITY_BOUNDS_DEFAULT)) {
            design$.setParameterType("bindingFutility", C_PARAM_NOT_APPLICABLE)
            design$.setParameterType("futilityBounds", C_PARAM_NOT_APPLICABLE)
        } else if (userFunctionCallEnabled &&
                any(design$futilityBounds > .getCriticalValues(design, 1:(design$kMax - 1)) - 0.01, na.rm = TRUE)) {
            stop(
                C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
                "'futilityBounds' (", .arrayToString(design$futilityBounds), ") ",
                "too extreme for this situation",
                call. = FALSE
            )
        }
    }

    .assertIsValidAlphaSpent(design, userFunctionCallEnabled)

    design$.initStages()

    # we use 7.5 instead of C_QNORM_THRESHOLD as threshold
    design$criticalValues[!is.na(design$criticalValues) & design$criticalValues <= -7.5] <- -Inf
    design$criticalValues[!is.na(design$criticalValues) & design$criticalValues >= 7.5] <- Inf

    design$criticalValues <- .applyDirectionOfAlternative(design$criticalValues,
        design$directionUpper,
        type = "negateIfLower", phase = "design"
    )

    design$futilityBounds[!is.na(design$futilityBounds) & design$futilityBounds <=
        C_FUTILITY_BOUNDS_DEFAULT] <- C_FUTILITY_BOUNDS_DEFAULT

    if (design$kMax == 1) {
        if (!identical(design$informationRates, 1)) {
            if (!is.na(design$informationRates)) {
                warning("Information rate", ifelse(length(design$informationRates) != 1, "s", ""), " ",
                    .arrayToString(design$informationRates, vectorLookAndFeelEnabled = TRUE),
                    " will be ignored",
                    call. = FALSE
                )
            }
            design$informationRates <- 1
        }
        design$.setParameterType("informationRates", C_PARAM_NOT_APPLICABLE)
        design$.setParameterType("stages", C_PARAM_NOT_APPLICABLE)
    }

    .assertIsNumericVector(delayedInformation, "delayedInformation", naAllowed = TRUE)
    if (all(is.na(delayedInformation))) {
        design$futilityBounds <- .applyDirectionOfAlternative(
            design$futilityBounds, directionUpper,
            type = "negateIfLower", phase = "design"
        )

        # delayed response design is disabled
        return(design)
    }

    if (all(!is.na(delayedInformation)) && all(delayedInformation < 1e-03)) {
        warning("At least one delayed information value must be >= 1e-03 to enable delayed response.",
            " 'delayedInformation' (", .arrayToString(delayedInformation), ") will be ignored",
            call. = FALSE
        )

        design$futilityBounds <- .applyDirectionOfAlternative(
            design$futilityBounds, directionUpper,
            type = "negateIfLower", phase = "design"
        )

        return(design)
    }

    # proceed with delayed response design
    .assertIsInClosedInterval(delayedInformation, "delayedInformation", lower = 0, upper = NULL)
    kMax <- design$kMax
    contRegionUpper <- .getCriticalValues(design)
    contRegionLower <- design$futilityBounds
    informationRates <- design$informationRates

    decisionCriticalValues <- numeric(kMax)
    reversalProbabilities <- numeric(kMax - 1)

    if (!all(is.na(delayedInformation)) &&
            (design$sided != 1 || all(design$futilityBounds <= C_FUTILITY_BOUNDS_DEFAULT + 1e-06))) {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "decision critical values for delayed response design are only available for ",
            "one-sided designs with valid futility bounds",
            call. = FALSE
        )
    }
    if (length(delayedInformation) == 1) {
        delayedInformation <- rep(delayedInformation, kMax - 1)
    }
    if (length(delayedInformation) != kMax - 1) {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'delayedInformation' (", .arrayToString(delayedInformation), ") must have length ",
            (kMax - 1), " (kMax - 1)",
            call. = FALSE
        )
    }

    indices <- which(delayedInformation > 0 & delayedInformation < 1e-03)
    n <- length(indices)
    if (n > 0) {
        warning("The", ifelse(n == 1, "", paste0(" ", n)), " delayed information value", ifelse(n == 1, "", "s"), " ",
            .arrayToString(delayedInformation[indices], mode = "and"),
            " will be replaced by 1e-03 to achieve reasonable results",
            call. = FALSE
        )
        delayedInformation[indices] <- 1e-03
    }

    # sensible interim choices are restricted by amount of delayed information
    eps <- design$informationRates[1:(design$kMax - 1)] + delayedInformation
    if (!anyNA(eps) && any(eps >= 1)) {
        stages <- which(eps >= 1)
        stagesInfo <- ifelse(length(stages) == 1, paste0(" ", stages), paste0("s ", .arrayToString(stages, mode = "and")))
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT,
            "'delayedInformation[stage] + informationRates[stage]' for ",
            "stage", stagesInfo, " too large (>= 1). Recruitment stop analysis information + pipeline data ",
            "information cannot exceed overall trial information. Instead, the ",
            "recruitment stop analysis would be skipped, directly proceeding to the ",
            "final analysis",
            call. = FALSE
        )
    }

    # loop iterating through the stages calculation the decision critical values
    for (stage in 1:(kMax - 1)) {
        if (!is.na(delayedInformation[stage]) && delayedInformation[stage] >= 1e-03 - 1e-06) {
            # information rate vector in case of recruitment stop at 'stage'
            informationRatesUponDelay <- c(
                informationRates[1:stage],
                informationRates[stage] + delayedInformation[stage]
            )
            if (stage == 1) {
                decisionCriticalValues[stage] <- .getOneDimensionalRoot(
                    function(secondCriticalValue) {
                        probs1 <- .getGroupSequentialProbabilities(
                            matrix(
                                c(
                                    contRegionUpper[stage], secondCriticalValue,
                                    C_UPPER_BOUNDS_DEFAULT, C_UPPER_BOUNDS_DEFAULT
                                ),
                                nrow = 2, byrow = TRUE
                            ), informationRatesUponDelay
                        )
                        probs2 <- .getGroupSequentialProbabilities(
                            matrix(
                                c(
                                    -C_UPPER_BOUNDS_DEFAULT, secondCriticalValue,
                                    contRegionLower[stage], C_UPPER_BOUNDS_DEFAULT
                                ),
                                nrow = 2, byrow = TRUE
                            ), informationRatesUponDelay
                        )
                        return(probs1[1, stage + 1] - probs2[2, stage + 1] + probs2[1, stage + 1])
                    },
                    lower = -C_UPPER_BOUNDS_DEFAULT,
                    upper = C_UPPER_BOUNDS_DEFAULT,
                    tolerance = design$tolerance
                )

                probs <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionUpper[stage], decisionCriticalValues[stage],
                            C_UPPER_BOUNDS_DEFAULT, C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRatesUponDelay
                )
            } else {
                decisionCriticalValues[stage] <- .getOneDimensionalRoot(function(secondCriticalValue) {
                    probs1 <- .getGroupSequentialProbabilities(
                        matrix(
                            c(
                                contRegionLower[1:(stage - 1)], contRegionUpper[stage], secondCriticalValue,
                                contRegionUpper[1:(stage - 1)], C_UPPER_BOUNDS_DEFAULT, C_UPPER_BOUNDS_DEFAULT
                            ),
                            nrow = 2, byrow = TRUE
                        ), informationRatesUponDelay
                    )
                    probs2 <- .getGroupSequentialProbabilities(
                        matrix(
                            c(
                                contRegionLower[1:(stage - 1)], -C_UPPER_BOUNDS_DEFAULT, secondCriticalValue,
                                contRegionUpper[1:(stage - 1)], contRegionLower[stage], C_UPPER_BOUNDS_DEFAULT
                            ),
                            nrow = 2, byrow = TRUE
                        ), informationRatesUponDelay
                    )
                    return(probs1[1, stage + 1] - probs2[2, stage + 1] + probs2[1, stage + 1])
                }, lower = -C_UPPER_BOUNDS_DEFAULT, upper = C_UPPER_BOUNDS_DEFAULT, tolerance = design$tolerance)

                probs <- .getGroupSequentialProbabilities(
                    matrix(
                        c(
                            contRegionLower[1:(stage - 1)], contRegionUpper[stage], decisionCriticalValues[stage],
                            contRegionUpper[1:(stage - 1)], C_UPPER_BOUNDS_DEFAULT, C_UPPER_BOUNDS_DEFAULT
                        ),
                        nrow = 2, byrow = TRUE
                    ), informationRatesUponDelay
                )
            }
            if (stage < kMax) {
                reversalProbabilities[stage] <- probs[1, stage + 1]
            }
        } else {
            decisionCriticalValues[stage] <- NA_real_
            reversalProbabilities[stage] <- NA_real_
        }
        decisionCriticalValues[kMax] <- contRegionUpper[kMax]

        alphaSpent <- .calculateDecisionProbabilities(
            sqrtShift = 0, informationRates,
            delayedInformation, contRegionUpper, contRegionLower, decisionCriticalValues
        )$power
    }

    decisionCriticalValues[decisionCriticalValues <= -C_UPPER_BOUNDS_DEFAULT + 1e-06] <- NA_real_
    decisionCriticalValues[decisionCriticalValues >= C_UPPER_BOUNDS_DEFAULT - 1e-06] <- NA_real_

    design$decisionCriticalValues <- decisionCriticalValues
    design$reversalProbabilities <- reversalProbabilities
    design$delayedInformation <- delayedInformation
    design$delayedInformation[design$delayedInformation < 1e-03] <- 0
    design$.setParameterType("decisionCriticalValues", C_PARAM_GENERATED)
    design$.setParameterType("reversalProbabilities", C_PARAM_GENERATED)
    design$futilityBounds <- .applyDirectionOfAlternative(
        design$futilityBounds, directionUpper,
        type = "negateIfLower", phase = "design"
    )

    warning("The delayed information design feature is experimental and ",
        "hence not fully validated (see www.rpact.com/experimental)",
        call. = FALSE
    )

    return(design)
}

# to avoid error messages in case of repeated p-values computation
.assertIsValidAlphaSpent <- function(design, userFunctionCallEnabled = TRUE) {
    if (!userFunctionCallEnabled) {
        return(invisible())
    }

    if (design$informationRates[design$kMax] != 1) {
        return(invisible())
    }

    if (is.na(design$alphaSpent[design$kMax]) || abs(design$alphaSpent[design$kMax] - design$alpha) > 1e-05) {
        stop(
            C_EXCEPTION_TYPE_RUNTIME_ISSUE, "critical values cannot be calculated ",
            "(alpha: ", design$alpha, "; alpha spent at maximum stage: ", design$alphaSpent[design$kMax], ")"
        )
    }
}

.assertIsValidBetaSpent <- function(design, ..., userFunctionCallEnabled = TRUE, iteration = 1) {
    if (!userFunctionCallEnabled) {
        return(invisible())
    }

    if (design$informationRates[design$kMax] != 1) {
        return(invisible())
    }

    if (iteration < 0) {
        stop(C_EXCEPTION_TYPE_RUNTIME_ISSUE, "critical values cannot be calculated")
    }

    if (is.na(design$betaSpent[design$kMax]) || abs(design$betaSpent[design$kMax] - design$beta) > 1e-05) {
        stop(
            C_EXCEPTION_TYPE_RUNTIME_ISSUE, "critical values cannot be calculated ",
            "(beta spent at maximum stage: ", design$betaSpent[design$kMax], ")"
        )
    }
}

#'
#' @title
#' Get Design Group Sequential
#'
#' @description
#' Provides adjusted boundaries and defines a group sequential design.
#'
#' @inheritParams param_kMax
#' @inheritParams param_alpha
#' @inheritParams param_beta
#' @inheritParams param_sided
#' @inheritParams param_typeOfDesign
#' @inheritParams param_informationRates
#' @param futilityBounds The futility bounds, defined on the scale defined by \code{futilityBoundsScale}.
#'        (numeric vector of length \code{kMax - 1}).
#' @param futilityBoundsScale Character. The scale of the futility bounds.
#'        Must be one of \code{"zValue"}, \code{"pValue"}, \code{"reverseCondPower"},
#'        \code{"condPowerAtObserved"}, or \code{"predictivePower"}.
#'        Default is \code{"zValue"}.
#' @inheritParams param_bindingFutility
#' @inheritParams param_directionUpper
#' @param deltaWT Delta for Wang & Tsiatis Delta class.
#' @param deltaPT1 Delta1 for Pampallona & Tsiatis class rejecting H0 boundaries.
#' @param deltaPT0 Delta0 for Pampallona & Tsiatis class rejecting H1 boundaries.
#' @param constantBoundsHP The constant bounds up to stage \code{kMax - 1} for the
#'        Haybittle & Peto design (default is \code{3}).
#' @param optimizationCriterion Optimization criterion for optimum design within
#'        Wang & Tsiatis class (\code{"ASNH1"}, \code{"ASNIFH1"},
#'        \code{"ASNsum"}), default is \code{"ASNH1"}, see details.
#' @param typeBetaSpending Type of beta spending. Type of of beta spending is one of the following:
#'        O'Brien & Fleming type beta spending, Pocock type beta spending,
#'        Kim & DeMets beta spending, Hwang, Shi & DeCani beta spending, user defined
#'        beta spending (\code{"bsOF"}, \code{"bsP"}, \code{"bsKD"},
#'        \code{"bsHSD"}, \code{"bsUser"}, default is \code{"none"}).
#' @param gammaA Parameter for alpha spending function.
#' @param gammaB Parameter for beta spending function.
#' @inheritParams param_userAlphaSpending
#' @param delayedInformation Delay of information for delayed response designs. Can be a numeric value or a
#' 		  numeric vector of length \code{kMax - 1}
#' @param userBetaSpending The user defined beta spending. Vector of length \code{kMax} containing the cumulative
#'        beta-spending up to each interim stage.
#' @param efficacyStops Logical vector of length \code{kMax - 1} indicating efficacy stops. Default is \code{NA}.
#' @param futilityStops Logical vector of length \code{kMax - 1} indicating futility stops. Default is \code{NA}.
#' @param twoSidedPower For two-sided testing, if \code{twoSidedPower = TRUE} is specified
#'        the sample size calculation is performed by considering both tails of the distribution.
#'        Default is \code{FALSE}, i.e., it is assumed that one tail probability is equal to 0 or the power
#'        should be directed to one part.
#' @param betaAdjustment For two-sided beta spending designs, if \code{betaAdjustement = TRUE} a linear
#' 		  adjustment of the beta spending values is performed if an overlapping of decision regions for futility
#' 		  stopping at earlier stages occurs, otherwise no adjustment is performed (default is \code{TRUE}).
#' @param tolerance The numerical tolerance, default is \code{1e-08}.
#' @inheritParams param_three_dots
#'
#' @template details_group_sequential_design
#'
#' @template return_object_trial_design
#' @template how_to_get_help_for_generics
#'
#' @family design functions
#'
#' @template examples_get_design_group_sequential
#'
#' @seealso \code{\link[=getFutilityBounds]{getFutilityBounds()}} for the
#'     specification of futility bounds on scales other than the z-value scale.
#' @seealso [Vignette: Enhanced Futility Bounds Specification](https://www.rpact.org/vignettes/planning/rpact_futility_bounds/)
#'
#' @export
#'
getDesignGroupSequential <- function(...,
        kMax = NA_integer_,
        alpha = NA_real_,
        beta = NA_real_,
        sided = 1L, # C_SIDED_DEFAULT
        informationRates = NA_real_,
        futilityBounds = NA_real_,
        typeOfDesign = c(
            "OF",
            "P",
            "WT",
            "PT",
            "HP",
            "WToptimum",
            "asP",
            "asOF",
            "asKD",
            "asHSD",
            "asUser",
            "noEarlyEfficacy"
        ), # C_DEFAULT_TYPE_OF_DESIGN,
        deltaWT = NA_real_,
        deltaPT1 = NA_real_,
        deltaPT0 = NA_real_,
        optimizationCriterion = c(
            "ASNH1",
            "ASNIFH1",
            "ASNsum"
        ), # C_OPTIMIZATION_CRITERION_DEFAULT
        gammaA = NA_real_,
        typeBetaSpending = c(
            "none",
            "bsP",
            "bsOF",
            "bsKD",
            "bsHSD",
            "bsUser"
        ), # C_TYPE_OF_DESIGN_BS_NONE
        userAlphaSpending = NA_real_,
        userBetaSpending = NA_real_,
        efficacyStops = NA,
        futilityStops = NA,
        gammaB = NA_real_,
        bindingFutility = NA,
        futilityBoundsScale = c(
            "zValue",
            "pValue",
            "reverseCondPower",
            "condPowerAtObserved",
            "predictivePower"
        ),
        directionUpper = NA,
        betaAdjustment = NA,
        constantBoundsHP = 3, # C_CONST_BOUND_HP_DEFAULT,
        twoSidedPower = NA,
        delayedInformation = NA_real_,
        tolerance = 1e-08 # C_DESIGN_TOLERANCE_DEFAULT
        ) {
    .warnInCaseOfUnknownArguments(functionName = "getDesignGroupSequential", ...)

    typeBetaSpending <- .matchArgument(typeBetaSpending, C_TYPE_OF_DESIGN_BS_NONE)
    futilityBoundsScale <- match.arg(futilityBoundsScale)

    .assertIsLogicalVector(efficacyStops, "efficacyStops", naAllowed = TRUE)
    .assertIsLogicalVector(futilityStops, "futilityStops", naAllowed = TRUE)

    designClass <- C_CLASS_NAME_TRIAL_DESIGN_GROUP_SEQUENTIAL

    design <- NULL
    if (!all(is.na(futilityBounds)) && .futilityBoundsCalculationRequiresDesign(futilityBoundsScale)) {
        design <- .getDesignGroupSequentialOrWithInterimStops(
            designClass = designClass,
            kMax = length(futilityBounds) + 1,
            alpha = alpha,
            beta = beta,
            sided = sided,
            informationRates = informationRates,
            futilityBounds = NA_real_,
            typeOfDesign = typeOfDesign,
            deltaWT = deltaWT,
            deltaPT1 = deltaPT1,
            deltaPT0 = deltaPT0,
            optimizationCriterion = optimizationCriterion,
            gammaA = gammaA,
            typeBetaSpending = typeBetaSpending,
            userAlphaSpending = userAlphaSpending,
            userBetaSpending = userBetaSpending,
            gammaB = gammaB,
            bindingFutility = NA,
            directionUpper = directionUpper,
            betaAdjustment = betaAdjustment,
            constantBoundsHP = constantBoundsHP,
            twoSidedPower = twoSidedPower,
            delayedInformation = delayedInformation,
            tolerance = tolerance,
            efficacyStops = efficacyStops,
            futilityStops = futilityStops
        )
    }

    futilityBounds <- .getFutilityBoundsFromArgs(
        futilityBounds = futilityBounds,
        futilityBoundsScale = futilityBoundsScale,
        functionName = "getDesignGroupSequential",
        design = design,
        ...
    )

    return(.getDesignGroupSequentialOrWithInterimStops(
        designClass = designClass,
        kMax = kMax,
        alpha = alpha,
        beta = beta,
        sided = sided,
        informationRates = informationRates,
        futilityBounds = futilityBounds,
        typeOfDesign = typeOfDesign,
        deltaWT = deltaWT,
        deltaPT1 = deltaPT1,
        deltaPT0 = deltaPT0,
        optimizationCriterion = optimizationCriterion,
        gammaA = gammaA,
        typeBetaSpending = typeBetaSpending,
        userAlphaSpending = userAlphaSpending,
        userBetaSpending = userBetaSpending,
        gammaB = gammaB,
        bindingFutility = bindingFutility,
        directionUpper = directionUpper,
        betaAdjustment = betaAdjustment,
        constantBoundsHP = constantBoundsHP,
        twoSidedPower = twoSidedPower,
        delayedInformation = delayedInformation,
        tolerance = tolerance,
        efficacyStops = efficacyStops,
        futilityStops = futilityStops
    ))
}

.getFixedSampleSize <- function(..., alpha, beta, sided, twoSidedPower) {
    .assertIsValidAlphaAndBeta(alpha = alpha, beta = beta)
    .assertIsValidSidedParameter(sided)

    if (sided == 1) {
        return((.getOneMinusQNorm(alpha) + .getOneMinusQNorm(beta))^2)
    }
    if (isTRUE(twoSidedPower)) {
        n <- .getOneDimensionalRoot(
            function(n) {
                stats::pnorm(-.getOneMinusQNorm(alpha / 2) - sqrt(n)) -
                    stats::pnorm(.getOneMinusQNorm(alpha / 2) - sqrt(n)) + beta
            },
            lower = 0,
            upper = 2 * (.getOneMinusQNorm(alpha / 2) + .getOneMinusQNorm(beta))^2,
            tolerance = 1e-08, callingFunctionInformation = ".getFixedSampleSize"
        )
    } else {
        n <- (.getOneMinusQNorm(alpha / 2) + .getOneMinusQNorm(beta))^2
    }
    return(n)
}

#' @title
#' Get Design Characteristics
#'
#' @description
#' Calculates the characteristics of a design and returns it.
#'
#' @inheritParams param_design
#' @inheritParams param_three_dots
#'
#' @details
#' Calculates the inflation factor (IF),
#' the expected reduction in sample size under H1, under H0, and under a value in between H0 and H1.
#' Furthermore, absolute information values are calculated
#' under the prototype case testing H0: mu = 0 against H1: mu = 1.
#'
#' @return Returns a \code{\link{TrialDesignCharacteristics}} object.
#' The following generics (R generic functions) are available for this result object:
#' \itemize{
#'   \item \code{\link[=names.FieldSet]{names()}} to obtain the field names,
#'   \item \code{\link[=print.FieldSet]{print()}} to print the object,
#'   \item \code{\link[=summary.ParameterSet]{summary()}} to display a summary of the object,
#'   \item \code{\link[=plot.ParameterSet]{plot()}} to plot the object,
#'   \item \code{\link[=as.data.frame.TrialDesignCharacteristics]{as.data.frame()}} to coerce the object to a \code{\link[base]{data.frame}},
#'   \item \code{\link[=as.matrix.FieldSet]{as.matrix()}} to coerce the object to a \code{\link[base]{matrix}}.
#' }
#' @template how_to_get_help_for_generics
#'
#' @family design functions
#'
#' @template examples_get_design_characteristics
#'
#' @export
#'
getDesignCharacteristics <- function(design = NULL, ...) {
    if (is.null(design)) {
        design <- .getDefaultDesign(..., type = "characteristics")
        .warnInCaseOfUnknownArguments(
            functionName = "getDesignCharacteristics",
            ignore = .getDesignArgumentsToIgnoreAtUnknownArgumentCheck(design, powerCalculationEnabled = FALSE), ...
        )
    } else {
        .assertIsTrialDesign(design)
        .warnInCaseOfUnknownArguments(functionName = "getDesignCharacteristics", ...)
        .warnInCaseOfTwoSidedPowerArgument(...)
    }

    return(.getDesignCharacteristics(design = design, userFunctionCallEnabled = TRUE))
}

.getDesignCharacteristics <- function(..., design, userFunctionCallEnabled = FALSE) {
    .assertIsTrialDesignInverseNormalOrGroupSequential(design)
    .assertDesignParameterExists(design, "sided", C_SIDED_DEFAULT)
    .assertIsValidSidedParameter(design$sided)

    if (userFunctionCallEnabled) {
        .validateAlphaAndBeta(design = design)
    }

    design$informationRates <- .getValidatedInformationRates(design, writeToDesign = FALSE)

    if ((design$typeOfDesign == C_TYPE_OF_DESIGN_PT ||
            .isBetaSpendingDesignType(design$typeBetaSpending)) && design$sided == 2 && design$kMax == 2) {
        design$futilityBounds[is.na(design$futilityBounds)] <- 0
    }

    design$futilityBounds <- .getValidatedFutilityBounds(design,
        writeToDesign = FALSE, twoSidedWarningForDefaultValues = FALSE
    )

    designCharacteristics <- TrialDesignCharacteristics$new(design = design)

    designCharacteristics$rejectionProbabilities <- rep(NA_real_, design$kMax)
    designCharacteristics$.setParameterType("rejectionProbabilities", C_PARAM_NOT_APPLICABLE)

    designCharacteristics$futilityProbabilities <- rep(NA_real_, design$kMax - 1)
    designCharacteristics$.setParameterType("futilityProbabilities", C_PARAM_NOT_APPLICABLE)

    nFixed <- .getFixedSampleSize(
        alpha = design$alpha,
        beta = design$beta,
        sided = design$sided,
        twoSidedPower = design$twoSidedPower
    )
    designCharacteristics$nFixed <- nFixed
    designCharacteristics$.setParameterType("nFixed", C_PARAM_GENERATED)

    design$criticalValues[design$criticalValues > 7.5] <- 7.5
    design$criticalValues[design$criticalValues < -7.5] <- -7.5
    if (length(design$decisionCriticalValues) > 0) {
        design$decisionCriticalValues[!is.na(design$decisionCriticalValues) &
            design$decisionCriticalValues > 7.5] <- 7.5
        design$decisionCriticalValues[!is.na(design$decisionCriticalValues) &
            design$decisionCriticalValues < -7.5] <- -7.5
    }
    informationRates <- design$informationRates

    if (design$kMax == 1) {
        designCharacteristics$shift <- nFixed
        designCharacteristics$.setParameterType("shift", C_PARAM_GENERATED)

        designCharacteristics$inflationFactor <- designCharacteristics$shift / nFixed
        designCharacteristics$.setParameterType("inflationFactor", C_PARAM_GENERATED)

        designCharacteristics$power <- 1 - design$beta
        designCharacteristics$.setParameterType("power", design$.getParameterType("power"))

        designCharacteristics$.setParameterType("information", C_PARAM_NOT_APPLICABLE)

        designCharacteristics$.setParameterType("averageSampleNumber1", C_PARAM_NOT_APPLICABLE)
        designCharacteristics$.setParameterType("averageSampleNumber01", C_PARAM_NOT_APPLICABLE)
        designCharacteristics$.setParameterType("averageSampleNumber0", C_PARAM_NOT_APPLICABLE)
        designCharacteristics$.setParameterType(".probs", C_PARAM_NOT_APPLICABLE)

        return(designCharacteristics)
    }

    criticalValues <- .getCriticalValues(design)
    if (!anyNA(design$delayedInformation) &&
            length(design$decisionCriticalValues) > 0) {
        contRegionUpper <- criticalValues
        contRegionLower <- design$futilityBounds
        informationRates <- design$informationRates
        decisionCriticalValues <- design$decisionCriticalValues
        informationRates <- design$informationRates
        delayedInformation <- design$delayedInformation
        kMax <- length(informationRates)

        shift <- .getOneDimensionalRoot(
            function(shift) {
                resultsH1 <- .calculateDecisionProbabilities(
                    sqrt(shift),
                    informationRates,
                    delayedInformation,
                    contRegionUpper,
                    contRegionLower,
                    decisionCriticalValues
                )
                return(resultsH1$power[kMax] - 1 + design$beta)
            },
            lower = 0,
            upper = 4 * nFixed,
            tolerance = design$tolerance,
            callingFunctionInformation = ".getDesignCharacteristics"
        )

        stopping <- numeric(kMax)
        futility <- numeric(kMax)
        rejectionProbabilities <- numeric(kMax)

        resultsH1 <- .calculateDecisionProbabilities(
            sqrtShift = sqrt(shift), informationRates, delayedInformation,
            contRegionUpper, contRegionLower, decisionCriticalValues
        )
        resultsH01 <- .calculateDecisionProbabilities(
            sqrtShift = sqrt(shift) / 2, informationRates, delayedInformation,
            contRegionUpper, contRegionLower, decisionCriticalValues
        )
        resultsH0 <- .calculateDecisionProbabilities(
            sqrtShift = 0, informationRates, delayedInformation,
            contRegionUpper, contRegionLower, decisionCriticalValues
        )

        designCharacteristics$shift <- shift
        designCharacteristics$.probs <- resultsH1$probs
        designCharacteristics$power <- resultsH1$power

        designCharacteristics$information <- informationRates * shift

        designCharacteristics$averageSampleNumber1 <-
            (shift - sum(resultsH1$stoppingProbabilities * (informationRates[kMax] -
                delayedInformation - informationRates[1:(kMax - 1)]) * shift)) / nFixed
        designCharacteristics$averageSampleNumber01 <-
            (shift - sum(resultsH01$stoppingProbabilities * (informationRates[kMax] -
                delayedInformation - informationRates[1:(kMax - 1)]) * shift)) / nFixed
        designCharacteristics$averageSampleNumber0 <-
            (shift - sum(resultsH0$stoppingProbabilities * (informationRates[kMax] -
                delayedInformation - informationRates[1:(kMax - 1)]) * shift)) / nFixed
        futilityProbabilities <- resultsH1$futilityProbabilities
        rejectionProbabilities <- resultsH1$power
        stoppingProbabilities <- resultsH1$stoppingProbabilities
        rejectionProbabilities[2:kMax] <- resultsH1$power[2:kMax] - rejectionProbabilities[1:(kMax - 1)]

        designCharacteristics$rejectionProbabilities <- rejectionProbabilities
        designCharacteristics$futilityProbabilities <- futilityProbabilities
    } else if ((design$typeOfDesign == C_TYPE_OF_DESIGN_PT ||
            .isBetaSpendingDesignType(design$typeBetaSpending)) && design$sided == 2) {
        design$futilityBounds[is.na(design$futilityBounds)] <- 0

        shift <- .getOneDimensionalRoot(
            function(shift) {
                decisionMatrix <- matrix(c(
                    -criticalValues - sqrt(shift * informationRates),
                    c(-design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]), 0),
                    c(design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]), 0),
                    criticalValues - sqrt(shift * informationRates)
                ), nrow = 4, byrow = TRUE)
                probs <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
                if (design$twoSidedPower) {
                    return(sum(probs[5, ] - probs[4, ] + probs[1, ]) - 1 + design$beta)
                } else {
                    return(sum(probs[5, ] - probs[4, ]) - 1 + design$beta)
                }
            },
            lower = 0, upper = 4 * (.getOneMinusQNorm(design$alpha / design$sided) + .getOneMinusQNorm(design$beta))^2,
            tolerance = design$tolerance, callingFunctionInformation = ".getDesignCharacteristics"
        )

        decisionMatrix <- matrix(c(
            -criticalValues - sqrt(shift * informationRates),
            c(-design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]), 0),
            c(design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]), 0),
            criticalValues - sqrt(shift * informationRates)
        ), nrow = 4, byrow = TRUE)
        probs <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
        designCharacteristics$shift <- shift
        designCharacteristics$.probs <- probs
        if (design$twoSidedPower) {
            designCharacteristics$power <- cumsum(probs[5, ] - probs[4, ] + probs[1, ])
            designCharacteristics$rejectionProbabilities <- probs[5, ] - probs[4, ] + probs[1, ]
        } else {
            designCharacteristics$power <- cumsum(probs[5, ] - probs[4, ])
            designCharacteristics$rejectionProbabilities <- probs[5, ] - probs[4, ]
        }
        if (design$kMax > 1) {
            designCharacteristics$futilityProbabilities <- probs[3, 1:(design$kMax - 1)] - probs[2, 1:(design$kMax - 1)]
        }

        designCharacteristics$information <- informationRates * shift
        designCharacteristics$averageSampleNumber1 <- .getAverageSampleNumber(
            design$kMax, design$informationRates, probs, shift, nFixed
        )

        decisionMatrix <- matrix(c(
            -criticalValues,
            c(-design$futilityBounds, 0),
            c(design$futilityBounds, 0),
            criticalValues
        ), nrow = 4, byrow = TRUE)
        probs0 <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
        designCharacteristics$averageSampleNumber0 <- .getAverageSampleNumber(
            design$kMax, design$informationRates, probs0, shift, nFixed
        )

        decisionMatrix <- matrix(c(
            -criticalValues - sqrt(shift * informationRates) / 2,
            c(-design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]) / 2, 0),
            c(design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]) / 2, 0),
            criticalValues - sqrt(shift * informationRates) / 2
        ), nrow = 4, byrow = TRUE)
        probs01 <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
        designCharacteristics$averageSampleNumber01 <- .getAverageSampleNumber(
            design$kMax, design$informationRates, probs01, shift, nFixed
        )

        design$futilityBounds[design$futilityBounds == 0] <- NA_real_
    } else {
        shift <- .getOneDimensionalRoot(
            function(shift) {
                if (design$sided == 2) {
                    decisionMatrix <- matrix(c(
                        -criticalValues - sqrt(shift * informationRates),
                        criticalValues - sqrt(shift * informationRates)
                    ), nrow = 2, byrow = TRUE)
                    probs <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
                    if (design$twoSidedPower) {
                        return(sum(probs[3, ] - probs[2, ] + probs[1, ]) - 1 + design$beta)
                    } else {
                        return(sum(probs[3, ] - probs[2, ]) - 1 + design$beta)
                    }
                } else {
                    shiftedFutilityBounds <- design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)])
                    shiftedFutilityBounds[design$futilityBounds <= C_FUTILITY_BOUNDS_DEFAULT] <-
                        C_FUTILITY_BOUNDS_DEFAULT
                    decisionMatrix <- matrix(c(
                        shiftedFutilityBounds, C_FUTILITY_BOUNDS_DEFAULT,
                        criticalValues - sqrt(shift * informationRates)
                    ), nrow = 2, byrow = TRUE)
                    probs <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
                    return(sum(probs[3, ] - probs[2, ]) - 1 + design$beta)
                }
            },
            lower = 0, upper = 4 * (.getOneMinusQNorm(design$alpha / design$sided) + .getOneMinusQNorm(design$beta))^2,
            tolerance = design$tolerance, callingFunctionInformation = ".getDesignCharacteristics"
        )

        if (design$sided == 2) {
            decisionMatrix <- matrix(c(
                -criticalValues - sqrt(shift * informationRates),
                criticalValues - sqrt(shift * informationRates)
            ), nrow = 2, byrow = TRUE)
        } else {
            shiftedFutilityBounds <- design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)])
            shiftedFutilityBounds[design$futilityBounds <= C_FUTILITY_BOUNDS_DEFAULT] <-
                C_FUTILITY_BOUNDS_DEFAULT
            decisionMatrix <- matrix(c(
                shiftedFutilityBounds, C_FUTILITY_BOUNDS_DEFAULT,
                criticalValues - sqrt(shift * informationRates)
            ), nrow = 2, byrow = TRUE)
        }
        probs <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
        designCharacteristics$shift <- shift
        designCharacteristics$.probs <- probs
        if (design$twoSidedPower) {
            designCharacteristics$power <- cumsum(probs[3, ] - probs[2, ] + probs[1, ])
            designCharacteristics$rejectionProbabilities <- probs[3, ] - probs[2, ] + probs[1, ]
        } else {
            designCharacteristics$power <- cumsum(probs[3, ] - probs[2, ])
            designCharacteristics$rejectionProbabilities <- probs[3, ] - probs[2, ]
        }

        if (design$kMax > 1) {
            if (design$sided == 2) {
                designCharacteristics$futilityProbabilities <- rep(0, design$kMax - 1)
            } else {
                designCharacteristics$futilityProbabilities <- probs[1, 1:(design$kMax - 1)]
                designCharacteristics$futilityProbabilities[design$futilityBounds == C_FUTILITY_BOUNDS_DEFAULT] <- 0
            }
            designCharacteristics$power <- .getNoEarlyEfficacyZeroCorrectedValues(
                design, designCharacteristics$power
            )
            designCharacteristics$rejectionProbabilities <- .getNoEarlyEfficacyZeroCorrectedValues(
                design, designCharacteristics$rejectionProbabilities
            )
        }
        designCharacteristics$information <- informationRates * shift
        designCharacteristics$averageSampleNumber1 <- .getAverageSampleNumber(
            design$kMax,
            design$informationRates, probs, shift, nFixed
        )
        if (design$sided == 2) {
            decisionMatrix <- matrix(c(-criticalValues, criticalValues), nrow = 2, byrow = TRUE)
        } else {
            decisionMatrix <- matrix(c(
                design$futilityBounds, C_FUTILITY_BOUNDS_DEFAULT,
                criticalValues
            ), nrow = 2, byrow = TRUE)
        }
        probs0 <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
        designCharacteristics$averageSampleNumber0 <- .getAverageSampleNumber(
            design$kMax,
            design$informationRates, probs0, shift, nFixed
        )
        if (design$sided == 2) {
            decisionMatrix <- matrix(c(
                -criticalValues - sqrt(shift * informationRates) / 2,
                criticalValues - sqrt(shift * informationRates) / 2
            ), nrow = 2, byrow = TRUE)
        } else {
            shiftedFutilityBounds <- design$futilityBounds - sqrt(shift * informationRates[1:(design$kMax - 1)]) / 2
            shiftedFutilityBounds[design$futilityBounds <= C_FUTILITY_BOUNDS_DEFAULT] <- C_FUTILITY_BOUNDS_DEFAULT
            decisionMatrix <- matrix(c(shiftedFutilityBounds, C_FUTILITY_BOUNDS_DEFAULT, criticalValues -
                sqrt(shift * informationRates) / 2), nrow = 2, byrow = TRUE)
        }
        probs01 <- .getGroupSequentialProbabilities(decisionMatrix, informationRates)
        designCharacteristics$averageSampleNumber01 <- .getAverageSampleNumber(
            design$kMax, design$informationRates, probs01, shift, nFixed
        )
    }
    design$criticalValues[design$criticalValues >= 7.5 - 1e-8] <- Inf
    design$criticalValues[design$criticalValues <= -7.5 + 1e-8] <- -Inf
    designCharacteristics$.setParameterType("shift", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("power", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType(".probs", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("rejectionProbabilities", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("information", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("futilityProbabilities", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("averageSampleNumber0", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("averageSampleNumber01", C_PARAM_GENERATED)
    designCharacteristics$.setParameterType("averageSampleNumber1", C_PARAM_GENERATED)

    designCharacteristics$inflationFactor <- shift / nFixed
    designCharacteristics$.setParameterType("inflationFactor", C_PARAM_GENERATED)

    if (is.na(designCharacteristics$inflationFactor) ||
            designCharacteristics$inflationFactor > 4 || designCharacteristics$inflationFactor < 1 - 1e-05) {
        stop(C_EXCEPTION_TYPE_RUNTIME_ISSUE, "inflation factor cannot be calculated")
    }

    return(designCharacteristics)
}

.getAverageSampleNumber <- function(kMax, informationRates, probs, shift, nFixed) {
    if (nrow(probs) == 3) {
        return((shift - sum((probs[3, 1:(kMax - 1)] - probs[2, 1:(kMax - 1)] + probs[1, 1:(kMax - 1)]) *
            (informationRates[kMax] - informationRates[1:(kMax - 1)]) * shift)) / nFixed)
    } else {
        return((shift - sum((probs[5, 1:(kMax - 1)] -
            probs[4, 1:(kMax - 1)] + probs[3, 1:(kMax - 1)] - probs[2, 1:(kMax - 1)] + probs[1, 1:(kMax - 1)]) *
            (informationRates[kMax] - informationRates[1:(kMax - 1)]) * shift)) / nFixed)
    }
}

#'
#' @title
#' Get Power And Average Sample Number
#'
#' @description
#' Returns the power and average sample number of the specified design.
#'
#' @inheritParams param_design
#' @inheritParams param_theta
#' @inheritParams param_nMax
#'
#' @details
#' This function returns the power and average sample number (ASN) of the specified
#' design for the prototype case which is testing H0: mu = mu0 in a one-sample design.
#' \code{theta} represents the standardized effect \code{(mu - mu0) / sigma} and power and ASN
#' is calculated for maximum sample size \code{nMax}.
#' For other designs than the one-sample test of a mean the standardized effect needs to be adjusted accordingly.
#'
#' @return Returns a \code{\link{PowerAndAverageSampleNumberResult}} object.
#' The following generics (R generic functions) are available for this result object:
#' \itemize{
#'   \item \code{\link[=names.FieldSet]{names()}} to obtain the field names,
#'   \item \code{\link[=print.FieldSet]{print()}} to print the object,
#'   \item \code{\link[=summary.ParameterSet]{summary()}} to display a summary of the object,
#'   \item \code{\link[=plot.ParameterSet]{plot()}} to plot the object,
#'   \item \code{\link[=as.data.frame.PowerAndAverageSampleNumberResult]{as.data.frame()}}
#'         to coerce the object to a \code{\link[base]{data.frame}},
#'   \item \code{\link[=as.matrix.FieldSet]{as.matrix()}} to coerce the object to a \code{\link[base]{matrix}}.
#' }
#' @template how_to_get_help_for_generics
#'
#' @family design functions
#'
#' @template examples_get_power_and_average_sample_number
#'
#' @export
#'
getPowerAndAverageSampleNumber <- function(design, theta = seq(-1, 1, 0.02), nMax = 100) {
    .assertIsTrialDesign(design)
    .assertIsSingleNumber(nMax, "nMax")
    .assertIsInClosedInterval(nMax, "nMax", lower = 1, upper = NULL)
    return(PowerAndAverageSampleNumberResult$new(design = design, theta = theta, nMax = nMax))
}

.getSimulatedRejectionsDelayedResponse <- function(delta, informationRates, delayedInformation,
        contRegionUpper, contRegionLower, decisionCriticalValues, iterations, seed = NA_real_) {
    seed <- .setSeed(seed)
    kMax <- length(informationRates)
    zVector <- numeric(kMax)
    reject <- 0L
    for (i in 1:iterations) {
        for (stage in 1:kMax) {
            if (stage == 1) {
                zVector[stage] <- stats::rnorm(1, delta * sqrt(informationRates[1]), 1)
            } else {
                zVector[stage] <- (sqrt(informationRates[stage - 1]) * zVector[stage - 1] +
                    sqrt(informationRates[stage] - informationRates[stage - 1]) *
                        stats::rnorm(1, delta * sqrt(informationRates[stage] - informationRates[stage - 1]), 1)) /
                    sqrt(informationRates[stage])
            }
            if (!is.na(decisionCriticalValues[stage]) && stage < kMax &&
                    (zVector[stage] > contRegionUpper[stage] || zVector[stage] < contRegionLower[stage])) {
                if ((sqrt(informationRates[stage]) * zVector[stage] + sqrt(delayedInformation[stage]) *
                        stats::rnorm(1, delta * sqrt(delayedInformation[stage]), 1)) /
                        sqrt(informationRates[stage] + delayedInformation[stage]) > decisionCriticalValues[stage]) {
                    reject <- reject + 1L
                }
                break
            }
            if (stage == kMax && zVector[stage] > decisionCriticalValues[stage]) {
                reject <- reject + 1L
            }
        }
    }
    simulatedAlpha <- reject / iterations
    return(list(
        simulatedAlpha = simulatedAlpha,
        delta = delta,
        iterations = iterations,
        seed = seed
    ))
}


#'
#' @title
#' Simulated Rejections Delayed Response
#'
#' @description
#' Simulates the rejection probability of a delayed response group sequential design with specified parameters.
#'
#' @inheritParams param_design
#' @param delta The delay value.
#' @param iterations The number of simulation iterations.
#' @inheritParams param_seed
#'
#' @details
#' By default, delta = 0, i.e., the Type error rate is simulated.
#'
#' @return Returns a list summarizing the rejection probabilities.
#'
#' @noRd
#'
getSimulatedRejectionsDelayedResponse <- function(design, ..., delta = 0, iterations = 10000, seed = NA_real_) {
    .assertIsTrialDesignInverseNormalOrGroupSequential(design)
    .assertIsSingleNumber(delta, "delta")
    .assertIsValidIterationsAndSeed(iterations, seed, zeroIterationsAllowed = FALSE)
    if (!design$.isDelayedResponseDesign()) {
        stop(
            C_EXCEPTION_TYPE_ILLEGAL_ARGUMENT, "'design' must be a delayed ",
            "response design with specified 'delayedInformation'",
            call. = FALSE
        )
    }
    startTime <- Sys.time()
    result <- .getSimulatedRejectionsDelayedResponse(
        delta = delta,
        informationRates = design$informationRates,
        delayedInformation = design$delayedInformation,
        contRegionUpper = .getCriticalValues(design),
        contRegionLower = design$futilityBounds,
        decisionCriticalValues = design$decisionCriticalValues,
        iterations = iterations,
        seed = seed
    )

    simulatedAlpha <- result$simulatedAlpha
    stdError <- sqrt(simulatedAlpha * (1 - simulatedAlpha) / iterations)
    ciLower <- simulatedAlpha - 1.96 * stdError
    ciUpper <- simulatedAlpha + 1.96 * stdError
    result$confidenceIntervall <- c(ciLower, ciUpper)
    # simulated Type I error rate is within the 95% error bounds
    result$alphaWithin95ConfidenceIntervall <- design$alpha > ciLower && design$alpha < ciUpper
    result$time <- Sys.time() - startTime
    return(result)
}
