# The following are versions of macros from Base which act as "standard syntax
# extensions":
#
# * They emit syntactic forms with special `Kind`s and semantics known to
#   lowering
# * There is no other Julia surface syntax for these `Kind`s.

# In order to implement these here without getting into bootstrapping problems,
# we just write them as plain old macro-named functions and add the required
# __context__ argument ourselves.
#
# TODO: @inline, @noinline, @inbounds, @simd, @ccall, @assume_effects
#
# TODO: Eventually move these to proper `macro` definitions and use
# `JuliaLowering.include()` or something. Then we'll be in the fun little world
# of bootstrapping but it shouldn't be too painful :)

# Note that `@ast __context__ __context__.macrocall [K"foo" ...]` is unhygienic,
# since `@ast` is meant for internal lowering use (it requires an explicit
# provenance argument, and then copies any syntax context from the provenance to
# any created syntax).  A real user-facing macro to replace it should use the
# provenance of the literal K"foo" expression in the file instead, and should
# not copy context (this is not hard to implement, but the provenance requires
# it and callers to be JL-lowered, which this file currently isn't.)

function Base.var"@nospecialize"(__context__::MacroContext, exs::SyntaxTree...)
    if length(exs) == 0
        @ast __context__ __context__.macrocall [K"meta"
            "nospecialize"::K"Identifier"]
    elseif length(exs) == 1 && kind(exs[1]) === K"="
        eq = exs[1]
        @ast __context__ __context__.macrocall [K"meta"
            "nospecialize"::K"Identifier" [K"kw"(eq) children(eq)...]]
    else
        @ast __context__ __context__.macrocall [K"meta"
            "nospecialize"::K"Identifier" exs...]
    end
end

# TODO: support all forms that the original supports
# function Base.var"@atomic"(__context__::MacroContext, ex)
#     @jl_assert kind(ex) == K"Identifier" || kind(ex) == K"::" (ex, "Expected identifier or declaration")
#     @ast __context__ __context__.macrocall [K"atomic" ex]
# end

# TODO: @label

function Base.var"@goto"(__context__::MacroContext, ex)
    @jl_assert kind(ex) == K"Identifier" ex
    @ast __context__ ex [K"symbolicgoto" ex]
end

function Base.var"@locals"(__context__::MacroContext)
    @ast __context__ __context__.macrocall [K"locals"]
end

@static if isdefined(Base, Symbol("@__FUNCTION__"))
function Base.var"@__FUNCTION__"(__context__::MacroContext)
    @ast __context__ __context__.macrocall [K"thisfunction"]
end
end

function Base.var"@isdefined"(__context__::MacroContext, ex)
    @ast __context__ __context__.macrocall [K"isdefined" ex]
end

function Base.var"@generated"(__context__::MacroContext)
    @ast __context__ __context__.macrocall [K"generated"]
end
function Base.var"@generated"(__context__::MacroContext, ex)
    if !(kind(ex) === K"function" ||
        kind(ex) === K"=" && is_eventually_call(ex[1]))
        throw(LoweringError(ex, "Expected a function argument to `@generated`"))
    end
    @ast __context__ __context__.macrocall [K"function"
        ex[1]
        [K"block"
            [K"if" [K"generated"]
                ex[2]
                [K"block"
                    [K"meta" "generated_only"::K"Identifier"]
                    [K"return" nothing::K"Value"]
                ]
            ]
        ]
    ]
end

function Base.var"@cfunction"(__context__::MacroContext, callable, return_type, arg_types)
    if kind(arg_types) != K"tuple"
        throw(MacroExpansionError(arg_types, "@cfunction argument types must be a literal tuple"))
    end
    arg_types_svec = @ast __context__ arg_types [K"call"
        [K"core" "svec"::K"Identifier"]
        children(arg_types)...
    ]
    if kind(callable) == K"$"
        fptr = callable[1]
        typ = Base.CFunction
    else
        # Kinda weird semantics here - without `$`, the callable is a top level
        # expression evaluated within the module where the `@cfunction` is
        # expanded into.
        fptr = @ast __context__ callable [K"inert"
            callable
        ]
        typ = Ptr{Cvoid}
    end
    @ast __context__ __context__.macrocall [K"cfunction"
        typ::K"Value"
        fptr
        return_type
        arg_types_svec
        [K"inert" "ccall"::K"Identifier"]
    ]
end

