#
###
# left multiplication
###

for op in (:*, :\)
    @eval function Base.$op(u::AbstractVecOrMat, L::AbstractSciMLOperator)
        oper = u isa Transpose ? transpose : adjoint
        return $op(oper(L), oper(u)) |> oper
    end
end

function LinearAlgebra.mul!(
        v::AbstractVecOrMat,
        u::AbstractVecOrMat,
        L::AbstractSciMLOperator
    )
    op = (u isa Transpose) | (v isa Transpose) ? transpose : adjoint
    mul!(op(v), op(L), op(u))
    return v
end

function LinearAlgebra.mul!(
        v::AbstractVecOrMat,
        u::AbstractVecOrMat,
        L::AbstractSciMLOperator,
        α,
        β
    )
    op = (u isa Transpose) | (v isa Transpose) ? transpose : adjoint
    mul!(op(v), op(L), op(u), α, β)
    return v
end

function LinearAlgebra.ldiv!(
        v::AbstractVecOrMat,
        u::AbstractVecOrMat,
        L::AbstractSciMLOperator
    )
    op = (u isa Transpose) | (v isa Transpose) ? transpose : adjoint
    ldiv!(op(v), op(L), op(u))
    return v
end

function LinearAlgebra.ldiv!(u::AbstractVecOrMat, L::AbstractSciMLOperator)
    op = (u isa Transpose) ? transpose : adjoint
    ldiv!(op(L), op(u))
    return u
end

###
# fallback wrappers
###

"""
$TYPEDEF
    AdjointOperator(L)

Lazy adjoint wrapper for an `AbstractSciMLOperator`.

# Arguments

  - `L::AbstractSciMLOperator`: Operator to adjoint.

# Fields

  - `L`: Wrapped operator.

# Interface Rules

Construct through `adjoint(L)` or `L'`. The wrapper delegates updates and
matrix-like application to the adjoint action of `L`; it is valid only when
`has_adjoint(L)` is true.
"""
struct AdjointOperator{T, LType} <: AbstractSciMLOperator{T}
    L::LType

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

"""
$TYPEDEF
    TransposedOperator(L)

Lazy transpose wrapper for an `AbstractSciMLOperator`.

# Arguments

  - `L::AbstractSciMLOperator`: Operator to transpose.

# Fields

  - `L`: Wrapped operator.

# Interface Rules

Construct through `transpose(L)`. The wrapper delegates updates and
matrix-like application to the transpose action of `L`; complex operators
must distinguish this from `adjoint(L)`.
"""
struct TransposedOperator{T, LType} <: AbstractSciMLOperator{T}
    L::LType

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

AbstractAdjointVecOrMat = Adjoint{T, <:AbstractVecOrMat} where {T}
AbstractTransposedVecOrMat = Transpose{T, <:AbstractVecOrMat} where {T}

has_adjoint(::AdjointOperator) = true
has_adjoint(L::TransposedOperator) = isreal(L) & has_adjoint(L.L)

islinear(L::AdjointOperator) = islinear(L.L)
islinear(L::TransposedOperator) = islinear(L.L)

Base.transpose(L::AdjointOperator) = conj(L.L)
Base.adjoint(L::TransposedOperator) = conj(L.L)

function Base.show(io::IO, L::AdjointOperator)
    show(io, L.L)
    return print(io, "'")
end

function Base.show(io::IO, L::TransposedOperator)
    print(io, "transpose(")
    show(io, L.L)
    return print(io, ")")
end

