"""
    $(TYPEDEF)

A utility struct stored inside `LinearProblem` to enable a symbolic interface. Intended for
use by ModelingToolkit.jl.

# Fields

$(TYPEDFIELDS)
"""
struct SymbolicLinearInterface{F1, F2, S, O, M}
    # the docstrings cannot start with a newline because otherwise the docs
    # builder fragments the list of fields into single-item lists (in TYPEDFIELDS)
    """A function which takes `A` and the parameter object `p` and updates `A` in-place."""
    update_A!::F1
    """A function which takes `b` and the parameter object `p` and updates `b` in-place."""
    update_b!::F2
    """The symbolic backend for the `LinearProblem`."""
    sys::S
    """A function which when given a symbolic expression returns a function `(u, p)`
    that computes the expression."""
    observed::O
    """Arbitrary metadata useful for the symbolic backend."""
    metadata::M
end

__has_sys(::SymbolicLinearInterface) = true
has_sys(::SymbolicLinearInterface) = true

SymbolicIndexingInterface.symbolic_container(sli::SymbolicLinearInterface) = sli.sys

function SymbolicIndexingInterface.observed(fn::SymbolicLinearInterface, sym)
    return if fn.observed !== nothing
        fn.observed(sym)
    elseif fn.sys !== nothing
        SymbolicIndexingInterface.observed(fn.sys, sym)
    else
        error("This `LinearProblem` does not support computing observed quantities.")
    end
end

@doc doc"""

Defines a linear system problem.
Documentation Page: <https://docs.sciml.ai/LinearSolve/stable/basics/LinearProblem/>

## Mathematical Specification of a Linear Problem

### Concrete `LinearProblem`

To define a `LinearProblem`, you simply need to give the `AbstractMatrix` ``A``
and an `AbstractVector` ``b`` which defines the linear system:

```math
Au = b
```

### Matrix-Free LinearProblem

For matrix-free versions, the specification of the problem is given by an
operator `A(u,p,t)` which computes `A*u`, or in-place as `A(du,u,p,t)`. These
are specified via the `AbstractSciMLOperator` interface. For more details, see
the [SciMLBase Documentation](https://docs.sciml.ai/SciMLBase/stable/).

Note that matrix-free versions of LinearProblem definitions are not compatible
with all solvers. To check a solver for compatibility, use the function `needs_concrete_A(alg::AbstractLinearAlgorithm)`.

## Problem Type

### Constructors

Optionally, an initial guess ``u₀`` can be supplied which is used for iterative
methods.

```julia
LinearProblem{isinplace}(A,b,p=NullParameters();u0=nothing,kwargs...)
LinearProblem(f::AbstractSciMLOperator,b,p=NullParameters();u0=nothing,kwargs...)
```

`isinplace` optionally sets whether the function is in-place or not, i.e. whether
the solvers are allowed to mutate. By default this is true for `AbstractMatrix`,
and for `AbstractSciMLOperator`s it matches the choice of the operator definition.

Parameters are optional, and if not given, then a `NullParameters()` singleton
will be used, which will throw nice errors if you try to index non-existent
parameters. Any extra keyword arguments are passed on to the solvers.

### Fields

* `A`: The representation of the linear operator.
* `b`: The right-hand side of the linear system.
* `p`: The parameters for the problem. Defaults to `NullParameters`. Currently unused.
* `u0`: The initial condition used by iterative solvers.
* `symbolic_interface`: An instance of `SymbolicLinearInterface` if the problem was
  generated by a symbolic backend.
* `kwargs`: The keyword arguments passed on to the solvers.
"""
struct LinearProblem{
        uType, isinplace, F, bType, P, I <: Union{SymbolicLinearInterface, Nothing}, K,
    } <:
    AbstractLinearProblem{bType, isinplace}
    A::F
    b::bType
    u0::uType
    p::P
    f::I
    kwargs::K
    @add_kwonly function LinearProblem{iip}(
            A, b, p = NullParameters(); u0 = nothing,
            f = nothing, kwargs...
        ) where {iip}
        warn_paramtype(p)
        new{typeof(u0), iip, typeof(A), typeof(b), typeof(p), typeof(f), typeof(kwargs)}(
            A, b, u0, p,
            f, kwargs
        )
    end
end

function LinearProblem(A, b, args...; kwargs...)
    return if A isa AbstractArray
        LinearProblem{true}(A, b, args...; kwargs...)
    elseif A isa Number
        LinearProblem{false}(A, b, args...; kwargs...)
    else
        LinearProblem{isinplace(A, 4)}(A, b, args...; kwargs...)
    end
end

SymbolicIndexingInterface.symbolic_container(prob::LinearProblem) = prob.f
SymbolicIndexingInterface.state_values(prob::LinearProblem) = prob.u0
SymbolicIndexingInterface.parameter_values(prob::LinearProblem) = prob.p
SymbolicIndexingInterface.is_time_dependent(::LinearProblem) = false
function SymbolicIndexingInterface.set_parameter!(
        valp::LinearProblem{A, B, C, D, E, <:SymbolicLinearInterface},
        val, idx
    ) where {A, B, C, D, E}
    set_parameter!(parameter_values(valp), val, idx)
    valp.f.update_A!(valp.A, valp.p)
    return valp.f.update_b!(valp.b, valp.p)
end

@doc doc"""
    LinearAliasSpecifier(; alias_A = nothing, alias_b = nothing, alias = nothing)

Holds information on what variables to alias
when solving a LinearProblem. Conforms to the AbstractAliasSpecifier interface. 

When a keyword argument is `nothing`, the default behaviour of the solver is used.

### Keywords

* `alias_A::Union{Bool, Nothing}`: alias the `A` array.
* `alias_b::Union{Bool, Nothing}`: alias the `b` array. 
* `alias::Union{Bool, Nothing}`: sets all fields of the `LinearAliasSpecifier` to `alias`. 

Creates a `LinearAliasSpecifier` where `alias_A` and `alias_b` default to `nothing`.
When `alias_A` or `alias_b` is nothing, the default value of the solver is used.
"""
struct LinearAliasSpecifier <: AbstractAliasSpecifier
    alias_A::Union{Bool, Nothing}
    alias_b::Union{Bool, Nothing}

    function LinearAliasSpecifier(; alias_A = nothing, alias_b = nothing, alias = nothing)
        return if alias == true
            new(true, true)
        elseif alias == false
            new(false, false)
        elseif isnothing(alias)
            new(alias_A, alias_b)
        end
    end
end