function ccall_macro_parse(ctx, exs)
    gc_safe=false
    opts = exs[1:end-1]
    ex = exs[end]
    for opt in opts
        @stm opt begin
            [K"=" [K"Identifier"] val] -> if syntax_name(opt[1]) != "gc_safe"
                throw(MacroExpansionError(opt[1], "unknown option name for ccall"))
            elseif !(kind(val) in KSet"Bool Value")
                throw(MacroExpansionError(val, "gc_safe must be true or false"))
            else
                gc_safe = val.value
            end
            _ -> throw(MacroExpansionError(opt, "bad option to ccall"))
        end
    end
    if length(opts) >= 2
        throw(MacroExpansionError(opts[2], "too many options provided to @ccall"))
    end

    (func, argts, rettype) = @stm ex begin
        [K"::" [K"call" f as...] r] -> let f_expanded = @stm f begin
            [K"." lib sym] -> @ast ctx f [K"tuple" sym lib]
            [K"inert" [K"Identifier"]] -> @ast ctx f [K"tuple" f]
            [K"Identifier"] -> @ast ctx f [K"tuple" [K"inert" f]]
            [K"$" x] -> let kx = kind(x)
                if kx in KSet"tuple String string" ||
                        (kx === K"Value" && x.value isa Tuple) ||
                        kx == K"inert" && !(kind(x[1]) == K"Value" && x[1].value isa Ptr)
                    throw(MacroExpansionError(
                        f, "interpolated value should be a variable or expression, not a literal name or tuple"))
                end
                x
            end
            _ -> throw(MacroExpansionError(
                f, "@ccall function name must be a symbol, a `.` node (e.g. `libc.printf`) or an interpolated function pointer (with `\$`)"))
        end
            (f_expanded, as, r)
        end
        [K"call" _...] -> throw(MacroExpansionError(
            ex, "expected a return type annotation `::SomeType`", position=:end))
        _ -> throw(MacroExpansionError(
            ex, "expected call expression with return type"))
    end

    # detect varargs
    varargs = nothing
    argstart = 1
    if length(argts) > 0 && kind(argts[1]) == K"parameters"
        varargs = children(argts[1])
        argstart = 2
    end

    # collect args and types
    args = SyntaxList()
    types = SyntaxList()
    function pusharg!(at)
        @stm at begin
            [K"::" a t] -> (push!(args, a); push!(types, t))
            _ -> throw(MacroExpansionError(
                at, "argument needs a type annotation"))
        end
    end

    for e in argts[argstart:end]
        pusharg!(e)
    end

    if !isnothing(varargs)
        num_required_args = length(args)
        if num_required_args == 0
            throw(MacroExpansionError(
                argts[1],
                "C ABI prohibits varargs without one required argument"))
        end
        for e in varargs
            pusharg!(e)
        end
    else
        num_required_args = 0 # Non-vararg call
    end

    return func, rettype, types, args, gc_safe, num_required_args
end

function ccall_macro_lower(ctx, ex, convention, func, rettype, types, args, gc_safe, num_required_args)
    if convention isa Tuple
        cconv_tuple = (convention..., gc_safe)
    else
        cconv_tuple = (convention, UInt16(0), gc_safe)
    end
    return @ast ctx ex [K"call"
        "ccall"::K"Identifier"
        func
        [K"cconv" cconv_tuple::K"Value" num_required_args::K"Value"]
        rettype
        [K"tuple" types...]
        args...
    ]
end

function Base.var"@ccall"(ctx::MacroContext)
    throw(ArgumentError("@ccall needs a function signature with a return type"))
end

function Base.var"@ccall"(ctx::MacroContext, exs...)
    ccall_macro_lower(ctx, exs[end], :ccall, ccall_macro_parse(ctx, exs)...)
end

function Base.GC.var"@preserve"(__context__::MacroContext, exs...)
    idents = exs[1:end-1]
    for e in idents
        if kind(e) != K"Identifier"
            throw(MacroExpansionError(e, "Preserved variable must be a symbol"))
        end
    end
    @ast __context__ __context__.macrocall [K"gc_preserve" exs[end] exs[1:end-1]...]
end

function Base.Experimental.var"@opaque"(__context__::MacroContext, ex)
    @jl_assert kind(ex) == K"->" ex
    @ast __context__ __context__.macrocall [K"opaque_closure"
        nothing::K"Value"
        nothing::K"Value"
        nothing::K"Value"
        true::K"Bool"
        ex
    ]
end

# @eval should mostly ignore hygiene against our system's best wishes.  Still
# attempt to preserve provenance.
function _at_eval_code(mc::MacroContext, mod_st::SyntaxTree, ex)
    sc = mc.macrocall.context::SyntaxContext
    val = remove_context(@ast mc mc.macrocall ("eval_result"::K"Identifier"))
    q = _legacy_quote_to_syntax((@ast mc mc.macrocall [K"quote" ex]), 0, true)
    new_sc = SyntaxContext(base_layer(sc).mod, sc.version)
    @ast mc mc.macrocall [K"block"
        [K"local"
            [K"="
                val
                [K"call" JuliaLowering.eval::K"Value"
                    mod_st
                    [K"call" JuliaSyntax.fill_context::K"Value" q new_sc::K"Value"]
                ]
            ]
        ]
        [K"unknown_head"(;value="latestworld-if-toplevel")]
        val
    ]
end
function Base.var"@eval"(__context__::MacroContext, ex)
    sc = __context__.macrocall.context::SyntaxContext
    mod = @ast __context__ __context__.macrocall base_layer(sc).mod::K"Value"
    _at_eval_code(__context__, mod, ex)
end

function Base.var"@eval"(__context__::MacroContext, mod, ex)
    _at_eval_code(__context__, mod, ex)
end

