# ===== FactorizationWorkspace =====

abstract type AbstractFactorizationWorkspace end

struct FactorizationWorkspace{T, I <: Integer} <: AbstractFactorizationWorkspace
    Mptr::FVector{I}
    Mval::FVector{T}
    Fval::FVector{T}
    Eval::FVector{T}
    piv::FVector{BlasInt}
    mval::FVector{I}
    fval::FVector{I}
end

function FactorizationWorkspace{T}(S::ChordalSymbolic{I}) where {T, I <: Integer}
    Mptr = FVector{I}(undef, S.nMptr)
    Mval = FVector{T}(undef, S.nMval)
    Fval = FVector{T}(undef, max(S.nFval * S.nFval, two(I)))
    Eval = FVector{T}(undef, S.nFval)
    piv  = FVector{BlasInt}(undef, S.nFval)
    mval = FVector{I}(undef, S.nNval)
    fval = FVector{I}(undef, S.nFval)
    return FactorizationWorkspace{T, I}(Mptr, Mval, Fval, Eval, piv, mval, fval)
end

function FactorizationWorkspace(L::ChordalTriangular{DIAG, UPLO, T}) where {DIAG, UPLO, T}
    return FactorizationWorkspace{T}(L.S)
end

function FactorizationWorkspace(F::AbstractFactorization)
    return FactorizationWorkspace(triangular(F))
end

# TODO: workspace for dense factorization
struct DenseFactorizationWorkspace <: AbstractFactorizationWorkspace end

function FactorizationWorkspace(::AbstractTriangular)
    return DenseFactorizationWorkspace()
end

