"""
Metadata about a binding
"""
mutable struct BindingInfo
    const id::IdTag                 # Unique integer identifying this binding
    const name::String
    const kind::Symbol              # :local :global :argument :static_parameter
    const node_id::SyntaxTree
    const mod::Union{Nothing,Module} # Set when `kind === :global`
    type::Union{Nothing,SyntaxTree}      # Type, for bindings declared like x::T = 10
    lambda_id::Int            # from scope resolution; 0 if unresolved
    is_const::Bool            # Constant, cannot be reassigned
    is_ssa::Bool              # Single assignment, defined before use
    is_internal::Bool         # True for internal bindings generated by the compiler
    is_ambiguous_local::Bool  # Local, but would be global in soft scope (ie, the REPL)
    unboxed::Bool # determined by binding_analysis.jl; flisp generally mutates `capt` instead

    # flisp: vinfo
    is_nospecialize::Bool # @nospecialize on this argument (only valid for kind == :argument)
    is_read::Bool
    is_called::Bool
    is_assigned::Bool
    is_assigned_once::Bool
    is_captured::Bool
    is_always_defined::Bool
    is_used_undef::Bool
end

"""
Metadata about "entities" (variables, constants, etc) in the program. Each
entity is associated to a unique integer id, the BindingId. A binding will be
inferred for each *name* in the user's source program by symbolic analysis of
the source.

However, bindings can also be introduced programmatically during lowering or
macro expansion: the primary key for bindings is the `BindingId` integer, not
a name.
"""
struct Bindings
    info::Vector{BindingInfo}
end

function BindingInfo(bindings::Bindings,
                     name::AbstractString, kind::Symbol, node_id::SyntaxTree;
                     mod::Union{Nothing,Module} = nothing,
                     type::Union{Nothing,SyntaxTree} = nothing,
                     lambda_id::Int = 0,
                     is_const::Bool = false,
                     is_ssa::Bool = false,
                     is_internal::Bool = false,
                     is_ambiguous_local::Bool = false,
                     unboxed::Bool = false,
                     is_nospecialize::Bool = false,
                     is_read::Bool = false,
                     is_called::Bool = false,
                     is_assigned::Bool = false,
                     is_assigned_once::Bool = false,
                     is_captured::Bool = false,
                     is_always_defined::Bool = is_ssa || kind === :argument,
                     is_used_undef::Bool = false)
    bid = next_binding_id(bindings)
    b = BindingInfo(
        bid, name, kind, node_id, mod, type, lambda_id, is_const, is_ssa,
        is_internal, is_ambiguous_local, unboxed, is_nospecialize,
        is_read, is_called, is_assigned, is_assigned_once, is_captured,
        is_always_defined, is_used_undef)
    add_binding(bindings, b)
    b
end

function Base.show(io::IO, binfo::BindingInfo)
    print(io, "BindingInfo(", binfo.id,
          ", ", repr(binfo.name),
          ", ", repr(binfo.kind),
          ", ", binfo.node_id)
    !isnothing(binfo.mod)    && print(io, ", mod=", binfo.mod)
    !isnothing(binfo.type)   && print(io, ", type=", binfo.type)
    print(io, ", lambda_id=", binfo.lambda_id)
    binfo.is_const           && print(io, ", is_const=true")
    binfo.is_ssa             && print(io, ", is_ssa=true")
    binfo.is_internal        && print(io, ", is_internal=true")
    binfo.is_ambiguous_local && print(io, ", is_ambiguous_local=true")
    binfo.unboxed            && print(io, ", unboxed=true")
    binfo.is_nospecialize    && print(io, ", is_nospecialize=true")
    binfo.is_read            && print(io, ", is_read=true")
    binfo.is_called          && print(io, ", is_called=true")
    binfo.is_assigned        && print(io, ", is_assigned=true")
    binfo.is_assigned_once   && print(io, ", is_assigned_once=true")
    binfo.is_captured        && print(io, ", is_captured=true")
    binfo.is_always_defined  && print(io, ", is_always_defined=true")
    binfo.is_used_undef      && print(io, ", is_used_undef=true")
    print(io, ")")
