All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.collection.ScatterMap.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:Suppress(
    "RedundantVisibilityModifier",
    "KotlinRedundantDiagnosticSuppress",
    "KotlinConstantConditions",
    "PropertyName",
    "ConstPropertyName",
    "PrivatePropertyName",
    "NOTHING_TO_INLINE"
)

package androidx.collection

import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
import kotlin.math.max

// A "flat" hash map based on abseil's flat_hash_map
// (see https://abseil.io/docs/cpp/guides/container). Unlike its C++
// equivalent, this hash map doesn't (and cannot) store the keys and values
// directly inside a table. Instead the references and keys are stored in
// 2 separate tables. The implementation could be made "flatter" by storing
// both keys and values in the same array but this yields no improvement.
//
// The main design goal of this container is to provide a generic, cache-
// friendly, *allocation free* hash map, with performance on par with
// LinkedHashMap to act as a suitable replacement for the common
// mutableMapOf() in Kotlin.
//
// The implementation is very similar, and is based, as the name suggests,
// on a flat table of values. To understand the implementation, let's first
// define the terminology used throughout this file:
//
// - Slot
//       An entry in the backing table; in practice a slot is a pair of
//       (key, value) stored in two separate allocations.
// - Metadata
//       Indicates the state of a slot (available, etc. see below) but
//       can also store part of the slot's hash.
// - Group
//       Metadata for multiple slots that can be manipulated as a unit to
//       speed up processing.
//
// To quickly and efficiently find any given slot, the implementation uses
// groups to compare up to 8 entries at a time. To achieve this, we use
// open-addressing probing quadratic probing
// (https://en.wikipedia.org/wiki/Quadratic_probing). See "A note on
// probing" down below for more information.
//
// The table's memory layout is organized around 3 arrays:
//
// - metadata
//       An array of metadata bytes, encoded as a LongArray (see below).
//       The size of this array depends on capacity, but is smaller since
//       the array encodes 8 metadata per Long. There is also padding at
//       the end to permit branchless probing.
// - keys
//       Holds references to the key stored in the map. An index i in
//       this array maps to the corresponding values in the values array.
//       This array always has the same size as the capacity of the map.
// - values
//       Holds references to the key stored in the map. An index i in
//       this array maps to the corresponding values in the keys array
//       This array always has the same size as the capacity of the map.
//
// A key's hash code is separated into two distinct hashes:
//
// - H1: the hash code's 25 most significant bits
// - H2: the hash code's 7 least significant bits
//
// H1 is used as an index into the slots, and a starting point for a probe
// whenever we need to seek an entry in the table. H2 is used to quickly
// filter out slots when looking for a specific key in the table.
//
// While H1 is used to initiate a probing sequence, it is never stored in
// the table. H2 is however stored in the metadata of a slot. The metadata
// for any given slot is a single byte, which can have one of four states:
//
// - Empty: unused slot
// - Deleted: previously used slot
// - Full: used slot
// - Sentinel: marker to avoid branching, used to stop iterations
//
// They have the following bit patterns:
//
//      Empty: 1 0 0 0 0 0 0 0
//    Deleted: 1 1 1 1 1 1 1 0
//       Full: 0 h h h h h h h  // h represents the lower 7 hash bits
//   Sentinel: 1 1 1 1 1 1 1 1
//
// Insertions, reads, removals, and replacements all need to perform the
// same basic operation: finding a specific slot in the table. This `find`
// operation works like this:
//
// - Compute H1 from the key's hash code
// - Initialize a probe sequence from H1, which will potentially visit
//   every group in the map (but usually stops at the first one)
// - For each probe offset, select an entire group (8 entries) and find
//   candidate slots in that group. This means finding slots with a
//   matching H2 hash. We then iterate over the matching slots and compare
//   the slot's key to the find's key. If we have a final match, we know
//   the index of the key/value pair in the table. If there is no match
//   and the entire group is empty, the key does not exist in the table.
//
// Matching a Group with H2 ensures that one of the matching slots is
// likely to hold the same key as the one we are looking for. It also lets
// us quickly skip entire chunks of the map (for instance during iteration
// if a Group contains only empty slots, we can ignore it entirely).
//
// Since the metadata of a slot is made of a single byte, we could use
// a ByteArray instead of a LongArray. However, using a LongArray makes
// constructing a group cheaper and guarantees aligned reads. As a result
// we use a form of virtual addressing: when looking for a group starting
// at index 3 for instance, we do not fetch the 4th entry in the array of
// metadata, but instead find the Long that holds the 4th byte and create
// a Group of 8 bytes starting from that byte. The details are explained
// below in the group() function.
//
// ** A note on probing **
//
// A probe is a virtual construct used to iterate over the groups in the
// hash table in some interesting order. To probe the tables, we must
// initiate probing using the hash at which we want to start, using a
// suitable mask, in our case the table's capacity.
//
// The sequence is a triangular progression of the form:
//
// `p(i) = GroupWidth * (i ^ 2 + i) / 2 + hash (mod mask + 1)`
//
// The first few entries in the metadata table are mirrored at the end of
// the table so when we inspect those candidates we must make sure to not
// use their offset directly but instead the "wrap around" values, hence
// the `mask + 1` modulo.
//
// This probe sequence visits every group exactly once if the number of
// groups is a power of two, since `(i ^ 2 + i) / 2` is a bijection in
// `Z / (2 ^ m)`. See https://en.wikipedia.org/wiki/Quadratic_probing
//
// Reference:
//    Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step
//    2017, Matt Kulukundis, https://www.youtube.com/watch?v=ncHmEUmJZf4

// Indicates that all the slot in a [Group] are empty
// 0x8080808080808080UL, see explanation in [BitmaskMsb]
internal const val AllEmpty = -0x7f7f7f7f_7f7f7f80L

internal const val Empty = 0b10000000L
internal const val Deleted = 0b11111110L

// Used to mark the end of the actual storage, used to end iterations
@PublishedApi internal const val Sentinel: Long = 0b11111111L

// The number of entries depends on [GroupWidth]. Since our group width
// is fixed to 8 currently, we add 7 entries after the sentinel. To
// satisfy the case of a 0 capacity map, we also add another entry full
// of sentinels. Since our lookups always fetch 2 longs from the array,
// we make sure we have enough
@JvmField
internal val EmptyGroup =
    longArrayOf(
        // NOTE: the first byte in the array's logical order is in the LSB
        -0x7f7f7f7f_7f7f7f01L, // Sentinel, Empty, Empty... or 0xFF80808080808080UL
        -1L // 0xFFFFFFFFFFFFFFFFUL
    )

// Width of a group, in bytes. Since we can only use types as large as
// Long we must fit our metadata bytes in a 64-bit word or smaller, which
// means we can only store up to 8 slots in a group. Ideally we could use
// 128-bit data types to benefit from NEON/SSE instructions and manipulate
// groups of 16 slots at a time.
internal const val GroupWidth = 8

// A group is made of 8 metadata, or 64 bits
internal typealias Group = Long

// Number of metadata present both at the beginning and at the end of
// the metadata array so we can use a [GroupWidth] probing window from
// any index in the table.
internal const val ClonedMetadataCount = GroupWidth - 1

