const StoreType{T} = Union{Tuple{Vararg{T}}, AbstractVector{T}}

@noinline function _throw_unequal_lengths(nk::Int, nv::Int)
    throw(ArgumentError("Number of keys ($nk) differs from number of values ($nv)."))
end

"""
    LittleDict(keys, vals) <: AbstractDict

An ordered dictionary type for small numbers of keys.
Rather than using `hash` or some other sophisticated measure
to store the vals in a clever arrangement,
it just keeps everything in a pair of lists.

While theoretically this has expected time complexity _O(n)_
(vs the hash-based [`OrderedDict`](@ref)/`Dict`'s expected time complexity _O(1)_,
and the search-tree-based `SortedDict`'s expected time complexity _O(log(n))_),
in practice it is really fast, because it is cache & SIMD friendly.

It is reasonable to expect it to outperform an `OrderedDict`,
with up to around 30 elements in general;
or with up to around 50 elements if using a `LittleDict` backed by `Tuples`
(see [`freeze`](@ref))
However, this depends on exactly how long `isequal` and `hash` take,
as well as on how many hash collisions occur etc.

!!! note
    When constructing a `LittleDict` it is faster to pass in the keys and
    values each as seperate lists. So if you have them seperately already,
    do `LittleDict(ks, vs)` not `LittleDict(zip(ks, vs))`.
    Furthermore, key and value lists that are passed as `Tuple`s will not require any
    copies to create the `LittleDict`, so `LittleDict(ks::Tuple, vs::Tuple)`
    is the fastest constructor of all.
"""
struct LittleDict{K, V, KS<:StoreType{K}, VS<:StoreType{V}} <: AbstractDict{K, V}
    keys::KS
    vals::VS

    function LittleDict{K, V, KS, VS}(keys, vals) where {K, V, KS, VS}
        nk = length(keys)
        nv = length(vals)
        nk == nv || _throw_unequal_lengths(Int(nk), Int(nv))
        return new{K, V, KS, VS}(keys, vals)
    end
    LittleDict{K, V, <:Tuple, <:Tuple}() where {K, V} = new{K, V, Tuple{}, Tuple{}}((), ())
    LittleDict{K, V, KS, VS}() where {K, V, KS, VS} = LittleDict{K, V, KS, VS}(KS(), VS())
end
LittleDict{K, V, KS, VS}(d::AbstractDict) where {K, V, KS<:StoreType{K}, VS<:StoreType{V}} = LittleDict{K, V, KS, VS}(collect(keys(d)), collect(values(d)))

function LittleDict{K,V}(ks::KS, vs::VS) where {K,V, KS<:StoreType,VS<:StoreType}
    return LittleDict{K, V, KS, VS}(ks, vs)
end

function LittleDict(ks::KS, vs::VS) where {KS<:StoreType,VS<:StoreType}
    return LittleDict{eltype(KS), eltype(VS)}(ks, vs)
end

# Other iterators should be copied to a Vector
LittleDict(ks, vs) = LittleDict(collect(ks), collect(vs))

function LittleDict{K, V}(itr) where {K,V}
    ks = K[]
    vs = V[]
    for val in itr
        if !(val isa Union{Tuple{Any, Any}, Pair})
            throw(ArgumentError(
                "LittleDict(kv): kv needs to be an iterator of tuples or pairs")
            )
        end
        k, v = val
        push!(ks, k)
        push!(vs, v)
    end
    return LittleDict(ks, vs)
end

LittleDict{K,V}(itr...) where {K,V} = LittleDict{K,V}(itr)
LittleDict(itr...) = LittleDict(itr)
LittleDict(itr::T) where T = LittleDict{kvtype(eltype(T))...}(itr)

# Avoid contention between the core constructor, and the list of elements
LittleDict(itr1::Pair, itr2::Pair) = LittleDict(first.([itr1, itr2]), last.([itr1,itr2]))
LittleDict(itr1::Pair) = LittleDict([first(itr1)], [last(itr1)])
LittleDict{K, V}(itr::Pair) where {K, V} = LittleDict{K,V}(K[first(itr)], V[last(itr)])

kvtype(::Any) = (Any, Any)
kvtype(::Type{Union{}}) = (Any,Any)

kvtype(::Type{Pair{K,V}}) where {K,V} = (K,V)
kvtype(::Type{Pair{<:Any,V}}) where {V} = (Any,V)
kvtype(::Type{Pair{K,<:Any}}) where {K} = (K,Any)

kvtype(::Type{Tuple{K,V}}) where {K,V} = (K,V)

"""
    freeze(dd::AbstractDict)

Render a dictionary immutable by converting it to a `Tuple`-backed
`LittleDict`.
The `Tuple`-backed `LittleDict` is faster than the `Vector`-backed `LittleDict`,
particularly when the keys are all concretely typed.
"""
function freeze(dd::AbstractDict)
    ks = Tuple(keys(dd))
    vs = Tuple(values(dd))
    return LittleDict(ks, vs)
end

isordered(::Type{<:LittleDict}) = true

