helpers.mjs

/**
 * Shallow merges two objects together. Used to pass simple options to functions.
 * Mutates the target object. Faster than deepMerge, so use when you don't need to merge nested objects or arrays.
 * 
 * @param {object} target The target object to merge into
 * @param {object} source The source object to merge from
 * @returns object The mutated target object with the source object's properties merged into it
 * @example
 * const target = { foo: 'bar' }
 * const source = { bar: 'baz' }
 * shallowMerge(target, source) // { foo: 'bar', bar: 'baz' }
 */
export function shallowMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
    target[key] = source[key]
  }

  return target
}

/**
 * Deep merge function that's mindful of arrays and objects. Mutates target object.
 * shallowMerge is faster than deepMerge, so use shallowMerge when you don't need to merge nested objects or arrays.
 * 
 * @param {object} target The target object to merge into
 * @param {object} source The source object to merge from
 * @returns object The mutated target object with the source object's properties merged into it
 * @example
 * const target = { foo: 'bar' }
 * const source = { bar: 'baz' }
 * deepMerge(target, source) // { foo: 'bar', bar: 'baz' }
 */
export function deepMerge(target, source) {
  if (isObject(source) && isObject(target)) {
    for (const key of Object.keys(source)) {
      if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
      target[key] = deepMerge(target[key], source[key])
    }
  } else if (isArray(source) && isArray(target)) {
    for (let i = 0; i < source.length; i++) {
      target[i] = deepMerge(target[i], source[i])
    }
  } else {
    target = source
  }
  return target
}

/**
 * Deep clone for data. The copy shares no mutable structure with the
 * original, the object graph survives — a value referenced twice is one
 * object in the copy too, and a cycle terminates — and prototypes are kept,
 * so a class instance clones into an instance of its class rather than a
 * plain object.
 *
 * The pair to {@link deepEqual}: whatever deepEqual reads as data, clone
 * reproduces, so `deepEqual(clone(x), x)` holds. Handles plain and
 * null-prototype objects, class instances, arrays including holes and
 * subclasses, Date, RegExp with its lastIndex, Map, Set, Error with its
 * non-enumerable message and stack, boxed primitives, ArrayBuffer, DataView
 * and typed arrays — views over one buffer clone into views over one buffer.
 *
 * **Anything it does not recognise is shared by reference, not copied** — a
 * function, a DOM node, a Promise, a WeakMap, a SharedArrayBuffer, a host
 * object. That is the difference worth having: `structuredClone` raises
 * DataCloneError on all of them, so it cannot touch an options object
 * carrying a callback or an element reference, which is most of the objects
 * a page actually holds. Reproducing a value it cannot inspect would be the
 * broken half-copy, so it shares instead — and never throws.
 *
 * Versus the field, from `bench/clone` against `structuredClone`, rfdc 1.4.1,
 * lodash 4.18.1, es-toolkit 1.50.0 and a JSON round-trip. rfdc is shown with
 * its defaults, which is what `rfdc()` gives you; `{circles: true}` buys it
 * the cycle row for 1–13%. The last four rows are semantics choices, not
 * defects in the others; the rest are data the copy either kept or lost:
 *
 * | Input | here | structuredClone | rfdc | lodash | es-toolkit | JSON |
 * |---|---|---|---|---|---|---|
 * | Date, Map, Set | cloned | cloned | cloned | cloned | cloned | `{}`, or an ISO string |
 * | RegExp | cloned | cloned | `{}` | cloned | cloned | `{}` |
 * | typed array, ArrayBuffer, DataView | cloned | cloned | `{}` | cloned | cloned | `{}` |
 * | two views over one buffer | one buffer | one buffer | lost | two buffers | two buffers | lost |
 * | cycle | terminates | terminates | stack overflow | terminates | terminates | TypeError |
 * | repeated reference | one object | one object | two objects | one object | one object | two objects |
 * | Error with an assigned `.code` | all of it | `.code` dropped | `.code` only | whole Error is `{}` | all of it | `.code` only |
 * | `undefined` value | kept | kept | kept | kept | kept | dropped |
 * | nesting 20,000 deep | RangeError | RangeError | RangeError | RangeError | RangeError | survives |
 * | function, Promise, WeakMap | shared | DataCloneError | function shared, rest lost | shared | shared | dropped |
 * | class instance | an instance | plain object | plain object | an instance | an instance | plain object |
 * | null-prototype object | kept null | gains `Object.prototype` | gains it | gains it | kept null | gains it |
 * | symbol key | kept | dropped | dropped | kept | kept | dropped |
 *
 * It recurses, so depth is bounded by the stack — 200 levels is nothing,
 * 20,000 throws. A JSON round-trip is the only column that survives that,
 * being a loop in C++, and it loses thirteen of the eighteen scored rows to
 * be there.
 *
 * The cost, on a flat eight-key object: rfdc 7.2M ops/s, this 4.0M,
 * es-toolkit 2.4M, lodash 2.0M, ramda 1.8M, a JSON round-trip 1.7M,
 * `structuredClone` 775k. rfdc leads the object shapes by 1.1–2.9×, and that
 * lead is what its defaults buy — the six rows above where the copy comes
 * back missing the data. If you clone plain acyclic JSON in a hot loop, use
 * rfdc; if you want the copy to still be the value you cloned, use this.
 *
 * Shape decides it, though, and this is not uniformly ahead. On an array of
 * 10,000 numbers es-toolkit does 14k ops/s against 3k here, with
 * `structuredClone` at 6k: a long flat run of primitives is what a per-key
 * walk is worst at and a specialised array path is best at. If that is your
 * data, this is the wrong function for it.
 *
 * Where `structuredClone` applies — pure data, no functions or nodes — it is
 * still the one to reach for if you are not already importing this: it ships
 * with the platform and costs you nothing. What it is not is reliably faster.
 * A structured clone is a serialise and a deserialise rather than a walk, so
 * it runs 1.5–5× behind on objects and roughly 2× ahead on that flat number
 * array, on top of dropping prototypes, symbol keys and an Error's own
 * properties.
 *
 * @template T
 * @param {T} o The value to clone
 * @returns {T} The cloned value; primitives, functions and symbols come back as themselves
 * @example
 * const obj = { foo: 'bar', when: new Date(0), tags: new Set(['a']) }
 * const copy = clone(obj)
 * copy.foo = 'baz'
 * console.log(obj.foo, copy.foo) // 'bar' 'baz'
 * console.log(copy.when instanceof Date, copy.tags.has('a')) // true true
 * @example
 * const options = { el: document.body, onDone: () => {} }
 * clone(options).onDone === options.onDone // => true, shared, not cloned
 * structuredClone(options) // => throws DataCloneError
 * @example
 * const cyclic = { name: 'root' }
 * cyclic.self = cyclic
 * clone(cyclic).self === clone(cyclic) // => false, but each copy's .self is itself
 */
export function clone(o) {
  if (o === null || typeof o !== 'object') return o
  return cloneCyclic(o, new WeakMap())
}

// Non-enumerable on an Error, so the own-property walk below never sees them.
// Carried with their descriptor rather than assigned, or the copy would show
// keys the original hides — and deepEqual, which walks own enumerable
// properties, would then call the two unequal.
const ERROR_PROPERTIES = ['message', 'stack', 'cause']

// Date, Map, Array and kin need their internal slot, which only their own
// constructor can create — so a subclass instance is built through the base
// and then re-pointed at the original's prototype. Object.create cannot make
// a real one.
function retarget(res, o) {
  const proto = getProto(o)
  if (getProto(res) !== proto) Object.setPrototypeOf(res, proto)
  return res
}

// Own enumerable string and symbol keys, matching what deepEqual compares and
// what structuredClone carries. Absent keys are never visited, which is how
// an array's holes stay holes.
function cloneOwn(o, res, seen) {
  const keys = Object.keys(o)
  for (let i = 0; i < keys.length; i++) {
    res[keys[i]] = cloneCyclic(o[keys[i]], seen)
  }
  const symbols = getSymbols(o)
  for (let i = 0; i < symbols.length; i++) {
    if (objPropEnumerable.call(o, symbols[i])) res[symbols[i]] = cloneCyclic(o[symbols[i]], seen)
  }
  return res
}

