/**
 * Stores key/value entries in a mutable hash map.
 *
 * `MutableHashMap` updates the same collection in place and supports fast
 * lookup, insertion, removal, clearing, and iteration. It combines a native
 * `Map` for ordinary JavaScript keys with hash buckets for keys that implement
 * Effect `Equal` and `Hash`, so callers can mix reference-based and structural
 * lookup in the same collection.
 *
 * @since 2.0.0
 */
import type { NonEmptyArray } from "./Array.ts"
import * as Equal from "./Equal.ts"
import { format } from "./Formatter.ts"
import { dual } from "./Function.ts"
import * as Hash from "./Hash.ts"
import { type Inspectable, NodeInspectSymbol, toJson } from "./Inspectable.ts"
import * as Option from "./Option.ts"
import type { Pipeable } from "./Pipeable.ts"
import { pipeArguments } from "./Pipeable.ts"
import { hasProperty } from "./Predicate.ts"

const TypeId = "~effect/collections/MutableHashMap"

/**
 * A mutable hash map that stores key-value pairs and supports both referential
 * and Effect structural equality.
 *
 * **When to use**
 *
 * Use as a mutable key-value map when in-place updates are acceptable and keys
 * may rely on Effect structural equality.
 *
 * **Details**
 *
 * Operations mutate the map in place. Keys that implement `Equal` / `Hash` can
 * be looked up structurally; other keys use normal JavaScript reference or
 * primitive equality.
 *
 * **Example** (Using a mutable hash map)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * // Create a mutable hash map with string keys and number values
 * const map: MutableHashMap.MutableHashMap<string, number> = MutableHashMap
 *   .empty()
 *
 * // Add some data
 * MutableHashMap.set(map, "count", 42)
 * MutableHashMap.set(map, "total", 100)
 *
 * // Use as iterable
 * for (const [key, value] of map) {
 *   console.log(`${key}: ${value}`)
 * }
 * // Output:
 * // count: 42
 * // total: 100
 *
 * // Convert to array
 * const entries = Array.from(map)
 * console.log(entries) // [["count", 42], ["total", 100]]
 * ```
 *
 * @see {@link empty} for creating an empty mutable hash map
 * @see {@link get} for reading values by key
 * @see {@link set} for mutating entries by key
 *
 * @category models
 * @since 2.0.0
 */
export interface MutableHashMap<out K, out V> extends Iterable<[K, V]>, Pipeable, Inspectable {
  readonly [TypeId]: typeof TypeId
  readonly backing: Map<K, V>
  readonly buckets: Map<number, NonEmptyArray<K>>
}

/**
 * Checks whether the specified value is a `MutableHashMap`, `false` otherwise.
 *
 * **When to use**
 *
 * Use to narrow an unknown value before treating it as a mutable hash map.
 *
 * **Details**
 *
 * The check looks for the `MutableHashMap` runtime marker.
 *
 * **Gotchas**
 *
 * The check does not validate the key or value types carried by the map.
 *
 * @see {@link MutableHashMap} for the mutable hash map interface
 *
 * @category refinements
 * @since 4.0.0
 */
export const isMutableHashMap = <K, V>(value: unknown): value is MutableHashMap<K, V> => hasProperty(value, TypeId)

const MutableHashMapProto: Omit<MutableHashMap<unknown, unknown>, "backing" | "buckets" | "bucketsSize"> = {
  [TypeId]: TypeId,
  [Symbol.iterator](this: MutableHashMap<unknown, unknown>): Iterator<[unknown, unknown]> {
    return this.backing[Symbol.iterator]()
  },
  toString() {
    return `MutableHashMap(${format(Array.from(this))})`
  },
  toJSON() {
    return {
      _id: "MutableHashMap",
      values: toJson(Array.from(this))
    }
  },
  [NodeInspectSymbol]() {
    return this.toJSON()
  },
  pipe() {
    return pipeArguments(this, arguments)
  }
}

/**
 * Creates an empty MutableHashMap.
 *
 * **When to use**
 *
 * Use to create a fresh mutable map before adding entries over time.
 *
 * **Details**
 *
 * Each call returns a new empty map instance.
 *
 * **Example** (Creating an empty map)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.empty<string, number>()
 *
 * // Add some entries
 * MutableHashMap.set(map, "key1", 42)
 * MutableHashMap.set(map, "key2", 100)
 *
 * console.log(MutableHashMap.size(map)) // 2
 * ```
 *
 * @see {@link make} for creating a map from explicit entries
 * @see {@link fromIterable} for creating a map from an iterable of entries
 *
 * @category constructors
 * @since 2.0.0
 */