# For now these are internal UnionAlls for dispatch purposes
const UnfrozenLittleDict{K, V} = LittleDict{K, V, <:AbstractVector{K}, <:AbstractVector{V}}
const FrozenLittleDict{K, V} = LittleDict{K, V, <:Tuple, <:Tuple}

##### Methods that all AbstractDicts should implement

Base.length(dd::LittleDict) = length(dd.keys)

function Base.getkey(dd::LittleDict, key, default)
    if key ∈ dd.keys
        return key
    else
        return default
    end
end

function Base.map!(f, iter::Base.ValueIterator{<:LittleDict})
    dict = iter.dict
    vals = dict.vals
    for i in 1:length(vals)
        @inbounds vals[i] = f(vals[i])
    end
    return iter
end

function Base.get(dd::LittleDict, key, default)
    @assert length(dd.keys) == length(dd.vals)
    for ii in 1:length(dd.keys)
        cand = @inbounds dd.keys[ii]
        isequal(cand, key) && return @inbounds(dd.vals[ii])
    end
    return default
end
function get(default::Base.Callable, dd::LittleDict, key)
    got = get(dd, key, NotFoundSentinel())
    if got isa NotFoundSentinel  # not found
        return default()
    else
        return got
    end
end

function Base.iterate(dd::LittleDict, ii=1)
    ii > length(dd.keys) && return nothing
    return (dd.keys[ii] => dd.vals[ii], ii+1)
end

# lazy reverse iteration
function Base.iterate(rdd::Iterators.Reverse{<:LittleDict}, ii=length(rdd.itr.keys))
    dd = rdd.itr
    ii < 1 && return nothing
    return (dd.keys[ii] => dd.vals[ii], ii-1)
end

function merge(d1::LittleDict, others::AbstractDict...)
    return merge((x,y)->y, d1, others...)
end

function mergewith(
    combine,
    d::LittleDict,
    others::AbstractDict...
    )
    K,V = _merge_kvtypes(d, others...)
    dc = LittleDict{K,V}(d)
    for d2 in others
        for (k2,v2) in d2
            got = get(dc, k2, NotFoundSentinel())
            if got isa NotFoundSentinel
                add_new!(dc, k2, v2)
            else
                # GOLDPLATE: ideally we would avoid iterating this twice
                # once for get and once for setindex!
                dc[k2]=combine(got, v2)
            end
        end
    end
    return dc
end

merge(combine::Function, d::LittleDict, others::AbstractDict...) = mergewith(combine, d, others...)

function Base.empty(dd::LittleDict{K,V}) where {K,V}
    LittleDict{K, V}(empty(getfield(dd, :keys)), empty(getfield(dd, :vals)))
end

######## Methods that all mutable AbstractDict's should implement

function Base.sizehint!(dd::UnfrozenLittleDict, sz::Integer)
    sizehint!(dd.keys, sz)
    sizehint!(dd.vals,sz)
    return dd
end

function add_new!(dd::UnfrozenLittleDict{K, V}, key, value) where {K, V}
    kk = convert(K, key)
    vv = convert(V, value)

    # if we can convert it to the right type, and the dict is unfrozen
    # then neither push can fail, so the dict length will remain in sync
    push!(dd.keys, kk)
    push!(dd.vals, vv)

    return dd
end

function Base.setindex!(dd::LittleDict{K,V, <:Any, <:Vector}, value, key) where {K,V}
    # Note we only care if the Value store is mutable (<:Vector)
    # As we can have immutable keys, if we are setting the value of an existing key

    # Assertion below commented out as by standards of carefully optimised
    # setindex! it has huge code (26%), this does mean that if someone has messed
    # with the fields of the LittleDict directly, then the @inbounds could be invalid
    #@assert length(dd.keys) == length(dd.vals)

    kk = convert(K, key)
    vv = convert(V, value)
    for ii in 1:length(dd.keys)
        cand = @inbounds dd.keys[ii]
        if isequal(cand, kk)
            @inbounds(dd.vals[ii] = vv)
            return dd
        end
    end
    add_new!(dd, key, value)
    return dd
end

function Base.pop!(dd::UnfrozenLittleDict)
    pop!(dd.keys)
    return pop!(dd.vals)
end

function Base.pop!(dd::UnfrozenLittleDict, key)
    @assert length(dd.keys) == length(dd.vals)

    for ii in 1:length(dd.keys)
        cand = @inbounds dd.keys[ii]
        if isequal(cand, key)
            deleteat!(dd.keys, ii)
            val = @inbounds dd.vals[ii]
            deleteat!(dd.vals, ii)
            return val
        end
    end
end

function Base.delete!(dd::UnfrozenLittleDict, key)
    pop!(dd, key)
    return dd
end

Base.empty!(dd::UnfrozenLittleDict) = (empty!(dd.keys); empty!(dd.vals); dd)

function get!(default::Base.Callable, dd::UnfrozenLittleDict, key)
    got = get(dd, key, NotFoundSentinel())
    if got isa NotFoundSentinel  # not found
        val = default()
        add_new!(dd, key, val)
        return val
    else
        return got
    end
end
get!(dd::UnfrozenLittleDict, key, default) = get!(()->default, dd, key)
