#-------------------------------------------------------------------------------
# Conversion to Base.Expr

"""
    @isexpr(ex, head)
    @isexpr(ex, head, nargs)

Type inference friendly replacement for `Meta.isexpr`.

When using the pattern
```
if @isexpr(ex, headsym)
    body
end
```
Julia's type inference knows `ex isa Expr` inside `body`. But `Meta.isexpr`
hides this information from the compiler, for whatever reason.
"""
macro isexpr(ex, head)
    ex isa Symbol || error("First argument to `@isexpr` must be a variable name")
    :($(esc(ex)) isa Expr && $(esc(ex)).head == $(esc(head)))
end

macro isexpr(ex, head, nargs)
    ex isa Symbol || error("First argument to `@isexpr` must be a variable name")
    :($(esc(ex)) isa Expr &&
      $(esc(ex)).head == $(esc(head)) &&
      length($(esc(ex)).args) == $(esc(nargs)))
end

function is_eventually_call(ex)
    return ex isa Expr && (ex.head === :call ||
        (ex.head === :where || ex.head === :(::)) && is_eventually_call(ex.args[1]))
end

function _reorder_parameters!(args::Vector{Any}, params_pos)
    p = 0
    for i = length(args):-1:1
        ai = args[i]
        if !@isexpr(ai, :parameters)
            break
        end
        p = i
    end
    if p == 0
        return
    end
    # nest frankentuples parameters sections
    for i = length(args)-1:-1:p
        pushfirst!((args[i]::Expr).args, pop!(args))
    end
    # Move parameters to args[params_pos]
    insert!(args, params_pos, pop!(args))
end

function _strip_parens(ex)
    while true
        if @isexpr(ex, :parens)
            if length(ex.args) == 1
                ex = ex.args[1]
            else
                # Only for error cases
                return Expr(:block, ex.args...)
            end
        else
            return ex
        end
    end
end

function _leaf_to_Expr(source, txtbuf, head, srcrange, node)
    k = kind(head)
    if k == K"core_@cmd"
        return GlobalRef(Core, Symbol("@cmd"))
    elseif k == K"MacroName" && view(source, srcrange) == "."
        return Symbol("@__dot__")
    elseif is_error(k)
        return k == K"error" ?
            Expr(:error) :
            Expr(:error, "$(_token_error_descriptions[k]): `$(source[srcrange])`")
    else
        val = isnothing(node) ? parse_julia_literal(txtbuf, head, srcrange) : node.val
        if val isa Union{Int128,UInt128,BigInt}
            # Ignore the values of large integers and convert them back to
            # symbolic/textural form for compatibility with the Expr
            # representation of these.
            str = replace(source[srcrange], '_'=>"")
            macname = val isa Int128  ? Symbol("@int128_str")  :
                      val isa UInt128 ? Symbol("@uint128_str") :
                      Symbol("@big_str")
            return Expr(:macrocall, GlobalRef(Core, macname), nothing, str)
        else
            return val
        end
    end
end

# Julia string literals in a `K"string"` node may be split into several chunks
# interspersed with trivia in two situations:
# 1. Triple quoted string indentation is trivia
# 2. An \ before newline removes the newline and any following indentation
#
# This function concatenating adjacent string chunks together as done in the
# reference parser.
function _string_to_Expr(k, args)
    args2 = Any[]
    i = 1
    while i <= length(args)
        if args[i] isa String
            if i < length(args) && args[i+1] isa String
                buf = IOBuffer()
                while i <= length(args) && args[i] isa String
                    write(buf, args[i]::String)
                    i += 1
                end
                push!(args2, String(take!(buf)))
            else
                push!(args2, args[i])
                i += 1
            end
        else
            ex = args[i]
            if @isexpr(ex, :parens, 1)
                ex = _strip_parens(ex)
                if ex isa String
                    # Wrap interpolated literal strings in (string) so we can
                    # distinguish them from the surrounding text (issue #38501)
                    # Ie, "$("str")"  vs  "str"
                    # https://github.com/JuliaLang/julia/pull/38692
                    ex = Expr(:string, ex)
                end
            end
            push!(args2, ex)
            i += 1
        end
    end
    if length(args2) == 1 && args2[1] isa String
        # If there's a single string remaining after joining, we unwrap
        # to give a string literal.
        #   """\n  a\n  b""" ==>  "a\nb"
        return only(args2)
    else
        # This only happens when k == K"string" or when an error has occurred. 
        return Expr(:string, args2...)
    end
end