export const empty = <K, V>(): MutableHashMap<K, V> => {
  const self = Object.create(MutableHashMapProto)
  self.backing = new Map()
  self.buckets = new Map()
  return self
}

/**
 * Creates a MutableHashMap from a variable number of key-value pairs.
 *
 * **When to use**
 *
 * Use to create a mutable hash map from explicit entries known at the call site.
 *
 * **Example** (Creating a map from entries)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(
 *   ["key1", 42],
 *   ["key2", 100],
 *   ["key3", 200]
 * )
 *
 * console.log(MutableHashMap.get(map, "key1")) // Some(42)
 * console.log(MutableHashMap.size(map)) // 3
 * ```
 *
 * @see {@link empty} for creating an empty map
 * @see {@link fromIterable} for creating a map from an iterable of entries
 *
 * @category constructors
 * @since 2.0.0
 */
export const make: <Entries extends Array<readonly [any, any]>>(
  ...entries: Entries
) => MutableHashMap<
  Entries[number] extends readonly [infer K, any] ? K : never,
  Entries[number] extends readonly [any, infer V] ? V : never
> = (...entries) => fromIterable(entries)

/**
 * Creates a MutableHashMap from an iterable collection of key-value pairs.
 *
 * **When to use**
 *
 * Use to create a mutable hash map from an existing iterable of entries.
 *
 * **Example** (Creating a map from an iterable)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const entries = [
 *   ["apple", 1],
 *   ["banana", 2],
 *   ["cherry", 3]
 * ] as const
 *
 * const map = MutableHashMap.fromIterable(entries)
 *
 * console.log(MutableHashMap.get(map, "banana")) // Some(2)
 * console.log(MutableHashMap.size(map)) // 3
 *
 * // Works with any iterable
 * const fromMap = MutableHashMap.fromIterable(new Map([["x", 10], ["y", 20]]))
 * console.log(MutableHashMap.get(fromMap, "x")) // Some(10)
 * ```
 *
 * @see {@link make} for creating a map from explicit entries
 * @see {@link empty} for creating an empty map
 *
 * @category constructors
 * @since 2.0.0
 */
export const fromIterable = <K, V>(entries: Iterable<readonly [K, V]>): MutableHashMap<K, V> => {
  const self = empty<K, V>()
  for (const [key, value] of entries) {
    set(self, key, value)
  }
  return self
}

/**
 * Looks up a key in the `MutableHashMap` safely.
 *
 * **When to use**
 *
 * Use to safely read a `MutableHashMap` value for a key as an `Option`.
 *
 * **Details**
 *
 * Returns `Some(value)` when an equal key is present and `None` when the key is
 * absent.
 *
 * **Example** (Getting a value)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(["key1", 42], ["key2", 100])
 *
 * console.log(MutableHashMap.get(map, "key1")) // Some(42)
 * console.log(MutableHashMap.get(map, "key3")) // None
 *
 * // Pipe-able version
 * const getValue = MutableHashMap.get("key1")
 * console.log(getValue(map)) // Some(42)
 * ```
 *
 * @see {@link has} for checking only whether a key is present
 * @see {@link set} for inserting or replacing a value by key
 *
 * @category elements
 * @since 2.0.0
 */
export const get: {
  <K>(key: K): <V>(self: MutableHashMap<K, V>) => Option.Option<V>
  <K, V>(self: MutableHashMap<K, V>, key: K): Option.Option<V>
} = dual<
  <K>(key: K) => <V>(self: MutableHashMap<K, V>) => Option.Option<V>,
  <K, V>(self: MutableHashMap<K, V>, key: K) => Option.Option<V>
>(2, <K, V>(self: MutableHashMap<K, V>, key: K): Option.Option<V> => {
  if (self.backing.has(key)) {
    return Option.some(self.backing.get(key)!)
  } else if (isSimpleKey(key)) {
    return Option.none()
  }
  const refKey = referentialKeysCache.get(self)
  if (refKey !== undefined) {
    return self.backing.has(refKey) ? Option.some(self.backing.get(refKey)!) : Option.none()
  }
  const hash = Hash.hash(key)
  const bucket = self.buckets.get(hash)
  if (bucket === undefined) {
    return Option.none()
  }
  return getFromBucket(self, bucket, key)
})