// Capacity to use as the first bump when capacity is initially 0
// We choose 6 so that the "unloaded" capacity maps to 7
internal const val DefaultScatterCapacity = 6

// Default empty map to avoid allocations
private val EmptyScatterMap = MutableScatterMap(0)

/** Returns an empty, read-only [ScatterMap]. */
@Suppress("UNCHECKED_CAST")
public fun  emptyScatterMap(): ScatterMap = EmptyScatterMap as ScatterMap

/** Returns a new [MutableScatterMap]. */
public fun  mutableScatterMapOf(): MutableScatterMap = MutableScatterMap()

/**
 * Returns a new [MutableScatterMap] with the specified contents, given as a list of pairs where the
 * first component is the key and the second is the value. If multiple pairs have the same key, the
 * resulting map will contain the value from the last of those pairs.
 */
public fun  mutableScatterMapOf(vararg pairs: Pair): MutableScatterMap =
    MutableScatterMap(pairs.size).apply { putAll(pairs) }

/**
 * [ScatterMap] is a container with a [Map]-like interface based on a flat hash table implementation
 * (the key/value mappings are not stored by nodes but directly into arrays). The underlying
 * implementation is designed to avoid all allocations on insertion, removal, retrieval, and
 * iteration. Allocations may still happen on insertion when the underlying storage needs to grow to
 * accommodate newly added entries to the table. In addition, this implementation minimizes memory
 * usage by avoiding the use of separate objects to hold key/value pairs.
 *
 * This implementation makes no guarantee as to the order of the keys and values stored, nor does it
 * make guarantees that the order remains constant over time.
 *
 * This implementation is not thread-safe: if multiple threads access this container concurrently,
 * and one or more threads modify the structure of the map (insertion or removal for instance), the
 * calling code must provide the appropriate synchronization. Multiple threads are safe to read from
 * this map concurrently if no write is happening.
 *
 * This implementation is read-only and only allows data to be queried. A mutable implementation is
 * provided by [MutableScatterMap].
 *
 * **Note**: when a [Map] is absolutely necessary, you can use the method [asMap] to create a thin
 * wrapper around a [ScatterMap]. Please refer to [asMap] for more details and caveats.
 *
 * **ScatterMap and SimpleArrayMap**: like [SimpleArrayMap], [ScatterMap]/[MutableScatterMap] is
 * designed to avoid the allocation of extra objects when inserting new entries in the map. However,
 * the implementation of [ScatterMap]/[MutableScatterMap] offers better performance characteristics
 * compared to [SimpleArrayMap] and is thus generally preferable. If memory usage is a concern,
 * [SimpleArrayMap] automatically shrinks its storage to avoid using more memory than necessary. You
 * can also control memory usage with [MutableScatterMap] by manually calling
 * [MutableScatterMap.trim].
 *
 * @see [MutableScatterMap]
 */
public sealed class ScatterMap {
    // NOTE: Our arrays are marked internal to implement inlined forEach{}
    // The backing array for the metadata bytes contains
    // `capacity + 1 + ClonedMetadataCount` entries, including when
    // the table is empty (see [EmptyGroup]).
    @PublishedApi @JvmField internal var metadata: LongArray = EmptyGroup

    @PublishedApi @JvmField internal var keys: Array = EMPTY_OBJECTS

    @PublishedApi @JvmField internal var values: Array = EMPTY_OBJECTS

    // We use a backing field for capacity to avoid invokevirtual calls
    // every time we need to look at the capacity
    @JvmField internal var _capacity: Int = 0

    /**
     * Returns the number of key-value pairs that can be stored in this map without requiring
     * internal storage reallocation.
     */
    public val capacity: Int
        get() = _capacity

    // We use a backing field for capacity to avoid invokevirtual calls
    // every time we need to look at the size
    @JvmField internal var _size: Int = 0

    /** Returns the number of key-value pairs in this map. */
    public val size: Int
        get() = _size

    /** Returns `true` if this map has at least one entry. */
    public fun any(): Boolean = _size != 0

    /** Returns `true` if this map has no entries. */
    public fun none(): Boolean = _size == 0

    /** Indicates whether this map is empty. */
    public fun isEmpty(): Boolean = _size == 0

    /** Returns `true` if this map is not empty. */
    public fun isNotEmpty(): Boolean = _size != 0

    /**
     * Returns the value corresponding to the given [key], or `null` if such a key is not present in
     * the map.
     */
    public operator fun get(key: K): V? {
        val index = findKeyIndex(key)
        @Suppress("UNCHECKED_CAST") return if (index >= 0) values[index] as V? else null
    }

    /**
     * Returns the value to which the specified [key] is mapped, or [defaultValue] if this map
     * contains no mapping for the key.
     */
    public fun getOrDefault(key: K, defaultValue: V): V {
        val index = findKeyIndex(key)
        if (index >= 0) {
            @Suppress("UNCHECKED_CAST") return values[index] as V
        }
        return defaultValue
    }

    /**
     * Returns the value for the given [key] if the value is present and not null. Otherwise,
     * returns the result of the [defaultValue] function.
     */
    public inline fun getOrElse(key: K, defaultValue: () -> V): V {
        return get(key) ?: defaultValue()
    }

    /**
     * Iterates over every key/value pair stored in this map by invoking the specified [block]
     * lambda.
     */
    @PublishedApi
    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
        val m = metadata
        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries

