attrsummary(name, _value) = string(name)
attrsummary(name, value::Number) = "$name=$value"
attrsummary(name, value::LineNumberNode) = "$name=L$(value.line)"
attrsummary(name, value::Module) = "$name=$value"

function _value_string(ex)
    k = kind(ex)
    str = k == K"Identifier"  ? syntax_name(ex)           :
          k == K"Placeholder" ? syntax_name(ex)           :
          k == K"SSAValue"    ? "%"                   :
          k == K"BindingId"   ? "#"                   :
          k == K"label"       ? "label"               :
          k == K"nothing"     ? "core.nothing"        :
          k == K"core"        ? "core.$(syntax_name(ex))" :
          k == K"top"         ? "top.$(syntax_name(ex))"  :
          k == K"Symbol"      ? ":$(syntax_name(ex))" :
          k == K"globalref"   ? "$(ex.mod).$(syntax_name(ex))" :
          k == K"slot"        ? "slot" :
          k == K"Slots"       ? "Slots" :
          k == K"LambdaBindings" ? "LambdaBindings" :
          k == K"latestworld" ? "latestworld" :
          k == K"static_parameter" ? "static_parameter" :
          k == K"symboliclabel" ? "label:$(syntax_name(ex))" :
          k == K"symbolicgoto" ? "goto:$(syntax_name(ex))" :
          k == K"oldsymbolicgoto" ? "goto:$(syntax_name(ex))" :
          k == K"SourceLocation" ?
              "SourceLocation:$(JuliaSyntax.filename(ex)):$(join(source_location(ex), ':'))" :
              k == K"Value" ?
              (ex.value isa SourceRef ?
              "SourceRef:$(JuliaSyntax.filename(ex)):$(join(source_location(ex), ':'))" :
              ex.value isa SyntaxContext ? "SyntaxContext(#=omitted=#)" : repr(ex.value)) :
              ex.value !== nothing ? repr(ex.value) : "::K\"$(untokenize(k))\""

    if kind(ex) in KSet"BindingId slot SSAValue static_parameter label"
        idstr = subscript_str(syntax_id(ex))
        str = "$(str)$idstr"
    end
    if k == K"slot" || k == K"BindingId"
        for p in provenance(ex)
            if kind(p) == K"Identifier"
                str = "$(str)/$(syntax_name(p))"
                break
            end
        end
    end
    return str
end

# Within JL, K"Placeholder" is used for never-read identifiers, but this magic
# symbol is used in the IR (its write-only properties are enforced in codegen).
const UNUSED = "#unused#"

function _show_syntax_tree(io, ex, indent, show_kinds, @nospecialize(parent_sc))
    nodestr = kind(ex) === K"unknown_head" ? ("unknown_head:"*syntax_name(ex)) :
        !is_leaf(ex) ? "[$(untokenize(head(ex)))]" : _value_string(ex)

    treestr = rpad(string(indent, nodestr), 40)
    if show_kinds && is_leaf(ex)
        treestr = treestr*" :: "*string(kind(ex))
    end

    std_attrs = Set([:value,:kind,:syntax_flags,:source,:context])
    attrstr = join([attrsummary(n, getproperty(ex, n))
                    for n in fieldnames(typeof(ex)) if n ∉ std_attrs &&
                        getproperty(ex, n) !== nothing], ",")
    print(io, rpad(treestr, 60))
    print(io, " | ")
    sc = ex.context
    if sc isa SyntaxContext && sc !== parent_sc
        print(io, sc)
        print(io, ",")
    end
    print(io, attrstr)
    println(io)

    if !is_leaf(ex)
        new_indent = indent*"  "
        for n in children(ex)
            _show_syntax_tree(io, n, new_indent, show_kinds, sc)
        end
    end
end

function Base.show(io::IO, ::MIME"text/plain", ex::SyntaxTree, show_kinds=true)
    assert_syntaxtree(ex)
    _show_syntax_tree(io, ex, "", show_kinds, nothing)
end
function _show_syntax_tree_sexpr(io, ex)
    if is_leaf(ex)
        if JuliaSyntax.is_error(ex)
            print(io, "(", untokenize(head(ex)), ")")
        else
            print(io, _value_string(ex))
        end
    else
        print(io, "(", untokenize(head(ex)))
        for n in children(ex)
            print(io, ' ')
            _show_syntax_tree_sexpr(io, n)
        end
        print(io, ')')
    end
end

function Base.show(io::IO, ::MIME"text/x.sexpression", node::SyntaxTree)
    assert_syntaxtree(node)
    _show_syntax_tree_sexpr(io, node)
end

function Base.show(io::IO, node::SyntaxTree)
    assert_syntaxtree(node)
    _show_syntax_tree_sexpr(io, node)
end

#-------------------------------------------------------------------------------
# Error handling

TODO(msg::AbstractString) = throw(ErrorException("Lowering TODO: $msg"))
TODO(ex::SyntaxTree, msg="") = throw(LoweringError(ex, "Lowering TODO: $msg"))