const referentialKeysCache = new WeakMap<any, any>()
const isSimpleKey = (u: unknown): boolean => typeof u !== "object" && typeof u !== "function"

/**
 * Returns an iterable over the keys in the `MutableHashMap`.
 *
 * **When to use**
 *
 * Use to iterate over the keys currently stored in a mutable hash map.
 *
 * **Example** (Reading keys)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(
 *   ["apple", 1],
 *   ["banana", 2],
 *   ["cherry", 3]
 * )
 *
 * const allKeys = Array.from(MutableHashMap.keys(map))
 * console.log(allKeys) // ["apple", "banana", "cherry"]
 *
 * // Useful for iteration or validation
 * const hasRequiredKeys = allKeys.includes("apple") && allKeys.includes("banana")
 * ```
 *
 * @see {@link values} for iterating over stored values
 * @see {@link has} for checking one key without iterating
 *
 * @category elements
 * @since 3.8.0
 */
export const keys = <K, V>(self: MutableHashMap<K, V>): Iterable<K> => self.backing.keys()

/**
 * Returns an iterable over the values in the `MutableHashMap`.
 *
 * **When to use**
 *
 * Use to iterate over the values currently stored in a mutable hash map.
 *
 * **Example** (Reading values)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(
 *   ["apple", 1],
 *   ["banana", 2],
 *   ["cherry", 3]
 * )
 *
 * const allValues = Array.from(MutableHashMap.values(map))
 * console.log(allValues) // [1, 2, 3]
 *
 * // Useful for calculations
 * const total = allValues.reduce((sum, value) => sum + value, 0)
 * console.log(total) // 6
 *
 * // Filter values
 * const largeValues = allValues.filter((value) => value > 1)
 * console.log(largeValues) // [2, 3]
 * ```
 *
 * @see {@link keys} for iterating over stored keys
 *
 * @category elements
 * @since 3.8.0
 */
export const values = <K, V>(self: MutableHashMap<K, V>): Iterable<V> => self.backing.values()

const getFromBucket = <K, V>(
  self: MutableHashMap<K, V>,
  bucket: NonEmptyArray<K>,
  key: K
): Option.Option<V> => {
  for (let i = 0, len = bucket.length; i < len; i++) {
    if (Equal.equals(key, bucket[i])) {
      const refKey = bucket[i]
      referentialKeysCache.set(key, refKey)
      return Option.some(self.backing.get(refKey)!)
    }
  }
  return Option.none()
}

/**
 * Checks whether the MutableHashMap contains the specified key.
 *
 * **When to use**
 *
 * Use to test whether a key is present in a `MutableHashMap` without reading
 * its value.
 *
 * **Example** (Checking for a key)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(["key1", 42], ["key2", 100])
 *
 * console.log(MutableHashMap.has(map, "key1")) // true
 * console.log(MutableHashMap.has(map, "key3")) // false
 *
 * // Pipe-able version
 * const hasKey = MutableHashMap.has("key1")
 * console.log(hasKey(map)) // true
 * ```
 *
 * @see {@link get} for reading the value as an `Option`
 *
 * @category elements
 * @since 2.0.0
 */
export const has: {
  <K>(key: K): <V>(self: MutableHashMap<K, V>) => boolean
  <K, V>(self: MutableHashMap<K, V>, key: K): boolean
} = dual<
  <K>(key: K) => <V>(self: MutableHashMap<K, V>) => boolean,
  <K, V>(self: MutableHashMap<K, V>, key: K) => boolean
>(2, (self, key) => Option.isSome(get(self, key)))

/**
 * Sets a key-value pair in the MutableHashMap, mutating the map in place.
 * If the key already exists, its value is updated.
 *
 * **When to use**
 *
 * Use to insert a new `MutableHashMap` entry or replace an existing entry in
 * place.
 *
 * **Example** (Setting key-value pairs)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.empty<string, number>()
 *
 * // Add new entries
 * MutableHashMap.set(map, "key1", 42)
 * MutableHashMap.set(map, "key2", 100)
 *
 * console.log(MutableHashMap.get(map, "key1")) // Some(42)
 * console.log(MutableHashMap.size(map)) // 2
 *
 * // Update existing entry
 * MutableHashMap.set(map, "key1", 999)
 * console.log(MutableHashMap.get(map, "key1")) // Some(999)
 *
 * // Pipe-able version
 * const setKey = MutableHashMap.set("key3", 300)
 * setKey(map)
 * console.log(MutableHashMap.size(map)) // 3
 * ```
 *
 * @see {@link modify} for updating an existing value with a function
 * @see {@link modifyAt} for setting or removing based on the current optional value
 * @see {@link remove} for deleting an entry by key
 *
 * @category mutations
 * @since 2.0.0
 */
