#' @examples
#' nrows <- c(3, 2, 5)
#' ncols <- c(2, 4, 8)
#'
#' A <- Reduce(kronecker, Map(matrix, Map(rnorm, nrows * ncols), nrows))
#'
#' all.equal(A, Reduce(kronecker, approx.kron(A, nrows, ncols)))
#'
#' @export
approx.kron <- function(A, nrows, ncols) {

    # rearrange kronecker product `A` into outer product `R`
    dim(A) <- c(rev(nrows), rev(ncols))
    axis.perm <- as.vector(t(matrix(seq_along(dim(A)), ncol = 2L))[, rev(seq_along(nrows))])
    R <- aperm(A, axis.perm, resize = FALSE)
    dim(R) <- nrows * ncols

    # scaling factor for every product component
    s <- sum(A^2)^(1 / length(dim(A)))

    # higher order SVD on `R` with one singular vector
    Map(function(mode, nr, nc) {
        s * `dim<-`(La.svd(mcrossprod(R, mode = mode), 1L, 0L)$u, c(nr, nc))
    }, seq_along(nrows), nrows, ncols)

}


#' Approximates Kronecker Product decomposition.
#'
#' Approximates the matrices `A` and `B` such that
#'      C = A %x% B
#' with `%x%` the kronecker product of the matrixes `A` and `B`
#' of dimensions `dimA` and `dimB` respectively.
#'
#' @param C desired kronecker product result.
#' @param dimA length 2 vector of dimensions of \code{A}.
#' @param dimB length 2 vector of dimensions of \code{B}.
#'
#' @return list with attributes `A` and `B`.
#'
#' @examples
#' A <- matrix(seq(14), 7, 2)
#' B <- matrix(c(1, 0), 3, 4)
#' C <- kronecker(A, B) # the same as 'C <- A %x% B'
#' approx.kronecker(C, dim(A), dim(B))
#'
#' @seealso C.F. Van Loan / Journal of Computational and Applied Mathematics
#'          123 (2000) 85-100 (pp. 93-95)
#'
#' @export
approx.kronecker <- function(C, dimA, dimB = dim(C) / dimA) {

    dim(C) <- c(dimB[1L], dimA[1L], dimB[2L], dimA[2L])
    R <- aperm(C, c(2L, 4L, 1L, 3L))
    dim(R) <- c(prod(dimA), prod(dimB))

    if (requireNamespace("RSpectra", quietly = TRUE)) {
        svdR <- RSpectra::svds(R, 1L)
    } else {
        svdR <- svd(R, 1L, 1L)
    }

    list(
        A = array(sqrt(svdR$d[1]) * svdR$u, dimA),
        B = array(sqrt(svdR$d[1]) * svdR$v, dimB)
    )
}

#' Kronecker Product Decomposition.
#'
#' Computes the components summation components `A_i`, `B_i` of a sum of
#' Kronecker products
#'      C = sum_i A_i %x% B_i
#' with the minimal estimated number of summands.
#'
#' @param C desired kronecker product result.
#' @param dimA length 2 vector of dimensions of \code{A}.
#' @param dimB length 2 vector of dimensions of \code{B}.
#' @param tol tolerance of singular values of \code{C} to determin the number of
#'  needed summands.
#'
#' @return list of lenghth with estimated number of summation components, each
#'  entry consists of a list with named entries \code{"A"} and \code{"B"} of
#'  dimensions \code{dimA} and \code{dimB}.
#'
#' @examples
#' As <- replicate(3, matrix(rnorm(2 * 7), 2), simplify = FALSE)
#' Bs <- replicate(3, matrix(rnorm(5 * 3), 5), simplify = FALSE)
#' C <- Reduce(`+`, Map(kronecker, As, Bs))
#'
#' decomposition <- decompose.kronecker(C, c(2, 7))
#'
#' reconstruction <- Reduce(`+`, Map(function(summand) {
#'     kronecker(summand[[1]], summand[[2]])
#' }, decomposition), array(0, dim(C)))
#'
#' stopifnot(all.equal(C, reconstruction))
#'
#' @export
decompose.kronecker <- function(C, dimA, dimB = dim(C) / dimA, tol = 1e-7) {
    # convert the equivalent outer product
    dim(C) <- c(dimB[1L], dimA[1L], dimB[2L], dimA[2L])
    C <- aperm(C, c(2L, 4L, 1L, 3L), resize = FALSE)
    dim(C) <- c(prod(dimA), prod(dimB))

    # Singular Valued Decomposition
    svdC <- La.svd(C)

    # Sum of Kronecker Components
    lapply(seq_len(sum(svdC$d > tol)), function(i) list(
        A = matrix(svdC$d[i] * svdC$u[, i], dimA),
        B = matrix(svdC$vt[i, ], dimB)
    ))
}

### Given that C is a Kronecker product this is a fast method but a bit
### unreliable in full generality.
# decompose.kronecker <- function(C, dimA, dimB = dim(C) / dimA) {
#     dim(C) <- c(dimB[1L], dimA[1L], dimB[2L], dimA[2L])
#     R <- aperm(C, c(2L, 4L, 1L, 3L))
#     dim(R) <- c(prod(dimA), prod(dimB))
#     max.index <- which.max(abs(R))
#     row.index <- ((max.index - 1L) %% nrow(R)) + 1L
#     col.index <- ((max.index - 1L) %/% nrow(R)) + 1L
#     max.elem <- if (abs(R[max.index]) > .Machine$double.eps) R[max.index] else 1
#     list(
#         A = array(R[, col.index], dimA),
#         B = array(R[row.index, ] / max.elem, dimB)
#     )
# }


# kron <- function(A, B) {
#     perm <- as.vector(t(matrix(seq_len(2 * length(dim(A))), ncol = 2)[, 2:1]))
#     K <- aperm(outer(A, B), perm)
#     dim(K) <- dim(A) * dim(B)
#     K
# }

# kronperm <- function(A) {
#     # force A to have even number of dimensions
#     dim(A) <- c(dim(A), rep(1L, length(dim(A)) %% 2L))
#     # compute axis permutation
#     perm <- as.vector(t(matrix(seq_along(dim(A)), ncol = 2)[, 2:1]))
#     # permute elements of A
#     K <- aperm(A, perm, resize = FALSE)
#     # collapse/set dimensions
#     dim(K) <- head(dim(A), length(dim(A)) / 2) * tail(dim(A), length(dim(A)) / 2)
#     K
# }

# p <- c(2, 3, 5)
# q <- c(3, 4, 7)
# A <- array(rnorm(prod(p)), p)
# B <- array(rnorm(prod(q)), q)
# all.equal(kronperm(outer(A, B)), kronecker(A, B))
# all.equal(kron(A, B), kronecker(A, B))


# dA <- c(2, 3, 5)
# dB <- c(3, 4, 7)
# A <- array(rnorm(prod(dA)), dA)
# B <- array(rnorm(prod(dB)), dB)
# X <- outer(A, B)

# r <- length(dim(X)) / 2
# I <- t(do.call(expand.grid, Map(seq_len, head(dim(X), r) * tail(dim(X), r))))
# K <- apply(rbind(
#     (I - 1) %/% tail(dim(X), r) + 1,
#     (I - 1) %% tail(dim(X), r) + 1
# ), 2, function(i) X[sum(c(1, cumprod(head(dim(X), -1))) * (i - 1)) + 1])
# dim(K) <- head(dim(X), r) * tail(dim(X), r)

# all.equal(kronecker(A, B), K)
