"""
    IdentityOperator(len)

Matrix-free identity operator of size `(len, len)`.

# Arguments

  - `len::Integer`: Number of rows and columns.

# Fields

  - `len::Int`: Stored operator dimension.

# Interface Rules

`IdentityOperator` is constant, square, invertible, and supports the generic
`AbstractSciMLOperator` application, caching, and trait interfaces. It is
returned unchanged when composed with a compatible operator.

# Examples

```julia
using SciMLOperators

IdentityOperator(2) * [3.0, 4.0] == [3.0, 4.0]
```
"""
struct IdentityOperator <: AbstractSciMLOperator{Bool}
    len::Int
end

# constructors
function Base.one(L::AbstractSciMLOperator)
    @assert issquare(L)
    N = size(L, 1)
    return IdentityOperator(N)
end

Base.convert(::Type{AbstractMatrix}, ii::IdentityOperator) = Diagonal(ones(Bool, ii.len))
has_concretization(::IdentityOperator) = true

# Copy method to avoid aliasing - IdentityOperator has no mutable fields, can return self
Base.copy(L::IdentityOperator) = L

# traits
Base.show(io::IO, ii::IdentityOperator) = print(io, "IdentityOperator($(ii.len))")
Base.size(ii::IdentityOperator) = (ii.len, ii.len)
Base.adjoint(A::IdentityOperator) = A
Base.transpose(A::IdentityOperator) = A
Base.conj(A::IdentityOperator) = A

LinearAlgebra.opnorm(::IdentityOperator, p::Real = 2) = true
for pred in (:issymmetric, :ishermitian, :isposdef)
    @eval LinearAlgebra.$pred(::IdentityOperator) = true
end
ArrayInterface.issingular(::IdentityOperator) = false

getops(::IdentityOperator) = ()
isconstant(::IdentityOperator) = true
islinear(::IdentityOperator) = true
has_adjoint(::IdentityOperator) = true
has_mul!(::IdentityOperator) = true
has_ldiv(::IdentityOperator) = true
has_ldiv!(::IdentityOperator) = true

# operator application
for op in (:*, :\)
    @eval function Base.$op(ii::IdentityOperator, v::AbstractVecOrMat)
        @assert size(v, 1) == ii.len
        return copy(v)
    end
end

@inline function LinearAlgebra.mul!(
        w::AbstractVecOrMat, ii::IdentityOperator, v::AbstractVecOrMat
    )
    @assert size(v, 1) == ii.len
    return copy!(w, v)
end

@inline function LinearAlgebra.mul!(
        w::AbstractVecOrMat,
        ii::IdentityOperator,
        v::AbstractVecOrMat,
        α,
        β
    )
    @assert size(v, 1) == ii.len
    return mul!(w, I, v, α, β)
end

function LinearAlgebra.ldiv!(w::AbstractVecOrMat, ii::IdentityOperator, v::AbstractVecOrMat)
    @assert size(v, 1) == ii.len
    return copy!(w, v)
end

function LinearAlgebra.ldiv!(ii::IdentityOperator, v::AbstractVecOrMat)
    @assert size(v, 1) == ii.len
    return v
end

