"""
$(DocStringExtensions.README)
"""
module SymbolicUtils

if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_methods"))
    @eval Base.Experimental.@compiler_options max_methods=1
end

using DocStringExtensions

export @syms, term, hasmetadata, getmetadata, setmetadata

using Moshi.Data: @data
import Moshi.Data as MData
using Moshi.Match: @match
using EnumX: @enumx
using TermInterface
using Setfield
import Setfield: PropertyLens
using SymbolicIndexingInterface
import Base: +, -, *, /, //, \, ^, ImmutableDict
using ConstructionBase
using TermInterface
import TermInterface: iscall, operation, arguments, metadata, maketerm, sorted_arguments
# For ReverseDiffExt
import ArrayInterface
import ExproniconLite as EL
import TaskLocalValues: TaskLocalValue
using WeakCacheSets: WeakCacheSet, getkey!
using Base: RefValue
import MacroTools
import PrecompileTools
PrecompileTools.@recompile_invalidations begin
    import MultivariatePolynomials as MP
    import DynamicPolynomials as DP
    import MutableArithmetics as MA
    import SparseArrays: SparseMatrixCSC, findnz, sparse
    using DataStructures
    import DataStructures: OrderedCollections
    using ReadOnlyArrays
    import Graphs
    import StaticArraysCore
end
import LinearAlgebra
using SciMLPublic: @public

macro manually_scope(val, expr, is_forced = false)
    @assert Meta.isexpr(val, :call)
    @assert val.args[1] == :(=>)

    var_name = val.args[2]
    new_val = val.args[3]
    old_name = gensym(:old_val)
    cur_name = gensym(:cur_val)
    retval_name = gensym(:retval)
    close_expr = :($var_name[] = $old_name)
    interpolated_expr = MacroTools.postwalk(expr) do ex
        if Meta.isexpr(ex, :$) && length(ex.args) == 1 && ex.args[1] == :$
            return cur_name
        else
            return ex
        end
    end
    basic_result = quote
        $cur_name = $var_name[] = $new_val
        $retval_name = try
            $interpolated_expr
        finally
            $close_expr
        end
    end
    is_forced && return quote
        $old_name = $var_name[]
        $basic_result
    end |> esc

    return quote
        $old_name = $var_name[]
        if $iszero($old_name)
            $basic_result
        else
            $cur_name = $old_name
            $retval_name = begin
                $interpolated_expr
            end
        end
        $retval_name
    end |> esc
end

# copied from https://github.com/JuliaLang/julia/blob/80f7db8e51b2ba1dd21e913611c23a6d5b75ecab/base/lock.jl#L371-L381
# and adapted for readlock/readunlock
macro readlock(l, expr)
    quote
        temp = $(esc(l))
        $readlock(temp)
        try
            $(esc(expr))
        finally
            $readunlock(temp)
        end
    end
end

include("cache.jl")
Base.@deprecate istree iscall

"""
    istree(x)

Deprecated spelling of [`iscall`](@ref). Returns `true` if `x` is a function call
expression, i.e. one for which [`operation`](@ref) and [`arguments`](@ref) are defined.

`istree` was the name used by the pre-`TermInterface` symbolic interface. It is retained
only so that older code keeps running, emits a deprecation warning on every call, and will
be removed in the next breaking release. New code should call `iscall` instead:

```julia
iscall(x)      # instead of istree(x)
```

Note that `iscall` is not merely a rename: the term interface splits the old notion of "is
this a tree node" into `iscall` (the expression is an `operation` applied to `arguments`)
and the container-specific predicates such as [`SymbolicUtils.issym`](@ref). Code that used
`istree` to mean "not a leaf" should check `iscall`; code that used `!istree(x)` to mean "is
a symbol" should check `SymbolicUtils.issym(x)`, which is the precise question.

See also: [`iscall`](@ref), [`operation`](@ref), [`arguments`](@ref).
"""
istree

include("small_array.jl")