for (op, LType, VType) in (
        (:adjoint, :AdjointOperator, :AbstractAdjointVecOrMat),
        (:transpose, :TransposedOperator, :AbstractTransposedVecOrMat),
    )
    # constructor
    @eval Base.$op(L::AbstractSciMLOperator) = $LType(L)

    @eval Base.convert(::Type{AbstractMatrix}, L::$LType) = $op(
        convert(
            AbstractMatrix,
            L.L
        )
    )
    @eval has_concretization(L::$LType) = has_concretization(L.L)

    # traits
    @eval Base.size(L::$LType) = size(L.L) |> reverse
    @eval Base.resize!(L::$LType, n::Integer) = (resize!(L.L, n); L)
    @eval Base.$op(L::$LType) = L.L

    @eval getops(L::$LType) = (L.L,)

    # Copy method to avoid aliasing
    @eval Base.copy(L::$LType) = $LType(copy(L.L))

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

    # SciML methods
    @eval isconstant(L::$LType) = isconstant(L.L)
    @eval has_mul(L::$LType) = has_mul(L.L)
    @eval has_mul!(L::$LType) = has_mul!(L.L)
    @eval has_ldiv(L::$LType) = has_ldiv(L.L)
    @eval has_ldiv!(L::$LType) = has_ldiv!(L.L)

    @eval function cache_internals(L::$LType, u::AbstractVecOrMat)
        @reset L.L = cache_operator(L.L, reshape(u, size(L, 1)))
        return L
    end

    # Like `ScaledOperator`, these wrappers hold no scratch of their own and so are
    # transparent to the sharing described in `getcache`. The reshape is the same one
    # `cache_internals` above does: what the wrapper is applied to is not what the operator
    # it wraps is applied to.
    @eval getcache(L::$LType) = getcache(L.L)
    @eval update_cache(L::$LType, new_cache) = @reset L.L = update_cache(L.L, new_cache)

    @eval function adopt_cache(L::$LType, cache, u::AbstractVecOrMat)
        inner = adopt_cache(L.L, cache, reshape(u, size(L, 1)))
        return inner === nothing ? nothing : (@reset L.L = inner)
    end

    # operator application
    @eval Base.:*(u::$VType, L::$LType) = $op(L.L * u.parent)
    @eval Base.:/(u::$VType, L::$LType) = $op(L.L \ u.parent)

    # v' ← u' * A'
    # v  ← A  * u
    @eval function LinearAlgebra.mul!(v::$VType, u::$VType, L::$LType)
        mul!(v.parent, L.L, u.parent)
        return v
    end

    # v' ← α * (u' * A') + β * v'
    # v  ← α * (A  * u ) + β * v
    @eval function LinearAlgebra.mul!(v::$VType, u::$VType, L::$LType, α, β)
        mul!(v.parent, L.L, u.parent, α, β)
        return v
    end

    # v' ← u' / A'
    # v  ← A  \ u
    @eval function LinearAlgebra.ldiv!(v::$VType, u::$VType, L::$LType)
        ldiv!(v.parent, L.L, u.parent)
        return v
    end

    # u' ← u' / A'
    # u  ← A  \ u
    @eval function LinearAlgebra.ldiv!(u::$VType, L::$LType)
        ldiv!(L.L, u.parent)
        return u
    end
end

# For AdjointOperator
# Out-of-place: v is action vector, u is update vector
function (L::AdjointOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    # Adjoint operator applied to v means L.L' * v
    # For matrices: (A')v = (v'A)'
    # This means we need to compute L.L(v', u, p, t)'
    # Update the operator first, then apply adjoint operator
    L_updated = update_coefficients(L.L, u, p, t; kwargs...)
    # (A')v = (v'A)' where v'A is computed by A'*v'
    return (L_updated' * v')'
end

# In-place: w is destination, v is action vector, u is update vector
function (L::AdjointOperator)(w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...)
    # Update the operator in-place
    update_coefficients!(L.L, u, p, t; kwargs...)
    # Use direct in-place multiplicatieon for adjoints
    mul!(w', v', L.L)
    return w
end

# In-place with scaling: w = α*(L*v) + β*w
function (L::AdjointOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    # Update the operator in-place
    update_coefficients!(L.L, u, p, t; kwargs...)
    mul!(w', v', L.L, α, β)
    return w
end

# For TransposedOperator
# Out-of-place
function (L::TransposedOperator)(v::AbstractVecOrMat, u, p, t; kwargs...)
    L_updated = update_coefficients(L.L, u, p, t; kwargs...)
    # (A^T)v = (v'A)' where v'A is computed by A'*v'
    return (L_updated' * v')'
end

# In-place
function (L::TransposedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t; kwargs...
    )
    update_coefficients!(L.L, u, p, t; kwargs...)
    mul!(w', v', L.L)
    return w
end

# In-place with scaling
function (L::TransposedOperator)(
        w::AbstractVecOrMat, v::AbstractVecOrMat, u, p, t, α, β; kwargs...
    )
    update_coefficients!(L.L, u, p, t; kwargs...)
    mul!(w', v', L.L, α, β)
    return w
end
#