function cloneCyclic(o, seen) {
  if (o === null || typeof o !== 'object') return o

  // Every clone is registered before its contents are filled in, so a
  // reference back into an unfinished object resolves to the copy. This runs
  // from depth zero, unlike deepEqual's guard: sharing is observable at the
  // top level, where a cycle is not.
  const seenClone = seen.get(o)
  if (seenClone !== undefined) return seenClone

  if (Array.isArray(o)) {
    const res = new Array(o.length)
    seen.set(o, res)
    return cloneOwn(o, retarget(res, o), seen)
  }

  const proto = getProto(o)
  if (proto === objProto || proto === null) {
    const res = proto === null ? Object.create(null) : {}
    seen.set(o, res)
    return cloneOwn(o, res, seen)
  }

  const tag = objTag.call(o)
  let res = null

  switch (tag) {
    // A class instance. The prototype carries the methods so it is kept, but
    // the constructor is not re-run — anything it set in a closure or a
    // private field is not an own property and does not come along.
    case '[object Object]':
      res = Object.create(proto)
      seen.set(o, res)
      return cloneOwn(o, res, seen)
    case '[object Map]':
      res = new Map()
      seen.set(o, res)
      retarget(res, o)
      for (const [key, value] of o) res.set(cloneCyclic(key, seen), cloneCyclic(value, seen))
      return cloneOwn(o, res, seen)
    case '[object Set]':
      res = new Set()
      seen.set(o, res)
      retarget(res, o)
      for (const value of o) res.add(cloneCyclic(value, seen))
      return cloneOwn(o, res, seen)
    // Built through Error and re-pointed rather than Object.create'd: the
    // [[ErrorData]] slot is what makes Object.prototype.toString answer
    // [object Error], and only a constructor can create it. A prototype-only
    // copy reads back as a plain object to anything dispatching on the tag,
    // deepEqual included.
    case '[object Error]':
      res = retarget(new Error(), o)
      seen.set(o, res)
      for (let i = 0; i < ERROR_PROPERTIES.length; i++) {
        const property = ERROR_PROPERTIES[i]
        const descriptor = getOwnDescriptor(o, property)
        if (descriptor === undefined) continue
        // V8 hangs .stack off the instance as an accessor over an internal
        // slot; copied as an accessor it would read the clone's missing slot
        // and answer undefined, so it is read here and written as data —
        // which is the shape structuredClone produces as well.
        Object.defineProperty(res, property, {
          value: cloneCyclic(o[property], seen),
          writable: true,
          enumerable: descriptor.enumerable,
          configurable: true
        })
      }
      return cloneOwn(o, res, seen)
    case '[object Date]':
      res = new Date(o.getTime())
      break
    case '[object RegExp]':
      res = new RegExp(o.source, o.flags)
      res.lastIndex = o.lastIndex
      break
    case '[object ArrayBuffer]':
      res = o.slice(0)
      break
    case '[object DataView]':
      // No own-key walk on this or a typed array: their indices are own
      // enumerable properties, and re-assigning every byte through the
      // property path after the bytes are already copied is pure waste.
      seen.set(o, res = new DataView(cloneCyclic(o.buffer, seen), o.byteOffset, o.byteLength))
      return retarget(res, o)
    // A boxed primitive's own indices are read-only — a String object's
    // characters — so the walk would throw in strict mode. Its value is all
    // the data it has.
    case '[object Number]':
    case '[object String]':
    case '[object Boolean]':
    case '[object Symbol]':
      seen.set(o, res = Object(o.valueOf()))
      return retarget(res, o)
    default: {
      // A typed array. The constructor comes from the tag rather than from
      // o.constructor, which is a writable property any caller can point at
      // something else; retarget restores a subclass prototype after.
      const ctor = ArrayBuffer.isView(o) ? globalThis[tag.slice(8, -1)] : null
      // Everything unrecognised — DOM node, Promise, WeakMap,
      // SharedArrayBuffer, host object — is shared by reference. It is
      // returned unregistered, which costs nothing: identity is preserved by
      // being the same object.
      if (typeof ctor !== 'function') return o
      seen.set(o, res = new ctor(cloneCyclic(o.buffer, seen), o.byteOffset, o.length))
      return retarget(res, o)
    }
  }

  seen.set(o, res)
  return cloneOwn(o, retarget(res, o), seen)
}

/**
 * Deep structural equality for data. Two values are equal when they hold the
 * same data, regardless of reference identity, property order or prototype.
 *
 * Semantics, where they differ from Node's `util.isDeepStrictEqual` (which
 * browsers don't have anyway): primitives compare by SameValueZero (NaN
 * equals NaN, 0 equals -0), prototypes are ignored (a class instance equals
 * a plain object with the same own properties), and functions compare by
 * reference only — functions are not data. Handles Date, RegExp, boxed
 * primitives, Map, Set, typed arrays, ArrayBuffer/SharedArrayBuffer/DataView,
 * symbol keys and cyclic structures. WeakMap/WeakSet contents are
 * unobservable, so two distinct weak collections are never equal.
 *
 * Versus the field, probed against fast-deep-equal 3.1.3 (`/es6`), dequal
 * 2.0.3, lodash.isequal 4.5.0 and Node 25 `util.isDeepStrictEqual`. The last
 * row is a semantics choice, not a defect in the others; the rest are facts:
 *
 * | Input | here | fast-deep-equal | dequal | lodash | Node util |
 * |---|---|---|---|---|---|
 * | cyclic structure | terminates | stack overflow | stack overflow on equal graphs | terminates | terminates |
 * | `{[sym]: 1}` vs `{[sym]: 2}` | not equal | equal — symbols ignored | equal — symbols ignored | not equal | not equal |
 * | Map/Set members matched deeply | yes | no — by reference | yes | yes | yes |
 * | two invalid dates | equal | not equal | not equal | equal | equal |
 * | `NaN` inside a typed array | equal | not equal | not equal | equal | equal |
 * | distinct WeakMaps | never equal | equal | equal | never equal | never equal |
 * | cross-realm twin (iframe, vm) | equal | not equal — realm-bound constructor check | not equal | equal | not equal |
 * | URLs / Errors with different content | not equal — toString plus own props, so an Error's `.code` counts | not equal | URL yes, Error missed | not equal | not equal |
 * | class instance vs same-shape plain object | equal — data is data | not equal — constructor check | not equal | not equal | not equal |
 *
 * The cost of the guarantees is small: the cycle guard only engages past
 * recursion depth 30 (a cycle always crosses that, shallow data never pays),
 * so on plain JSON this sits within ~15% of fast-deep-equal and dequal on
 * nested documents and ~1.5× behind on tiny flat objects — where the
 * remaining gap is the symbol-key pass they skip — at ~1KB min+gzip. If you
 * compare acyclic symbol-free JSON a million times in a loop, use
 * fast-deep-equal; if you want the answer to be right on the full range of
 * inputs, use this.
 *
 * @param {*} a The first value
 * @param {*} b The second value
 * @returns boolean True when a and b are structurally equal
 * @example
 * deepEqual({ a: 1, b: [1, 2] }, { b: [1, 2], a: 1 }) // => true
 * deepEqual([1, 2], [2, 1]) // => false
 * deepEqual(new Set([1, 2]), new Set([2, 1])) // => true
 * deepEqual(NaN, NaN) // => true
 * const x = {}
 * x.self = x
 * const y = {}
 * y.self = y
 * deepEqual(x, y) // => true
 */
export function deepEqual(a, b) {
  return deepEqualCyclic(a, b, 0, null)
}

// The cycle guard engages only past this recursion depth. Sound because a
// cycle grows the recursion depth without bound, so it always crosses the
// gate and every pair below it gets tracked — while acyclic data shallower
// than this never pays the WeakMap and Set allocations.
const CYCLE_GUARD_DEPTH = 30

// Returns true when this pair is already on the comparison stack — assumed
// equal, because if the structures actually differ some pair further up the
// stack returns false, so the assumption never decides the result on its own.
// Otherwise returns the (possibly just created) tracking map.
function trackPair(a, b, seen) {
  if (seen === null) seen = new WeakMap()
  const pairs = seen.get(a)
  if (pairs) {
    if (pairs.has(b)) return true
    pairs.add(b)
  } else {
    seen.set(a, new Set([b]))
  }
  return seen
}

function sameValueZero(a, b) {
  return a === b || (a !== a && b !== b)
}