# Shared fixups for Expr children in cases where the type of the parent node
# affects the child layout.
function _fixup_Expr_children!(head, loc, args)
    k = kind(head)
    eq_to_kw_in_call = ((k == K"call" || k == K"dotcall") &&
                        is_prefix_call(head)) || k == K"ref"
    eq_to_kw_in_params = k != K"vect"   && k != K"curly" &&
                         k != K"braces" && k != K"ref"
    coalesce_dot = k in KSet"call dotcall curly" ||
                   (k == K"quote" && flags(head) == COLON_QUOTE)
    for i in 1:length(args)
        arg = args[i]
        was_parens = @isexpr(arg, :parens)
        arg = _strip_parens(arg)
        if @isexpr(arg, :(=)) && eq_to_kw_in_call && i > 1 
            arg = Expr(:kw, arg.args...)
        elseif k != K"parens" && @isexpr(arg, :., 1) && arg.args[1] isa Tuple
            h, a = arg.args[1]::Tuple{SyntaxHead,Any}
            arg = ((!was_parens && coalesce_dot && i == 1) ||
                   (k == K"comparison" && iseven(i)) ||
                   is_syntactic_operator(h)) ?
                Symbol(".", a) : Expr(:., a)
        elseif @isexpr(arg, :parameters) && eq_to_kw_in_params
            pargs = arg.args
            for j = 1:length(pargs)
                pj = pargs[j]
                if @isexpr(pj, :(=))
                    pargs[j] = Expr(:kw, pj.args...)
                end
            end
        elseif k == K"let" && i == 1 && @isexpr(arg, :block)
            filter!(a -> !(a isa LineNumberNode), arg.args)
        end
        args[i] = arg
    end
    return args
end