        for (i in 0..lastIndex) {
            var slot = m[i]
            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
                // Branch-less if (i == lastIndex) 7 else 8
                // i - lastIndex returns a negative value when i < lastIndex,
                // so 1 is set as the MSB. By inverting and shifting we get
                // 0 when i < lastIndex, 1 otherwise.
                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
                for (j in 0 until bitCount) {
                    if (isFull(slot and 0xffL)) {
                        val index = (i shl 3) + j
                        block(index)
                    }
                    slot = slot shr 8
                }
                if (bitCount != 8) return
            }
        }
    }

    /**
     * Iterates over every key/value pair stored in this map by invoking the specified [block]
     * lambda.
     */
    public inline fun forEach(block: (key: K, value: V) -> Unit) {
        val k = keys
        val v = values

        forEachIndexed { index -> @Suppress("UNCHECKED_CAST") block(k[index] as K, v[index] as V) }
    }

    /** Iterates over every key stored in this map by invoking the specified [block] lambda. */
    public inline fun forEachKey(block: (key: K) -> Unit) {
        val k = keys

        forEachIndexed { index -> @Suppress("UNCHECKED_CAST") block(k[index] as K) }
    }

    /** Iterates over every value stored in this map by invoking the specified [block] lambda. */
    public inline fun forEachValue(block: (value: V) -> Unit) {
        val v = values

        forEachIndexed { index -> @Suppress("UNCHECKED_CAST") block(v[index] as V) }
    }

    /** Returns true if all entries match the given [predicate]. */
    public inline fun all(predicate: (K, V) -> Boolean): Boolean {
        forEach { key, value -> if (!predicate(key, value)) return false }
        return true
    }

    /** Returns true if at least one entry matches the given [predicate]. */
    public inline fun any(predicate: (K, V) -> Boolean): Boolean {
        forEach { key, value -> if (predicate(key, value)) return true }
        return false
    }

    /** Returns the number of entries in this map. */
    public fun count(): Int = size

    /** Returns the number of entries matching the given [predicate]. */
    public inline fun count(predicate: (K, V) -> Boolean): Int {
        var count = 0
        forEach { key, value -> if (predicate(key, value)) count++ }
        return count
    }

    /** Returns true if the specified [key] is present in this hash map, false otherwise. */
    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0

    /** Returns true if the specified [key] is present in this hash map, false otherwise. */
    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0

    /** Returns true if the specified [value] is present in this hash map, false otherwise. */
    public fun containsValue(value: V): Boolean {
        forEachValue { v -> if (value == v) return true }
        return false
    }

    /**
     * Creates a String from the elements separated by [separator] and using [prefix] before and
     * [postfix] after, if supplied.
     *
     * When a non-negative value of [limit] is provided, a maximum of [limit] items are used to
     * generate the string. If the collection holds more than [limit] items, the string is
     * terminated with [truncated].
     *
     * [transform] may be supplied to convert each element to a custom String.
     */
    @JvmOverloads
    public fun joinToString(
        separator: CharSequence = ", ",
        prefix: CharSequence = "",
        postfix: CharSequence = "", // I know this should be suffix, but this is kotlin's name
        limit: Int = -1,
        truncated: CharSequence = "...",
        transform: ((key: K, value: V) -> CharSequence)? = null
    ): String = buildString {
        append(prefix)
        var index = 0
        [email protected] { key, value ->
            if (index == limit) {
                append(truncated)
                return@buildString
            }
            if (index != 0) {
                append(separator)
            }
            if (transform == null) {
                append(key)
                append('=')
                append(value)
            } else {
                append(transform(key, value))
            }
            index++
        }
        append(postfix)
    }

    /**
     * Returns the hash code value for this map. The hash code the sum of the hash codes of each
     * key/value pair.
     */
    public override fun hashCode(): Int {
        var hash = 0

        forEach { key, value -> hash += key.hashCode() xor value.hashCode() }

        return hash
    }

    /**
     * Compares the specified object [other] with this hash map for equality. The two objects are
     * considered equal if [other]:
     * - Is a [ScatterMap]
     * - Has the same [size] as this map
     * - Contains key/value pairs equal to this map's pair
     */
    public override fun equals(other: Any?): Boolean {
        if (other === this) {
            return true
        }

        if (other !is ScatterMap<*, *>) {
            return false
        }
        if (other.size != size) {
            return false
        }

        @Suppress("UNCHECKED_CAST") val o = other as ScatterMap

        forEach { key, value ->
            if (value == null) {
                if (o[key] != null || !o.containsKey(key)) {
                    return false
                }
            } else if (value != o[key]) {
                return false
            }
        }

        return true
    }

    /**
     * Returns a string representation of this map. The map is denoted in the string by the `{}`.
     * Each key/value pair present in the map is represented inside '{}` by a substring of the form
     * `key=value`, and pairs are separated by `, `.
     */
    public override fun toString(): String {
        if (isEmpty()) {
            return "{}"
        }

        val s = StringBuilder().append('{')
        var i = 0
        forEach { key, value ->
            s.append(if (key === this) "(this)" else key)
            s.append("=")
            s.append(if (value === this) "(this)" else value)
            i++
            if (i < _size) {
                s.append(',').append(' ')
            }
        }

        return s.append('}').toString()
    }

    internal fun asDebugString(): String = buildString {
        append('{')
        append("metadata=[")
        for (i in 0 until capacity) {
            when (val metadata = readRawMetadata(metadata, i)) {
                Empty -> append("Empty")
                Deleted -> append("Deleted")
                else -> append(metadata)
            }
            append(", ")
        }
        append("], ")
        append("keys=[")
        for (i in keys.indices) {
            append(keys[i])
            append(", ")
        }
        append("], ")
        append("values=[")
        for (i in values.indices) {
            append(values[i])
            append(", ")
        }
        append("]")
        append('}')
    }

    /**
     * Scans the hash table to find the index in the backing arrays of the specified [key]. Returns
     * -1 if the key is not present.
     */
    internal inline fun findKeyIndex(key: K): Int {
        val hash = hash(key)
        val hash2 = h2(hash)

        val probeMask = _capacity
        var probeOffset = h1(hash) and probeMask
        var probeIndex = 0

        while (true) {
            val g = group(metadata, probeOffset)
            var m = g.match(hash2)
            while (m.hasNext()) {
                val index = (probeOffset + m.get()) and probeMask
                if (keys[index] == key) {
                    return index
                }
                m = m.next()
            }

            if (g.maskEmpty() != 0L) {
                break
            }

            probeIndex += GroupWidth
            probeOffset = (probeOffset + probeIndex) and probeMask
        }

        return -1
    }

    /**
     * Wraps this [ScatterMap] with a [Map] interface. The [Map] is backed by the [ScatterMap], so
     * changes to the [ScatterMap] are reflected in the [Map]. If the [ScatterMap] is modified while
     * an iteration over the [Map] is in progress, the results of the iteration are undefined.
     *
     * **Note**: while this method is useful to use this [ScatterMap] with APIs accepting [Map]
     * interfaces, it is less efficient to do so than to use [ScatterMap]'s APIs directly. While the
     * [Map] implementation returned by this method tries to be as efficient as possible, the
     * semantics of [Map] may require the allocation of temporary objects for access and iteration.
     */
    public fun asMap(): Map = MapWrapper(this)
}

/**
 * [MutableScatterMap] is a container with a [Map]-like interface based on a flat hash table
 * implementation (the key/value mappings are not stored by nodes but directly into arrays). The
 * underlying implementation is designed to avoid all allocations on insertion, removal, retrieval,
 * and iteration. Allocations may still happen on insertion when the underlying storage needs to
 * grow to accommodate newly added entries to the table. In addition, this implementation minimizes
 * memory usage by avoiding the use of separate objects to hold key/value pairs.
 *
 * This implementation makes no guarantee as to the order of the keys and values stored, nor does it
 * make guarantees that the order remains constant over time.
 *
 * This implementation is not thread-safe: if multiple threads access this container concurrently,
 * and one or more threads modify the structure of the map (insertion or removal for instance), the
 * calling code must provide the appropriate synchronization. Multiple threads are safe to read from
 * this map concurrently if no write is happening.
 *
 * **Note**: when a [Map] is absolutely necessary, you can use the method [asMap] to create a thin
 * wrapper around a [MutableScatterMap]. Please refer to [asMap] for more details and caveats.
 *
 * **Note**: when a [MutableMap] is absolutely necessary, you can use the method [asMutableMap] to
 * create a thin wrapper around a [MutableScatterMap]. Please refer to [asMutableMap] for more
 * details and caveats.
 *
 * **MutableScatterMap and SimpleArrayMap**: like [SimpleArrayMap], [MutableScatterMap] is designed
 * to avoid the allocation of extra objects when inserting new entries in the map. However, the
 * implementation of [MutableScatterMap] offers better performance characteristics compared to
 * [SimpleArrayMap] and is thus generally preferable. If memory usage is a concern, [SimpleArrayMap]
 * automatically shrinks its storage to avoid using more memory than necessary. You can also control
 * memory usage with [MutableScatterMap] by manually calling [MutableScatterMap.trim].
 *
 * @param initialCapacity The initial desired capacity for this container. The container will honor
 *   this value by guaranteeing its internal structures can hold that many entries without requiring
 *   any allocations. The initial capacity can be set to 0.
 * @constructor Creates a new [MutableScatterMap]
 * @see Map
 */