// Hoisted once — property loads off the globals on every visited pair are
// measurable on hot paths, the same reason fast-deep-equal hoists them
const objHasOwn = Object.prototype.hasOwnProperty
const objPropEnumerable = Object.prototype.propertyIsEnumerable
const objTag = Object.prototype.toString
const objProto = Object.prototype
const getProto = Object.getPrototypeOf
const getSymbols = Object.getOwnPropertySymbols
const getOwnDescriptor = Object.getOwnPropertyDescriptor

function deepEqualCyclic(a, b, depth, seen) {
  if (a === b || (a !== a && b !== b)) return true
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false

  const isArr = Array.isArray(a)
  if (isArr !== Array.isArray(b)) return false

  if (isArr) {
    if (a.length !== b.length) return false
    if (seen !== null || depth > CYCLE_GUARD_DEPTH) {
      const tracked = trackPair(a, b, seen)
      if (tracked === true) return true
      seen = tracked
    }
    for (let i = 0; i < a.length; i++) {
      if (!deepEqualCyclic(a[i], b[i], depth + 1, seen)) return false
    }
    return true
  }

  // Plain-vs-plain is the hot path — resolved on prototypes alone, no tag
  // strings. A class instance is not "plain" here yet still equals a plain
  // object with the same own properties: that pair falls through to the tag
  // path below, where both sides read [object Object] and walk as objects.
  const protoA = getProto(a)
  const protoB = getProto(b)
  if (protoA !== objProto && protoA !== null || protoB !== objProto && protoB !== null) {
    const tag = objTag.call(a)
    if (tag !== objTag.call(b)) return false

    switch (tag) {
      // valueOf unwraps boxed primitives and turns a Date into its timestamp —
      // two invalid dates are both NaN, which SameValueZero treats as equal
      case '[object Date]':
      case '[object Number]':
      case '[object Boolean]':
      case '[object String]':
        return sameValueZero(a.valueOf(), b.valueOf())
      // a boxed symbol unwraps to its primitive, which equals by reference
      // only — the same rule unboxed symbols get; toString would call two
      // distinct symbols with the same description equal
      case '[object Symbol]':
        return a.valueOf() === b.valueOf()
      case '[object RegExp]':
        return a.source === b.source && a.flags === b.flags
      // weak collection contents cannot be enumerated — equality cannot be
      // verified, so refuse to claim it
      case '[object WeakMap]':
      case '[object WeakSet]':
        return false
      // A DeepSet keeps its members in private fields, which this walk cannot
      // reach: without this line two DeepSets holding different values read as
      // two objects with no own properties and compare equal, which is the
      // silent wrong answer. Refusing is the conservative half of the same
      // rule the weak collections get — compare [...a] and [...b] to ask the
      // question this declines to answer.
      case '[object DeepSet]':
        return false
    }

    if (tag === '[object ArrayBuffer]' || tag === '[object SharedArrayBuffer]' || tag === '[object DataView]') {
      a = tag === '[object DataView]' ? new Uint8Array(a.buffer, a.byteOffset, a.byteLength) : new Uint8Array(a)
      b = tag === '[object DataView]' ? new Uint8Array(b.buffer, b.byteOffset, b.byteLength) : new Uint8Array(b)
    }

    if (ArrayBuffer.isView(a)) {
      if (a.length !== b.length) return false
      for (let i = 0; i < a.length; i++) {
        if (!sameValueZero(a[i], b[i])) return false
      }
      return true
    }

    if (tag === '[object Set]' || tag === '[object Map]') {
      if (a.size !== b.size) return false
      if (seen !== null || depth > CYCLE_GUARD_DEPTH) {
        const tracked = trackPair(a, b, seen)
        if (tracked === true) return true
        seen = tracked
      }
      const isMap = tag === '[object Map]'
      // Phase one, allocation-free: members found in b by SameValueZero. A
      // Set hit is a forced pairing — keys are SameValueZero-unique within
      // each collection. A Map hit with mismatched values is not: the
      // matching value may sit under a distinct deep-equal key, so the pair
      // defers to phase two on both sides. Only object keys defer — a
      // primitive key deep-equals nothing but its SameValueZero self, so its
      // value mismatch is final.
      let missed = null
      let deferred = null
      for (const key of a.keys()) {
        if (b.has(key)) {
          if (isMap && !deepEqualCyclic(a.get(key), b.get(key), depth + 1, seen)) {
            if (typeof key !== 'object' || key === null) return false
            if (missed === null) missed = []
            missed.push(key)
            if (deferred === null) deferred = []
            deferred.push(key)
          }
        } else {
          if (missed === null) missed = []
          missed.push(key)
        }
      }
      if (missed === null) return true
      // Phase two: the misses matched pairwise against b's leftovers by deep
      // comparison — O(n·m) when members are objects, sized for data, not
      // for bulk indexes. undefined doubles as the consumed-slot sentinel,
      // which cannot collide with an undefined member: one present on both
      // sides pairs in phase one, one present on one side only can match
      // nothing here and the size check already forces inequality.
      const unused = []
      for (const key of b.keys()) {
        if (!a.has(key)) unused.push(key)
      }
      // deferred keys exist in both maps by SameValueZero, so the !a.has
      // filter above excluded them from b's leftovers — they re-enter here
      // or phase two could never re-pair them
      if (deferred !== null) {
        for (let i = 0; i < deferred.length; i++) unused.push(deferred[i])
      }
      outer: for (let i = 0; i < missed.length; i++) {
        for (let j = 0; j < unused.length; j++) {
          if (unused[j] === undefined) continue
          if (!deepEqualCyclic(missed[i], unused[j], depth + 1, seen)) continue
          if (isMap && !deepEqualCyclic(a.get(missed[i]), b.get(unused[j]), depth + 1, seen)) continue
          unused[j] = undefined
          continue outer
        }
        return false
      }
      return true
    }

    // Host objects that stringify themselves — URL, Error and kin — carry
    // their data in toString, not in own enumerable properties; the walk
    // below alone would call any two of them equal. toString gates first,
    // then the walk still runs, because an Error's data can outgrow its
    // string form — .code assigned on a Node error is an own enumerable
    // property toString never shows. Exotic tags only: a plain
    // [object Object] never reaches here, so a class instance with a custom
    // toString still equals its plain-object twin structurally.
    if (tag !== '[object Object]' && typeof a.toString === 'function' && a.toString !== objTag) {
      if (a.toString() !== b.toString()) return false
    }
  }

  if (seen !== null || depth > CYCLE_GUARD_DEPTH) {
    const tracked = trackPair(a, b, seen)
    if (tracked === true) return true
    seen = tracked
  }

  const keys = Object.keys(a)
  if (keys.length !== Object.keys(b).length) return false
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const valueB = b[key]
    if (!deepEqualCyclic(a[key], valueB, depth + 1, seen)) return false
    // hasOwnProperty deferred to the one ambiguous case: an undefined read
    // that could be either an own undefined or a missing key
    if (valueB === undefined && !objHasOwn.call(b, key)) return false
  }

  const symbolsA = getSymbols(a)
  const symbolsB = getSymbols(b)
  if (symbolsA.length > 0 || symbolsB.length > 0) {
    let countA = 0
    for (let i = 0; i < symbolsA.length; i++) {
      if (objPropEnumerable.call(a, symbolsA[i])) countA++
    }
    let countB = 0
    for (let i = 0; i < symbolsB.length; i++) {
      if (objPropEnumerable.call(b, symbolsB[i])) countB++
    }
    if (countA !== countB) return false
    for (let i = 0; i < symbolsA.length; i++) {
      const symbol = symbolsA[i]
      if (!objPropEnumerable.call(a, symbol)) continue
      if (!objPropEnumerable.call(b, symbol)) return false
      if (!deepEqualCyclic(a[symbol], b[symbol], depth + 1, seen)) return false
    }
  }

  return true
}

// The contract that keeps dedupe() exact while its hash stays cheap: the fold
// may merge values deepEqual tells apart — the in-bucket deepEqual pass
// separates those — but must never split values deepEqual calls equal, or a
// duplicate lands in a fresh bucket and survives. Every branch below is
// written against that one-way rule.
const FNV_SEED = 2166136261
// Past this depth everything folds to one token: cycles terminate, and two
// equal values truncate at the same node, so the cap cannot split them.
const FOLD_DEPTH_CAP = 32

// Scratch pair for reading a double's bits; safe to share because it is
// written and read back before any recursive call can touch it.
const foldF64 = new Float64Array(1)
const foldU32 = new Uint32Array(foldF64.buffer)