export const set: {
  <K, V>(key: K, value: V): (self: MutableHashMap<K, V>) => MutableHashMap<K, V>
  <K, V>(self: MutableHashMap<K, V>, key: K, value: V): MutableHashMap<K, V>
} = dual<
  <K, V>(key: K, value: V) => (self: MutableHashMap<K, V>) => MutableHashMap<K, V>,
  <K, V>(self: MutableHashMap<K, V>, key: K, value: V) => MutableHashMap<K, V>
>(3, <K, V>(self: MutableHashMap<K, V>, key: K, value: V) => {
  if (self.backing.has(key) || isSimpleKey(key)) {
    self.backing.set(key, value)
    return self
  }
  let refKey = referentialKeysCache.get(self)
  if (refKey !== undefined && self.backing.has(refKey)) {
    self.backing.set(refKey, value)
    return self
  }

  const hash = Hash.hash(key)
  const bucket = self.buckets.get(hash)
  if (bucket === undefined) {
    self.buckets.set(hash, [key])
    self.backing.set(key, value)
    return self
  }

  refKey = getRefKey(bucket, key)
  if (refKey === undefined) {
    bucket.push(key)
    refKey = key
  }
  self.backing.set(refKey, value)
  return self
})

const getRefKey = <K>(
  bucket: NonEmptyArray<K>,
  key: K
) => {
  for (let i = 0, len = bucket.length; i < len; i++) {
    if (Equal.equals(key, bucket[i])) {
      referentialKeysCache.set(key, bucket[i])
      return bucket[i]
    }
  }
}

/**
 * Updates the value of the specified key within the MutableHashMap if it exists.
 * If the key doesn't exist, the map remains unchanged.
 *
 * **When to use**
 *
 * Use to transform an existing `MutableHashMap` value in place without
 * inserting missing keys.
 *
 * **Example** (Modifying existing values)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(["count", 5], ["total", 100])
 *
 * // Increment existing value
 * MutableHashMap.modify(map, "count", (n) => n + 1)
 * console.log(MutableHashMap.get(map, "count")) // Some(6)
 *
 * // Double existing value
 * MutableHashMap.modify(map, "total", (n) => n * 2)
 * console.log(MutableHashMap.get(map, "total")) // Some(200)
 *
 * // Try to modify non-existent key (no effect)
 * MutableHashMap.modify(map, "missing", (n) => n + 1)
 * console.log(MutableHashMap.has(map, "missing")) // false
 *
 * // Pipe-able version
 * const increment = MutableHashMap.modify("count", (n: number) => n + 1)
 * increment(map)
 * ```
 *
 * @see {@link set} for inserting or replacing a value directly
 * @see {@link modifyAt} for handling both missing and existing keys
 *
 * @category mutations
 * @since 2.0.0
 */
export const modify: {
  <K, V>(key: K, f: (v: V) => V): (self: MutableHashMap<K, V>) => MutableHashMap<K, V>
  <K, V>(self: MutableHashMap<K, V>, key: K, f: (v: V) => V): MutableHashMap<K, V>
} = dual<
  <K, V>(key: K, f: (v: V) => V) => (self: MutableHashMap<K, V>) => MutableHashMap<K, V>,
  <K, V>(self: MutableHashMap<K, V>, key: K, f: (v: V) => V) => MutableHashMap<K, V>
>(3, <K, V>(self: MutableHashMap<K, V>, key: K, f: (v: V) => V) => {
  const hasKey = self.backing.has(key)
  if (hasKey || isSimpleKey(key)) {
    if (hasKey) {
      self.backing.set(key, f(self.backing.get(key)!))
    }
    return self
  }
  let refKey = referentialKeysCache.get(self)
  if (refKey !== undefined && self.backing.has(refKey)) {
    self.backing.set(refKey, f(self.backing.get(refKey)!))
    return self
  }

  const hash = Hash.hash(key)
  const bucket = self.buckets.get(hash)
  if (bucket === undefined) {
    return self
  }

  refKey = getRefKey(bucket, key)
  if (refKey === undefined) {
    return self
  }
  self.backing.set(refKey, f(self.backing.get(refKey)!))
  return self
})