# Convert internal node of the JuliaSyntax parse tree to an Expr
function _internal_node_to_Expr(source, srcrange, head, childranges, childheads, args)
    k = kind(head)
    if (k == K"var" || k == K"char") && length(args) == 1
        # Ideally we'd like `@check length(args) == 1` as an invariant for all
        # K"var" and K"char" nodes, but this discounts having embedded error
        # nodes when ignore_errors=true is set.
        return args[1]
    elseif k == K"string" || k == K"cmdstring"
        return _string_to_Expr(k, args)
    end

    loc = source_location(LineNumberNode, source, first(srcrange))
    endloc = source_location(LineNumberNode, source, last(srcrange))

    _fixup_Expr_children!(head, loc, args)

    headstr = untokenize(head, include_flag_suff=false)
    headsym = !isnothing(headstr) ?
              Symbol(headstr) :
              error("Can't untokenize head of kind $(k)")

    if k == K"?"
        headsym = :if
    elseif k == K"=" && !is_decorated(head)
        a2 = args[2]
        if is_eventually_call(args[1])
            if @isexpr(a2, :block)
                pushfirst!(a2.args, loc)
            else
                # Add block for short form function locations
                args[2] = Expr(:block, loc, a2)
            end
        end
    elseif k == K"macrocall"
        _reorder_parameters!(args, 2)
        insert!(args, 2, loc)
    elseif k == K"block" || (k == K"toplevel" && !has_flags(head, TOPLEVEL_SEMICOLONS_FLAG))
        if isempty(args)
            push!(args, loc)
        else
            resize!(args, 2*length(args))
            for i = length(childranges):-1:1
                args[2*i] = args[i]
                args[2*i-1] = source_location(LineNumberNode, source, first(childranges[i]))
            end
        end
        if k == K"block" && has_flags(head, PARENS_FLAG)
            popfirst!(args)
        end
    elseif k == K"doc"
        headsym = :macrocall
        args = [GlobalRef(Core, Symbol("@doc")), loc, args...]
    elseif k == K"dotcall" || k == K"call"
        # Julia's standard `Expr` ASTs have children stored in a canonical
        # order which is often not always source order. We permute the children
        # here as necessary to get the canonical order.
        if is_infix_op_call(head) || is_postfix_op_call(head)
            args[2], args[1] = args[1], args[2]
        end
        # Lower (call x ') to special ' head
        if is_postfix_op_call(head) && args[1] == Symbol("'")
            popfirst!(args)
            headsym = Symbol("'")
        end
        # Move parameters blocks to args[2]
        _reorder_parameters!(args, 2)
        if headsym === :dotcall
            funcname = args[1]
            if is_prefix_call(head)
                headsym = :.
                args = Any[funcname, Expr(:tuple, args[2:end]...)]
            else
                # operator calls
                headsym = :call
                if funcname isa Symbol
                    args[1] = Symbol(:., funcname)
                end # else funcname could be an Expr(:error), just propagate it
            end
        end
    elseif k == K"." && length(args) == 1 && is_operator(childheads[1])
        # Hack: Here we preserve the head of the operator to determine whether
        # we need to coalesce it with the dot into a single symbol later on.
        args[1] = (childheads[1], args[1])
    elseif k == K"ref" || k == K"curly"
        # Move parameters blocks to args[2]
        _reorder_parameters!(args, 2)
    elseif k == K"for"
        a1 = args[1]
        if @isexpr(a1, :cartesian_iterator)
            args[1] = Expr(:block, a1.args...)
        end
        # Add extra line number node for the `end` of the block. This may seem
        # useless but it affects code coverage.
        push!(args[2].args, endloc)
    elseif k == K"while"
        # Line number node for the `end` of the block as in `for` loops.
        push!(args[2].args, endloc)
    elseif k in KSet"tuple vect braces"
        # Move parameters blocks to args[1]
        _reorder_parameters!(args, 1)
    elseif k == K"where"
        if length(args) == 2
            a2 = args[2]
            if @isexpr(a2, :braces)
                a2a = a2.args
                _reorder_parameters!(a2a, 2)
                args = Any[args[1], a2a...]
            end
        end
    elseif k == K"try"
        # Try children in source order:
        #   try_block catch_var catch_block else_block finally_block
        # Expr ordering:
        #   try_block catch_var catch_block [finally_block] [else_block]
        try_ = args[1]
        catch_var = false
        catch_ = false
        else_ = false
        finally_ = false
        for i in 2:length(args)
            a = args[i]
            if @isexpr(a, :catch)
                catch_var = a.args[1]
                catch_ = a.args[2]
            elseif @isexpr(a, :else)
                else_ = only(a.args)
            elseif @isexpr(a, :finally)
                finally_ = only(a.args)
            elseif @isexpr(a, :error)
                finally_ = Expr(:block, a) # Unclear where to put this but here will do?
            else
                @assert false "Illegal $a subclause in `try`"
            end
        end
        args = Any[try_, catch_var, catch_]
        if finally_ !== false || else_ !== false
            push!(args, finally_)
            if else_ !== false
                push!(args, else_)
            end
        end
    elseif k == K"generator"
        # Reconstruct the nested Expr form for generator from our flatter
        # source-ordered `generator` format.
        gen = args[1]
        for j = length(args):-1:2
            aj = args[j]
            if @isexpr(aj, :cartesian_iterator)
                gen = Expr(:generator, gen, aj.args...)
            else
                gen = Expr(:generator, gen, aj)
            end
            if j < length(args)
                # Additional `for`s flatten the inner generator
                gen = Expr(:flatten, gen)
            end
        end
        return gen
    elseif k == K"filter"
        @assert length(args) == 2
        iterspec = args[1]
        outargs = Any[args[2]]
        if @isexpr(iterspec, :cartesian_iterator)
            append!(outargs, iterspec.args)
        else
            push!(outargs, iterspec)
        end
        args = outargs
    elseif k == K"nrow" || k == K"ncat"
        # For lack of a better place, the dimension argument to nrow/ncat
        # is stored in the flags
        pushfirst!(args, numeric_flags(flags(head)))
    elseif k == K"typed_ncat"
        insert!(args, 2, numeric_flags(flags(head)))
    elseif k == K"elseif"
        # Block for conditional's source location
        args[1] = Expr(:block, loc, args[1])
    elseif k == K"->"
        a2 = args[2]
        if @isexpr(a2, :block)
            pushfirst!(a2.args, loc)
        else
            # Add block for source locations
            args[2] = Expr(:block, loc, args[2])
        end
    elseif k == K"function"
        if length(args) > 1
            a1 = args[1]
            if @isexpr(a1, :tuple)
                # Convert to weird Expr forms for long-form anonymous functions.
                #
                # (function (tuple (... xs)) body) ==> (function (... xs) body)
                if length(a1.args) == 1 && (a11 = a1.args[1]; @isexpr(a11, :...))
                    # function (xs...) \n body end
                    args[1] = a11
                end
            end
            pushfirst!((args[2]::Expr).args, loc)
        end
    elseif k == K"macro"
        if length(args) > 1
            pushfirst!((args[2]::Expr).args, loc)
        end
    elseif k == K"module"
        pushfirst!(args, !has_flags(head, BARE_MODULE_FLAG))
        pushfirst!((args[3]::Expr).args, loc)
    elseif k == K"inert"
        return QuoteNode(only(args))
    elseif k == K"quote" && length(args) == 1
        a1 = only(args)
        if !(a1 isa Expr || a1 isa QuoteNode || a1 isa Bool)
            # Flisp parser does an optimization here: simple values are stored
            # as inert QuoteNode rather than in `Expr(:quote)` quasiquote
            return QuoteNode(a1)
        end
    elseif k == K"do" && length(args) == 3
        return Expr(:do, args[1], Expr(:->, args[2], args[3]))
    elseif k == K"let"
        a1 = args[1]
        if @isexpr(a1, :block)
            a1a = (args[1]::Expr).args
            # Ugly logic to strip the Expr(:block) in certian cases for compatibility
            if length(a1a) == 1
                a = a1a[1]
                if a isa Symbol || @isexpr(a, :(=)) || @isexpr(a, :(::))
                    args[1] = a
                end
            end
        end
    elseif k == K"local" || k === K"global"
        if length(args) == 1
            a1 = args[1]
            if @isexpr(a1, :const)
                # Normalize `local const` to `const local`
                args[1] = Expr(headsym, (a1::Expr).args...)
                headsym = :const
            elseif @isexpr(a1, :tuple)
                # Normalize `global (x, y)` to `global x, y`
                args = a1.args
            end
        end
    elseif k == K"return" && isempty(args)
        push!(args, nothing)
    elseif k == K"juxtapose"
        headsym = :call
        pushfirst!(args, :*)
    elseif k == K"struct"
        pushfirst!(args, has_flags(head, MUTABLE_FLAG))
    elseif k == K"importpath"
        headsym = :.
        for i = 1:length(args)
            ai = args[i]
            if ai isa QuoteNode
                # Permit nonsense additional quoting such as
                # import A.(:b).:c
                args[i] = ai.value
            end
        end
    elseif k == K"wrapper"
        # This should only happen for errors wrapped next to what should have
        # been single statements or atoms - represent these as blocks.
        headsym = :block
    end

    return Expr(headsym, args...)