#--------------------------------------------------------------------------------
# The following `@islocal` and `@inert` are macros for special syntax known to
# lowering which don't exist in Base but arguably should.
#
# For now we have our own versions
function var"@islocal"(__context__::MacroContext, ex)
    @jl_assert kind(ex) == K"Identifier" ex
    @ast __context__ __context__.macrocall [K"islocal" ex]
end

"""
A non-interpolating quoted expression.

For example,

```julia
@inert quote
    \$x
end
```

does not take `x` from the surrounding scope - instead it leaves the
interpolation `\$x` intact as part of the expression tree.

TODO: What is the correct way for `@inert` to work? ie which of the following
should work?

```julia
@inert quote
   body
end

@inert begin
   body
end

@inert x

@inert \$x
```

The especially tricky cases involve nested interpolation ...
```julia
quote
    @inert \$x
end

@inert quote
    quote
        \$x
    end
end

@inert quote
    quote
        \$\$x
    end
end
```

etc. Needs careful thought - we should probably just copy what lisp does with
quote+quasiquote 😅
"""
function var"@inert"(__context__::MacroContext, ex)
    @jl_assert kind(ex) == K"quote" ex
    @ast __context__ __context__.macrocall [K"inert" ex]
end

# `quote`/`inert` for syntaxtree
function var"@syntaxinert"(__context__::MacroContext, st)
    @ast __context__ __context__.macrocall [K"syntaxinert" st]
end
function var"@syntaxquote"(__context__::MacroContext, st)
    @ast __context__ __context__.macrocall [K"syntaxquote" st]
end
# not particularly good or useful, as @syntaxquote must expand first
function var"@syntaxunquote"(__context__::MacroContext, st)
    @ast __context__ __context__.macrocall [K"syntaxunquote" st]
end

# If the syntax version allows, convert quote/$ to syntaxquote/syntaxunquote.
# This is just a convenient way to create SyntaxTree with full provenance
# without dedicated surface syntax, mainly for testing metaprogramming in JL.
# It is insufficient in many ways, e.g. not all forms can be expressed (need
# surface syntax)
function var"@legacy_quote_to_syntax"(__context__::MacroContext, st)
    @jl_assert kind(st) === K"quote" || kind(st) === K"inert" st
    if is_flisp_compat(__context__.macrocall)
        st
    elseif kind(st) === K"inert"
        @mknode(st; kind=K"syntaxinert") # parser simplifies quote to inert
    else
        _legacy_quote_to_syntax(st, 0, false)
    end
end
function _legacy_quote_to_syntax(st::SyntaxTree, depth, force::Bool)
    k = kind(st)
    if k === K"quote" && depth == 0 && (force || !is_flisp_compat(st))
        @jl_assert numchildren(st) == 1 st
        @mknode(st; kind=K"syntaxquote", children=
            mapsyntax(c->_legacy_quote_to_syntax(c, depth+1, force), children(st)))
    elseif k === K"$" && depth == 1 && (force || !is_flisp_compat(st))
        @jl_assert numchildren(st) == 1 (st, "bad multi-syntaxunquote")
        @mknode(st; kind=K"syntaxunquote")
    else
        depth2 = k === K"quote" ? depth + 1 : k === K"$" ? depth - 1 : depth
        cs = SyntaxList()
        for c in children(st)
            # Convert multi-unquote to single unquote
            if depth2 == 1 && kind(c) === K"$" && numchildren(c) > 1
                for c2 in children(c)
                    push!(cs, @ast _ c [K"$" c2])
                end
            else
                push!(cs, c)
            end
        end
        cs_out = mapsyntax(c->_legacy_quote_to_syntax(c, depth2, force), cs)
        cs_out == children(st) ? st : @mknode(st; children=cs_out)
    end
end
macro legacy_quote_to_syntax(x)
    esc(x)
end

"""
Retrieve the syntax version of the macrocall
"""
function var"@syntax_version"(__context__::MacroContext)
    (__context__.macrocall.context::SyntaxContext).version
end
macro syntax_version()
    JL_OLD_SYNTAX_VERSION
end

"""
Set the syntax version of some syntax.  This can be used to define macros
producing older syntax than the current version.
"""
function var"@syntax_version"(__context__::MacroContext, ver_st, st)
    kind(st) === K"macro" || throw(LoweringError(
        st, "`@syntax_version version macro` only supports macro definitions"))
    ver = JuliaLowering.eval(syntax_module(ver_st), ver_st)
    ver isa VersionNumber || throw(LoweringError(
        ver_st, "version argument should be literal `v\"...\" call`"))
    _ensure_syntax_version(st, ver)
end
macro syntax_version(_, x)
    throw(ArgumentError("@syntax_version can't set version when lowering with flisp"))
end

function _ensure_syntax_version(st, ver::VersionNumber)
    st_sc = st.context::SyntaxContext
    sc = st_sc.version == ver ? st_sc :
        SyntaxContext(st_sc.layer, st_sc.unexpanded, ver, st_sc.internal)

    if is_leaf(st) || numchildren(st) == 0
        st_sc == sc ? st : @mknode(st; context=sc)
    else
        out = mapchildren(c->_ensure_syntax_version(c, ver), st)
        (st_sc === sc && out === st) ? out : @mknode(st; context=sc)
    end
end