/**
 * Updates or removes the specified key using a function from the current
 * optional value to the next optional value.
 *
 * **When to use**
 *
 * Use to decide whether to insert, update, or remove a key based on its current
 * optional value.
 *
 * **Example** (Updating or removing a key)
 *
 * ```ts
 * import { MutableHashMap, Option } from "effect"
 *
 * const map = MutableHashMap.make(["count", 5])
 *
 * // Update existing key
 * MutableHashMap.modifyAt(
 *   map,
 *   "count",
 *   (option) => Option.map(option, (n) => n * 2)
 * )
 * console.log(MutableHashMap.get(map, "count")) // Some(10)
 *
 * // Add new key
 * MutableHashMap.modifyAt(
 *   map,
 *   "new",
 *   (option) => Option.isNone(option) ? Option.some(42) : option
 * )
 * console.log(MutableHashMap.get(map, "new")) // Some(42)
 *
 * // Remove key by returning None
 * MutableHashMap.modifyAt(map, "count", () => Option.none())
 * console.log(MutableHashMap.has(map, "count")) // false
 *
 * // Conditional update
 * MutableHashMap.modifyAt(
 *   map,
 *   "new",
 *   (option) => Option.filter(option, (n) => n > 50) // Remove if <= 50
 * )
 * console.log(MutableHashMap.has(map, "new")) // false (42 <= 50)
 * ```
 *
 * @see {@link modify} for updating only when the key already exists
 * @see {@link set} for inserting or replacing directly
 * @see {@link remove} for deleting directly
 *
 * @category mutations
 * @since 2.0.0
 */
export const modifyAt: {
  <K, V>(key: K, f: (value: Option.Option<V>) => Option.Option<V>): (self: MutableHashMap<K, V>) => MutableHashMap<K, V>
  <K, V>(self: MutableHashMap<K, V>, key: K, f: (value: Option.Option<V>) => Option.Option<V>): MutableHashMap<K, V>
} = dual<
  <K, V>(
    key: K,
    f: (value: Option.Option<V>) => Option.Option<V>
  ) => (self: MutableHashMap<K, V>) => MutableHashMap<K, V>,
  <K, V>(
    self: MutableHashMap<K, V>,
    key: K,
    f: (value: Option.Option<V>) => Option.Option<V>
  ) => MutableHashMap<K, V>
>(3, (self, key, f) => {
  const current = get(self, key)
  const result = f(current)
  if (Option.isNone(result)) {
    if (Option.isSome(current)) {
      remove(self, key)
    }
    return self
  }
  set(self, key, result.value)
  return self
})

/**
 * Removes the specified key from the MutableHashMap, mutating the map in place.
 * If the key doesn't exist, the map remains unchanged.
 *
 * **When to use**
 *
 * Use to delete one key from a mutable hash map in place.
 *
 * **Example** (Removing a key)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(
 *   ["key1", 42],
 *   ["key2", 100],
 *   ["key3", 200]
 * )
 *
 * console.log(MutableHashMap.size(map)) // 3
 *
 * // Remove existing key
 * MutableHashMap.remove(map, "key2")
 * console.log(MutableHashMap.size(map)) // 2
 * console.log(MutableHashMap.has(map, "key2")) // false
 *
 * // Remove non-existent key (no effect)
 * MutableHashMap.remove(map, "nonexistent")
 * console.log(MutableHashMap.size(map)) // 2
 *
 * // Pipe-able version
 * const removeKey = MutableHashMap.remove("key1")
 * removeKey(map)
 * console.log(MutableHashMap.size(map)) // 1
 * ```
 *
 * @see {@link clear} for removing all entries
 * @see {@link modifyAt} for conditionally removing based on the current value
 *
 * @category mutations
 * @since 2.0.0
 */
export const remove: {
  <K>(key: K): <V>(self: MutableHashMap<K, V>) => MutableHashMap<K, V>
  <K, V>(self: MutableHashMap<K, V>, key: K): MutableHashMap<K, V>
} = dual<
  <K>(key: K) => <V>(self: MutableHashMap<K, V>) => MutableHashMap<K, V>,
  <K, V>(self: MutableHashMap<K, V>, key: K) => MutableHashMap<K, V>