public class MutableScatterMap(initialCapacity: Int = DefaultScatterCapacity) :
    ScatterMap() {
    // Number of entries we can add before we need to grow
    private var growthLimit = 0

    init {
        requirePrecondition(initialCapacity >= 0) { "Capacity must be a positive value." }
        initializeStorage(unloadedCapacity(initialCapacity))
    }

    private fun initializeStorage(initialCapacity: Int) {
        val newCapacity =
            if (initialCapacity > 0) {
                // Since we use longs for storage, our capacity is never < 7, enforce
                // it here. We do have a special case for 0 to create small empty maps
                max(7, normalizeCapacity(initialCapacity))
            } else {
                0
            }
        _capacity = newCapacity
        initializeMetadata(newCapacity)
        keys = if (newCapacity == 0) EMPTY_OBJECTS else arrayOfNulls(newCapacity)
        values = if (newCapacity == 0) EMPTY_OBJECTS else arrayOfNulls(newCapacity)
    }

    private fun initializeMetadata(capacity: Int) {
        metadata =
            if (capacity == 0) {
                EmptyGroup
            } else {
                // Round up to the next multiple of 8 and find how many longs we need
                val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
                LongArray(size).apply {
                    fill(AllEmpty)
                    writeRawMetadata(this, capacity, Sentinel)
                }
            }
        initializeGrowth()
    }

    private fun initializeGrowth() {
        growthLimit = loadedCapacity(capacity) - _size
    }

    /**
     * Returns the value to which the specified [key] is mapped, if the value is present in the map
     * and not `null`. Otherwise, calls `defaultValue()` and puts the result in the map associated
     * with [key].
     */
    public inline fun getOrPut(key: K, defaultValue: () -> V): V {
        return get(key) ?: defaultValue().also { set(key, it) }
    }

    /**
     * Retrieves a value for [key] and computes a new value based on the existing value (or `null`
     * if the key is not in the map). The computed value is then stored in the map for the given
     * [key].
     *
     * @return value computed by `computeBlock`.
     */
    public inline fun compute(key: K, computeBlock: (key: K, value: V?) -> V): V {
        val index = findInsertIndex(key)
        val inserting = index < 0

        @Suppress("UNCHECKED_CAST")
        val computedValue = computeBlock(key, if (inserting) null else values[index] as V)

        // Skip Array.set() if key is already there
        if (inserting) {
            val insertionIndex = index.inv()
            keys[insertionIndex] = key
            values[insertionIndex] = computedValue
        } else {
            values[index] = computedValue
        }
        return computedValue
    }

    /**
     * Creates a new mapping from [key] to [value] in this map. If [key] is already present in the
     * map, the association is modified and the previously associated value is replaced with
     * [value]. If [key] is not present, a new entry is added to the map, which may require to grow
     * the underlying storage and cause allocations.
     */
    public operator fun set(key: K, value: V) {
        val index = findInsertIndex(key).let { index -> if (index < 0) index.inv() else index }
        keys[index] = key
        values[index] = value
    }

    /**
     * Creates a new mapping from [key] to [value] in this map. If [key] is already present in the
     * map, the association is modified and the previously associated value is replaced with
     * [value]. If [key] is not present, a new entry is added to the map, which may require to grow
     * the underlying storage and cause allocations. Return the previous value associated with the
     * [key], or `null` if the key was not present in the map.
     */
    public fun put(key: K, value: V): V? {
        val index = findInsertIndex(key).let { index -> if (index < 0) index.inv() else index }
        val oldValue = values[index]
        keys[index] = key
        values[index] = value

        @Suppress("UNCHECKED_CAST") return oldValue as V?
    }

    /**
     * Puts all the [pairs] into this map, using the first component of the pair as the key, and the
     * second component as the value.
     */
    public fun putAll(@Suppress("ArrayReturn") pairs: Array>) {
        for ((key, value) in pairs) {
            this[key] = value
        }
    }

    /**
     * Puts all the [pairs] into this map, using the first component of the pair as the key, and the
     * second component as the value.
     */
    public fun putAll(pairs: Iterable>) {
        for ((key, value) in pairs) {
            this[key] = value
        }
    }

    /**
     * Puts all the [pairs] into this map, using the first component of the pair as the key, and the
     * second component as the value.
     */
    public fun putAll(pairs: Sequence>) {
        for ((key, value) in pairs) {
            this[key] = value
        }
    }

    /** Puts all the key/value mappings in the [from] map into this map. */
    public fun putAll(from: Map) {
        from.forEach { (key, value) -> this[key] = value }
    }

    /** Puts all the key/value mappings in the [from] map into this map. */
    public fun putAll(from: ScatterMap) {
        from.forEach { key, value -> this[key] = value }
    }

    /**
     * Puts the key/value mapping from the [pair] in this map, using the first element as the key,
     * and the second element as the value.
     */
    public inline operator fun plusAssign(pair: Pair) {
        this[pair.first] = pair.second
    }

    /**
     * Puts all the [pairs] into this map, using the first component of the pair as the key, and the
     * second component as the value.
     */
    public inline operator fun plusAssign(
        @Suppress("ArrayReturn") pairs: Array>
    ): Unit = putAll(pairs)

    /**
     * Puts all the [pairs] into this map, using the first component of the pair as the key, and the
     * second component as the value.
     */
    public inline operator fun plusAssign(pairs: Iterable>): Unit = putAll(pairs)

    /**
     * Puts all the [pairs] into this map, using the first component of the pair as the key, and the
     * second component as the value.
     */
    public inline operator fun plusAssign(pairs: Sequence>): Unit = putAll(pairs)

    /** Puts all the key/value mappings in the [from] map into this map. */
    public inline operator fun plusAssign(from: Map): Unit = putAll(from)

    /** Puts all the key/value mappings in the [from] map into this map. */
    public inline operator fun plusAssign(from: ScatterMap): Unit = putAll(from)

    /**
     * Removes the specified [key] and its associated value from the map. If the [key] was present
     * in the map, this function returns the value that was present before removal.
     */
    public fun remove(key: K): V? {
        val index = findKeyIndex(key)
        if (index >= 0) {
            return removeValueAt(index)
        }
        return null
    }

    /**
     * Removes the specified [key] and its associated value from the map if the associated value
     * equals [value]. Returns whether the removal happened.
     */
    public fun remove(key: K, value: V): Boolean {
        val index = findKeyIndex(key)
        if (index >= 0) {
            if (values[index] == value) {
                removeValueAt(index)
                return true
            }
        }
        return false
    }

    /** Removes any mapping for which the specified [predicate] returns true. */
    public inline fun removeIf(predicate: (K, V) -> Boolean) {
        forEachIndexed { index ->
            @Suppress("UNCHECKED_CAST")
            if (predicate(keys[index] as K, values[index] as V)) {
                removeValueAt(index)
            }
        }
    }

    /** Removes the specified [key] and its associated value from the map. */
    public inline operator fun minusAssign(key: K) {
        remove(key)
    }

    /** Removes the specified [keys] and their associated value from the map. */
    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array) {
        for (key in keys) {
            remove(key)
        }
    }

    /** Removes the specified [keys] and their associated value from the map. */
    public inline operator fun minusAssign(keys: Iterable) {
        for (key in keys) {
            remove(key)
        }
    }

    /** Removes the specified [keys] and their associated value from the map. */
    public inline operator fun minusAssign(keys: Sequence) {
        for (key in keys) {
            remove(key)
        }
    }

    /** Removes the specified [keys] and their associated value from the map. */
    public inline operator fun minusAssign(keys: ScatterSet) {
        keys.forEach { key -> remove(key) }
    }

    /** Removes the specified [keys] and their associated value from the map. */
    public inline operator fun minusAssign(keys: ObjectList) {
        keys.forEach { key -> remove(key) }
    }

    @PublishedApi
    internal fun removeValueAt(index: Int): V? {
        _size -= 1

        // TODO: We could just mark the entry as empty if there's a group
        //       window around this entry that was already empty
        writeMetadata(metadata, _capacity, index, Deleted)
        keys[index] = null
        val oldValue = values[index]
        values[index] = null

        @Suppress("UNCHECKED_CAST") return oldValue as V?
    }

    /** Removes all mappings from this map. */
    public fun clear() {
        _size = 0
        if (metadata !== EmptyGroup) {
            metadata.fill(AllEmpty)
            writeRawMetadata(metadata, _capacity, Sentinel)
        }
        values.fill(null, 0, _capacity)
        keys.fill(null, 0, _capacity)
        initializeGrowth()
    }

    /**
     * Scans the hash table to find the index at which we can store a value for the give [key]. If
     * the key already exists in the table, its index will be returned, otherwise the `index.inv()`
     * of an empty slot will be returned. Calling this function may cause the internal storage to be
     * reallocated if the table is full.
     */
    @PublishedApi
    internal fun findInsertIndex(key: K): Int {
        val hash = hash(key)
        val hash1 = h1(hash)
        val hash2 = h2(hash)

        val probeMask = _capacity
        var probeOffset = hash1 and probeMask
        var probeIndex = 0

        while (true) {
            val g = group(metadata, probeOffset)
            var m = g.match(hash2)
            while (m.hasNext()) {
                val index = (probeOffset + m.get()) and probeMask
                if (keys[index] == key) {
                    return index
                }
                m = m.next()
            }

            if (g.maskEmpty() != 0L) {
                break
            }

            probeIndex += GroupWidth
            probeOffset = (probeOffset + probeIndex) and probeMask
        }

        var index = findFirstAvailableSlot(hash1)
        if (growthLimit == 0 && !isDeleted(metadata, index)) {
            adjustStorage()
            index = findFirstAvailableSlot(hash1)
        }

        _size += 1
        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
        writeMetadata(metadata, _capacity, index, hash2.toLong())

        return index.inv()
    }

    /**
     * Finds the first empty or deleted slot in the table in which we can store a value without
     * resizing the internal storage.
     */
    private fun findFirstAvailableSlot(hash1: Int): Int {
        val probeMask = _capacity
        var probeOffset = hash1 and probeMask
        var probeIndex = 0

        while (true) {
            val g = group(metadata, probeOffset)
            val m = g.maskEmptyOrDeleted()
            if (m != 0L) {
                return (probeOffset + m.lowestBitSet()) and probeMask
            }
            probeIndex += GroupWidth
            probeOffset = (probeOffset + probeIndex) and probeMask
        }
    }

    /**
     * Trims this [MutableScatterMap]'s storage so it is sized appropriately to hold the current
     * mappings.
     *
     * Returns the number of empty entries removed from this map's storage. Returns be 0 if no
     * trimming is necessary or possible.
     */
    public fun trim(): Int {
        val previousCapacity = _capacity
        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
        if (newCapacity < previousCapacity) {
            resizeStorage(newCapacity)
            return previousCapacity - _capacity
        }
        return 0
    }

    /**
     * Grow internal storage if necessary. This function can instead opt to remove deleted entries
     * from the table to avoid an expensive reallocation of the underlying storage. This "rehash in
     * place" occurs when the current size is <= 25/32 of the table capacity. The choice of 25/32 is
     * detailed in the implementation of abseil's `raw_hash_set`.
     */
    internal fun adjustStorage() { // Internal to prevent inlining
        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
            dropDeletes()
        } else {
            resizeStorage(nextCapacity(_capacity))
        }
    }

    // Internal to prevent inlining
    internal fun dropDeletes() {
        val metadata = metadata
        val capacity = _capacity
        val keys = keys
        val values = values

        // Converts Sentinel and Deleted to Empty, and Full to Deleted
        convertMetadataForCleanup(metadata, capacity)

        var index = 0

        // Drop deleted items and re-hashes surviving entries
        while (index != capacity) {
            var m = readRawMetadata(metadata, index)
            // Formerly Deleted entry, we can use it as a swap spot
            if (m == Empty) {
                index++
                continue
            }

            // Formerly Full entries are now marked Deleted. If we see an
            // entry that's not marked Deleted, we can ignore it completely
            if (m != Deleted) {
                index++
                continue
            }

            val hash = hash(keys[index])
            val hash1 = h1(hash)
            val targetIndex = findFirstAvailableSlot(hash1)

            // Test if the current index (i) and the new index (targetIndex) fall
            // within the same group based on the hash. If the group doesn't change,
            // we don't move the entry
            val probeOffset = hash1 and capacity
            val newProbeIndex = ((targetIndex - probeOffset) and capacity) / GroupWidth
            val oldProbeIndex = ((index - probeOffset) and capacity) / GroupWidth

            if (newProbeIndex == oldProbeIndex) {
                val hash2 = h2(hash)
                writeRawMetadata(metadata, index, hash2.toLong())

                // Copies the metadata into the clone area
                metadata[metadata.lastIndex] = metadata[0]

                index++
                continue
            }

            m = readRawMetadata(metadata, targetIndex)
            if (m == Empty) {
                // The target is empty so we can transfer directly
                val hash2 = h2(hash)
                writeRawMetadata(metadata, targetIndex, hash2.toLong())
                writeRawMetadata(metadata, index, Empty)

                keys[targetIndex] = keys[index]
                keys[index] = null

                values[targetIndex] = values[index]
                values[index] = null
            } else /* m == Deleted */ {
                // The target isn't empty so we use an empty slot denoted by
                // swapIndex to perform the swap
                val hash2 = h2(hash)
                writeRawMetadata(metadata, targetIndex, hash2.toLong())

                val oldKey = keys[targetIndex]
                keys[targetIndex] = keys[index]
                keys[index] = oldKey

                val oldValue = values[targetIndex]
                values[targetIndex] = values[index]
                values[index] = oldValue

                // Since we exchanged two slots we must repeat the process with
                // element we just moved in the current location
                index--
            }

            // Copies the metadata into the clone area
            metadata[metadata.lastIndex] = metadata[0]

            index++
        }

        initializeGrowth()
    }

    // Internal to prevent inlining
    internal fun resizeStorage(newCapacity: Int) {
        val previousMetadata = metadata
        val previousKeys = keys
        val previousValues = values
        val previousCapacity = _capacity

        initializeStorage(newCapacity)

        val newMetadata = metadata
        val newKeys = keys
        val newValues = values
        val capacity = _capacity

        for (i in 0 until previousCapacity) {
            if (isFull(previousMetadata, i)) {
                val previousKey = previousKeys[i]
                val hash = hash(previousKey)
                val index = findFirstAvailableSlot(h1(hash))

                writeMetadata(newMetadata, capacity, index, h2(hash).toLong())
                newKeys[index] = previousKey
                newValues[index] = previousValues[i]
            }
        }
    }

    /**
     * Wraps this [ScatterMap] with a [MutableMap] interface. The [MutableMap] is backed by the
     * [ScatterMap], so changes to the [ScatterMap] are reflected in the [MutableMap] and
     * vice-versa. If the [ScatterMap] is modified while an iteration over the [MutableMap] is in
     * progress (and vice- versa), the results of the iteration are undefined.
     *
     * **Note**: while this method is useful to use this [MutableScatterMap] with APIs accepting
     * [MutableMap] interfaces, it is less efficient to do so than to use [MutableScatterMap]'s APIs
     * directly. While the [MutableMap] implementation returned by this method tries to be as
     * efficient as possible, the semantics of [MutableMap] may require the allocation of temporary
     * objects for access and iteration.
     */
    public fun asMutableMap(): MutableMap = MutableMapWrapper(this)
}

