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

"""
    AbstractTag

Marker supertype for named tag heads used by QuantumSavory protocols.

`AbstractTag` describes the type stored at the head of a typed [`Tag`](@ref),
such as `EntanglementCounterpart` in
`Tag(EntanglementCounterpart, remote_node, remote_slot, pair_id)`. It does not
replace the `Tag` sum type itself. Generic `Tag(::DataType, ...)` construction
and querying remain available for types that do not subtype `AbstractTag`.

Custom tag heads supplied through protocol fields declared as
`Type{<:AbstractTag}` must be concrete subtypes of this marker:

```julia
struct MyTag <: AbstractTag end
```
"""
abstract type AbstractTag end

"""
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