function foldString(h, s) {
  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619)
  return h
}

// SameValueZero folds: every NaN one token, -0 folded as 0 — anything finer
// would split pairs sameValueZero calls equal
function foldNumber(h, n) {
  if (n !== n) return Math.imul(h ^ 0x7A3, 16777619)
  foldF64[0] = n === 0 ? 0 : n
  h = Math.imul(h ^ foldU32[0], 16777619)
  return Math.imul(h ^ foldU32[1], 16777619)
}

function foldValue(h, v, depth) {
  if (v === null) return Math.imul(h ^ 1, 16777619)
  const t = typeof v
  if (t === 'number') return foldNumber(Math.imul(h ^ 2, 16777619), v)
  if (t === 'string') return foldString(Math.imul(h ^ 3, 16777619), v)
  if (t === 'boolean') return Math.imul(h ^ (v ? 4 : 5), 16777619)
  if (t === 'bigint') return foldString(Math.imul(h ^ 6, 16777619), String(v))
  // undefined, symbol, function — deepEqual equates these by reference only,
  // so one token per type is coarse but never wrong
  if (t !== 'object') return Math.imul(h ^ 7, 16777619)
  if (depth > FOLD_DEPTH_CAP) return Math.imul(h ^ 8, 16777619)
  if (Array.isArray(v)) {
    h = Math.imul(h ^ 9, 16777619)
    for (let i = 0; i < v.length; i++) h = foldValue(h, v[i], depth + 1)
    return Math.imul(h ^ 10, 16777619)
  }
  const proto = getProto(v)
  if (proto !== objProto && proto !== null) {
    const tag = objTag.call(v)
    // A class instance keeps the [object Object] tag and falls through to the
    // plain walk: deepEqual calls it equal to its plain twin, so it has to
    // fold like one.
    if (tag !== '[object Object]') {
      h = foldString(h, tag)
      switch (tag) {
        case '[object Date]':
        case '[object Number]':
          return foldNumber(h, v.valueOf())
        case '[object String]':
          return foldString(h, v.valueOf())
        case '[object Boolean]':
          return Math.imul(h ^ (v.valueOf() ? 4 : 5), 16777619)
        case '[object RegExp]':
          return foldString(foldString(h, v.source), v.flags)
        // Members fold commutatively — each hashed from the seed, the results
        // summed — because deepEqual matches Set members and Map entries in
        // any order, and an order-sensitive fold would split two equal
        // collections. Deep-equal members fold identically, so equal
        // collections reach the same sum. Every member is folded and never a
        // prefix: a prefix follows iteration order, which is order-sensitive
        // again. Summed, not XORed — a Set may hold two distinct members that
        // are deepEqual, and XOR would cancel that pair into the hash of a Set
        // without either.
        case '[object Set]': {
          h = foldNumber(h, v.size)
          let sum = 0
          for (const m of v) sum = (sum + foldValue(FNV_SEED, m, depth + 1)) >>> 0
          return foldNumber(h, sum)
        }
        case '[object Map]': {
          h = foldNumber(h, v.size)
          let sum = 0
          // The key is mixed into the value asymmetrically, so Map{a → b} and
          // Map{b → a} do not reach the same entry hash.
          for (const [k, val] of v) {
            const pair = (Math.imul(foldValue(FNV_SEED, k, depth + 1), 0x9E3779B1) ^ foldValue(FNV_SEED, val, depth + 1)) >>> 0
            sum = (sum + pair) >>> 0
          }
          return foldNumber(h, sum)
        }
      }
      if (ArrayBuffer.isView(v)) return foldNumber(h, v.byteLength)
      // URL, Error, ArrayBuffer and other exotic hosts fold by tag alone —
      // coarse buckets the in-bucket deepEqual resolves
      return h
    }
  }
  // symbol keys are not folded — near-twins split inside the bucket instead
  const keys = Object.keys(v).sort()
  h = Math.imul(h ^ 11, 16777619)
  for (let i = 0; i < keys.length; i++) {
    h = foldString(h, keys[i])
    h = foldValue(h, v[keys[i]], depth + 1)
  }
  return Math.imul(h ^ 12, 16777619)
}

/**
 * Removes structural duplicates from an array — deepEqual decides what a
 * duplicate is, so property order, prototype and reference identity don't
 * matter, contents do. The first occurrence of every distinct value is kept,
 * in order, and the input array is left untouched.
 *
 * The platform's `new Set(arr)` dedupes by reference identity and lodash's
 * `uniqWith(arr, isEqual)` compares every pair — O(N²). Here every value
 * folds to a 32-bit FNV-1a hash in a single walk, values collide into
 * buckets, and deepEqual runs only within a bucket: linear in practice. The
 * hash is free to be coarse — a shared bucket costs one comparison, while
 * correctness comes from deepEqual alone.
 *
 * Sets and Maps fold their members too, commutatively, so insertion order
 * never reaches the hash and equal collections still land in one bucket. The
 * bill is a walk per member on every collection: a pile of large Sets costs
 * more to fold than a pile of small documents, and a single pair of them is a
 * job for {@link deepEqual} rather than for this. The bucket-then-verify idea is
 * HashCache (2013,
 * https://stamat.wordpress.com/2013/07/03/javascript-quickly-find-very-large-objects-in-a-large-array/)
 * with the CRC32-over-canonical-string hash replaced by a fold over the live
 * values — no string is ever built.
 *
 * @param {Array} arr The array to dedupe
 * @returns {Array} A new array: first occurrence of every distinct value, in order
 * @example
 * dedupe([{ a: 1, b: 2 }, { b: 2, a: 1 }]) // => [{ a: 1, b: 2 }]
 * dedupe([NaN, NaN, 0, -0]) // => [NaN, 0]
 * dedupe([new Set([1, 2]), new Set([2, 1])]) // => [new Set([1, 2])]
 */
export function dedupe(arr) {
  const set = new DeepSet()
  for (let i = 0; i < arr.length; i++) set.add(arr[i])
  return [...set]
}

/**
 * A Set that decides membership by structure rather than by reference, so
 * `has` answers the question `new Set()` cannot: is a value like this one
 * already in here? {@link deepEqual} defines "like", exactly as it does for
 * {@link dedupe}, and the two share one bucket-then-verify pass — a DeepSet is
 * that pass kept instead of thrown away.
 *
 * Reach for it when the same unchanging pile is asked about repeatedly.
 * `arr.some(x => deepEqual(x, value))` beats it outright for a single lookup:
 * a fold has to read the whole value before it can say a word, where deepEqual
 * abandons most candidates after a key or two. Building the index only pays
 * back across queries, from around thirty of them, and that figure barely
 * moves between a thousand values and a hundred thousand — build and scan both
 * scale with the pile, so their ratio does not.
 * `bench/dedupe/membership.bench.mjs` regenerates it.
 *
 * **A value must not be mutated while it is in here.** Membership is decided
 * by contents, so changing a value changes the bucket it should live in while
 * it sits in the old one, and it becomes unfindable — by `has`, and by its own
 * reference. `new Set()` has no such rule, because reference identity survives
 * mutation and structure does not. Add copies if the originals move.
 *
 * Insertion order is preserved and the first occurrence of every distinct
 * value is the one kept, so `[...new DeepSet(arr)]` is `dedupe(arr)`. Values
 * are held as given, `-0` included — the platform's `Set` normalises that to
 * `0` and this does not, which is why an array backs the order rather than a
 * `Set`.
 *
 * Two DeepSets are never {@link deepEqual} to each other: their members live in
 * private fields a structural walk cannot reach, and refusing is better than
 * the wrong answer that walk would otherwise give. Compare `[...a]` and
 * `[...b]`.
 *
 * No `delete` or `clear`: insertion order is an array, so removal would be
 * linear in the size of the set rather than the O(1) the name implies, and
 * nothing has needed it yet. Build, then query.
 *
 * @example
 * const seen = new DeepSet([{ a: 1 }, { b: 2 }])
 * seen.has({ a: 1 })          // => true — a different object, same structure
 * seen.has({ a: 2 })          // => false
 * seen.add({ b: 2 }).size     // => 2 — already present, not added again
 * [...new DeepSet([{ a: 1 }, { a: 1 }])]  // => [{ a: 1 }]
 */