"""
An error with detailed printing containing one or more SyntaxTrees and one
message per tree.  If `!internal`, caused by bad user code in `syntax` (flisp:
`Expr(:error, msg)`).
"""
struct LoweringError <: Exception
    sts::SyntaxList
    msgs::Vector{String}
    internal::Bool
end

@noinline LoweringError(ex::SyntaxTree, msg::String) =
    LoweringError(SyntaxList(ex), String[msg], false)

function Base.showerror(io::IO, exc::LoweringError; show_detail=true)
    println(io, exc.internal ? "internal lowering bug:" : "LoweringError:")
    for i in eachindex(exc.sts)
        st = exc.sts[i]
        msg = exc.msgs[i]
        src = sourceref(st)
        highlight(io, src; note=msg)
        if exc.internal || src isa LineNumberNode
            print(io, "\nExpression:\n  ")
            show(io, MIME"text/x.sexpression"(), st)
            # TODO: no parents available here; need to place them in LoweringError
            parents = SyntaxList()
            isempty(parents) || print(io, "\nContaining expressions:")
            for p in parents
                print(io, "\n  ")
                show(io, MIME"text/x.sexpression"(), p)
            end
        end
        i !== lastindex(exc.sts) && print(io, "\n\n")
    end

    if (show_detail || exc.internal) && !isempty(exc.sts)
        print(io, "\n\nDetailed provenance:\n  ")
        _show_provtree(io, exc.sts[1], "  ")
    end
end

function _show_provtree(io::IO, ex::SyntaxTree, indent)
    print(io, ex)
    if ex.source !== nothing
        printstyled(io, " @$(ex.jl_source)", color=:light_black)
    end
    prov = provenance(ex)

    print(io, "\n")

    src = ex.source
    msrc = JuliaSyntax.macro_prov(ex)
    printstyled(io, string(
        indent, msrc === nothing ? "└─ " : "├─ "); color=:light_black)
    if src isa SyntaxTree
        _show_provtree(io, src, string(indent, msrc === nothing ? "   " : "│  "))
    else
        @jl_assert ex.source isa Union{LineNumberNode, SourceRef} ex
        src = sourceref(ex)
        fn = filename(src)
        line, _ = source_location(src)
        printstyled(io, "@ $fn:$line\n", color=:light_black)
    end
    if msrc isa SyntaxTree
        printstyled(io, string(indent, "└─ "); color=:light_black)
        _show_provtree(io, msrc, indent*"   ")
    end
end

function showprov(io::IO, exs::AbstractVector;
                  note=nothing, include_location::Bool=true, highlight_kwargs...)
    for (i,ex) in enumerate(Iterators.reverse(exs))
        sr = sourceref(ex)
        if i > 1
            print(io, "\n\n")
        end
        k = kind(ex)
        ex_note = !isnothing(note) ? note :
            i > 1 && k == K"macrocall"  ? "in macro expansion" :
            i > 1 && k == K"$"          ? "interpolated here"  :
            "in source"
        highlight(io, sr; note=ex_note, highlight_kwargs...)

        if include_location
            line, _ = source_location(sr)
            locstr = "$(filename(sr)):$line"
            JuliaSyntax._printstyled(io, "\n# @ $locstr", fgcolor=:light_black)
        end
    end
end

function showprov(io::IO, ex::SyntaxTree; showprov_kwargs...)
    showprov(io, flattened_provenance(ex); showprov_kwargs...)
end

function subscript_str(i)
     replace(string(i),
             "0"=>"₀", "1"=>"₁", "2"=>"₂", "3"=>"₃", "4"=>"₄",
             "5"=>"₅", "6"=>"₆", "7"=>"₇", "8"=>"₈", "9"=>"₉")
end

function _deref_ssa(stmts, ex)
    while kind(ex) == K"SSAValue"
        ex = stmts[syntax_id(ex)]
    end
    ex
end

function _is_define_method_call(e)
    kind(e) == K"call" && numchildren(e) >= 1 &&
        kind(e[1]) == K"core" && syntax_name(e[1]) == "define_method"
end

function _find_method_lambda(ex0, name)
    ex = kind(ex0) === K"thunk" ? ex0[1] : ex0
    @jl_assert kind(ex) == K"code_info" ex
    # Heuristic search through outer thunk for the method in question.
    stmts = children(ex[2])
    for e in stmts
        if _is_define_method_call(e) && numchildren(e) == 5
            # define_method(module, fname, sig, lam)
            sig = _deref_ssa(stmts, e[4])
            @jl_assert kind(sig) == K"call" ex
            arg_types = _deref_ssa(stmts, sig[2])
            @jl_assert kind(arg_types) == K"call" ex
            self_type = _deref_ssa(stmts, arg_types[2])
            if kind(self_type) == K"globalref" && occursin(name, syntax_name(self_type))
                return e[5]
            end
        end
    end
end

function print_ir(io::IO, ex, method_filter=nothing)
    @jl_assert kind(ex) == K"code_info" || kind(ex) == K"thunk" ex
    if !isnothing(method_filter)
        filtered = _find_method_lambda(ex, method_filter)
        if isnothing(filtered)
            @warn "Method not found with method filter $method_filter"
        else
            ex = filtered
        end
    end
    _print_ir(io, ex, "")