export istree, operation, arguments, sorted_arguments, iscall, unwrap_const
# Sym, Term,
# Add, Mul and Pow
PrecompileTools.@recompile_invalidations begin
    include("types.jl")
    include("terminterface.jl")
    include("hashconsing.jl")
    include("inner_ctors.jl")
    include("safe_ctors.jl")
    include("symbolic_ops/common.jl")
    include("symbolic_ops/addsub.jl")
    include("symbolic_ops/mul.jl")
    include("symbolic_ops/div.jl")
    include("symbolic_ops/pow.jl")
    include("symbolic_ops/getindex.jl")
end

export BS
include("syms.jl")
export @arrayop
include("arrayop.jl")
export @makearray
include("arraymaker.jl")

# Methods on symbolic objects
export ifelse_eager, ifelse_branching
PrecompileTools.@recompile_invalidations begin
using SpecialFunctions, NaNMath
include("methods.jl")
include("printing.jl")
end
# LinkedList, simplification utilities
include("utils.jl")

# Tree inspection
include("inspect.jl")
export Rewriters

# A library for composing together expr -> expr functions

using Combinatorics: permutations, combinations
export @rule, @acrule, RuleSet

# Rule type and @rule macro
include("rule.jl")
include("matchers.jl")
include("rewriters.jl")

# Convert to an efficient multi-variate polynomial representation
import DynamicPolynomials
export expand
include("polyform.jl")

# Term ordering
include("ordering.jl")

# Default rules for expression simplification
include("simplify_rules.jl")

# API = simplify + substitute
export simplify
include("simplify.jl")

export substitute
include("substitute.jl")

include("ordereddigraph.jl")
include("graph_traversal.jl")

export IRStructure, populate_ir!, print_ir, get_reachability
include("irstructure.jl")

include("code.jl")

PrecompileTools.@recompile_invalidations begin
    include("despecialize.jl")
end

@public add_worker, mul_worker
@public BasicSymbolic, unwrap, isadd, ismul
@public symtype, issym, isterm, isdiv, Sym
@public isconst
@public ispow
@public fntype_ret_type
@public FnType
@public Mapper, Mapreducer

# `simplify_rules.jl` does `using .Rewriters`, so these already resolve as
# `SymbolicUtils.X` and callers depend on that spelling. Declare them public here so
# reaching them through this module is API rather than a side effect of that import.
@public Empty, IfElse, If, Chain, RestartedChain, Fixpoint, Postwalk, Prewalk, PassThrough

PrecompileTools.@setup_workload begin
    fold1 = Val{false}()
    fold2 = Val{true}()
    PrecompileTools.@compile_workload begin
        @syms x y f(t) q[1:5]
        Sym{SymReal}(:a; type = Real, shape = ShapeVecT())
        x + y
        x * y
        x / y
        x ^ y
        x ^ 5
        6 ^ x
        x - y
        -y
        2y
        symtype(y)
        f(x)
        (5x / 5)
        z = 2
        x ^ z
        Const{SymReal}(x; unsafe=true)
        show(devnull, x ^ 2 + y * x + y / 3x)
        expand((x + y) ^ 2)
        simplify(x ^ (1//2) + (sin(x) ^ 2 + cos(x) ^ 2) + 2(x + y) - x - y)
        ex = x + 2y + sin(x)
        rules1 = Dict(x => y)
        rules2 = Dict(x => 1)
        # Running `fold = Val(true)` invalidates the precompiled statements
        # for `fold = Val(false)` and itself doesn't precompile anyway.
        substitute(ex, rules1)
        # substitute(ex, rules1; fold = fold1)
        # substitute(ex, rules2; fold = fold1)
        # substitute(ex, rules2)
        # substitute(ex, rules1; fold = fold2)
        # substitute(ex, rules2; fold = fold2)
        q[1]
        q'q
        sorted_arguments(x ^ 2 + 2x + x / y + q[1] + f(x) ^ x)
    end
end

precompile(Tuple{typeof(Base.show), Base.TTY, SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}})
precompile(Tuple{typeof(Core.kwcall), NamedTuple{(:color,), Tuple{Symbol}}, typeof(Base.printstyled), Base.TTY, Int64})

let BS = BasicSymbolic{SymReal}
    precompile(Tuple{typeof(Base.collect), BS})
    precompile(Tuple{typeof(SymbolicUtils._getindex), Type{SymReal}, BS, Int64, Vararg{Int64}})
end

end # module