export class DeepSet {
  // hash → the values that folded to it, usually one. deepEqual settles a
  // shared bucket; the hash never decides anything on its own.
  #buckets = new Map()
  // Insertion order, and the only place a value is held exactly as given.
  // Deliberately not a Set: that would normalise -0 to 0 and hand back a value
  // the caller never passed in.
  #values = []

  /**
   * @param {Iterable} [values] Values to add, in order; duplicates are dropped
   */
  constructor(values) {
    if (values === undefined || values === null) return
    for (const value of values) this.add(value)
  }

  /**
   * @returns {number} How many structurally distinct values are held
   */
  get size() {
    return this.#values.length
  }

  /**
   * @param {*} value The value to look for
   * @returns {boolean} True when a structurally equal value is already held
   */
  has(value) {
    const bucket = this.#buckets.get(foldValue(FNV_SEED, value, 0) >>> 0)
    if (bucket === undefined) return false
    for (let i = 0; i < bucket.length; i++) {
      if (deepEqual(bucket[i], value)) return true
    }
    return false
  }

  /**
   * Adds a value unless one structurally equal to it is already held, in which
   * case the one already here stays and this call changes nothing.
   *
   * @param {*} value The value to add
   * @returns {DeepSet} This set, so calls chain
   */
  add(value) {
    const key = foldValue(FNV_SEED, value, 0) >>> 0
    const bucket = this.#buckets.get(key)
    if (bucket === undefined) {
      this.#buckets.set(key, [value])
      this.#values.push(value)
      return this
    }
    for (let i = 0; i < bucket.length; i++) {
      if (deepEqual(bucket[i], value)) return this
    }
    bucket.push(value)
    this.#values.push(value)
    return this
  }

  /**
   * @returns {Iterator} The held values, in the order they were added
   */
  [Symbol.iterator]() {
    return this.#values[Symbol.iterator]()
  }

  // Gives instances the [object DeepSet] tag, which is what lets deepEqual
  // refuse them by name rather than walking them and finding nothing.
  get [Symbol.toStringTag]() {
    return 'DeepSet'
  }
}

/**
 * Check if an object is empty
 *
 * @param {object} o The object to check
 * @returns boolean True if the object is empty, false otherwise
 * @example
 * isEmptyObject({}) // => true
 * isEmptyObject({ foo: 'bar' }) // => false
 */
export function isEmptyObject(o) {
  return Object.keys(o).length === 0
}

/**
 * Check if an array is empty, substitute for Array.length === 0
 * 
 * @param {Array} o The array to check
 * @returns boolean True if the array is empty, false otherwise
 * @example
 * isEmptyArray([]) // => true
 * isEmptyArray([1, 2, 3]) // => false
 */
export function isEmptyArray(o) {
  return o.length === 0
}

/**
 * Check if a variable is empty
 * 
 * @param {any} o The variable to check
 * @returns boolean True if the variable is empty, false otherwise
 * @example
 * isEmpty({}) // => true
 * isEmpty([]) // => true
 * isEmpty('') // => true
 * isEmpty(null) // => false
 * isEmpty(undefined) // => false
 * isEmpty(0) // => false
 */
export function isEmpty(o) {
  if (isObject(o)) {
    return isEmptyObject(o)
  } else if (isArray(o)) {
    return isEmptyArray(o)
  } else if (isString(o)) {
    return o === ''
  }
  return false
}

/**
 * Try to convert a string to a boolean
 * 
 * @param {string} str The string to convert
 * @returns boolean The converted boolean or undefined if conversion failed
 * @example
 * stringToBoolean('true') // => true
 * stringToBoolean('false') // => false
 * stringToBoolean('foo') // => undefined
 */
export function stringToBoolean(str) {
  if (/^\s*(true|false)\s*$/i.test(str)) return str.trim().toLowerCase() === 'true'
}

/**
 * Try to convert a string to a number
 * 
 * @param {string} str The string to convert
 * @returns number The converted number or undefined if conversion failed
 * @example
 * stringToNumber('1') // => 1
 * stringToNumber('1.5') // => 1.5
 * stringToNumber('foo') // => undefined
 * stringToNumber('1foo') // => undefined
 */
export function stringToNumber(str) {
  if (/^\s*-?\d+\s*$/.test(str)) return parseInt(str)
  if (/^\s*-?\d+\.\d+\s*$/.test(str)) return parseFloat(str)
}

/**
 * Try to convert a string to an array
 * 
 * @param {string} str The string to convert
 * @returns array The converted array or undefined if conversion failed
 * @example
 * stringToArray('[1, 2, 3]') // => [1, 2, 3]
 * stringToArray('foo') // => undefined
 * stringToArray('1') // => undefined
 * stringToArray('{"foo": "bar"}') // => undefined
 */
export function stringToArray(str) {
  if (!/^\s*\[.*\]\s*$/.test(str)) return
  try {
    return JSON.parse(str)
  } catch (e) {}
}

/**
 * Try to convert a string to an object
 * 
 * @param {string} str The string to convert
 * @returns object The converted object or undefined if conversion failed
 * @example
 * stringToObject('{ "foo": "bar" }') // => { foo: 'bar' }
 * stringToObject('foo') // => undefined
 * stringToObject('1') // => undefined
 * stringToObject('[1, 2, 3]') // => undefined
 */
export function stringToObject(str) {
  if (!/^\s*\{.*\}\s*$/.test(str)) return
  try {
    return JSON.parse(str)
  } catch (e) {}
}

/**
 * Try to convert a string to a regex
 * 
 * @param {string} str The string to convert
 * @returns regex The converted regex or undefined if conversion failed
 * @example
 * stringToRegex('/foo/i') // => /foo/i
 * stringToRegex('foo') // => undefined
 * stringToRegex('1') // => undefined
 */
export function stringToRegex(str) {
  if (typeof str !== 'string') return
  const match = str.match(/^\s*\/(.*?)\/([gimsuy]*)\s*$/)
  if (!match) return
  try {
    return new RegExp(match[1], match[2])
  } catch (e) {}
}

/**
 * Try to convert a string to a primitive
 * 
 * @param {string} str The string to convert
 * @returns {null|boolean|int|float|string} The converted primitive or input string if conversion failed
 * @example
 * stringToPrimitive('null') // => null
 * stringToPrimitive('true') // => true
 * stringToPrimitive('false') // => false
 * stringToPrimitive('1') // => 1
 * stringToPrimitive('1.5') // => 1.5
 * stringToPrimitive('foo') // => 'foo'
 * stringToPrimitive('1foo') // => '1foo'
 */
export function stringToPrimitive(str) {
  if (/^\s*null\s*$/.test(str)) return null
  const bool = stringToBoolean(str)
  if (bool !== undefined) return bool
  return stringToNumber(str) ?? str
}

/**
 * Try to convert a string to a data type
 * 
 * @param {string} str The string to convert
 * @returns any The converted data type or input string if conversion failed
 * @example
 * stringToData('null') // => null
 * stringToData('true') // => true
 * stringToData('false') // => false
 * stringToData('1') // => 1
 * stringToData('1.5') // => 1.5
 * stringToData('foo') // => 'foo'
 * stringToData('1foo') // => '1foo'
 * stringToData('[1, 2, 3]') // => [1, 2, 3]
 * stringToData('{ "foo": "bar" }') // => { foo: 'bar' }
 * stringToData('/foo/i') // => /foo/i
 */
export function stringToType(str) {
  if (/^\s*null\s*$/.test(str)) return null
  const bool = stringToBoolean(str)
  if (bool !== undefined) return bool
  return stringToNumber(str) ?? stringToArray(str) ?? stringToObject(str) ?? stringToRegex(str) ?? str
}

/**
 * If provided variable is an object
 * 
 * @param {any} o 
 * @returns boolean
 * @example
 * isObject({}) // => true
 * isObject([]) // => false
 * isObject(null) // => false
 */
export function isObject(o) {
  return typeof o === 'object' && !Array.isArray(o) && o !== null
}

/**
 * If provided variable is an array. Just a wrapper for Array.isArray
 * 
 * @param {any} o
 * @returns boolean
 * @example
 * isArray([]) // => true
 * isArray({}) // => false
 */
export function isArray(o) {
  return Array.isArray(o)
}

/**
 * If provided variable is a string. Just a wrapper for typeof === 'string'
 * 
 * @param {any} o
 * @returns boolean
 * @example
 * isString('foo') // => true
 * isString({}) // => false
 */