internal inline fun convertMetadataForCleanup(metadata: LongArray, capacity: Int) {
    val end = (capacity + 7) shr 3
    for (i in 0 until end) {
        // Converts Sentinel and Deleted to Empty, and Full to Deleted
        val maskedGroup = metadata[i] and BitmaskMsb
        metadata[i] = (maskedGroup.inv() + (maskedGroup ushr 7)) and BitmaskLsb.inv()
    }

    val lastIndex = metadata.lastIndex
    // Restores the sentinel that we overwrote above
    metadata[lastIndex - 1] =
        (Sentinel shl 56) or (metadata[lastIndex - 1] and 0x00ffffff_ffffffffL)
    // Copies the metadata into the clone area
    metadata[lastIndex] = metadata[0]
}

internal fun findEmptySlot(metadata: LongArray, start: Int, end: Int): Int {
    for (i in start until end) {
        if (readRawMetadata(metadata, i) == Empty) {
            return i
        }
    }
    return -1
}

/**
 * Returns the hash code of [k]. The hash spreads low bits to to minimize collisions in high 25-bits
 * that are used for probing.
 */
internal inline fun hash(k: Any?): Int {
    // scramble bits to account for collisions between similar hash values.
    val hash = k.hashCode() * MurmurHashC1
    // spread low bits into high bits that are used for probing
    return hash xor (hash shl 16)
}

