nsubsystems(s::StateRef) = length(s.registers) # nsubsystems(s.state[]) TODO this had to change because of references to "padded" states, but we probably still want to track more detailed information (e.g. how much have we overpadded)
nsubsystems_padded(s::StateRef) = nsubsystems(s.state[])
nsubsystems(r::Register) = length(r.staterefs)
nsubsystems(r::RegRef) = 1

function swap!(reg1::Register, reg2::Register, i1::Int, i2::Int; time=nothing)
    if reg1===reg2 && i1==i2
        return
    end
    if reg1.accesstimes[i1] != reg2.accesstimes[i2]
        maxtime = max(reg1.accesstimes[i1], reg2.accesstimes[i2])
        maxtime = isnothing(time) ? maxtime : max(maxtime, time)
        uptotime!(reg1[i1], maxtime)
        uptotime!(reg2[i2], maxtime)
    end
    state1, state2 = reg1.staterefs[i1], reg2.staterefs[i2]
    stateind1, stateind2 = reg1.stateindices[i1], reg2.stateindices[i2]
    reg1.staterefs[i1], reg2.staterefs[i2] = state2, state1
    reg1.stateindices[i1], reg2.stateindices[i2] = stateind2, stateind1
    if !isnothing(state1)
        state1.registers[stateind1] = reg2
        state1.registerindices[stateind1] = i2
    end
    if !isnothing(state2)
        state2.registers[stateind2] = reg1
        state2.registerindices[stateind2] = i1
    end
end
swap!(r1::RegRef, r2::RegRef; time=nothing) = swap!(r1.reg, r2.reg, r1.idx, r2.idx; time)

#subsystemcompose(s...) = reduce(subsystemcompose, s)


# TODO use a trait system to select the type of composition
# - do they need to be collapsed
# - do they have unused slots that can be refilled
# - are they just naively composed together
"""Deduplicate state references by identity and compute the subsystem-index offset
each reference would have inside a composition of all of them."""
function unique_staterefs_with_offsets(staterefs)
    unique_refs = StateRef[]
    for ref in unique(objectid, staterefs)
        isnothing(ref) && throw(ArgumentError("Cannot compose unassigned register slots."))
        push!(unique_refs, ref)
    end
    offsets = IdDict{StateRef,Int}()
    offset = 0
    for ref in unique_refs
        offsets[ref] = offset
        offset += nsubsystems_padded(ref)
    end
    return unique_refs, offsets
end

"""Ensure that the all slots of the given registers are represented by one single state object, i.e. that all the register slots are tracked in the same Hilbert space."""
function subsystemcompose(regs::Base.AbstractVecOrTuple{Register}, indices) # TODO add a type constraint on regs
    # Get all references to states that matter, removing duplicates
    staterefs, offsets = unique_staterefs_with_offsets(
        [r.staterefs[i] for (r,i) in zip(regs,indices)]
    )
    # Prepare the larger state object
    newstate = subsystemcompose([s.state[] for s in staterefs]...)
    # Prepare the new state reference
    newregisters = vcat([s.registers for s in staterefs]...)
    newregisterindices = vcat([s.registerindices for s in staterefs]...)
    newref = StateRef(newstate, newregisters, newregisterindices)
    # Update all registers to point to the new state reference
    for (r,i) in zip(newregisters,newregisterindices)
        isnothing(r) && continue
        oldref = r.staterefs[i]
        r.staterefs[i] = newref
        offset = offsets[oldref]
        r.stateindices[i] += offset
    end
    newref
end
subsystemcompose(refs::RegRef...) = subsystemcompose([r.reg for r in refs], [r.idx for r in refs]) # TODO temporary arrays are allocated here