export function isString(o) {
  return typeof o === 'string'
}

/**
 * If provided variable is a function, substitute for typeof === 'function'
 * 
 * @param {any} o
 * @returns boolean
 * @example
 * isFunction(function() {}) // => true
 * isFunction({}) // => false
 */
export function isFunction(o) {
  return typeof o === 'function'
}

/**
 * If object property is a function
 * 
 * @param {object} obj
 * @param {string} propertyName
 * @returns boolean
 * @example
 * const obj = { foo: 'bar', baz: function() {} }
 * propertyIsFunction(obj, 'foo') // => false
 * propertyIsFunction(obj, 'baz') // => true
 */
export function propertyIsFunction(obj, propertyName) {
  return obj.hasOwnProperty(propertyName) && isFunction(obj[propertyName])
}

/**
 * If object property is a string
 * 
 * @param {object} obj
 * @param {string} propertyName
 * @returns boolean
 * @example
 * const obj = { foo: 'bar', baz: function() {} }
 * propertyIsString(obj, 'foo') // => true
 * propertyIsString(obj, 'baz') // => false
 */
export function propertyIsString(obj, propertyName) {
  return obj.hasOwnProperty(propertyName) && isString(obj[propertyName])
}

/**
 * Transforms a dash separated string to camelCase
 *
 * @param {string} str
 * @returns boolean
 * @example
 * transformDashToCamelCase('foo-bar') // => 'fooBar'
 * transformDashToCamelCase('foo-bar-baz') // => 'fooBarBaz'
 * transformDashToCamelCase('foo') // => 'foo'
 * transformDashToCamelCase('fooBarBaz-qux') // => 'fooBarBazQux'
 */
export function transformDashToCamelCase(str) {
  return str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase() });
}

/**
 * Transforms a camelCase string to dash separated string
 * 
 * @param {string} str
 * @returns boolean
 * @example
 * transformCamelCaseToDash('fooBar') // => 'foo-bar'
 * transformCamelCaseToDash('fooBarBaz') // => 'foo-bar-baz'
 * transformCamelCaseToDash('foo') // => 'foo'
 * transformDashToCamelCase('fooBarBaz-qux') // => 'foo-bar-baz-qux'
 */
export function transformCamelCaseToDash(str) {
  return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}

/**
 * Maps an array of objects by a property name
 * 
 * @param {Array} arr
 * @param {string} propertyName
 * @returns object
 * @example
 * const arr = [{ foo: 'bar' }, { foo: 'baz' }]
 * mapByProperty(arr, 'foo') // => { bar: { foo: 'bar' }, baz: { foo: 'baz' } }
 */
export function mapByProperty(arr, propertyName) {
  const res = {}
  for (let i = 0; i < arr.length; i++) {
    res[arr[i][propertyName]] = arr[i]
  }
  return res
}

/**
 * Maps an array of objects by a property name to another property name
 * 
 * @param {Array} arr
 * @param {string} keyPropertyName
 * @param {string} valuePropertyName
 * @returns object
 * @example
 * const arr = [{ foo: 'bar', baz: 'qux' }, { foo: 'quux', baz: 'corge' }]
 * mapPropertyToProperty(arr, 'foo', 'baz') // => { bar: 'qux', quux: 'corge' }
 */
export function mapPropertyToProperty(arr, keyPropertyName, valuePropertyName) {
  const res = {}
  for (let i = 0; i < arr.length; i++) {
    res[arr[i][keyPropertyName]] = arr[i][valuePropertyName]
  }
  return res
}

/** The letters NFKD cannot help with, because they carry no separable combining mark: the
 * ones written through the glyph (`Đ`, `Ø`, `Ŧ`), the ones that are two letters wearing one
 * (`Æ`, `ß`, `Þ`), and Turkish `ı`, which is a letter in its own right rather than an `i`
 * missing something. Serbian `đ` is the one that matters here; the rest are the same shape
 * of problem next door.
 *
 * Deliberately stops at the living orthographies. The IPA and Africanist letters — `Ɔ`, `Ɛ`,
 * `Ʃ`, `Ʒ` and the click letters — survive whole, because in the texts they appear in the
 * letter *is* the content and folding it to `O`, `E`, `S`, `3` would destroy the word. */
const PLAIN = {
  Æ: 'AE', æ: 'ae', Œ: 'OE', œ: 'oe', ß: 'ss', ẞ: 'SS', Þ: 'TH', þ: 'th',
  Đ: 'D', đ: 'd', Ð: 'D', ð: 'd', Ø: 'O', ø: 'o', Ł: 'L', ł: 'l', Ŀ: 'L', ŀ: 'l',
  Ħ: 'H', ħ: 'h', Ŧ: 'T', ŧ: 't', Ǥ: 'G', ǥ: 'g', Ŋ: 'N', ŋ: 'n', ı: 'i'
}

/** Runs *before* NFKD, and the order is the point: `Ŀ` has a compatibility decomposition to
 * `L` + `·`, so after NFKD there is no `Ŀ` left to match and the middle dot survives into the
 * output as a stray character. Every replacement is ASCII, so decomposing afterwards has
 * nothing left to do to them. */
const PLAIN_RE = new RegExp(`[${Object.keys(PLAIN).join('')}]`, 'g')

/**
 * A string flattened to plain Latin: the accents taken off, the letters that are two letters
 * wearing one glyph spelled out, and the stroked letters given their plain form. Case is
 * preserved — `.toLowerCase()` afterwards if a comparison needs it.
 *
 * Two mechanisms, because Unicode offers no single one. NFKD decomposes and the combining
 * marks are dropped, which handles `é` and `ź` and unpacks the ligatures and digraphs that
 * have a compatibility decomposition — `fi`, `DŽ`, `LJ`, `IJ`. What is left has no separable
 * mark to strip and is mapped by hand: without that pass `removeAccents('Đorđe')` returns
 * `Đorđe` unchanged, and a `[^\w]` filter downstream then deletes the letter outright rather
 * than plainifying it.
 *
 * Only the scripts that share the Latin alphabet are touched. Cyrillic, Greek, Arabic and
 * CJK come through whole, because mapping those to Latin is transliteration — a different
 * job, answered per language rather than per character.
 *
 * @param {string} inputString
 * @returns string
 * @example
 * removeAccents('áéíóú') // => 'aeiou'
 * removeAccents('ÁÉÍÓÚ') // => 'AEIOU'
 * removeAccents('señor') // => 'senor'
 * removeAccents('Crème Brûlée') // => 'Creme Brulee'
 * removeAccents('Đorđe') // => 'Dorde'
 * removeAccents('Łódź') // => 'Lodz'
 * removeAccents('Nørrebro') // => 'Norrebro'
 * removeAccents('Æ') // => 'AE'
 * removeAccents('Œ') // => 'OE'
 * removeAccents('ß') // => 'ss'
 * removeAccents('Straẞe') // => 'Strasse'
 * removeAccents('Þingvellir') // => 'THingvellir'
 * removeAccents('DŽungla') // => 'DZungla'
 * removeAccents('fi') // => 'fi'
 * removeAccents('Београд') // => 'Београд', a script with no Latin in it is left alone
 */
export function removeAccents(inputString) {
  return inputString.replace(PLAIN_RE, (c) => PLAIN[c]).normalize('NFKD').replace(/[\u0300-\u036f]/g, '').normalize('NFC')
}

/**
 * Whether a label answers what has been typed — the match a filtering list needs.
 *
 * "Contains" and not "starts with": the reader looking through a list of cities for `york`
 * knows New York is not spelled that way and is asking for the one word they remember. Both
 * sides go through `removeAccents` and lower case, so a search typed with diacritics and one
 * typed without both land on the same labels.
 *
 * An empty search matches everything, which is what makes an unfiltered list the same code
 * path as a filtered one.
 *
 * Not `slugify`, further down this same file, which starts the same way and then keeps
 * going: that one is for URLs, so it drops everything outside `[\w0-9-]` and leaves
 * `Београд` and `北京` as empty strings. A search box that cannot find a Cyrillic city on a
 * Serbian site is not a smaller bug than one that cannot fold an accent.
 *
 * @param {string} label
 * @param {string} search
 * @returns {boolean}
 * @example
 * matchesSearch('New York', 'york') // => true
 * matchesSearch('Šipka', 'sipka') // => true
 * matchesSearch('Lemon', '') // => true
 * matchesSearch('New York', 'boston') // => false
 */
