const TagElementTypes = Union{Symbol, Int, DataType}
const EMPTY_TAG_ID_VECTOR = Int128[]

"""
Tags are used to represent classical metadata describing the state (or even history) of nodes and their registers. The library allows the construction of custom tags using the `Tag` constructor. Currently tags are implemented as instances of a [sum type](https://github.com/MasonProtter/SumTypes.jl) and have fairly constrained structure. Most of them are constrained to contain only Symbol instances and integers.

Here is an example of such a generic tag:

```jldoctest
julia> Tag(:sometagdescriptor, 1, 2, -3)
SymbolIntIntInt(:sometagdescriptor, 1, 2, -3)::Tag
```

A tag can have a custom `DataType` as first argument, in which case additional customizability in printing is available. E.g. consider the [`EntanglementHistory`] tag used to track how pairs were entangled before a swap happened.

```jldoctest
julia> using QuantumSavory.ProtocolZoo: EntanglementHistory

julia> Tag(EntanglementHistory, 1, 2, 3, 4, 5)
Was entangled to 1.2 with chunk id 0, but swapped with .5 which was entangled to 3.4 with chunk id 0
```

See also: [`tag!`](@ref), [`query`](@ref)
"""
@sum_type Tag :hidden begin
    Symbol(::Symbol)
    SymbolInt(::Symbol, ::Int)
    SymbolIntInt(::Symbol, ::Int, ::Int)
    SymbolIntIntInt(::Symbol, ::Int, ::Int, ::Int)
    SymbolIntIntIntInt(::Symbol, ::Int, ::Int, ::Int, ::Int)
    SymbolIntIntIntIntInt(::Symbol, ::Int, ::Int, ::Int, ::Int, ::Int)
    SymbolIntIntIntIntIntInt(::Symbol, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int)
    SymbolFloat(::Symbol, ::Float64)
    SymbolFloatFloat(::Symbol, ::Float64, ::Float64)
    Type(::DataType)
    TypeInt(::DataType, ::Int)
    TypeIntInt(::DataType, ::Int, ::Int)
    TypeIntIntInt(::DataType, ::Int, ::Int, ::Int)
    TypeIntIntIntInt(::DataType, ::Int, ::Int, ::Int, ::Int)
    TypeIntIntIntIntInt(::DataType, ::Int, ::Int, ::Int, ::Int, ::Int)
    TypeIntIntIntIntIntInt(::DataType, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int)
    TypeIntIntIntIntIntIntInt(::DataType, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int)
    TypeIntIntIntIntIntIntIntInt(::DataType, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int, ::Int)
    TypeIntFloat(::DataType, ::Int, ::Float64)
    TypeIntIntFloat(::DataType, ::Int, ::Int, ::Float64)
    TypeIntIntIntFloat(::DataType, ::Int, ::Int, ::Int, ::Float64)
    TypeIntIntIntIntFloat(::DataType, ::Int, ::Int, ::Int, ::Int, ::Float64)
    TypeIntIntIntIntIntFloat(::DataType, ::Int, ::Int, ::Int, ::Int, ::Int, ::Float64)
    TypeSymbol(::DataType, ::Symbol)
    TypeSymbolInt(::DataType, ::Symbol, ::Int)
    TypeSymbolIntInt(::DataType, ::Symbol, ::Int, ::Int)
    Forward(::Tag, ::Int)
end

"""Tag types available in the taggging and tag-querying system.

See also: [`query`](@ref), [`tag!`](@ref), [`Wildcard`](@ref)"""
const tag_types = Tag'

Base.@propagate_inbounds Base.getindex(tag::Tag, i::Int) = SumTypes.unwrap(tag).data[i]
Base.length(tag::Tag) = length(SumTypes.unwrap(tag).data)
Base.iterate(tag::Tag, state=1) = state > length(tag) ? nothing : (SumTypes.unwrap(tag)[state],state+1)

function SumTypes.show_sumtype(io::IO, x::Tag)
    unwrapped = SumTypes.unwrap(x)
    payload = unwrapped.data
    sym = SumTypes.get_name(unwrapped)
    if isempty(payload)
        print(io, String(sym), "::Tag")
    else
        first_field = payload[1]
        if first_field isa DataType && first_field !== Int
            print(io, first_field(Base.tail(payload)...))
        else
            print(io, String(sym), '(', join((repr(field) for field ∈ payload), ", "), ")::Tag")
        end
    end
end

Base.convert(::Type{Tag}, x::Tag) = x
Base.convert(::Type{Tag}, x) = Tag(x)

@inline _indexable_tag_head(head) = head isa TagElementTypes ? head : nothing
@inline _query_index_head(head::TagElementTypes) = head
@inline _query_index_head(_) = nothing

# Generate allocation-free head extraction from a tag. The generic
# Tag accessors go through SumTypes.unwrap and allocate on this hot query path.
let
    cases = []
    for (tagsymbol, tagvariant) in pairs(tag_types)
        sig = methods(tagvariant)[1].sig.parameters[2:end]
        args = (:head, :b, :c, :d, :e, :f, :g, :h, :i)[1:length(sig)]
        if !isempty(sig) && sig[1] <: TagElementTypes
            push!(cases, :($tagsymbol($(args...)) => head))
        else
            push!(cases, :($tagsymbol($(args...)) => nothing))
        end
    end

    eval(quote
        @inline function _tag_index_head(tag::Tag)
            @cases tag begin
                $(cases...)
            end
        end
    end)
end

# Create a constructor for each tag variant
for (tagsymbol, tagvariant) in pairs(tag_types)
    sig = methods(tagvariant)[1].sig.parameters[2:end]
    args = (:a, :b, :c, :d, :e, :f, :g, :h, :i)[1:length(sig)]
    argssig = [:($a::$t) for (a,t) in zip(args, sig)]
    eval(quote function Tag($(argssig...))
        ($tagvariant)($(args...))
    end end)
end