// C1 constant from MurmurHash implementation: https://en.wikipedia.org/wiki/MurmurHash#Algorithm
internal const val MurmurHashC1: Int = 0xcc9e2d51.toInt()

// Returns the "H1" part of the specified hash code. In our implementation,
// it is simply the top-most 25 bits
internal inline fun h1(hash: Int) = hash ushr 7

// Returns the "H2" part of the specified hash code. In our implementation,
// this corresponds to the lower 7 bits
internal inline fun h2(hash: Int) = hash and 0x7f

// Assumes [capacity] was normalized with [normalizedCapacity].
// Returns the next 2^m - 1
internal fun nextCapacity(capacity: Int) =
    if (capacity == 0) {
        DefaultScatterCapacity
    } else {
        capacity * 2 + 1
    }

// n -> nearest 2^m - 1
internal fun normalizeCapacity(n: Int) =
    if (n > 0) (0xffffffff.toInt() ushr n.countLeadingZeroBits()) else 0

// Computes the growth based on a load factor of 7/8 for the general case.
// When capacity is < GroupWidth - 1, we use a load factor of 1 instead
internal fun loadedCapacity(capacity: Int): Int {
    // Special cases where x - x / 8 fails
    if (GroupWidth <= 8 && capacity == 7) {
        return 6
    }
    // If capacity is < GroupWidth - 1 we end up here and this formula
    // will return `capacity` in this case, which is what we want
    return capacity - capacity / 8
}

// Inverse of loadedCapacity()
internal fun unloadedCapacity(capacity: Int): Int {
    // Special cases where x + (x - 1) / 7
    if (GroupWidth <= 8 && capacity == 7) {
        return 8
    }
    return capacity + (capacity - 1) / 7
}

/** Reads a single byte from the long array at the specified [offset] in *bytes*. */
@PublishedApi
internal inline fun readRawMetadata(data: LongArray, offset: Int): Long {
    // Take the Long at index `offset / 8` and shift by `offset % 8`
    // A longer explanation can be found in [group()].
    return (data[offset shr 3] shr ((offset and 0x7) shl 3)) and 0xff
}

/**
 * Writes a single byte into the long array at the specified [offset] in *bytes* and copies it, if
 * necessary, into the cloned bytes section at the end of the array.
 *
 * NOTE: [value] must be a single byte, accepted here as a Long to avoid unnecessary conversions.
 */
internal inline fun writeMetadata(data: LongArray, capacity: Int, offset: Int, value: Long) {
    writeRawMetadata(data, offset, value)

    // Mirroring
    // We could/should write a single byte when cloning the metadata by calling
    // writeRawMetadata(), but since our implementation uses Longs for storage,
    // we always write a full Long. We can skip a bit of unnecessary work by just
    // copying the whole group the index falls into.
    // When index is in 0..7, we copy the group over the control bytes at the end of
    // the array, otherwise the group is copied onto itself (cloneIndex shr 3 == index shr 3)
    // TODO: We could further reduce the work we do by always copying index 0 to
    //       lastIndex, but is it interesting in terms of data caches?
    val cloneIndex =
        ((offset - ClonedMetadataCount) and capacity) + (ClonedMetadataCount and capacity)
    data[cloneIndex shr 3] = data[offset shr 3]
}

/**
 * Writes a single byte into the long array at the specified [offset] in *bytes*.
 *
 * NOTE: [value] must be a single byte, accepted here as a Long to avoid unnecessary conversions.
 */
internal inline fun writeRawMetadata(data: LongArray, offset: Int, value: Long) {
    // See [group()] for details. First find the index i in the LongArray,
    // then find the number of bits we need to shift by
    val i = offset shr 3
    val b = (offset and 0x7) shl 3
    // Mask the source data with 0xFF in the right place, then and [value]
    // moved to the right spot
    data[i] = (data[i] and (0xffL shl b).inv()) or (value shl b)
}

internal inline fun isEmpty(metadata: LongArray, index: Int) =
    readRawMetadata(metadata, index) == Empty

internal inline fun isDeleted(metadata: LongArray, index: Int) =
    readRawMetadata(metadata, index) == Deleted

internal inline fun isFull(metadata: LongArray, index: Int): Boolean =
    readRawMetadata(metadata, index) < 0x80L

@PublishedApi internal inline fun isFull(value: Long): Boolean = value < 0x80L