export function matchesSearch(label, search) {
  const needle = removeAccents(search.trim()).toLowerCase()
  return needle === '' || removeAccents(label).toLowerCase().includes(needle)
}

/**
 * Strip HTML tags from a string
 * 
 * @param {string} inputString
 * @returns string
 * @example
 * stripHTMLTags('<span>foo</span>') // => 'foo'
 * stripHTMLTags('<span>foo</span> <span>bar</span>') // => 'foo bar'
 */
export function stripHTMLTags(inputString) {
  return inputString.replace(/<[^>]*>/g, '')
}

/**
 * Slugify a string, e.g. 'Foo Bar' => 'foo-bar'. Similar to WordPress' sanitize_title(). Will remove accents and HTML tags.
 *
 * Anything outside `[\w0-9-]` is dropped, so a script with no Latin in it — `Београд`, `北京`
 * — slugifies to the empty string. That is the job: a slug is for a URL. `matchesSearch` is
 * the one for a search box, which keeps those scripts whole.
 *
 * @param {string} str
 * @returns string
 * @example
 * slugify('Foo Bar') // => 'foo-bar'
 * slugify('Foo Bar <span>baz</span>') // => 'foo-bar-baz'
 * slugify('Đorđe Balašević') // => 'dorde-balasevic'
 * slugify('Łódź') // => 'lodz'
 */
export function slugify(str) {
  str = removeAccents(str.trim()).toLowerCase()
  str = stripHTMLTags(str)
  return str.replace(/\s+|\.+|\/+|\\+|—+|–+/g, '-').replace(/[^\w0-9\-]+/g, '').replace(/-{2,}/g, '-').replace(/^-|-$/g, '')
}

/**
 * Humanize a slug, e.g. 'foo-bar' => 'Foo Bar'. Opposite of slugify. Replaces dashes and underscores with spaces and applies the chosen casing.
 *
 * @param {string} str
 * @param {'title'|'sentence'|'upper'|'lower'} [casing='title'] Casing to apply: 'title' capitalizes each word, 'sentence' capitalizes the first word only, 'upper' converts to upper case, 'lower' converts to lower case
 * @returns string
 * @example
 * humanize('foo-bar') // => 'Foo Bar'
 * humanize('foo_bar-baz') // => 'Foo Bar Baz'
 * humanize('foo-bar', 'sentence') // => 'Foo bar'
 * humanize('foo-bar', 'upper') // => 'FOO BAR'
 * humanize('Foo-Bar', 'lower') // => 'foo bar'
 */
export function humanize(str, casing = 'title') {
  str = str.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim()
  switch (casing) {
    case 'upper': return str.toUpperCase()
    case 'lower': return str.toLowerCase()
    case 'sentence': return str.charAt(0).toUpperCase() + str.slice(1)
    default: return str.replace(/\b\w/g, c => c.toUpperCase())
  }
}

/**
 * Check if object has multiple properties
 * 
 * @param {object} obj
 * @param {string|array} properties
 * @returns boolean
 * @example
 * const obj = { foo: 'bar', baz: 'qux' }
 * hasOwnProperties(obj, ['foo', 'baz']) // => true
 * hasOwnProperties(obj, ['foo', 'baz', 'qux']) // => false
 */
export function hasOwnProperties(obj, properties) {
  if(!isArray(properties)) properties = [properties]
  for (let i = 0; i < properties.length; i++) {
    if (!obj.hasOwnProperty(properties[i])) return false
  }
  return true
}

/**
 * Finds the closest number to the set goal in an array to a given number
 * 
 * @param {number} goal Number to search for
 * @param {Array} arr Array of numbers to search in
 * @returns number
 * @example
 * closestNumber(10, [1, 2, 3, 4, 5, 6, 7, 8, 9]) // => 9
 * closestNumber(10, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]) // => 9
 * closestNumber(10, [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 9.5]) // => 9.5
 * closestNumber(10, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) // => 10
 */
export function closestNumber(goal, arr) {
  return arr.reduce(function(prev, curr) {
    return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev
  })
}

/**
 * Truncate a string to a given number of words
 * 
 * @param {string} str String to truncate
 * @param {number} numWords Number of words to truncate to
 * @param {string} ellipsis Ellipsis to append to the end of the string
 * @returns string
 * @example
 * truncateString('foo bar baz', 2) // => 'foo bar…'
 * truncateString('foo bar baz', 2, '...') // => 'foo bar...'
 * truncateString('foo bar. baz', 2, '...') // => 'foo bar. ...'
 */
export function truncateString(str, numWords, ellipsis = '…') {
  const words = str.trim().split(' ')
  if (words.length <= numWords) return str
  if (numWords <= 0) return ''
  if (/[.?!]$/.test(words[numWords - 1]) && ellipsis.trim() !== '') ellipsis = ` ${ellipsis}`
  return words.slice(0, numWords).join(' ') + ellipsis
}

/**
 * Generates a random integer between two values, inclusive of both
 * 
 * @param {number} min Minimum value
 * @param {number} max Maximum value
 * @param {boolean} safe Defaults to false, if true will use a cryptographically secure random number generator
 * @returns number
 * @example
 * randomIntInclusive(1, 10) // => 1
 * randomIntInclusive(1, 10) // => 10
 * randomIntInclusive(1, 10) // => 5
 */
export function randomIntInclusive(min, max, safe = false) {
  min = Number(min)
  max = Number(max)
  if (isNaN(min) || isNaN(max)) throw new TypeError('Both min and max must be numbers')
  if (min > max) [min, max] = [max, min]
  min = Math.round(min)
  max = Math.round(max)
  if (min === max) return min
  const rand = safe ? random() : Math.random()
  return Math.floor(rand * (max - min + 1)) + min
}

/**
 * Gets fixed number of digits after the decimal point
 * 
 * @param {number} number Number to fix
 * @param {number} digits Number of digits to fix to
 * @returns number
 * @example
 * fixed(1.234, 2) // => 1.23
 * fixed(1.235, 2) // => 1.24
 * fixed(1.234) // => 1
 * fixed(1.234, 0) // => 1
 * fixed(1.234, 5) // => 1.234
 * @note Gotta ask myself why I wrote this function in the first place... 🤔 It's just not useful in a lot of cases lol...
 */
export function fixed(number, digits) {
  if (!digits) return parseInt(number)
  return parseFloat(number.toFixed(digits))
}

/**
 * Calculates the percentage of a number in relation to another number
 * 
 * @param {number} num Number to calculate percentage of
 * @param {number} total Total number
 * @returns number
 * @example
 * percentage(1, 10) // => 10
 * percentage(5, 10) // => 50
 * percentage(10, 10) // => 100
 * percentage(0, 10) // => 0
 * percentage(10, 2) // => 500
 */
export function percentage(num, total) {
  if (Number.isNaN(num) || Number.isNaN(total) || total === 0) return 0
  return num / total * 100
}

/**
 * Pick properties from an object, returning a new object with only the picked properties
 *
 * @param {object} obj The object to pick properties from
 * @param {Array|string} props Properties to pick, can be an array of strings or a single string
 * @returns object A new object with only the picked properties
 * @example
 * pickProperties({ foo: 'bar', baz: 'qux' }, 'foo') // => { foo: 'bar' }
 * pickProperties({ foo: 'bar', baz: 'qux' }, ['foo', 'baz']) // => { foo: 'bar', baz: 'qux' }
 */
export function pickProperties(obj, props) {
  const res = {}
  if (!props) return res
  if (!isArray(props)) props = [props]
  for (let i = 0; i < props.length; i++) {
    if (obj.hasOwnProperty(props[i])) res[props[i]] = obj[props[i]]
  }
  return res
}

/**
 * Remove properties from an object
 *
 * @param {object} obj The object to remove properties from
 * @param {Array|string} props Properties to remove, can be an array of strings or a single string
 * @param {boolean} clone Defaults to true, will clone the object before removing properties
 * @returns object The object without the removed properties
 * @example
 * rejectProperties({ foo: 'bar', baz: 'qux' }, 'foo') // => { baz: 'qux' }
 * rejectProperties({ foo: 'bar', baz: 'qux' }, ['foo', 'baz']) // => {}
 */