end


# Stack entry for build_tree Expr conversion.
# We'd use `Tuple{UnitRange{Int},SyntaxHead,Any}` instead, but that's an
# abstract type due to the `Any` and tuple covariance which destroys
# performance.
struct _BuildExprStackEntry
    srcrange::UnitRange{Int}
    head::SyntaxHead
    ex::Any
end

function build_tree(::Type{Expr}, stream::ParseStream;
                    filename=nothing, first_line=1, kws...)
    source = SourceFile(stream, filename=filename, first_line=first_line)
    txtbuf = unsafe_textbuf(stream)
    args = Any[]
    childranges = UnitRange{Int}[]
    childheads = SyntaxHead[]
    entry = build_tree(_BuildExprStackEntry, stream; kws...) do head, srcrange, nodechildren
        if is_trivia(head) && !is_error(head)
            return nothing
        end
        k = kind(head)
        if isnothing(nodechildren)
            ex = _leaf_to_Expr(source, txtbuf, head, srcrange, nothing)
        else
            resize!(childranges, length(nodechildren))
            resize!(childheads, length(nodechildren))
            resize!(args, length(nodechildren))
            for (i,c) in enumerate(nodechildren)
                childranges[i] = c.srcrange
                childheads[i] = c.head
                args[i] = c.ex
            end
            ex = _internal_node_to_Expr(source, srcrange, head, childranges, childheads, args)
        end
        return _BuildExprStackEntry(srcrange, head, ex)
    end
    loc = source_location(LineNumberNode, source, first(entry.srcrange))
    only(_fixup_Expr_children!(SyntaxHead(K"None",EMPTY_FLAGS), loc, Any[entry.ex]))
end

function _to_expr(node::SyntaxNode)
    if !haschildren(node)
        offset, txtbuf = _unsafe_wrap_substring(sourcetext(node.source))
        return _leaf_to_Expr(node.source, txtbuf, head(node), range(node) .+ offset, node)
    end
    cs = children(node)
    args = Any[_to_expr(c) for c in cs]
    _internal_node_to_Expr(node.source, range(node), head(node), range.(cs), head.(cs), args)
end

function Base.Expr(node::SyntaxNode)
    ex = _to_expr(node)
    loc = source_location(LineNumberNode, node.source, first(range(node)))
    only(_fixup_Expr_children!(SyntaxHead(K"None",EMPTY_FLAGS), loc, Any[ex]))
end

