#' Tensor/Matrix valued Predictors for Regression and Dimension Reduction
#'
#' Multiple methods using tensor/matrix valued predictors for regression or
#' sufficient dimension reduction.
#'
#' @author Daniel Kapla
#'
#' @import stats
#'
#' @docType package
#' @useDynLib tensorPredictors, .registration = TRUE
"_PACKAGE"

log.likelihood <- function(par, X, Fy, Delta.inv, da, db) {
    alpha <- matrix(par[1:prod(da)], da[1L])
    beta <- matrix(par[(prod(da) + 1):length(par)], db[1L])
    error <- X - tcrossprod(Fy, kronecker(alpha, beta))
    sum(error * (error %*% Delta.inv))
}

#' @export
tensor_predictor <- function(X, Fy, p, t, k = 1L, r = 1L, d1 = 1L, d2 = 1L,
                             method = "KPIR_LS",
                             eps1 = 1e-2, eps2 = 1e-2, maxit = 10L) {
    # Validate method using unexact matching.
    methods <- list(KPIR_LS = "KPIR_LS", KPIR_MLE = "KPIR_MLE",
                    KPFC1 = "KPFC1", KPFC2 = "KPFC2", KPFC3 = "KPFC3")
    method <- methods[[toupper(method), exact = FALSE]]
    if (is.null(method)) {
        stop("Unable to determine method.")
    }

    if (method %in% c("KPIR_LS", "KPIR_MLE")) {
        ## Step 1:
        # OLS estimate of the model `X = F_y B + epsilon`.
        B <- t(solve(crossprod(Fy), crossprod(Fy, X)))

        # Estimate alpha, beta as nearest kronecker approximation.
        c(alpha, beta) %<-% approx.kronecker(B, c(t, r), c(p, k))

        if (method == "KPIR_LS") {
            # Estimate Delta.
            B <- kronecker(alpha, beta)
            rank <- if (ncol(Fy) == 1) 1L else qr(Fy)$rank
            Delta <- crossprod(X - tcrossprod(Fy, B)) / (nrow(X) - rank)

        } else { # KPIR_MLE
            # Estimate initial Delta.
            B <- kronecker(alpha, beta)
            Delta <- crossprod(X - tcrossprod(Fy, B)) / nrow(X)

            for (. in 1:maxit) {
                # Optimize log-likelihood for alpha, beta with fixed Delta.
                opt <- optim(c(alpha, beta), log.likelihood, gr = NULL,
                             X, Fy, matpow(Delta, -1), c(t, r), c(p, k))
                # Store previous alpha, beta and Delta (for break consition).
                Delta.last <- Delta
                B.last     <- B
                # Extract optimized alpha, beta.
                alpha <- matrix(opt$par[1:(t * r)], t, r)
                beta <- matrix(opt$par[(t * r + 1):length(opt$par)], p, k)
                # Calc new Delta with likelihood optimized alpha, beta.
                B <- kronecker(alpha, beta)
                Delta <- crossprod(X - tcrossprod(Fy, B)) / nrow(X)
                # Check break condition 1.
                if (norm(Delta - Delta.last, 'F') < eps1 * norm(Delta, 'F')) {
                    # Check break condition 2.
                    if (norm(B - B.last, 'F') < eps2 * norm(B, 'F')) {
                        break
                    }
                }
            }
        }

        # Construct basis from alpha and beta.
        Gamma_1 <- if(d1 > 1L) La.svd(alpha, d1, 0L)$u
                   else        alpha / norm(alpha, 'F')
        Gamma_2 <- if(d2 > 1L) La.svd(beta,  d2, 0L)$u
                   else        beta / norm(beta, 'F')
        Gamma <- kronecker(Gamma_1, Gamma_2)
    } else if (method %in% c("KPFC1", "KPFC2", "KPFC3")) {
        ## Step 1:
        # OLS extimate of the model `X = F_y B + epsilon`.
        B <- t(solve(crossprod(Fy), crossprod(Fy, X)))

        ## Step 2:
        # Estimate Delta_mle.
        P_Fy <- Fy %*% solve(crossprod(Fy), t(Fy))
        Q_Fy <- diag(nrow(P_Fy)) - P_Fy
        Delta_fit <- crossprod(X, P_Fy %*% X) / nrow(X)
        Delta_res <- crossprod(X, Q_Fy %*% X) / nrow(X)
        # Compute Delta_mle using equation (7).
        D <- matpow(Delta_res, -0.5)
        Delta <- with(La.svd(D %*% Delta_fit %*% D), {
            K <- diag(c(rep(0, d1 * d2), d[-(1:(d1 * d2))]))
            D <- matpow(Delta_res, 0.5)
            Delta_res + (D %*% u %*% tcrossprod(K, u) %*% D)
        })

        ## Step 3:
        # Set Gamma to be the first `d = d1 * d2` eigenvectors of (25).
        D <- matpow(Delta, -0.5)
        Gamma <- with(La.svd(D %*% Delta_fit %*% D, d1 * d2, 0L), {
            La.svd(matpow(Delta, 0.5) %*% u[, 1:(d1 * d2)])$u
        })

        if (method == "KPFC1") {
            # Compute lower_gamma using (26).
            D <- crossprod(Gamma, matpow(Delta, -1))
            lower_gamma <- solve(D %*% Gamma, D %*% B)

            ## Step 4a:
            # Calc MLE estimate of B.
            B <- Gamma %*% lower_gamma
            # Using the VLP approx. for a kronecker product factorization.
            c(alpha, beta) %<-% approx.kronecker(B, c(t, r), c(p, k))

            # Construct basis from alpha and beta.
            Gamma_1 <- if(d1 > 1L) La.svd(alpha, d1, 0L)$u
                       else        alpha / norm(alpha, 'F')
            Gamma_2 <- if(d2 > 1L) La.svd(beta,  d2, 0L)$u
                       else        beta / norm(beta, 'F')
            Gamma <- kronecker(Gamma_1, Gamma_2)

        } else { # KPFC2, KPFC3
            ## Step 4b:
            # Estimate Gamma's as nearest kronecker approximation of Gamma.
            c(Gamma_1, Gamma_2) %<-% approx.kronecker(Gamma, c(t, d1), c(p, d2))
            Gamma <- kronecker(Gamma_1, Gamma_2)
            # Compute lower_gamma using (26).
            D <- crossprod(Gamma, matpow(Delta, -1))
            lower_gamma <- solve(D %*% Gamma, D %*% B)

            if (prod(dim(lower_gamma)) == 1) {
                # If lower_gamma is a scalar, then alpha, beta is only scaled.
                # (shortcut)
                lg1 <- lg2 <- sqrt(abs(as.vector(lower_gamma)))
                alpha <- lg1 * Gamma_1
                beta <- lg2 * Gamma_2
            } else if (method == "KPFC2") {
                ## Step 5c:
                c(alpha, beta) %<-% approx.kronecker(Gamma %*% lower_gamma,
                                                     c(t, r), c(p, k))
            } else { # KPFC3
                ## Step 5d:
                c(lg1, lg2) %<-% approx.kronecker(lower_gamma,
                                                  c(d1, r), c(d2, k))
                alpha <- Gamma_1 %*% lg1
                beta <- Gamma_2 %*% lg2
            }
        }
    }

    return(structure(
        list(alpha = alpha,
             beta = beta,
             Gamma = Gamma,
             Gamma_1 = Gamma_1, Gamma_2 = Gamma_2,
             Delta = Delta),
        class = c("tensor_predictor", method)
    ))
}

#' Reduce new data using estimated reduction.
#'
#' @param object estimated reduction object.
#' @param data new data to apply reduce
#' @param use which type of reduction/internal variables should be used
#'
#' @export
reduce <- function(object, data, use = 'Gamma') {
    if (use == 'Gamma') {
        projection <- object$Gamma
    } else if (use == 'alpha_beta') {
        projection <- kronecker(object$alpha, object$beta)
    } else {
        stop("Unkown 'use' parameter value.")
    }

    # ensure alignement of multiple calls.
    if (projection[1] < 0) {
        projection <- -projection
    }

    return(data %*% projection)
}