>(2, <K, V>(self: MutableHashMap<K, V>, key_: K) => {
  if (isSimpleKey(key_)) {
    self.backing.delete(key_)
    return self
  }

  const key = referentialKeysCache.get(self) ?? key_
  const hash = Hash.hash(key)
  const bucket = self.buckets.get(hash)
  if (bucket === undefined) {
    return self
  }
  for (let i = 0, len = bucket.length; i < len; i++) {
    const bkey = bucket[i]
    if (bkey === key || Equal.equals(key, bkey)) {
      self.backing.delete(bkey)
      bucket.splice(i, 1)
      break
    }
  }
  if (bucket.length === 0) {
    self.buckets.delete(hash)
  }
  return self
})

/**
 * Removes all key-value pairs from the MutableHashMap, mutating the map in place.
 * The map becomes empty after this operation.
 *
 * **When to use**
 *
 * Use to empty a mutable hash map while keeping the same map instance.
 *
 * **Example** (Clearing all entries)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.make(
 *   ["key1", 42],
 *   ["key2", 100],
 *   ["key3", 200]
 * )
 *
 * console.log(MutableHashMap.size(map)) // 3
 *
 * // Clear all entries
 * MutableHashMap.clear(map)
 *
 * console.log(MutableHashMap.size(map)) // 0
 * console.log(MutableHashMap.has(map, "key1")) // false
 *
 * // Can still add new entries after clearing
 * MutableHashMap.set(map, "new", 999)
 * console.log(MutableHashMap.size(map)) // 1
 * ```
 *
 * @see {@link remove} for deleting one key
 * @see {@link empty} for creating a fresh empty map
 *
 * @category mutations
 * @since 2.0.0
 */
export const clear = <K, V>(self: MutableHashMap<K, V>) => {
  self.backing.clear()
  self.buckets.clear()
  return self
}

/**
 * Returns the number of key-value pairs in the MutableHashMap.
 *
 * **When to use**
 *
 * Use to read how many entries are currently stored in the mutable hash map.
 *
 * **Example** (Checking map size)
 *
 * ```ts
 * import { MutableHashMap } from "effect"
 *
 * const map = MutableHashMap.empty<string, number>()
 * console.log(MutableHashMap.size(map)) // 0
 *
 * MutableHashMap.set(map, "key1", 42)
 * MutableHashMap.set(map, "key2", 100)
 * console.log(MutableHashMap.size(map)) // 2
 *
 * MutableHashMap.remove(map, "key1")
 * console.log(MutableHashMap.size(map)) // 1
 *
 * MutableHashMap.clear(map)
 * console.log(MutableHashMap.size(map)) // 0
 * ```
 *
 * @see {@link isEmpty} for checking whether the map has no entries
 *
 * @category elements
 * @since 2.0.0
 */
export const size = <K, V>(self: MutableHashMap<K, V>): number => self.backing.size

/**
 * Returns `true` when the `MutableHashMap` contains no key-value pairs.
 *
 * **When to use**
 *
 * Use to branch on whether a mutable map currently has any entries.
 *
 * @see {@link size} for reading the exact number of entries
 *
 * @category predicates
 * @since 2.0.0
 */
export const isEmpty = <K, V>(self: MutableHashMap<K, V>): boolean => self.backing.size === 0

/**
 * Runs a callback for each key-value pair in the `MutableHashMap`.
 *
 * **When to use**
 *
 * Use to run a synchronous side-effecting callback for every key-value pair in
 * an existing mutable map.
 *
 * **Details**
 *
 * Iteration follows the backing map's order. The callback receives the value
 * first and the key second, matching `Map.prototype.forEach`.
 *
 * @see {@link keys} for iterating only keys
 * @see {@link values} for iterating only values
 *
 * @category traversing
 * @since 2.0.0
 */
export const forEach: {
  <K, V>(f: (value: V, key: K) => void): (self: MutableHashMap<K, V>) => void
  <K, V>(self: MutableHashMap<K, V>, f: (value: V, key: K) => void): void
} = dual(2, <K, V>(self: MutableHashMap<K, V>, f: (value: V, key: K) => void) => {
  self.backing.forEach(f)
})