# Out-of-place: v is action vector, u is update vector
function (ii::IdentityOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    @assert size(v, 1) == ii.len
    update_coefficients(ii, u, p, t; kwargs...)
    return copy(v)
end

# In-place: w is destination, v is action vector, u is update vector
@inline function (ii::IdentityOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...
    )
    @assert size(v, 1) == ii.len
    return copy!(w, v)
end

# In-place with scaling: w = α*(ii*v) + β*w
@inline function (ii::IdentityOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    @assert size(v, 1) == ii.len
    return mul!(w, I, v, α, β)
end

# operator fusion with identity returns operator itself
for op in (:*, :∘)
    @eval function Base.$op(left::IdentityOperator, right::IdentityOperator)
        @assert left.len == right.len
        return left
    end

    @eval function Base.$op(ii::IdentityOperator, A::AbstractSciMLOperator)
        @assert size(A, 1) == ii.len
        return A
    end

    @eval function Base.$op(A::AbstractSciMLOperator, ii::IdentityOperator)
        @assert size(A, 2) == ii.len
        return A
    end

end

function Base.:\(ii::IdentityOperator, A::AbstractSciMLOperator)
    @assert size(A, 1) == ii.len
    return A
end

function Base.:/(A::AbstractSciMLOperator, ii::IdentityOperator)
    @assert size(A, 2) == ii.len
    return A
end

"""
    NullOperator(M, N)
    NullOperator(N)

Matrix-free zero operator of size `(M, N)`. The one-argument constructor
creates a square zero operator.

# Arguments

  - `M::Integer`: Number of output rows.
  - `N::Integer`: Number of input columns.

# Fields

  - `M::Int`: Stored output dimension.
  - `N::Int`: Stored input dimension.

# Interface Rules

The operator is constant and linear. It returns a zero result with the input
container's element type and batch shape, and composition with a compatible
operator remains a `NullOperator` with the composed dimensions.

# Examples

```julia
using SciMLOperators

NullOperator(2, 3) * ones(3) == zeros(2)
```
"""
struct NullOperator <: AbstractSciMLOperator{Bool}
    M::Int
    N::Int
end

# constructors
NullOperator(N::Integer) = NullOperator(N, N)

for op in (:*, :∘)
    @eval function Base.$op(ii::IdentityOperator, nn::NullOperator)
        @assert ii.len == nn.M
        return nn
    end

    @eval function Base.$op(nn::NullOperator, ii::IdentityOperator)
        @assert nn.N == ii.len
        return nn
    end
end

function Base.zero(L::AbstractSciMLOperator)
    return NullOperator(size(L)...)
end

function Base.convert(::Type{AbstractMatrix}, nn::NullOperator)
    return nn.M == nn.N ? Diagonal(zeros(Bool, nn.N)) : zeros(Bool, size(nn))
end
has_concretization(::NullOperator) = true

# Copy method to avoid aliasing - NullOperator has no mutable fields, can return self
Base.copy(L::NullOperator) = L

# traits
function Base.show(io::IO, nn::NullOperator)
    return nn.M == nn.N ? print(io, "NullOperator($(nn.M))") :
        print(io, "NullOperator($(nn.M), $(nn.N))")
end
Base.size(nn::NullOperator) = (nn.M, nn.N)
Base.adjoint(A::NullOperator) = NullOperator(A.N, A.M)
Base.transpose(A::NullOperator) = NullOperator(A.N, A.M)
Base.conj(A::NullOperator) = A
LinearAlgebra.opnorm(::NullOperator, p::Real = 2) = false
for pred in (:issymmetric, :ishermitian)
    @eval LinearAlgebra.$pred(nn::NullOperator) = issquare(nn)
end
LinearAlgebra.isposdef(::NullOperator) = false

getops(::NullOperator) = ()
isconstant(::NullOperator) = true
islinear(::NullOperator) = true
Base.iszero(::NullOperator) = true
has_adjoint(::NullOperator) = true
has_mul!(::NullOperator) = true

# operator application
function _null_output(nn::NullOperator, v::AbstractVecOrMat)
    @assert size(v, 1) == nn.N
    nn.M == nn.N && return zero(v)
    return v isa AbstractMatrix ? zeros(eltype(v), nn.M, size(v, 2)) : zeros(eltype(v), nn.M)
end

Base.:*(nn::NullOperator, v::AbstractVecOrMat) = _null_output(nn, v)

function LinearAlgebra.mul!(w::AbstractVecOrMat, nn::NullOperator, v::AbstractVecOrMat)
    @assert size(v, 1) == nn.N
    @assert size(w, 1) == nn.M
    return lmul!(false, w)
end

function LinearAlgebra.mul!(
        w::AbstractVecOrMat,
        nn::NullOperator,
        v::AbstractVecOrMat,
        α,
        β
    )
    @assert size(v, 1) == nn.N
    @assert size(w, 1) == nn.M
    return lmul!(β, w)
end

# Out-of-place: v is action vector, u is update vector
function (nn::NullOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    return _null_output(nn, v)
end

# In-place: w is destination, v is action vector, u is update vector
function (nn::NullOperator)(w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...)
    @assert size(v, 1) == nn.N
    @assert size(w, 1) == nn.M
    lmul!(false, w)
    return w
end

# In-place with scaling: w = α*(nn*v) + β*w
function (nn::NullOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    @assert size(v, 1) == nn.N
    @assert size(w, 1) == nn.M
    lmul!(β, w)
    return w
end

# operator fusion, composition
for op in (:*, :∘)
    @eval function Base.$op(left::NullOperator, right::NullOperator)
        @assert left.N == right.M
        return NullOperator(left.M, right.N)
    end

    @eval function Base.$op(nn::NullOperator, A::AbstractSciMLOperator)
        @assert size(A, 1) == nn.N
        return NullOperator(nn.M, size(A, 2))
    end

    @eval function Base.$op(A::AbstractSciMLOperator, nn::NullOperator)
        @assert size(A, 2) == nn.M
        return NullOperator(size(A, 1), nn.N)
    end
end

Base.:*(::AbstractSciMLScalarOperator, nn::NullOperator) = nn
Base.:*(nn::NullOperator, ::AbstractSciMLScalarOperator) = nn

# operator addition, subtraction with NullOperator returns operator itself
for op in (:+, :-)
    @eval function Base.$op(left::NullOperator, right::NullOperator)
        @assert size(left) == size(right)
        return left
    end

    @eval function Base.$op(nn::NullOperator, A::AbstractSciMLOperator)
        @assert size(A) == size(nn)
        return A
    end

    @eval function Base.$op(A::AbstractSciMLOperator, nn::NullOperator)
        @assert size(A) == size(nn)
        return A
    end
end

Base.:+(nn::NullOperator, α::AbstractSciMLScalarOperator) = α
Base.:+(α::AbstractSciMLScalarOperator, nn::NullOperator) = α
Base.:-(nn::NullOperator, α::AbstractSciMLScalarOperator) = -α
Base.:-(α::AbstractSciMLScalarOperator, nn::NullOperator) = α

"""
$TYPEDEF

    ScaledOperator(λ, L)

Lazy scalar multiple of an `AbstractSciMLOperator`, representing `λ * L`.

# Arguments

  - `λ`: A number, `UniformScaling`, or `AbstractSciMLScalarOperator`.
  - `L::AbstractSciMLOperator`: Operator being scaled.

# Fields

  - `λ`: Lazy scalar factor.
  - `L`: Wrapped operator.

# Interface Rules

Updates, caching, and trait queries are forwarded to both fields. The result
is linear exactly when `L` is linear; division traits additionally require a
nonzero current scalar value. Prefer `λ * L` to constructing this type
directly.
"""
struct ScaledOperator{
        T,
        λType,
        LType,
    } <: AbstractSciMLOperator{T}
    λ::λType
    L::LType

    function ScaledOperator(
            λ::AbstractSciMLScalarOperator{Tλ},
            L::AbstractSciMLOperator{TL}
        ) where {Tλ, TL}
        T = promote_type(Tλ, TL)
        return new{T, typeof(λ), typeof(L)}(λ, L)
    end
end

# constructors
for T in SCALINGNUMBERTYPES[2:end]
    @eval function ScaledOperator(λ::$T, L::AbstractSciMLOperator)
        T2 = _promote_operator_eltype(λ, L)
        Λ = λ isa UniformScaling ? UniformScaling(T2(λ.λ)) : T2(λ)
        return ScaledOperator(ScalarOperator(Λ), L)
    end
end

for T in SCALINGNUMBERTYPES
    @eval function ScaledOperator(λ::$T, L::ScaledOperator)
        λ = λ * L.λ
        return ScaledOperator(λ, L.L)
    end

    for LT in SCALINGCOMBINETYPES
        @eval Base.:*(λ::$T, L::$LT) = ScaledOperator(λ, L)
        @eval Base.:*(L::$LT, λ::$T) = ScaledOperator(λ, L)

        @eval Base.:\(λ::$T, L::$LT) = ScaledOperator(inv(λ), L)
        @eval Base.:\(L::$LT, λ::$T) = ScaledOperator(λ, inv(L))

        @eval Base.:/(L::$LT, λ::$T) = ScaledOperator(inv(λ), L)
        @eval Base.:/(λ::$T, L::$LT) = ScaledOperator(λ, inv(L))
    end
end

# Special cases for constant scalars. These simplify the structure when applicable
for T in SCALINGNUMBERTYPES[2:end]
    @eval function Base.:*(α::$T, L::ScaledOperator)
        isconstant(L.λ) && return ScaledOperator(α * L.λ, L.L)
        return ScaledOperator(L.λ, α * L.L) # Try to propagate the rule
    end
    @eval function Base.:*(L::ScaledOperator, α::$T)
        isconstant(L.λ) && return ScaledOperator(α * L.λ, L.L)
        return ScaledOperator(L.λ, α * L.L) # Try to propagate the rule
    end
end

Base.:+(L::AbstractSciMLOperator) = L
Base.:-(L::AbstractSciMLOperator{T}) where {T} = ScaledOperator(-one(T), L)

# Special cases for constant scalars. These simplify the structure when applicable
function Base.:-(L::ScaledOperator)
    isconstant(L.λ) && return ScaledOperator(-L.λ, L.L)
    return ScaledOperator(L.λ, -L.L) # Try to propagate the rule
end

function Base.convert(::Type{AbstractMatrix}, L::ScaledOperator)
    return convert(Number, L.λ) * convert(AbstractMatrix, L.L)
end
has_concretization(L::ScaledOperator) = has_concretization(L.λ) & has_concretization(L.L)

# traits
function Base.show(io::IO, L::ScaledOperator{T}) where {T}
    show(io, L.λ)
    print(io, " * ")
    return show(io, L.L)
end
Base.size(L::ScaledOperator) = size(L.L)
for op in (
        :adjoint,
        :transpose,
    )
    @eval Base.$op(L::ScaledOperator) = ScaledOperator($op(L.λ), $op(L.L))
end
Base.conj(L::ScaledOperator) = conj(L.λ) * conj(L.L)
Base.resize!(L::ScaledOperator, n::Integer) = (resize!(L.L, n); L)
LinearAlgebra.opnorm(L::ScaledOperator, p::Real = 2) = abs(L.λ) * opnorm(L.L, p)

function update_coefficients(L::ScaledOperator, u, p, t; kwargs...)
    @reset L.L = update_coefficients(L.L, u, p, t; kwargs...)
    @reset L.λ = update_coefficients(L.λ, u, p, t; kwargs...)

    return L
end

function update_coefficients!(L::ScaledOperator, u, p, t; kwargs...)
    update_coefficients!(L.L, u, p, t; kwargs...)
    update_coefficients!(L.λ, u, p, t; kwargs...)

    return nothing
end

getops(L::ScaledOperator) = (L.λ, L.L)

# Copy method to avoid aliasing
function Base.copy(L::ScaledOperator)
    return ScaledOperator(copy(L.λ), copy(L.L))
end

isconstant(L::ScaledOperator) = isconstant(L.L) & isconstant(L.λ)
islinear(L::ScaledOperator) = islinear(L.L)
Base.iszero(L::ScaledOperator) = iszero(L.L) | iszero(L.λ)
has_adjoint(L::ScaledOperator) = has_adjoint(L.L)
has_mul(L::ScaledOperator) = has_mul(L.L)
has_mul!(L::ScaledOperator) = has_mul!(L.L)
has_ldiv(L::ScaledOperator) = has_ldiv(L.L) & !iszero(L.λ)
has_ldiv!(L::ScaledOperator) = has_ldiv!(L.L) & !iszero(L.λ)

function cache_internals(L::ScaledOperator, v::AbstractVecOrMat)
    @reset L.L = cache_operator(L.L, v)
    @reset L.λ = cache_operator(L.λ, v)
    return L
end

# A scaling holds no scratch of its own, so it is transparent to the sharing described in
# `getcache`: the cache one field down is exactly as interchangeable as one held directly.
# Forwarding matters more than it looks — `A - B` lowers to `AddedOperator(A, -B)` with
# `-B` a `ScaledOperator`, so without this the commonest sum of all shares nothing.
getcache(L::ScaledOperator) = getcache(L.L)
update_cache(L::ScaledOperator, new_cache) = @reset L.L = update_cache(L.L, new_cache)

function adopt_cache(L::ScaledOperator, cache, v)
    inner = adopt_cache(L.L, cache, v)
    inner === nothing && return nothing

    @reset L.L = inner
    @reset L.λ = cache_operator(L.λ, v)
    return L
end

# getindex
Base.getindex(L::ScaledOperator, i::Int) = L.coeff * L.L[i]
Base.getindex(L::ScaledOperator, I::Vararg{Int, N}) where {N} = L.λ * L.L[I...]

factorize(L::ScaledOperator) = L.λ * factorize(L.L)
for fact in (
        :lu, :lu!,
        :qr, :qr!,
        :cholesky, :cholesky!,
        :ldlt, :ldlt!,
        :bunchkaufman, :bunchkaufman!,
        :lq, :lq!,
        :svd, :svd!,
    )
    @eval LinearAlgebra.$fact(L::ScaledOperator, args...) = L.λ *
        LinearAlgebra.$fact(L.L, args...)
end

# operator application, inversion
Base.:*(L::ScaledOperator, v::AbstractVecOrMat) = L.λ * (L.L * v)
Base.:\(L::ScaledOperator, v::AbstractVecOrMat) = L.λ \ (L.L \ v)

@inline function LinearAlgebra.mul!(
        w::AbstractVecOrMat, L::ScaledOperator, v::AbstractVecOrMat
    )
    iszero(L.λ) && return lmul!(false, w)
    a = convert(Number, L.λ)
    return mul!(w, L.L, v, a, false)
end

@inline function LinearAlgebra.mul!(
        w::AbstractVecOrMat,
        L::ScaledOperator,
        v::AbstractVecOrMat,
        α,
        β
    )
    iszero(L.λ) && return lmul!(β, w)
    a = convert(Number, L.λ * α)
    return mul!(w, L.L, v, a, β)
end

function LinearAlgebra.ldiv!(w::AbstractVecOrMat, L::ScaledOperator, v::AbstractVecOrMat)
    ldiv!(w, L.L, v)
    return ldiv!(L.λ, w)
end

function LinearAlgebra.ldiv!(L::ScaledOperator, v::AbstractVecOrMat)
    ldiv!(L.λ, v)
    return ldiv!(L.L, v)
end

# Out-of-place: v is action vector, u is update vector
function (L::ScaledOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    L = update_coefficients(L, u, p, t; kwargs...)
    if iszero(L.λ)
        return zero(v)
    else
        return L.λ * (L.L * v)
    end
end

# In-place: w is destination, v is action vector, u is update vector
@inline function (L::ScaledOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...
    )
    update_coefficients!(L.λ, u, p, t; kwargs...)
    a = convert(Number, L.λ)
    return L.L(w, v, u, p, t, a, false; kwargs...)
end

# In-place with scaling: w = α*(L*v) + β*w
@inline function (L::ScaledOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    update_coefficients!(L.λ, u, p, t; kwargs...)
    a = convert(Number, L.λ * α)
    return L.L(w, v, u, p, t, a, β; kwargs...)
end

"""
    AddedOperator(A, B, ...)

Lazy sum of compatible `AbstractSciMLOperator`s.

# Arguments

  - `A, B, ...`: One or more operators with identical sizes.

# Fields

  - `ops`: Tuple of component operators. Nested sums are flattened.

# Interface Rules

The action is the sum of each component action. Updates, caching, and traits
are forwarded componentwise; a trait is true only when every required
component supports it. Use `A + B` rather than constructing this type
directly.
"""
struct AddedOperator{
        T,
        O <: Tuple{Vararg{AbstractSciMLOperator}},
    } <: AbstractSciMLOperator{T}
    ops::O

    function AddedOperator(ops)
        @assert !isempty(ops)
        # Flatten nested AddedOperators
        ops_flat = _flatten_added_operators(ops)
        _check_AddedOperator_sizes(ops_flat)
        T = mapreduce(eltype, promote_type, ops_flat)
        return new{T, typeof(ops_flat)}(ops_flat)
    end
end

function AddedOperator(ops::AbstractSciMLOperator...)
    return AddedOperator(ops)
end

AddedOperator(L::AbstractSciMLOperator) = L

# Helper function to flatten nested AddedOperators
@generated function _flatten_added_operators(ops::Tuple)
    exprs = ()
    for i in 1:length(ops.parameters)
        T = ops.parameters[i]
        if T <: AddedOperator
            # If this element is an AddedOperator, unpack its ops
            exprs = (exprs..., :(ops[$i].ops...))
        else
            # Otherwise, keep the element as-is
            exprs = (exprs..., :(ops[$i]))
        end
    end

    return quote
        tuple($(exprs...))
    end
end

@generated function _check_AddedOperator_sizes(ops::Tuple)
    ops_types = ops.parameters
    N = length(ops_types)
    sz_expr_list = ()
    sz_expr = :(sz = size(first(ops)))
    for i in 2:N
        sz_expr_list = (sz_expr_list..., :(size(ops[$i]) == sz))
    end

    return quote
        $sz_expr
        @assert all(tuple($(sz_expr_list...))) "Dimension mismatch: cannot add operators of different sizes."
        nothing
    end
end

# constructors
Base.:+(A::AbstractSciMLOperator, B::AbstractMatrix) = A + MatrixOperator(B)
Base.:+(A::AbstractMatrix, B::AbstractSciMLOperator) = MatrixOperator(A) + B

Base.:+(ops::AbstractSciMLOperator...) = reduce(+, ops)
Base.:+(A::AbstractSciMLOperator, B::AbstractSciMLOperator) = AddedOperator(A, B)
Base.:+(A::AbstractSciMLOperator, B::AddedOperator) = AddedOperator(A, B.ops...)
Base.:+(A::AddedOperator, B::AbstractSciMLOperator) = AddedOperator(A.ops..., B)
Base.:+(A::AddedOperator, B::AddedOperator) = AddedOperator(A.ops..., B.ops...)

function Base.:+(A::AddedOperator, Z::NullOperator)
    @assert size(A) == size(Z)
    return A
end

function Base.:+(Z::NullOperator, A::AddedOperator)
    @assert size(A) == size(Z)
    return A
end

function Base.:-(A::AddedOperator, Z::NullOperator)
    @assert size(A) == size(Z)
    return A
end

function Base.:-(Z::NullOperator, A::AddedOperator)
    @assert size(A) == size(Z)
    return -A
end

Base.:-(A::AddedOperator) = AddedOperator(map(-, A.ops))
Base.:-(A::AbstractSciMLOperator, B::AbstractSciMLOperator) = AddedOperator(A, -B)
Base.:-(A::AbstractSciMLOperator, B::AbstractMatrix) = A - MatrixOperator(B)
Base.:-(A::AbstractMatrix, B::AbstractSciMLOperator) = MatrixOperator(A) - B
Base.:-(A::AddedOperator, B::AbstractSciMLOperator) = AddedOperator(A.ops..., -B)
Base.:-(A::AbstractSciMLOperator, B::AddedOperator) = AddedOperator(A, (-B).ops...)
Base.:-(A::AddedOperator, B::AddedOperator) = AddedOperator(A.ops..., (-B).ops...)

function Base.:+(A::AddedOperator, α::AbstractSciMLScalarOperator)
    @assert issquare(A)
    return iszero(α) ? A : AddedOperator(A.ops..., α * IdentityOperator(size(A, 1)))
end

function Base.:+(α::AbstractSciMLScalarOperator, A::AddedOperator)
    @assert issquare(A)
    return iszero(α) ? A : AddedOperator(α * IdentityOperator(size(A, 1)), A.ops...)
end

function Base.:-(A::AddedOperator, α::AbstractSciMLScalarOperator)
    @assert issquare(A)
    return iszero(α) ? A : AddedOperator(A.ops..., (-α) * IdentityOperator(size(A, 1)))
end

function Base.:-(α::AbstractSciMLScalarOperator, A::AddedOperator)
    @assert issquare(A)
    return AddedOperator(α * IdentityOperator(size(A, 1)), (-A).ops...)
end

for op in (:+, :-)
    for T in SCALINGNUMBERTYPES
        for LT in SCALINGCOMBINETYPES
            @eval function Base.$op(L::$LT, λ::$T)
                @assert issquare(L)
                iszero(λ) && return L
                N = size(L, 1)
                Id = IdentityOperator(N)
                return AddedOperator(L, $op(λ) * Id)
            end

            @eval function Base.$op(λ::$T, L::$LT)
                @assert issquare(L)
                iszero(λ) && return $op(L)
                N = size(L, 1)
                Id = IdentityOperator(N)
                return AddedOperator(λ * Id, $op(L))
            end
        end
    end
end

for T in SCALINGNUMBERTYPES[2:end]
    @eval function Base.:*(λ::$T, L::AddedOperator)
        ops = map(op -> λ * op, L.ops)
        return AddedOperator(ops)
    end

    @eval function Base.:*(L::AddedOperator, λ::$T)
        ops = map(op -> λ * op, L.ops)
        return AddedOperator(ops)
    end

    @eval function Base.:/(L::AddedOperator, λ::$T)
        ops = map(op -> op / λ, L.ops)
        return AddedOperator(ops)
    end
end

function Base.convert(::Type{AbstractMatrix}, L::AddedOperator)
    return sum(op -> convert(AbstractMatrix, op), L.ops)
end
has_concretization(L::AddedOperator) = all(has_concretization, L.ops)

# traits
function Base.show(io::IO, L::AddedOperator)
    print(io, "(")
    show(io, L.ops[1])
    for i in 2:length(L.ops)
        print(io, " + ")
        show(io, L.ops[i])
    end
    return print(io, ")")
end
Base.size(L::AddedOperator) = size(first(L.ops))
for op in (
        :adjoint,
        :transpose,
    )
    @eval Base.$op(L::AddedOperator) = AddedOperator($op.(L.ops)...)
end
Base.conj(L::AddedOperator) = AddedOperator(conj.(L.ops))
function Base.resize!(L::AddedOperator, n::Integer)
    for op in L.ops
        resize!(op, n)
    end
    return L
end

@generated function update_coefficients(L::AddedOperator, u, p, t; kwargs...)
    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    updates = [:(update_coefficients(L.ops[$i], u, p, t; kwargs...)) for i in 1:N]
    return :(AddedOperator($(Expr(:tuple, updates...))))
end

@generated function update_coefficients!(L::AddedOperator, u, p, t; kwargs...)
    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    updates = [:(update_coefficients!(L.ops[$i], u, p, t; kwargs...)) for i in 1:N]
    return Expr(:block, updates..., :(nothing))
end

getops(L::AddedOperator) = L.ops

# Copy method to avoid aliasing
function Base.copy(L::AddedOperator)
    return AddedOperator(map(copy, L.ops))
end

islinear(L::AddedOperator) = all(islinear, getops(L))
Base.iszero(L::AddedOperator) = all(iszero, getops(L))
has_adjoint(L::AddedOperator) = all(has_adjoint, L.ops)

@generated function cache_internals(L::AddedOperator, v::AbstractVecOrMat)
    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    syms = [Symbol(:op_, i) for i in 1:N]

    # `mul!` applies the summands one after another, so no two of their caches are ever
    # live at the same time and a summand can reuse the scratch of an earlier one. Only
    # summands of the same concrete type can qualify — that is what pins their element
    # types — so the candidates are picked out here, at compile time, and each summand
    # tries them all before allocating anything of its own.
    stmts = map(1:N) do i
        donors = Expr(:tuple, syms[findall(j -> ops_types[j] === ops_types[i], 1:(i - 1))]...)
        earlier = Expr(:tuple, syms[1:(i - 1)]...)
        return :($(syms[i]) = _cache_summand(L.ops[$i], $donors, $earlier, v))
    end

    return quote
        $(stmts...)
        return AddedOperator($(Expr(:tuple, syms...)))
    end
end

getindex(L::AddedOperator, i::Int) = sum(op -> op[i], L.ops)
getindex(L::AddedOperator, I::Vararg{Int, N}) where {N} = sum(op -> op[I...], L.ops)

function Base.:*(L::AddedOperator, v::AbstractVecOrMat)
    return sum(op -> iszero(op) ? zero(v) : op * v, L.ops)
end

@generated function LinearAlgebra.mul!(
        w::AbstractVecOrMat, L::AddedOperator, v::AbstractVecOrMat
    )
    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    applications = [:(mul!(w, L.ops[$i], v, true, true)) for i in 2:N]
    return Expr(:block, :(mul!(w, L.ops[1], v)), applications..., :w)
end

@generated function LinearAlgebra.mul!(
        w::AbstractVecOrMat,
        L::AddedOperator,
        v::AbstractVecOrMat,
        α,
        β
    )
    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    applications = [:(mul!(w, L.ops[$i], v, α, true)) for i in 1:N]
    return Expr(:block, :(lmul!(β, w)), applications..., :w)
end

# Out-of-place: v is action vector, u is update vector
function (L::AddedOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    # We don't need to update coefficients of L, as op(v, u, p, t) will do it for each op
    return sum(op -> op(v, u, p, t; kwargs...), L.ops)
end

# In-place: w is destination, v is action vector, u is update vector
@generated function (L::AddedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...
    )
    # We don't need to update coefficients of L, as op(w, v, u, p, t) will do it for each op

    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    applications = [:(L.ops[$i](w, v, u, p, t, true, true; kwargs...)) for i in 2:N]
    return Expr(:block, :(L.ops[1](w, v, u, p, t; kwargs...)), applications..., :w)
end

# In-place with scaling: w = α*(L*v) + β*w
@generated function (L::AddedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    # We don't need to update coefficients of L, as op(w, v, u, p, t) will do it for each op

    T = L.parameters[1]
    ops_types = L.parameters[2].parameters
    N = length(ops_types)
    applications = [:(L.ops[$i](w, v, u, p, t, α, true; kwargs...)) for i in 2:N]
    return Expr(:block, :(L.ops[1](w, v, u, p, t, α, β; kwargs...)), applications..., :w)
end

"""
    ComposedOperator(A, B, ...)

Lazy composition representing `A * B * ...`, with the rightmost operator
applied first.

# Arguments

  - `A, B, ...`: One or more dimension-compatible operators.

# Fields

  - `ops`: Tuple of component operators in multiplication order.
  - `cache`: Intermediate arrays used by in-place multiplication, or `nothing`.

# Interface Rules

Updates are forwarded to every component. Call `cache_operator` before
repeated in-place application when intermediate storage is required. The
composition is linear, convertible, or supports a trait only when all
components satisfy the corresponding contract.
"""
struct ComposedOperator{T, O, C} <: AbstractSciMLOperator{T}
    """ Tuple of N operators to be applied in reverse"""
    ops::O
    """ cache for 3 and 5 argument mul! """
    cache::C

    function ComposedOperator(ops, cache)
        @assert !isempty(ops)
        for i in reverse(2:length(ops))
            opcurr = ops[i]
            opnext = ops[i - 1]
            @assert size(opcurr, 1) == size(opnext, 2) "Dimension mismatch: cannot compose
          operators of sizes $(size(opnext)), and $(size(opcurr))."
        end

        T = promote_type(eltype.(ops)...)
        return new{T, typeof(ops), typeof(cache)}(ops, cache)
    end
end

function ComposedOperator(ops::AbstractSciMLOperator...; cache = nothing)
    return ComposedOperator(ops, cache)
end

# constructors
for op in (:*, :∘)
    @eval Base.$op(ops::AbstractSciMLOperator...) = reduce($op, ops)
    @eval Base.$op(A::AbstractSciMLOperator, B::AbstractSciMLOperator) = ComposedOperator(
        A,
        B
    )
    @eval Base.$op(A::ComposedOperator, B::AbstractSciMLOperator) = ComposedOperator(
        A.ops...,
        B
    )
    @eval Base.$op(A::AbstractSciMLOperator, B::ComposedOperator) = ComposedOperator(
        A,
        B.ops...
    )
    @eval Base.$op(A::ComposedOperator, B::ComposedOperator) = ComposedOperator(
        A.ops...,
        B.ops...
    )
end

for op in (:*, :∘)
    # identity
    @eval function Base.$op(ii::IdentityOperator, A::ComposedOperator)
        @assert size(A, 1) == ii.len
        return A
    end

    @eval function Base.$op(A::ComposedOperator, ii::IdentityOperator)
        @assert size(A, 2) == ii.len
        return A
    end

    # null operator
    @eval function Base.$op(nn::NullOperator, A::ComposedOperator)
        @assert size(A, 1) == nn.N
        return NullOperator(nn.M, size(A, 2))
    end

    @eval function Base.$op(A::ComposedOperator, nn::NullOperator)
        @assert size(A, 2) == nn.M
        return NullOperator(size(A, 1), nn.N)
    end

    # scalar operator
    @eval function Base.$op(λ::AbstractSciMLScalarOperator, L::ComposedOperator)
        return ScaledOperator(λ, L)
    end

    @eval function Base.$op(L::ComposedOperator, λ::AbstractSciMLScalarOperator)
        return ScaledOperator(λ, L)
    end
end

function Base.convert(::Type{AbstractMatrix}, L::ComposedOperator)
    return prod(op -> convert(AbstractMatrix, op), L.ops)
end
has_concretization(L::ComposedOperator) = all(has_concretization, L.ops)

# traits
function Base.show(io::IO, L::ComposedOperator)
    print(io, "(")
    show(io, L.ops[1])
    for i in 2:length(L.ops)
        print(io, " * ")
        show(io, L.ops[i])
    end
    return print(io, ")")
end
Base.size(L::ComposedOperator) = (size(first(L.ops), 1), size(last(L.ops), 2))
for op in (
        :adjoint,
        :transpose,
    )
    @eval Base.$op(L::ComposedOperator) = ComposedOperator(
        $op.(reverse(L.ops))...;
        cache = iscached(L) ? reverse(L.cache) : nothing
    )
end
Base.conj(L::ComposedOperator) = ComposedOperator(conj.(L.ops); cache = L.cache)
function Base.resize!(L::ComposedOperator, n::Integer)
    for op in L.ops
        resize!(op, n)
    end

    for v in L.cache
        resize!(v, n)
    end

    return L
end

LinearAlgebra.opnorm(L::ComposedOperator) = prod(opnorm, L.ops)

function update_coefficients(L::ComposedOperator, u, p, t; kwargs...)
    ops = ()
    for op in L.ops
        ops = (ops..., update_coefficients(op, u, p, t; kwargs...))
    end

    return @reset L.ops = ops
end

getops(L::ComposedOperator) = L.ops
getcache(L::ComposedOperator) = L.cache
adopt_cache(L::ComposedOperator, cache, v) = cache_internals(update_cache(L, cache), v)

# Copy method to avoid aliasing
function Base.copy(L::ComposedOperator)
    return ComposedOperator(
        map(copy, L.ops),
        L.cache === nothing ? nothing : deepcopy(L.cache)
    )
end

islinear(L::ComposedOperator) = all(islinear, L.ops)
Base.iszero(L::ComposedOperator) = all(iszero, getops(L))
has_adjoint(L::ComposedOperator) = all(has_adjoint, L.ops)
has_mul(L::ComposedOperator) = all(has_mul, L.ops)
has_mul!(L::ComposedOperator) = all(has_mul!, L.ops)
has_ldiv(L::ComposedOperator) = all(has_ldiv, L.ops)
has_ldiv!(L::ComposedOperator) = all(has_ldiv!, L.ops)

factorize(L::ComposedOperator) = prod(factorize, L.ops)
for fact in (
        :lu, :lu!,
        :qr, :qr!,
        :cholesky, :cholesky!,
        :ldlt, :ldlt!,
        :bunchkaufman, :bunchkaufman!,
        :lq, :lq!,
        :svd, :svd!,
    )
    @eval LinearAlgebra.$fact(L::ComposedOperator, args...) = prod(
        op -> LinearAlgebra.$fact(op, args...),
        reverse(L.ops)
    )
end

# operator application
# https://github.com/SciML/SciMLOperators.jl/pull/94
#Base.:*(L::ComposedOperator, u::AbstractVecOrMat) = foldl((acc, op) -> op * acc, reverse(L.ops); init=u)
#Base.:\(L::ComposedOperator, u::AbstractVecOrMat) = foldl((acc, op) -> op \ acc, L.ops; init=u)

function Base.:\(L::ComposedOperator, v::AbstractVecOrMat)
    for op in L.ops
        v = op \ v
    end

    return v
end

@generated function Base.:*(L::ComposedOperator, v::AbstractVecOrMat)
    N = length(L.parameters[2].parameters)  # Number of operators

    # Generate the composition in reverse order
    # v = L.ops[N] * v
    # v = L.ops[N-1] * v
    # ...
    # v = L.ops[1] * v
    exprs = []
    for i in N:-1:1
        push!(exprs, :(v = L.ops[$i] * v))
    end

    return quote
        $(exprs...)
        v
    end
end

@generated function cache_self(L::ComposedOperator, v::AbstractVecOrMat)
    N = length(L.parameters[2].parameters)  # Number of operators

    # Build cache from the end backwards (starting from zero(v))
    # For each operator from N down to 2, create a cache entry
    exprs = [:(cache = (zero(v),))]

    for i in N:-1:2
        push!(
            exprs, quote
                op = L.ops[$i]
                M = size(op, 1)
                sz = v isa AbstractMatrix ? (M, K) : (M,)

                T = if op isa FunctionOperator
                    # FunctionOperator isn't guaranteed to play by the rules of
                    # `promote_type`. For example, an irFFT is a complex operation
                    # that accepts complex vector and returns ones.
                    output_eltype(op)
                else
                    promote_type(eltype.((op, cache[1]))...)
                end

                cache = (similar(v, T, sz), cache...)
            end
        )
    end

    return quote
        K = size(v, 2)
        $(exprs...)
        @reset L.cache = cache
        L
    end
end

@generated function cache_internals(L::ComposedOperator, v::AbstractVecOrMat)
    N = length(L.parameters[2].parameters)  # Number of operators

    # Cache each operator with its corresponding cache entry
    exprs = []
    for i in N:-1:1
        push!(exprs, :(ops = (cache_operator(L.ops[$i], L.cache[$i]), ops...)))
    end

    return quote
        if isnothing(L.cache)
            L = cache_self(L, v)
        end

        ops = ()
        $(exprs...)
        @reset L.ops = ops
    end
end

@generated function LinearAlgebra.mul!(w::AbstractVecOrMat, L::ComposedOperator, v::AbstractVecOrMat)
    N = length(L.parameters[2].parameters)  # Number of operators

    # Generate the mul! calls in reverse order
    # vecs conceptually is (w, L.cache[1], L.cache[2], ..., L.cache[N-1], v)
    # For i in reverse(1:N):
    #   mul!(vecs[i], L.ops[i], vecs[i+1])

    exprs = []
    for i in N:-1:1
        if i == N
            # Last operator: mul!(L.cache[N-1], L.ops[N], v)
            push!(exprs, :(mul!(L.cache[$(N - 1)], L.ops[$i], v)))
        elseif i == 1
            # First operator: mul!(w, L.ops[1], L.cache[1])
            push!(exprs, :(mul!(w, L.ops[$i], L.cache[1])))
        else
            # Middle operators: mul!(L.cache[i-1], L.ops[i], L.cache[i])
            push!(exprs, :(mul!(L.cache[$(i - 1)], L.ops[$i], L.cache[$i])))
        end
    end

    return quote
        @assert iscached(L) """cache needs to be set up for operator of type
        $L. Set up cache by calling `cache_operator(L, v)`"""

        $(exprs...)
        w
    end
end

function LinearAlgebra.mul!(
        w::AbstractVecOrMat,
        L::ComposedOperator,
        v::AbstractVecOrMat,
        α,
        β
    )
    @assert iscached(L) """cache needs to be set up for operator of type
    $L. Set up cache by calling `cache_operator(L, v)`."""

    cache = L.cache[end]
    copy!(cache, w)

    mul!(w, L, v)
    lmul!(α, w)
    return axpy!(β, cache, w)
end

@generated function LinearAlgebra.ldiv!(w::AbstractVecOrMat, L::ComposedOperator, v::AbstractVecOrMat)
    N = length(L.parameters[2].parameters)  # Number of operators

    # Generate the ldiv! calls in forward order
    # vecs conceptually is (v, reverse(L.cache[1:(N-1)])..., w)
    # = (v, L.cache[N-1], L.cache[N-2], ..., L.cache[1], w)
    # For i in 1:N:
    #   ldiv!(vecs[i+1], L.ops[i], vecs[i])

    exprs = []
    for i in 1:N
        if i == 1
            # First operator: ldiv!(L.cache[N-1], L.ops[1], v)
            push!(exprs, :(ldiv!(L.cache[$(N - 1)], L.ops[$i], v)))
        elseif i == N
            # Last operator: ldiv!(w, L.ops[N], L.cache[1])
            push!(exprs, :(ldiv!(w, L.ops[$i], L.cache[1])))
        else
            # Middle operators: ldiv!(L.cache[N-i], L.ops[i], L.cache[N-i+1])
            push!(exprs, :(ldiv!(L.cache[$(N - i)], L.ops[$i], L.cache[$(N - i + 1)])))
        end
    end

    return quote
        @assert iscached(L) """cache needs to be set up for operator of type
        $L. Set up cache by calling `cache_operator(L, v)`."""

        $(exprs...)
        w
    end
end

function LinearAlgebra.ldiv!(L::ComposedOperator, v::AbstractVecOrMat)
    for i in 1:length(L.ops)
        ldiv!(L.ops[i], v)
    end
    return v
end

# Out-of-place: v is action vector, u is update vector
function (L::ComposedOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    L = update_coefficients(L, u, p, t; kwargs...)
    result = v
    for op in reverse(L.ops)
        result = op(result, u, p, t; kwargs...)
    end
    return result
end

# In-place: w is destination, v is action vector, u is update vector
@generated function (L::ComposedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...
    )
    N = length(L.parameters[2].parameters)  # Number of operators

    # Generate the operator call expressions in reverse order
    # vecs conceptually is (w, L.cache[1], L.cache[2], ..., L.cache[N-1], v)
    # For i in reverse(1:N):
    #   L.ops[i](vecs[i], vecs[i+1], u, p, t; kwargs...)

    exprs = []
    for i in N:-1:1
        if i == N
            # Last operator: L.ops[N](L.cache[N-1], v, u, p, t; kwargs...)
            push!(exprs, :(L.ops[$i](L.cache[$(N - 1)], v, u, p, t; kwargs...)))
        elseif i == 1
            # First operator: L.ops[1](w, L.cache[1], u, p, t; kwargs...)
            push!(exprs, :(L.ops[$i](w, L.cache[1], u, p, t; kwargs...)))
        else
            # Middle operators: L.ops[i](L.cache[i-1], L.cache[i], u, p, t; kwargs...)
            push!(exprs, :(L.ops[$i](L.cache[$(i - 1)], L.cache[$i], u, p, t; kwargs...)))
        end
    end

    return quote
        update_coefficients!(L, u, p, t; kwargs...)
        @assert iscached(L) "Cache needs to be set up for ComposedOperator. Call cache_operator(L, u) first."

        $(exprs...)
        w
    end
end

# In-place with scaling: w = α*(L*v) + β*w
function (L::ComposedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    update_coefficients!(L, u, p, t; kwargs...)
    @assert iscached(L) "Cache needs to be set up for ComposedOperator. Call cache_operator(L, u) first."

    cache = L.cache[end]
    copy!(cache, w)

    L(w, v, u, p, t; kwargs...)
    lmul!(α, w)
    return axpy!(β, cache, w)
end

"""
    InvertedOperator(L)

Lazy inverse of an `AbstractSciMLOperator`.

# Arguments

  - `L::AbstractSciMLOperator`: Operator whose current action is inverted.

# Fields

  - `L`: Wrapped operator.
  - `cache`: Optional work arrays used by in-place division.

# Interface Rules

Construct this type through `inv(L)`. The wrapped operator must support the
division operations required by the chosen application form. Updates and
caching are forwarded to `L`; inversion does not materialize a matrix.
"""
struct InvertedOperator{T, LType, C} <: AbstractSciMLOperator{T}
    L::LType
    cache::C

    function InvertedOperator(L::AbstractSciMLOperator{T}, cache) where {T}
        return new{T, typeof(L), typeof(cache)}(L, cache)
    end
end

function InvertedOperator(L::AbstractSciMLOperator{T}; cache = nothing) where {T}
    return InvertedOperator(L, cache)
end

function InvertedOperator(A::AbstractMatrix{T}; cache = nothing) where {T}
    return InvertedOperator(MatrixOperator(A), cache)
end

Base.inv(L::AbstractSciMLOperator) = InvertedOperator(L)

Base.:\(A::AbstractSciMLOperator, B::AbstractSciMLOperator) = inv(A) * B
Base.:/(A::AbstractSciMLOperator, B::AbstractSciMLOperator) = A * inv(B)

function Base.convert(::Type{AbstractMatrix}, L::InvertedOperator)
    return inv(convert(AbstractMatrix, L.L))
end
has_concretization(L::InvertedOperator) = has_concretization(L.L)

function Base.show(io::IO, L::InvertedOperator)
    print(io, "1 / ")
    return show(io, L.L)
end
Base.size(L::InvertedOperator) = size(L.L) |> reverse
function Base.transpose(L::InvertedOperator)
    return InvertedOperator(transpose(L.L); cache = iscached(L) ? L.cache' : nothing)
end
function Base.adjoint(L::InvertedOperator)
    return InvertedOperator(adjoint(L.L); cache = iscached(L) ? L.cache' : nothing)
end
Base.conj(L::InvertedOperator) = InvertedOperator(conj(L.L); cache = L.cache)
function Base.resize!(L::InvertedOperator, n::Integer)
    resize!(L.L, n)
    resize!(L.cache, n)

    return L
end

function update_coefficients(L::InvertedOperator, u, p, t; kwargs...)
    if !isconstant(L.L)
        @reset L.L = update_coefficients(L.L, u, p, t; kwargs...)
    end
    return L
end

getops(L::InvertedOperator) = (L.L,)
getcache(L::InvertedOperator) = L.cache
adopt_cache(L::InvertedOperator, cache, v) = cache_internals(update_cache(L, cache), v)
islinear(L::InvertedOperator) = islinear(L.L)
isconvertible(::InvertedOperator) = false

has_mul(L::InvertedOperator) = has_ldiv(L.L)
has_mul!(L::InvertedOperator) = has_ldiv!(L.L)
has_ldiv(L::InvertedOperator) = has_mul(L.L)
has_ldiv!(L::InvertedOperator) = has_mul!(L.L)

# Method forwarding for InvertedOperator (previously using @forward from MacroTools)
# LinearAlgebra methods
LinearAlgebra.issymmetric(L::InvertedOperator) = LinearAlgebra.issymmetric(L.L)
LinearAlgebra.ishermitian(L::InvertedOperator) = LinearAlgebra.ishermitian(L.L)
LinearAlgebra.isposdef(L::InvertedOperator) = LinearAlgebra.isposdef(L.L)
LinearAlgebra.opnorm(L::InvertedOperator) = LinearAlgebra.opnorm(L.L)

# SciML methods
isconstant(L::InvertedOperator) = isconstant(L.L)
has_adjoint(L::InvertedOperator) = has_adjoint(L.L)

Base.:*(L::InvertedOperator, u::AbstractVecOrMat) = L.L \ u
Base.:\(L::InvertedOperator, u::AbstractVecOrMat) = L.L * u

# Copy method to avoid aliasing
function Base.copy(L::InvertedOperator)
    return InvertedOperator(
        copy(L.L),
        L.cache === nothing ? nothing : deepcopy(L.cache)
    )
end

function cache_self(L::InvertedOperator, u::AbstractVecOrMat)
    cache = zero(u)
    @reset L.cache = cache
    return L
end

function cache_internals(L::InvertedOperator, u::AbstractVecOrMat)
    @reset L.L = cache_operator(L.L, u)
    return L
end

function LinearAlgebra.mul!(w::AbstractVecOrMat, L::InvertedOperator, v::AbstractVecOrMat)
    return ldiv!(w, L.L, v)
end

function LinearAlgebra.mul!(
        w::AbstractVecOrMat,
        L::InvertedOperator,
        v::AbstractVecOrMat,
        α,
        β
    )
    @assert iscached(L) """cache needs to be set up for operator of type
    $L. Set up cache by calling `cache_operator(L, v)`."""

    copy!(L.cache, w)
    ldiv!(w, L.L, v)
    lmul!(α, w)
    return axpy!(β, L.cache, w)
end

function LinearAlgebra.ldiv!(w::AbstractVecOrMat, L::InvertedOperator, v::AbstractVecOrMat)
    return mul!(w, L.L, v)
end

function LinearAlgebra.ldiv!(L::InvertedOperator, v::AbstractVecOrMat)
    @assert iscached(L) """cache needs to be set up for operator of type
    $L. Set up cache by calling `cache_operator(L, v)`."""

    copy!(L.cache, v)
    return mul!(v, L.L, L.cache)
end

# Out-of-place: v is action vector, u is update vector
function (L::InvertedOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    L = update_coefficients(L, u, p, t; kwargs...)
    return L.L \ v
end

# In-place: w is destination, v is action vector, u is update vector
function (L::InvertedOperator)(w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...)
    update_coefficients!(L, u, p, t; kwargs...)
    ldiv!(w, L.L, v)
    return w
end

# In-place with scaling: w = α*(L*v) + β*w
function (L::InvertedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    update_coefficients!(L, u, p, t; kwargs...)
    @assert iscached(L) "Cache needs to be set up for InvertedOperator. Call cache_operator(L, u) first."

    copy!(L.cache, w)
    ldiv!(w, L.L, v)
    lmul!(α, w)
    axpy!(β, L.cache, w)
    return w
end
#