export function rejectProperties(obj, props, clone = true) {
  if (clone) obj = { ...obj }
  if (!props) return obj
  if (!isArray(props)) props = [props]
  for (let i = 0; i < props.length; i++) {
    if (obj.hasOwnProperty(props[i])) delete obj[props[i]]
  }
  return obj
}

/**
 * Pick elements from an array by index, returning a new array of the picked elements
 *
 * @param {Array} arr The array to pick elements from
 * @param {Array|number} indexes Indexes to pick, can be an array of numbers or a single number
 * @returns array | undefined A new array of the picked elements, or undefined if arr is not an array
 * @example
 * pickArrayElements(['foo', 'bar', 'baz'], 0) // => ['foo']
 * pickArrayElements(['foo', 'bar', 'baz'], [0, 2]) // => ['foo', 'baz']
 */
export function pickArrayElements(arr, indexes) {
  if (!isArray(arr)) return
  if (!isArray(indexes)) indexes = [indexes]
  const res = []
  for (let i = 0; i < indexes.length; i++) {
    if (arr.hasOwnProperty(indexes[i])) res.push(arr[indexes[i]])
  }
  return res
}

/**
 * Remove elements from an array by index. Indexes may be passed in any order and are deduplicated.
 *
 * @param {Array} arr The array to remove elements from
 * @param {Array|number} indexes Indexes to remove, can be an array of numbers or a single number
 * @param {boolean} clone Defaults to true, will clone the array before removing elements
 * @returns array | undefined The array without the removed elements, or undefined if arr is not an array
 * @example
 * rejectArrayElements(['foo', 'bar', 'baz'], 0) // => ['bar', 'baz']
 * rejectArrayElements(['foo', 'bar', 'baz'], [2, 0]) // => ['bar']
 */
export function rejectArrayElements(arr, indexes, clone = true) {
  if (!isArray(arr)) return
  if (clone) arr = [...arr]
  if (!isArray(indexes)) indexes = [indexes]
  indexes = [...new Set(indexes)].sort((a, b) => b - a)
  for (let i = 0; i < indexes.length; i++) {
    if (arr.hasOwnProperty(indexes[i])) arr.splice(indexes[i], 1)
  }
  return arr
}

/**
 * Pick properties from an object or elements from an array
 * 
 * @param {Array} obj Object or array to pick properties or elements from
 * @param {Array | string | number} props Properties to remove, can be an array of strings or a single string or number
 * @returns object | array | undefined
 * @example
 * 
 * pick({ foo: 'bar', bar: 'baz', baz: 'qux' }) // => {}
 * pick({}, []) // => {}
 * pick(null, 'foo') // => undefined
 * pick({ foo: 'bar', bar: 'baz', baz: 'qux' }, undefined) // => {}
 * pick({ foo: 'bar', bar: 'baz', baz: 'qux' }, 'foo') // => { foo: 'bar'}
 * pick({ foo: 'bar', bar: 'baz', baz: 'qux' }, ['foo', 'baz']) // => { foo: 'bar', baz: 'qux' }
 * 
 * pick(['foo', 'bar', 'baz'], []) // => []
 * pick([], []) // => []
 * pick(null, 0) // => undefined
 * pick(['foo', 'bar', 'baz'], undefined) // => []
 * pick(['foo', 'bar', 'baz'], 0) // => ['foo']
 * pick(['foo', 'bar', 'baz'], [0, 2]) // => ['foo', 'baz']
 * pick(['foo', 'bar', 'baz'], [0, 2, 3]) // => ['foo', 'baz']
 */
export function pick(obj, props) {
  return isObject(obj) ? pickProperties(obj, props) : pickArrayElements(obj, props)
}

/**
 * Remove properties from an object or elements from an array
 * 
 * @param {Array} obj Object or array to remove properties or elements from
 * @param {Array | string | number} props Properties to remove, can be an array of strings or a single string or number
 * @param {boolean} clone Defaults to true, will clone the object or array before removing properties or elements.
 * @returns object | array | undefined
 * @example
 * 
 * reject({ foo: 'bar', bar: 'baz', baz: 'qux' }) // => {}
 * reject({}, []) // => {}
 * reject(null, 'foo') // => undefined
 * reject({ foo: 'bar', bar: 'baz', baz: 'qux' }, undefined) // => {}
 * reject({ foo: 'bar', bar: 'baz', baz: 'qux' }, 'foo') // => { bar: 'baz', baz: 'qux' }
 * reject({ foo: 'bar', bar: 'baz', baz: 'qux' }, ['foo', 'baz']) // => { bar: 'baz' }
 * 
 * reject(['foo', 'bar', 'baz'], []) // => []
 * reject([], []) // => []
 * reject(null, 0) // => undefined
 * reject(['foo', 'bar', 'baz'], undefined) // => []
 * reject(['foo', 'bar', 'baz'], 0) // => ['bar', 'baz']
 * reject(['foo', 'bar', 'baz'], [0, 2]) // => ['bar']
 * reject(['foo', 'bar', 'baz'], [0, 2, 3]) // => ['bar']
 */
export function reject(obj, props, clone = true) {
  return isObject(obj) ? rejectProperties(obj, props, clone) : rejectArrayElements(obj, props, clone)
}

/**
 * Basic timestamp first UID generator that's good enough for most use cases but not for security purposes.
 * There's an extremely small chance of collision, so create a map object to check for collisions if you're worried about that.
 * 
 * - `Date.now().toString(16)` is used for the timestamp, which is a base16 representation of the current timestamp in milliseconds.
 * - `random().toString(16).substring(2)` is used for the random number, which is a base16 representation of a random number between 0 and 1, with the first two characters removed.
 * 
 * @param {boolean} safe Defaults to false, if true will use a cryptographically secure random number generator for the random number improving security but reducing performance. If crypto is not available, will use Math.random() instead.
 * @returns string
 * @example
 * basicUID() // => '18d4613e4d2-750bf066ac6158'
 */
export function basicUID(safe = false) {
  const rand = safe ? random() : Math.random()
  return Date.now().toString(16)+'-'+rand.toString(16).substring(2)
}

function cryptoUUIDFallback() {
  return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
   (c ^ Math.random() * 16 >> c / 4).toString(16)
  )
}

// Taken from https://stackoverflow.com/a/2117523/5437943
function cryptoRandomUUIDFallback() {
  return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  )
}

/**
 * Generates a UUID v4
 * - Uses crypto.randomUUID if available
 * - Uses crypto.getRandomValues if available
 * - Uses a fallback if neither is available, which is not safe because it uses Math.random() instead of a cryptographically secure random number generator
 * 
 * I'm bad at crypto and bitwise operations, not my cup of tea, so I had to rely on StackOverflow for the fallback: https://stackoverflow.com/a/2117523/5437943
 * 
 * @param {boolean} safe Defaults to true, if false will use a fallback that's not cryptographically secure but significantly faster
 * @returns string
 * @example
 * generateUUID() // UUID v4, example 09ed0fe4-8eb6-4c2a-a8d3-a862b7513294
 */
export function generateUUID(safe = true) {
  if (typeof crypto === 'undefined' || !safe) return cryptoUUIDFallback()
  if (crypto.randomUUID) return crypto.randomUUID()
  if (crypto.getRandomValues) return cryptoRandomUUIDFallback()
  return cryptoUUIDFallback()
}

/**
 * Generates a random number between 0 and 1, inclusive of 0 but not inclusive of 1.
 * 
 * - Uses crypto.getRandomValues if available
 * - Uses Math.random() if crypto.getRandomValues is not available
 * 
 * @returns number
 * @example
 * random() // => 0.123456789
 */
export function random() {
  if (typeof crypto === 'undefined') return Math.random()
  if (crypto.getRandomValues) return crypto.getRandomValues(new Uint32Array(1))[0] / 4_294_967_296 // 2^32 = 4294967296
  return Math.random()
}

/**
 * Access nested object properties using a path
 * 
 * @param {object} obj The object to access
 * @param {Array|string} path The path to access
 * @returns {*} The value of the accessed property
 * 
 * @example
 * const obj = { foo: { bar: 'baz' } }
 * getObjectValueByPath(obj, 'foo.bar') // => 'baz'
 */
export function getObjectValueByPath(obj, path) {
  if (typeof path === 'string') path = path.split('.');
  return path.reduce((acc, part) => acc !== null && acc !== undefined ? acc[part] : undefined, obj);
}