end

# TODO: JuliaLowering-the-module should always print the same way, ignoring parent modules
function _print_ir(io::IO, ex0, indent)
    added_indent = "    "
    (ex, is_toplevel_thunk) = kind(ex0) === K"thunk" ? (ex0[1],true) : (ex0,false)
    @jl_assert ((kind(ex) == K"lambda" || kind(ex) == K"code_info")
                && kind(ex[2]) == K"block") ex
    if !is_toplevel_thunk && kind(ex) == K"code_info"
        slots = ex[1].value
        print(io, indent, "slots: [")
        for (i,slot) in enumerate(slots)
            print(io, "slot$(subscript_str(i))/$(slot.name)")
            flags = String[]
            slot.is_nospecialize   && push!(flags, "nospecialize")
            !slot.is_read          && push!(flags, "!read")
            slot.is_single_assign  && push!(flags, "single_assign")
            slot.is_maybe_undef    && push!(flags, "maybe_undef")
            slot.is_called         && push!(flags, "called")
            if !isempty(flags)
                print(io, "($(join(flags, ",")))")
            end
            if i < length(slots)
                print(io, " ")
            end
        end
        println(io, "]")
    end
    stmts = children(ex[2])
    for (i, e) in enumerate(stmts)
        lno = rpad(i, 3)
        if _is_define_method_call(e) && numchildren(e) == 5
            # define_method(module, fname, sig, lam)
            print(io, indent, lno, " (call core.define_method ",
                  string(e[2]), " ", string(e[3]), " ", string(e[4]))
            if kind(e[5]) == K"lambda" || kind(e[5]) == K"code_info"
                println(io)
                print(io, indent, "    --- code_info")
                println(io)
                _print_ir(io, e[5], indent*added_indent)
            else
                println(io, " ", string(e[5]), ")")
            end
        elseif kind(e) == K"opaque_closure_method"
            @jl_assert numchildren(e) == 5 e
            print(io, indent, lno, " --- opaque_closure_method ")
            for i=1:4
                print(io, " ", e[i])
            end
            println(io)
            _print_ir(io, e[5], indent*added_indent)
        elseif kind(e) == K"code_info"
            println(io, indent, lno, " --- ", "code_info")
            _print_ir(io, e, indent*added_indent)
        else
            code = string(e)
            println(io, indent, lno, " ", code)
        end
    end
end

# Wrap a function body in Base.Compiler.@zone for profiling
if isdefined(Base.Compiler, Symbol("@zone")) && DEBUG
    macro fzone(str, f)
        @assert(f isa Expr && f.head === :function && length(f.args) === 2 && str isa String,
                "usage: @fzone name_string <function expression>")
        esc(Expr(:function, f.args[1],
                 # Use source of our caller, not of this macro.
                 Expr(:macrocall, :(Base.Compiler.var"@zone"), __source__, str, f.args[2])))
    end
else
    macro fzone(str, f)
        esc(f)
    end
end

function _flatten_blocks(st::SyntaxTree)
    if kind(st) === K"block"
        out = SyntaxList()
        for c in children(st)
            append!(out, _flatten_blocks(c))
        end
        # special case: an empty final block has value nothing
        if (length(children(st)) > 0 && kind(st[end]) === K"block" &&
            numchildren(st[end]) == 0)
            push!(out, @ast _ st[end] (::K"nothing"))
        end
        return out
    elseif is_quoted(st)
        SyntaxList(st)
    else
        SyntaxList(mapchildren(flatten_blocks, st))
    end
end

# Splat the contents of any block in `st` whose parent is also a block
function flatten_blocks(st::SyntaxTree)
    if kind(st) === K"block"
        @mknode(st; children=_flatten_blocks(st))
    elseif is_quoted(st)
        st
    else
        mapchildren(flatten_blocks, st)
    end
end

# Hack.  Used for assignment to variables with `decl`, since the type may change
# between assignments.  flisp: renumber-assigned-ssavalues
function renumber_assigned_ssavalues(ctx, st)
    ssamap = Dict{IdTag, IdTag}()
    _find_assigned_ssavars!(ctx, ssamap, st)
    isempty(ssamap) && return st
    _replace_binding_ids(ctx, ssamap, st)
end
function _find_assigned_ssavars!(ctx, ssamap, st)
    (is_leaf(st) || is_quoted(st)) && return
    if kind(st) == K"=" && kind(st[1]) == K"BindingId"
        b = get_binding(ctx, st[1])
        b.is_ssa || return
        ssamap[b.id] = syntax_id(ssavar(ctx, st[1], b.name))
    end
    foreach(e->_find_assigned_ssavars!(ctx, ssamap, e), children(st))
end
function _replace_binding_ids(ctx, ssamap, st)
    if kind(st) == K"BindingId"
        id = get(ssamap, syntax_id(st), nothing)
        isnothing(id) ? st : newleaf(st, K"BindingId", id)
    elseif is_leaf(st) || is_quoted(st)
        st
    else
        mapchildren(e->_replace_binding_ids(ctx, ssamap, e), st)
    end
end