"""
    cholesky!(F[, pivot::PivotingStrategy]; check=true, reg=NoRegularization())

Perform a Cholesky factorization of a positive definite matrix.

### Example

Use [`ChordalCholesky`](@ref) to construct a factorization object,
and use [`cholesky!`](@ref) to perform the factorization.

```julia-repl
julia> using CliqueTrees.Multifrontal, LinearAlgebra

julia> A = [
           4  2  0  0  2
           2  5  0  0  3
           0  0  4  2  0
           0  0  2  5  2
           2  3  0  2  7
       ];

julia> F = cholesky!(ChordalCholesky(A))
5×5 FChordalCholesky{:L, Float64, Int64} with 10 stored entries:
 2.0   ⋅    ⋅    ⋅    ⋅
 0.0  2.0   ⋅    ⋅    ⋅
 0.0  1.0  2.0   ⋅    ⋅
 1.0  0.0  0.0  2.0   ⋅
 0.0  1.0  1.0  1.0  2.0
```

### Parameters

  - `F`: positive definite matrix
  - `pivot`: pivoting strategy
  - `check`: if `check = true`, then the function errors if `F`
    is not positive definite
  - `reg`: dynamic regularization strategy

"""
function LinearAlgebra.cholesky!(
        F::AbstractCholesky{UPLO, T},
        pivot::PivotingStrategy=NoPivot();
        check::Bool=true,
        reg::AbstractRegularization=NoRegularization(),
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    W = FactorizationWorkspace(F)
    return cholesky!(W, F, pivot; check, reg, tol)
end

function LinearAlgebra.cholesky!(
        W::AbstractFactorizationWorkspace,
        F::AbstractCholesky{UPLO, T},
        pivot::PivotingStrategy=NoPivot();
        check::Bool=true,
        reg::AbstractRegularization=NoRegularization(),
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    signs = Ones{T}(size(F, 1))
    return factorize!(W, F, pivot; signs, reg, check, tol)
end

function LinearAlgebra.cholesky!(
        L::ChordalTriangular{:N, UPLO, T},
        pivot::NoPivot=NoPivot();
        check::Bool=true,
        reg::AbstractRegularization=NoRegularization(),
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    W = FactorizationWorkspace(L)
    return cholesky!(W, L, pivot; check, reg, tol)
end

function LinearAlgebra.cholesky!(
        L::ChordalTriangular{:N, UPLO, T},
        pivot::RowMaximum;
        check::Bool=true,
        reg::AbstractRegularization=NoRegularization(),
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    W = FactorizationWorkspace(L)
    return cholesky!(W, L, pivot; check, reg, tol)
end

function LinearAlgebra.cholesky!(
        W::FactorizationWorkspace,
        L::ChordalTriangular{:N, UPLO, T},
        pivot::PivotingStrategy=NoPivot();
        check::Bool=true,
        reg::AbstractRegularization=NoRegularization(),
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    signs = Ones{T}(size(L, 1))
    d = Ones{T}(size(L, 1))
    return factorize!(W, L, d, pivot; signs, reg, check, tol)
end

"""
    ldlt!(F[, pivot::PivotingStrategy]; check=true, signs=Zeros(size(F, 1)), reg=NoRegularization())

Perform an LDLᵀ factorization of a quasi-definite matrix.

### Example

```julia-repl
julia> using CliqueTrees.Multifrontal, LinearAlgebra

julia> A = [
           4  2  0  0  2
           2  5  0  0  3
           0  0  4  2  0
           0  0  2  5  2
           2  3  0  2  7
       ];

julia> F = ldlt!(ChordalLDLt(A))
5×5 FChordalLDLt{:L, Float64, Int64} with 10 stored entries:
 1.0   ⋅    ⋅    ⋅    ⋅
 0.0  1.0   ⋅    ⋅    ⋅
 0.0  0.5  1.0   ⋅    ⋅
 0.5  0.0  0.0  1.0   ⋅
 0.0  0.5  0.5  0.5  1.0

 4.0   ⋅    ⋅    ⋅    ⋅
  ⋅   4.0   ⋅    ⋅    ⋅
  ⋅    ⋅   4.0   ⋅    ⋅
  ⋅    ⋅    ⋅   4.0   ⋅
  ⋅    ⋅    ⋅    ⋅   4.0
```

### Parameters

   - `F`: quasi-definite matrix
   - `pivot`: pivoting strategy
   - `check`: if `check = true`, then the function errors
     if the matrix is singular
   - `signs`: pivot signs
   - `reg`: dynamic regularization strategy

"""
function LinearAlgebra.ldlt!(
        F::AbstractLDLt{UPLO, T},
        pivot::PivotingStrategy=NoPivot();
        signs::AbstractVector=Zeros{T}(size(F, 1)),
        reg::AbstractRegularization=NoRegularization(),
        check::Bool=true,
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    W = FactorizationWorkspace(F)
    return ldlt!(W, F, pivot; signs, reg, check, tol)
end

function LinearAlgebra.ldlt!(
        W::AbstractFactorizationWorkspace,
        F::AbstractLDLt{UPLO, T},
        pivot::PivotingStrategy=NoPivot();
        signs::AbstractVector=Zeros{T}(size(F, 1)),
        reg::AbstractRegularization=NoRegularization(),
        check::Bool=true,
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    return factorize!(W, F, pivot; signs, reg, check, tol)
end

function LinearAlgebra.ldlt!(
        L::ChordalTriangular{:U, UPLO, T},
        d::AbstractVector{T},
        pivot::PivotingStrategy=NoPivot();
        signs::AbstractVector=Zeros{T}(size(L, 1)),
        reg::AbstractRegularization=NoRegularization(),
        check::Bool=true,
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    W = FactorizationWorkspace(L)
    return ldlt!(W, L, d, pivot; signs, reg, check, tol)
end

function LinearAlgebra.ldlt!(
        W::FactorizationWorkspace,
        L::ChordalTriangular{:U, UPLO, T},
        d::AbstractVector{T},
        pivot::PivotingStrategy=NoPivot();
        signs::AbstractVector=Zeros{T}(size(L, 1)),
        reg::AbstractRegularization=NoRegularization(),
        check::Bool=true,
        tol::Real=-one(real(T)),
    ) where {UPLO, T}
    return factorize!(W, L, d, pivot; signs, reg, check, tol)
end

# ===== factorize! Level 1: AbstractFactorization =====

function factorize!(
        W::AbstractFactorizationWorkspace,
        F::AbstractFactorization{DIAG, UPLO, T},
        pivot::PivotingStrategy;
        signs::AbstractVector,
        reg::AbstractRegularization,
        check::Bool,
        tol::Real,
    ) where {DIAG, UPLO, T}
    S = permuteto(T, signs, F.perm)

    if pivot isa NoPivot
        info = factorize!(W, triangular(F), F.d, pivot; signs=S, reg, check, tol)
    else
        info = factorize!(W, triangular(F), F.d, pivot, F.perm, F.invp; signs=S, reg, check, tol)
    end

    if iszero(info)
        F.info[] = info
    else
        F.info[] = F.perm[info]
    end

    return F
end

# ===== factorize! Level 2: ChordalTriangular with kwargs =====

function factorize!(
        W::FactorizationWorkspace,
        L::ChordalTriangular{DIAG, UPLO, T, I},
        d::AbstractVector,
        pivot::NoPivot;
        signs::AbstractVector,
        reg::AbstractRegularization,
        check::Bool,
        tol::Real,
    ) where {DIAG, UPLO, T, I <: Integer}
    @assert checksigns(signs, reg)

    info = factorize!(W, L, d, pivot, signs, reg, tol)
    check && checkinfo(info, L.diag)

    return info
end

function factorize!(
        W::FactorizationWorkspace,
        L::ChordalTriangular{DIAG, UPLO, T, I},
        d::AbstractVector,
        pivot::RowMaximum,
        perm::AbstractVector,
        invp::AbstractVector;
        signs::AbstractVector,
        reg::AbstractRegularization,
        check::Bool,
        tol::Real,
    ) where {DIAG, UPLO, T, I <: Integer}
    @assert checksigns(signs, reg)

    info = factorize!(W, L, d, pivot, perm, invp, signs, reg, tol)

    if check && ispositive(info)
        # throw(RankDeficientException(info))
    end

    return info
end

# ===== factorize! Level 3: all positional (core computation) =====

function factorize!(
        W::FactorizationWorkspace,
        L::ChordalTriangular{DIAG, UPLO, T, I},
        d::AbstractVector{T},
        ::NoPivot,
        signs::AbstractVector{T},
        reg::AbstractRegularization,
        tol::Real,
    ) where {DIAG, UPLO, T, I <: Integer}
    R = initialize(L, signs, reg)
    return chol_impl!(W, L, d, signs, R)
end

function chol_impl!(
        W::FactorizationWorkspace,
        L::ChordalTriangular{DIAG, UPLO, T, I},
        d::AbstractVector{T},
        signs::AbstractVector{T},
        R::AbstractRegularization,
    ) where {DIAG, UPLO, T, I <: Integer}
    return chol_impl!(W.Mptr, W.Mval, W.Fval, L, d, signs, R)
end

function chol_impl!(
        Mptr::AbstractVector{I},
        Mval::AbstractVector{T},
        Fval::AbstractVector{T},
        L::ChordalTriangular{:N, UPLO, T, I},
        R::AbstractRegularization=NoRegularization(),
    ) where {UPLO, T, I <: Integer}
    n = ncl(L)
    d = Ones{T}(n)
    S = Ones{T}(n)
    info = chol_impl!(Mptr, Mval, Fval, L, d, S, R)
    return info
end

function chol_impl!(
        Mptr::AbstractVector{I},
        Mval::AbstractVector{T},
        Fval::AbstractVector{T},
        L::ChordalTriangular{DIAG, UPLO, T, I},
        d::AbstractVector{T},
        S::AbstractVector{T},
        R::AbstractRegularization=NoRegularization(),
    ) where {DIAG, UPLO, T, I <: Integer}
    info = chol_impl!(
        Mptr, Mval,
        L.S.Dptr, L.Dval,
        L.S.Lptr, L.Lval,
        d, Fval,
        L.S.res, L.S.rel, L.S.chd,
        L.uplo, S, R, L.diag)

    return info
end

function chol_impl!(
        Mptr::AbstractVector{I},
        Mval::AbstractVector{T},
        Dptr::AbstractVector{I},
        Dval::AbstractVector{T},
        Lptr::AbstractVector{I},
        Lval::AbstractVector{T},
        d::AbstractVector{T},
        Fval::AbstractVector{T},
        res::AbstractGraph{I},
        rel::AbstractGraph{I},
        chd::AbstractGraph{I},
        uplo::Val{UPLO},
        S::AbstractVector{T},
        R::AbstractRegularization,
        diag::Val{DIAG},
    ) where {T, I <: Integer, UPLO, DIAG}

    ns = zero(I); Mptr[one(I)] = one(I)

    for j in vertices(res)
        ns, info = chol_loop!(Mptr, Mval, Dptr, Dval, Lptr, Lval, d, Fval, res, rel, chd, ns, j, uplo, S, R, diag)

        if isnegative(info)
            return info
        elseif ispositive(info)
            return info + pointers(res)[j] - one(I)
        end
    end

    return zero(I)
end

function chol_loop!(
        Mptr::AbstractVector{I},
        Mval::AbstractVector{T},
        Dptr::AbstractVector{I},
        Dval::AbstractVector{T},
        Lptr::AbstractVector{I},
        Lval::AbstractVector{T},
        d::AbstractVector{T},
        Fval::AbstractVector{T},
        res::AbstractGraph{I},
        rel::AbstractGraph{I},
        chd::AbstractGraph{I},
        ns::I,
        j::I,
        uplo::Val{UPLO},
        S::AbstractVector{T},
        R::AbstractRegularization,
        diag::Val{DIAG},
    ) where {T, I <: Integer, UPLO, DIAG}
    nn = eltypedegree(res, j)

    if isone(nn)
        return chol_loop_nod!(
            Mptr, Mval,
            Dptr, Dval,
            Lptr, Lval,
            d, Fval,
            res, rel, chd, ns, j, uplo, S, R, diag
        )
    else
        return chol_loop_snd!(
            Mptr, Mval,
            Dptr, Dval,
            Lptr, Lval,
            d, Fval,
            res, rel, chd, ns, nn, j, uplo, S, R, diag
        )
    end
end

function chol_loop_snd!(
        Mptr::AbstractVector{I},
        Mval::AbstractVector{T},
        Dptr::AbstractVector{I},
        Dval::AbstractVector{T},
        Lptr::AbstractVector{I},
        Lval::AbstractVector{T},
        d::AbstractVector{T},
        Fval::AbstractVector{T},
        res::AbstractGraph{I},
        rel::AbstractGraph{I},
        chd::AbstractGraph{I},
        ns::I,
        nn::I,
        j::I,
        uplo::Val{UPLO},
        S::AbstractVector{T},
        R::AbstractRegularization,
        diag::Val{DIAG},
    ) where {T, I <: Integer, UPLO, DIAG}
    #
    # na is the size of the separator at node j
    #
    #     na = | sep(j) |
    #
    na = eltypedegree(rel, j)
    #
    # nj is the size of the bag at node j
    #
    #     nj = | bag(j) |
    #
    nj = nn + na
    #
    # F is the frontal matrix at node j
    #
    #           nn  na
    #     F = [ F₁₁     ] nn
    #         [ F₂₁ F₂₂ ] na
    #
    F = reshape(view(Fval, oneto(nj * nj)), nj, nj)

    F₁₁ = view(F, oneto(nn),      oneto(nn))
    F₂₂ = view(F, nn + one(I):nj, nn + one(I):nj)

    if UPLO === :L
        F₂₁ = view(F, nn + one(I):nj, oneto(nn))
    else
        F₂₁ = view(F, oneto(nn), nn + one(I):nj)
    end
    #
    # L is part of the Cholesky factor
    #
    #          res(j)
    #     L = [ D₁₁  ] res(j)
    #         [ L₂₁  ] sep(j)
    #
    Dp = Dptr[j]
    Lp = Lptr[j]
    d₁₁ = view(d, neighbors(res, j))
    S₁₁ = view(S, neighbors(res, j))
    D₁₁ = reshape(view(Dval, Dp:Dp + nn * nn - one(I)), nn, nn)

    if UPLO === :L
        L₂₁ = reshape(view(Lval, Lp:Lp + nn * na - one(I)), na, nn)
    else
        L₂₁ = reshape(view(Lval, Lp:Lp + nn * na - one(I)), nn, na)
    end
    #
    #     F ← 0
    #
    zerotri!(F, uplo)

    for i in Iterators.reverse(neighbors(chd, j))
        #
        # add the update matrix for child i to F
        #
        #   F ← F + Rᵢ Sᵢ Rᵢᵀ
        #
        chol_send!(F, Mptr, Mval, rel, ns, i, uplo)
        ns -= one(I)
    end
    #
    # add F to L
    #
    #     L ← L + F
    #
    addtri!(D₁₁, F₁₁, uplo)
    addrec!(L₂₁, F₂₁)
    #
    # factorize D₁₁ and solve for L₂₁
    #
    #     D₁₁ ← cholesky(D₁₁)
    #     L₂₁ ← L₂₁ D₁₁⁻ᴴ
    #
    info = chol_factor!(D₁₁, L₂₁, Fval, d₁₁, S₁₁, R, uplo, diag)
    !iszero(info) && return ns, convert(I, info)

    if ispositive(na)
        #
        # M₂₂ is the update matrix for node j
        #
        ns += one(I)
        strt = Mptr[ns]
        stop = Mptr[ns + one(I)] = strt + na * na
        M₂₂ = reshape(view(Mval, strt:stop - one(I)), na, na)
        #
        #     M₂₂ ← F₂₂
        #
        copytri!(M₂₂, F₂₂, uplo)

        if UPLO === :L
            trans = Val(:N)
        else
            trans = Val(:C)
        end
        #
        #     M₂₂ ← M₂₂ - L₂₁ L₂₁ᴴ
        #
        syrk!(uplo, trans, -one(real(T)), Fval, L₂₁, d₁₁, one(real(T)), M₂₂, diag)
    end

    return ns, zero(I)
end

# Fast path for nn = 1 (residual size is 1)
# In this case, diagonal blocks are scalars and off-diagonal blocks are vectors
function chol_loop_nod!(
        Mptr::AbstractVector{I},
        Mval::AbstractVector{T},
        Dptr::AbstractVector{I},
        Dval::AbstractVector{T},
        Lptr::AbstractVector{I},
        Lval::AbstractVector{T},
        d::AbstractVector{T},
        Fval::AbstractVector{T},
        res::AbstractGraph{I},
        rel::AbstractGraph{I},
        chd::AbstractGraph{I},
        ns::I,
        j::I,
        uplo::Val{UPLO},
        S::AbstractVector{T},
        R::AbstractRegularization,
        diag::Val{DIAG},
    ) where {T, I <: Integer, UPLO, DIAG}
    #
    # nn = 1 (the size of the residual at node j)
    #
    nn = one(I)
    #
    # na is the size of the separator at node j
    #
    #     na = | sep(j) |
    #
    na = eltypedegree(rel, j)
    #
    # nj is the size of the bag at node j
    #
    #     nj = | bag(j) | = 1 + na
    #
    nj = nn + na
    #
    # F is the frontal matrix at node j
    #
    #           1   na
    #     F = [ f₁₁     ] 1
    #         [ f₂₁ F₂₂ ] na
    #
    F = reshape(view(Fval, oneto(nj * nj)), nj, nj)

    F₂₂ = view(F, nn + one(I):nj, nn + one(I):nj)

    if UPLO === :L
        f₂₁ = view(F, nn + one(I):nj, one(I))
    else
        f₂₁ = view(F, one(I), nn + one(I):nj)
    end
    #
    # L is part of the Cholesky factor (d₁₁ is scalar, l₂₁ is vector)
    #
    #          res(j)
    #     L = [ d₁₁  ] res(j)
    #         [ l₂₁  ] sep(j)
    #
    Dp = Dptr[j]
    Lp = Lptr[j]
    Rp = pointers(res)[j]
    s₁ = S[Rp]
    l₂₁ = view(Lval, Lp:Lp + na - one(I))
    #
    #     F ← 0
    #
    zerotri!(F, uplo)

    for i in Iterators.reverse(neighbors(chd, j))
        #
        # add the update matrix for child i to F
        #
        #   F ← F + Rᵢ Sᵢ Rᵢᵀ
        #
        chol_send!(F, Mptr, Mval, rel, ns, i, uplo)
        ns -= one(I)
    end
    #
    # add F to L (scalar and vector additions)
    #
    #     d₁₁ ← d₁₁ + f₁₁
    #     l₂₁ ← l₂₁ + f₂₁
    #
    d₁₁ = Dval[Dp] + F[one(I)]
    addrec!(l₂₁, f₂₁)
    #
    # regularize d₁₁
    #
    d₁₁ = regularize(R, s₁, real(d₁₁), l₂₁)

    if iszero(d₁₁)
        return ns, one(I)
    end

    if DIAG === :N
        d₁₁ = sqrt(d₁₁)
        Dval[Dp] = d₁₁
    else
        d[Rp] = d₁₁
    end

    if ispositive(na)
        #
        # M₂₂ is the update matrix for node j
        #
        ns += one(I)
        strt = Mptr[ns]
        stop = Mptr[ns + one(I)] = strt + na * na
        M₂₂ = reshape(view(Mval, strt:stop - one(I)), na, na)
        #
        #     M₂₂ ← F₂₂
        #
        copytri!(M₂₂, F₂₂, uplo)
        #
        #     l₂₁ ← l₂₁ / d₁₁
        #
        rdiv!(l₂₁, d₁₁)

        if DIAG === :U
            #
            #     M₂₂ ← M₂₂ - d₁₁ l₂₁ l₂₁ᴴ
            #
            syr!(uplo, -d₁₁, l₂₁, M₂₂)
        else
            #
            #     M₂₂ ← M₂₂ - l₂₁ l₂₁ᴴ
            #
            syr!(uplo, -one(real(T)), l₂₁, M₂₂)
        end
    end

    return ns, zero(I)
end

@inline function chol_factor!(
        D::AbstractMatrix{T},
        L::AbstractMatrix{T},
        W::AbstractVector{T},
        d::AbstractVector{T},
        S::AbstractVector{T},
        R::AbstractRegularization,
        uplo::Val{UPLO},
        diag::Val{DIAG},
    ) where {T, UPLO, DIAG}
    @assert size(D, 1) == size(D, 2)

    if DIAG === :U
        @assert length(d) == size(D, 1)
        @assert length(D) <= length(W)

        if UPLO === :L
            @assert size(L, 2) == size(D, 1)
        else
            @assert size(L, 1) == size(D, 1)
        end
    end
    #
    # factorize D
    #
    #     D ← cholesky(D)
    #
    info = potrf!(uplo, W, D, d, S, R, diag)

    if iszero(info) && !isempty(L)
        if UPLO === :L
            side = Val(:R)
        else
            side = Val(:L)
        end
        #
        #     L ← L D⁻ᴴ
        #
        trsm!(side, uplo, Val(:C), diag, one(T), D, L)
        cdiv!(side, diag, L, d)
    end

    return info
end

# ===== GMW81 Specialization =====
#
# GMW81 requires blocked factorization with access to both D and L
#

function chol_factor!(
        D::AbstractMatrix{T},
        L::AbstractMatrix{T},
        W::AbstractVector{T},
        d::AbstractVector{T},
        S::AbstractVector{T},
        R::GMW81,
        uplo::Val{UPLO},
        diag::Val{DIAG},
    ) where {T, UPLO, DIAG}
    if UPLO === :L
        @assert size(D, 1) == size(D, 2) == size(L, 2)
    else
        @assert size(D, 1) == size(D, 2) == size(L, 1)
    end

    if DIAG === :U
        @assert length(d) == size(D, 1)
    end

    n = size(D, 1)

    @inbounds for bstrt in 1:THRESHOLD:n
        bstop = min(bstrt + THRESHOLD - 1, n)

        if UPLO === :L
            Dbb = view(D, bstrt:n, bstrt:bstop)
            Lnb = view(L, :, bstrt:bstop)
        else
            Dbb = view(D, bstrt:bstop, bstrt:n)
            Lnb = view(L, bstrt:bstop, :)
        end

        dbb = view(d, bstrt:bstop)
        Sbb = view(S, bstrt:bstop)

        info = chol_factor_block!(Dbb, Lnb, dbb, Sbb, R, uplo, diag)
        !iszero(info) && return bstrt + info - 1

        if bstop < n
            Drr = view(D, bstop + 1:n, bstop + 1:n)

            if UPLO === :L
                Drb = view(D, bstop + 1:n, bstrt:bstop)
                tA = Val(:N)
                tB = Val(:C)
            else
                Drb = view(D, bstrt:bstop, bstop + 1:n)
                tA = Val(:C)
                tB = Val(:N)
            end

            syrk!(uplo, tA, -one(real(T)), W, Drb, dbb, one(real(T)), Drr, diag)

            if !isempty(L)
                if UPLO === :L
                    Lnr = view(L, :, bstop + 1:n)
                    A = Lnb
                    B = Drb
                else
                    Lnr = view(L, bstop + 1:n, :)
                    A = Drb
                    B = Lnb
                end

                gemm!(tA, tB, -one(T), W, A, B, dbb, one(T), Lnr, diag)
            end
        end
    end

    return 0
end

function chol_factor_block!(
        D::AbstractMatrix{T},
        L::AbstractMatrix{T},
        d::AbstractVector{T},
        S::AbstractVector{T},
        R::GMW81,
        uplo::Val{:L},
        diag::Val{DIAG},
    ) where {T, DIAG}
    @assert size(D, 2) == size(L, 2)

    if DIAG === :U
        @assert length(d) == size(D, 2)
    end

    @inbounds for j in axes(D, 2)
        Djj = regularize(R, S, D, L, real(D[j, j]), j, uplo)

        if DIAG === :U
            d[j] = Djj
        else
            D[j, j] = Djj = sqrt(Djj)
        end

        iDjj = inv(Djj)

        for i in j + 1:size(D, 1)
            D[i, j] *= iDjj
        end

        for i in axes(L, 1)
            L[i, j] *= iDjj
        end

        for k in j + 1:size(D, 2)
            Dkj = D[k, j]
            cDkj = conj(Dkj)

            if DIAG === :N
                D[k, k] -= abs2(Dkj)
            else
                D[k, k] -= Djj * abs2(Dkj)
            end

            for i in k + 1:size(D, 1)
                if DIAG === :N
                    D[i, k] -= D[i, j] * cDkj
                else
                    D[i, k] -= D[i, j] * Djj * cDkj
                end
            end

            for i in axes(L, 1)
                if DIAG === :N
                    L[i, k] -= L[i, j] * cDkj
                else
                    L[i, k] -= L[i, j] * Djj * cDkj
                end
            end
        end
    end

    return 0
end

function chol_factor_block!(
        D::AbstractMatrix{T},
        L::AbstractMatrix{T},
        d::AbstractVector{T},
        S::AbstractVector{T},
        R::GMW81,
        uplo::Val{:U},
        diag::Val{DIAG},
    ) where {T, DIAG}
    @assert size(D, 1) == size(L, 1)

    if DIAG === :U
        @assert length(d) == size(D, 1)
    end

    @inbounds for j in axes(D, 1)
        for i in j + 1:size(D, 2)
            for k in 1:j - 1
                if DIAG === :U
                    D[j, i] -= D[k, i] * d[k] * conj(D[k, j])
                else
                    D[j, i] -= D[k, i] * conj(D[k, j])
                end
            end
        end

        for i in axes(L, 2)
            for k in 1:j - 1
                if DIAG === :U
                    L[j, i] -= L[k, i] * d[k] * conj(D[k, j])
                else
                    L[j, i] -= L[k, i] * conj(D[k, j])
                end
            end
        end

        Djj = real(D[j, j])

        for k in 1:j - 1
            if DIAG === :U
                Djj -= d[k] * abs2(D[k, j])
            else
                Djj -= abs2(D[k, j])
            end
        end

        Djj = regularize(R, S, D, L, Djj, j, uplo)

        if DIAG === :U
            d[j] = Djj
        else
            D[j, j] = Djj = sqrt(Djj)
        end

        iDjj = inv(Djj)

        for i in j + 1:size(D, 2)
            D[j, i] *= iDjj
        end

        for i in axes(L, 2)
            L[j, i] *= iDjj
        end
    end

    return 0
end

function chol_send!(
        F::AbstractMatrix{T},
        ptr::AbstractVector{I},
        val::AbstractVector{T},
        rel::AbstractGraph{I},
        ns::I,
        i::I,
        uplo::Val{UPLO},
    ) where {UPLO, T, I <: Integer}
    #
    # na is the size of the separator at node i
    #
    #     na = | sep(j) |
    #
    na = eltypedegree(rel, i)
    #
    # inj is the subset inclusion
    #
    #     inj: sep(i) → bag(parent(i))
    #
    inj = neighbors(rel, i)
    #
    # M is the update matrix from child i
    #
    strt = ptr[ns]
    M = reshape(view(val, strt:strt + na * na - one(I)), na, na)
    #
    # add M to F
    #
    #     F ← F + inj M injᵀ
    #
    addscattertri!(F, M, inj, uplo)
    return
end