end

Bindings() = Bindings(Vector{BindingInfo}())

next_binding_id(bindings::Bindings) = length(bindings.info) + 1

function add_binding(bindings::Bindings, binding)
    if next_binding_id(bindings) != binding.id
        error("Use next_binding_id() to create a valid binding id")
    end
    push!(bindings.info, binding)
end

function syntax_id(ex::SyntaxTree)
    @jl_assert kind(ex) in KSet"BindingId SSAValue slot static_parameter label" ex
    ex.value::IdTag
end

function get_binding(bindings::Bindings, x)::BindingInfo
    id = x isa SyntaxTree ? syntax_id(x) : x
    bindings.info[id]
end

function get_binding(ctx::AbstractLoweringContext, x)::BindingInfo
    get_binding(ctx.bindings::Bindings, x)
end

function _new_binding(bindings::Bindings, srcref::SyntaxTree,
                      name::AbstractString, kind::Symbol; kws...)
    # A binding is only useful when it shows up in the tree, so create its tree
    # node eagerly and share it among uses (see `binding_ex`)
    bid = next_binding_id(bindings)
    ex = @ast _ srcref bid::K"BindingId"
    b = BindingInfo(bindings, name, kind, ex; kws...)
    return b
end

# Create a new SSA binding
function ssavar(ctx::AbstractLoweringContext, srcref, name="tmp")
    binding_ex(ctx, _new_binding(ctx.bindings, srcref, name, :local;
                                 is_ssa=true, is_internal=true))
end

# Create a new local mutable binding or lambda argument
function new_local_binding(ctx::AbstractLoweringContext, srcref, name;
                           kind=:local, kws...)
    @jl_assert kind === :local || kind === :argument srcref
    nameref = newleaf(srcref, K"Identifier", name)
    b = _new_binding(ctx.bindings, nameref, name, kind; is_internal=true, kws...)
    lbindings = current_lambda_bindings(ctx)
    if !isnothing(lbindings)
        init_lambda_binding(lbindings, b, false)
    end
    binding_ex(ctx, b)
end

function new_global_binding(ctx::AbstractLoweringContext, srcref, name, mod; kws...)
    nameref = newleaf(srcref, K"Identifier", name)
    binding_ex(ctx, _new_binding(
        ctx.bindings, nameref, name, :global; is_internal=true, mod=mod, kws...))
end

function binding_ex(ctx::AbstractLoweringContext, b::BindingInfo)
    b.node_id
end
binding_ex(ctx, id::IdTag) = binding_ex(ctx, get_binding(ctx, id))
binding_type_ex(ctx::AbstractLoweringContext, b::BindingInfo) =
    b.type

# One lambda's variables
struct LambdaBindings
    # Binding ID of #self#
    self::IdTag
    # For finding the parent lambda in variable analysis
    scope_id::ScopeId
    # A map from every referenced local binding ID to whether the local is
    # captured (true) or native to this lambda (false).  References in inner
    # lambdas count: `inner.locals_capt[id]` implies `haskey(locals_capt, id)`
    # TODO: If we use scope ID as a lambda ID and give BindingInfo a field
    # noting which lambda it belongs to, we could just make this a BitSet of
    # vars present, where we tell if a binding is captured by comparing
    # this.scope_id with the BindingInfo's scope_id.
    locals_capt::Dict{IdTag,Bool}
end

LambdaBindings(self::IdTag = 0, scope_id::ScopeId = 0) =
    LambdaBindings(self, scope_id, Dict{IdTag,Bool}())

function init_lambda_binding(bindings::LambdaBindings, b::BindingInfo, capt::Bool)
    bindings.locals_capt[b.id] = capt
    b.lambda_id = bindings.scope_id
end

function lambda_bindings(st::SyntaxTree)
    @jl_assert kind(st) === K"LambdaBindings" st
    st.value::LambdaBindings
end