// Bitmasks in our context are abstract bitmasks. They represent a bitmask
// for a Group. i.e. bit 1 is the second least significant byte in the group.
// These bits are also called "abstract bits". For example, given the
// following group of metadata and a group width of 8:
//
// 0x7700550033001100
//   |   |   |   | |___ bit 0 = 0x00
//   |   |   |   |_____ bit 1 = 0x11
//   |   |   |_________ bit 3 = 0x33
//   |   |_____________ bit 5 = 0x55
//   |_________________ bit 7 = 0x77
//
// This is useful when performing group operations to figure out, for
// example, which metadata is set or not.
//
// A static bitmask is a read-only bitmask that allows performing simple
// queries such as [lowestBitSet].
internal typealias StaticBitmask = Long

// A dynamic bitmask is a bitmask that can be iterated on to retrieve,
// for instance, the index of all the "abstract bits" set on the group.
// This assumes the abstract bits are set to either 0x00 (for unset) and
// 0x80 (for set).
internal typealias Bitmask = Long

@PublishedApi internal inline fun StaticBitmask.lowestBitSet(): Int = countTrailingZeroBits() shr 3

/**
 * Returns the index of the next set bit in this mask. If invoked before checking [hasNext], this
 * function returns an invalid index (8).
 */
internal inline fun Bitmask.get() = lowestBitSet()

/**
 * Moves to the next set bit and returns the modified bitmask, call [get] to get the actual index.
 * If this function is called before checking [hasNext], the result is invalid.
 */
internal inline fun Bitmask.next() = this and (this - 1L)

/** Returns true if this [Bitmask] contains more set bits. */
internal inline fun Bitmask.hasNext() = this != 0L

// Least significant bits in the bitmask, one for each metadata in the group
@PublishedApi internal const val BitmaskLsb: Long = 0x01010101_01010101L

// Most significant bits in the bitmask, one for each metadata in the group
//
// NOTE: Ideally we'd use a ULong here, defined as 0x8080808080808080UL but
// using ULong/UByte makes us take a ~10% performance hit on get/set compared to
// a Long. And since Kotlin hates signed constants, we have to use
// -0x7f7f7f7f7f7f7f80L instead of the more sensible 0x8080808080808080L (and
// 0x8080808080808080UL.toLong() isn't considered a constant)
@PublishedApi internal const val BitmaskMsb: Long = -0x7f7f7f7f_7f7f7f80L // srsly Kotlin @#!

/**
 * Creates a [Group] from a metadata array, starting at the specified offset. [offset] must be a
 * valid index in the source array.
 */
internal inline fun group(metadata: LongArray, offset: Int): Group {
    // A Group is a Long read at an arbitrary byte-grained offset inside the
    // Long array. To read the Group, we need to read 2 Longs: one for the
    // most significant bits (MSBs) and one for the least significant bits
    // (LSBs).
    // Let's take an example, with a LongArray of 2 and an offset set to 1
    // byte. We need to read 7 bytes worth of LSBs in Long 0 and 1 byte worth
    // of MSBs in Long 1 (remember we index the bytes from LSB to MSB so in
    // the example below byte 0 is 0x11 and byte 11 is 0xAA):
    //
    //  ___________________ LongArray ____________________
    // |                                                  |
    // [88 77 66 55 44 33 22 11], [FF EE DD CC BB AA 00 99]
    // |_________Long0_______ _|  |_________Long1_______ _|
    //
    // To retrieve the Group we first find the index of Long0 by taking the
    // offset divided by 8. Then offset modulo 8 gives us how many bits we
    // need to shift by. With offset = 1:
    //
    // index = offset / 8 == 0
    // remainder = offset % 8 == 1
    // bitsToShift = remainder * 8
    //
    // LSBs = LongArray[index] >>> bitsToShift
    // MSBs = LongArray[index + 1] << (64 - bitsToShift)
    //
    // We now have:
    //
    // LSBs == 0x0088776655443322
    // MSBs == 0x9900000000000000
    //
    // However we can't just combine MSBs and LSBs with an OR when the offset
    // is a multiple of 8, because we would be attempting to shift left by 64
    // which is a no-op. This means we need to mask the MSBs with 0x0 when
    // offset is 0, and with 0xFF…FF when offset is != 0. We do this by taking
    // the negative value of `bitsToShift`, which will set the MSB when the value
    // is not 0, and doing a signed shift to the right to duplicate it:
    //
    // Group = LSBs | (MSBs & (-b >> 63)
    //
    // Note: since b is only ever 0, 8, 16, 24, 32, 48, 56, or 64, we don't
    // need to shift by 63, we could shift by only 5
    val i = offset shr 3
    val b = (offset and 0x7) shl 3
    return (metadata[i] ushr b) or (metadata[i + 1] shl (64 - b) and (-(b.toLong()) shr 63))
}

/**
 * Returns a [Bitmask] in which every abstract bit set means the corresponding metadata in that slot
 * is equal to [m].
 */
@PublishedApi
internal inline fun Group.match(m: Int): Bitmask {
    // BitmaskLsb * m replicates the byte `m` on every byte of the Long
    // and XOR-ing with `this` will give us a Long in which every non-zero
    // byte indicates a match
    val x = this xor (BitmaskLsb * m)
    // Turn every non-zero byte into 0x80
    return (x - BitmaskLsb) and x.inv() and BitmaskMsb
}

/** Returns a [Bitmask] in which every abstract bit set indicates an empty slot. */
internal inline fun Group.maskEmpty(): Bitmask {
    return (this and (this.inv() shl 6)) and BitmaskMsb
}

/** Returns a [Bitmask] in which every abstract bit set indicates an empty or deleted slot. */
@PublishedApi
internal inline fun Group.maskEmptyOrDeleted(): Bitmask {
    return (this and (this.inv() shl 7)) and BitmaskMsb
}

private class MapEntry(override val key: K, override val value: V) : Map.Entry

private class Entries(private val parent: ScatterMap) : Set> {
    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun iterator(): Iterator> {
        return iterator {
            parent.forEachIndexed { index ->
                @Suppress("UNCHECKED_CAST")
                yield(MapEntry(parent.keys[index] as K, parent.values[index] as V))
            }
        }
    }

    override fun containsAll(elements: Collection>): Boolean =
        elements.all { parent[it.key] == it.value }

    override fun contains(element: Map.Entry): Boolean = parent[element.key] == element.value
}

private class Keys(private val parent: ScatterMap) : Set {
    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun iterator(): Iterator = iterator { parent.forEachKey { key -> yield(key) } }

    override fun containsAll(elements: Collection): Boolean =
        elements.all { parent.containsKey(it) }

    override fun contains(element: K): Boolean = parent.containsKey(element)
}

private class Values(private val parent: ScatterMap) : Collection {
    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun iterator(): Iterator = iterator {
        parent.forEachValue { value -> yield(value) }
    }

    override fun containsAll(elements: Collection): Boolean =
        elements.all { parent.containsValue(it) }

    override fun contains(element: V): Boolean = parent.containsValue(element)
}

// TODO: While not mandatory, it would be pertinent to throw a
//       ConcurrentModificationException when the underlying ScatterMap
//       is modified while iterating over keys/values/entries. To do
//       this we should probably have some kind of generation ID in
//       ScatterMap that would be incremented on any add/remove/clear
//       or rehash.
private open class MapWrapper(private val parent: ScatterMap) : Map {
    private var _entries: Entries? = null
    override val entries: Set>
        get() = _entries ?: Entries(parent).apply { _entries = this }

    private var _keys: Keys? = null
    override val keys: Set
        get() = _keys ?: Keys(parent).apply { _keys = this }

    private var _values: Values? = null
    override val values: Collection
        get() = _values ?: Values(parent).apply { _values = this }

    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun get(key: K): V? = parent[key]

    override fun containsValue(value: V): Boolean = parent.containsValue(value)

    override fun containsKey(key: K): Boolean = parent.containsKey(key)
}

private class MutableMapEntry(
    val keys: Array,
    val values: Array,
    val index: Int
) : MutableMap.MutableEntry {

    @Suppress("UNCHECKED_CAST")
    override fun setValue(newValue: V): V {
        val oldValue = values[index]
        values[index] = newValue
        return oldValue as V
    }

    @Suppress("UNCHECKED_CAST")
    override val key: K
        get() = keys[index] as K

    @Suppress("UNCHECKED_CAST")
    override val value: V
        get() = values[index] as V
}

private class MutableEntries(private val parent: MutableScatterMap) :
    MutableSet> {

    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun iterator(): MutableIterator> =
        object : MutableIterator> {
            var iterator: Iterator>
            var current = -1

            init {
                iterator = iterator {
                    parent.forEachIndexed { index ->
                        current = index
                        yield(MutableMapEntry(parent.keys, parent.values, current))
                    }
                }
            }

            override fun hasNext(): Boolean = iterator.hasNext()

            override fun next(): MutableMap.MutableEntry = iterator.next()

            override fun remove() {
                if (current != -1) {
                    parent.removeValueAt(current)
                    current = -1
                }
            }
        }

    override fun clear() = parent.clear()

    override fun containsAll(elements: Collection>): Boolean {
        return elements.all { parent[it.key] == it.value }
    }

    override fun contains(element: MutableMap.MutableEntry): Boolean =
        parent[element.key] == element.value

    override fun addAll(elements: Collection>): Boolean {
        throw UnsupportedOperationException()
    }

    override fun add(element: MutableMap.MutableEntry): Boolean {
        throw UnsupportedOperationException()
    }

    override fun retainAll(elements: Collection>): Boolean {
        var changed = false
        parent.forEachIndexed { index ->
            var found = false
            for (entry in elements) {
                if (entry.key == parent.keys[index] && entry.value == parent.values[index]) {
                    found = true
                    break
                }
            }
            if (!found) {
                parent.removeValueAt(index)
                changed = true
            }
        }
        return changed
    }

    override fun removeAll(elements: Collection>): Boolean {
        var changed = false
        parent.forEachIndexed { index ->
            for (entry in elements) {
                if (entry.key == parent.keys[index] && entry.value == parent.values[index]) {
                    parent.removeValueAt(index)
                    changed = true
                    break
                }
            }
        }
        return changed
    }

    override fun remove(element: MutableMap.MutableEntry): Boolean {
        val index = parent.findKeyIndex(element.key)
        if (index >= 0 && parent.values[index] == element.value) {
            parent.removeValueAt(index)
            return true
        }
        return false
    }
}

private class MutableKeys(private val parent: MutableScatterMap) : MutableSet {
    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun iterator(): MutableIterator =
        object : MutableIterator {
            val iterator = iterator { parent.forEachIndexed { index -> yield(index) } }
            var current: Int = -1

            override fun hasNext(): Boolean = iterator.hasNext()

            override fun next(): K {
                current = iterator.next()
                @Suppress("UNCHECKED_CAST") return parent.keys[current] as K
            }

            override fun remove() {
                if (current >= 0) {
                    parent.removeValueAt(current)
                    current = -1
                }
            }
        }

    override fun clear() = parent.clear()

    override fun addAll(elements: Collection): Boolean {
        throw UnsupportedOperationException()
    }

    override fun add(element: K): Boolean {
        throw UnsupportedOperationException()
    }

    override fun retainAll(elements: Collection): Boolean {
        var changed = false
        parent.forEachIndexed { index ->
            if (parent.keys[index] !in elements) {
                parent.removeValueAt(index)
                changed = true
            }
        }
        return changed
    }

    override fun removeAll(elements: Collection): Boolean {
        var changed = false
        parent.forEachIndexed { index ->
            if (parent.keys[index] in elements) {
                parent.removeValueAt(index)
                changed = true
            }
        }
        return changed
    }

    override fun remove(element: K): Boolean {
        val index = parent.findKeyIndex(element)
        if (index >= 0) {
            parent.removeValueAt(index)
            return true
        }
        return false
    }

    override fun containsAll(elements: Collection): Boolean =
        elements.all { parent.containsKey(it) }

    override fun contains(element: K): Boolean = parent.containsKey(element)
}

private class MutableValues(private val parent: MutableScatterMap) :
    MutableCollection {
    override val size: Int
        get() = parent._size

    override fun isEmpty(): Boolean = parent.isEmpty()

    override fun iterator(): MutableIterator =
        object : MutableIterator {
            val iterator = iterator { parent.forEachIndexed { index -> yield(index) } }
            var current: Int = -1

            override fun hasNext(): Boolean = iterator.hasNext()

            override fun next(): V {
                current = iterator.next()
                @Suppress("UNCHECKED_CAST") return parent.values[current] as V
            }

            override fun remove() {
                if (current >= 0) {
                    parent.removeValueAt(current)
                    current = -1
                }
            }
        }

    override fun clear() = parent.clear()

    override fun addAll(elements: Collection): Boolean {
        throw UnsupportedOperationException()
    }

    override fun add(element: V): Boolean {
        throw UnsupportedOperationException()
    }

    override fun retainAll(elements: Collection): Boolean {
        var changed = false
        parent.forEachIndexed { index ->
            if (parent.values[index] !in elements) {
                parent.removeValueAt(index)
                changed = true
            }
        }
        return changed
    }

    override fun removeAll(elements: Collection): Boolean {
        var changed = false
        parent.forEachIndexed { index ->
            if (parent.values[index] in elements) {
                parent.removeValueAt(index)
                changed = true
            }
        }
        return changed
    }

    override fun remove(element: V): Boolean {
        parent.forEachIndexed { index ->
            if (parent.values[index] == element) {
                parent.removeValueAt(index)
                return true
            }
        }
        return false
    }

    override fun containsAll(elements: Collection): Boolean =
        elements.all { parent.containsValue(it) }

    override fun contains(element: V): Boolean = parent.containsValue(element)
}

private class MutableMapWrapper(private val parent: MutableScatterMap) :
    MapWrapper(parent), MutableMap {

    private var _entries: MutableEntries? = null
    override val entries: MutableSet>
        get() = _entries ?: MutableEntries(parent).apply { _entries = this }

    private var _keys: MutableKeys? = null
    override val keys: MutableSet
        get() = _keys ?: MutableKeys(parent).apply { _keys = this }

    private var _values: MutableValues? = null
    override val values: MutableCollection
        get() = _values ?: MutableValues(parent).apply { _values = this }

    override fun clear() = parent.clear()

    override fun remove(key: K): V? = parent.remove(key)

    override fun putAll(from: Map) {
        from.forEach { (key, value) -> parent[key] = value }
    }

    override fun put(key: K, value: V): V? = parent.put(key, value)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy