// Copyright 2018-2026 the Deno authors. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.

// deno-lint-ignore-file
(function () {
const { core, primordials } = __bootstrap;
const { Buffer } = core.loadExtScript("ext:deno_node/internal/buffer.mjs");
const {
  getOwnNonIndexProperties,
  ONLY_ENUMERABLE,
  SKIP_SYMBOLS,
} = core.loadExtScript("ext:deno_node/internal_binding/util.ts");
const lazyAssert = core.createLazyLoader("node:assert");
const {
  isAnyArrayBuffer,
  isArrayBufferView,
  isBigIntObject,
  isBooleanObject,
  isBoxedPrimitive,
  isCryptoKey,
  isDate,
  isFloat16Array,
  isFloat32Array,
  isFloat64Array,
  isKeyObject,
  isMap,
  isNumberObject,
  isPromise,
  isRegExp,
  isSet,
  isStringObject,
  isSymbolObject,
  isWeakMap,
  isWeakSet,
} = core.loadExtScript("ext:deno_node/internal/util/types.ts");
const { kKeyObject } = core.loadExtScript(
  "ext:deno_node/internal/crypto/constants.ts",
);
const { isError } = core.loadExtScript("ext:deno_node/internal/util.mjs");
const lazyUrl = () => core.loadExtScript("ext:deno_node/internal/url.ts");

const {
  Array,
  ArrayBuffer,
  ArrayIsArray,
  ArrayPrototypeFilter,
  ArrayPrototypePush,
  BigInt,
  BigInt64Array,
  BigIntPrototypeValueOf,
  BigUint64Array,
  Boolean,
  BooleanPrototypeValueOf,
  DataView,
  Date,
  DatePrototypeGetTime,
  Error,
  Float32Array,
  Float64Array,
  Function,
  Int16Array,
  Int32Array,
  Int8Array,
  Map,
  Number,
  NumberIsNaN,
  NumberPrototypeValueOf,
  Object,
  ObjectGetOwnPropertyDescriptor,
  ObjectGetOwnPropertySymbols: getOwnSymbols,
  ObjectGetPrototypeOf,
  ObjectIs,
  ObjectKeys,
  ObjectPrototypeHasOwnProperty: hasOwn,
  ObjectPrototypePropertyIsEnumerable: hasEnumerable,
  ObjectPrototypeToString,
  Promise,
  RegExp,
  SafeSet,
  Set,
  String,
  StringPrototypeValueOf,
  Symbol,
  SymbolPrototypeValueOf,
  TypedArrayPrototypeGetByteLength: getByteLength,
  TypedArrayPrototypeGetSymbolToStringTag,
  Uint16Array,
  Uint32Array,
  Uint8Array,
  Uint8ClampedArray,
  WeakMap,
  WeakSet,
} = primordials;

type Memo = {
  set: Set<unknown> | undefined;
  a: unknown;
  b: unknown;
  c: unknown;
  d: unknown;
  deep: boolean;
};

enum valueType {
  noIterator,
  isArray,
  isSet,
  isMap,
}

const wellKnownConstructors = new SafeSet()
  .add(Array)
  .add(ArrayBuffer)
  .add(BigInt)
  .add(BigInt64Array)
  .add(BigUint64Array)
  .add(Boolean)
  .add(Buffer)
  .add(DataView)
  .add(Date)
  .add(Error)
  // TODO(Tango992): add Float16Array
  // .add(Float16Array)
  .add(Float32Array)
  .add(Float64Array)
  .add(Function)
  .add(Int16Array)
  .add(Int32Array)
  .add(Int8Array)
  .add(Map)
  .add(Number)
  .add(Object)
  .add(Promise)
  .add(RegExp)
  .add(Set)
  .add(String)
  .add(Symbol)
  .add(Uint16Array)
  .add(Uint32Array)
  .add(Uint8Array)
  .add(Uint8ClampedArray)
  .add(WeakMap)
  .add(WeakSet);

const kStrict = 1;
const kLoose = 0;
const kPartial = 2;
const kStrictWithoutPrototypes = 3;

// Check if they have the same source and flags
function areSimilarRegExps(a: RegExp, b: RegExp): boolean {
  return a.source === b.source &&
    a.flags === b.flags &&
    a.lastIndex === b.lastIndex;
}

function isPartialUint8Array(a: Uint8Array, b: Uint8Array): boolean {
  const lenA = getByteLength(a);
  const lenB = getByteLength(b);
  if (lenA < lenB) {
    return false;
  }
  let offsetA = 0;
  for (let offsetB = 0; offsetB < lenB; offsetB++) {
    while (!ObjectIs(a[offsetA], b[offsetB])) {
      offsetA++;
      if (offsetA > lenA - lenB + offsetB) {
        return false;
      }
    }
    offsetA++;
  }
  return true;
}

function isPartialArrayBufferView(
  a: ArrayBufferView,
  b: ArrayBufferView,
): boolean {
  if (a.byteLength < b.byteLength) {
    return false;
  }
  return isPartialUint8Array(
    new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
    new Uint8Array(b.buffer, b.byteOffset, b.byteLength),
  );
}

function areSimilarFloatArrays(
  a: ArrayBufferView,
  b: ArrayBufferView,
): boolean {
  const len = getByteLength(a);
  if (len !== getByteLength(b)) {
    return false;
  }
  for (let offset = 0; offset < len; offset++) {
    if (a[offset] !== b[offset]) {
      return false;
    }
  }
  return true;
}

function areSimilarTypedArrays(
  a: ArrayBufferView,
  b: ArrayBufferView,
): boolean {
  if (a.byteLength !== b.byteLength) {
    return false;
  }
  return Buffer.compare(
    new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
    new Uint8Array(b.buffer, b.byteOffset, b.byteLength),
  ) === 0;
}

function areEqualArrayBuffers(buf1: ArrayBuffer, buf2: ArrayBuffer): boolean {
  return buf1.byteLength === buf2.byteLength &&
    Buffer.compare(new Uint8Array(buf1), new Uint8Array(buf2)) === 0;
}

function isEqualBoxedPrimitive(val1: unknown, val2: unknown) {
  if (isNumberObject(val1)) {
    return isNumberObject(val2) &&
      ObjectIs(NumberPrototypeValueOf(val1), NumberPrototypeValueOf(val2));
  }
  if (isStringObject(val1)) {
    return isStringObject(val2) &&
      StringPrototypeValueOf(val1) === StringPrototypeValueOf(val2);
  }
  if (isBooleanObject(val1)) {
    return isBooleanObject(val2) &&
      BooleanPrototypeValueOf(val1) === BooleanPrototypeValueOf(val2);
  }
  if (isBigIntObject(val1)) {
    return isBigIntObject(val2) &&
      BigIntPrototypeValueOf(val1) === BigIntPrototypeValueOf(val2);
  }
  if (isSymbolObject(val1)) {
    return isSymbolObject(val2) &&
      SymbolPrototypeValueOf(val1) === SymbolPrototypeValueOf(val2);
  }
  /* c8 ignore next */
  lazyAssert().fail(`Unknown boxed type ${val1}`);
}

function isEnumerableOrIdentical(
  val1: unknown,
  val2: unknown,
  prop: string,
  mode: number,
  memos: Memo | null,
): boolean {
  return hasEnumerable(val2, prop) || // This is handled by Object.keys()
    (mode === kPartial &&
      (val2[prop] === undefined ||
        (prop === "message" && val2[prop] === ""))) ||
    innerDeepEqual(val1[prop], val2[prop], mode, memos);
}

function innerDeepEqual(
  val1: unknown,
  val2: unknown,
  mode: number,
  memos: Memo | null | undefined,
): boolean {
  // All identical values are equivalent, as determined by ===.
  if (val1 === val2) {
    return val1 !== 0 || ObjectIs(val1, val2) || mode === kLoose;
  }

  // Check more closely if val1 and val2 are equal.
  if (mode !== kLoose) {
    if (typeof val1 === "number") {
      // Check for NaN
      // eslint-disable-next-line no-self-compare
      return val1 !== val1 && val2 !== val2;
    }
    if (
      typeof val2 !== "object" ||
      typeof val1 !== "object" ||
      val1 === null ||
      val2 === null
    ) {
      return false;
    }
  } else {
    if (val1 === null || typeof val1 !== "object") {
      return (val2 === null || typeof val2 !== "object") &&
        // Check for NaN
        // eslint-disable-next-line eqeqeq, no-self-compare
        (val1 == val2 || (val1 !== val1 && val2 !== val2));
    }
    if (val2 === null || typeof val2 !== "object") {
      return false;
    }
  }
  return objectComparisonStart(val1, val2, mode, memos);
}

function objectComparisonStart(
  val1: object,
  val2: object,
  mode: number,
  memos: Memo | null | undefined,
): boolean {
  if (mode === kStrict) {
    if (
      wellKnownConstructors.has(val1.constructor) ||
      (val1.constructor !== undefined && !hasOwn(val1, "constructor"))
    ) {
      if (val1.constructor !== val2.constructor) {
        return false;
      }
    } else if (ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)) {
      return false;
    }
  }

  const val1Tag = ObjectPrototypeToString(val1);
  const val2Tag = ObjectPrototypeToString(val2);

  if (val1Tag !== val2Tag) {
    return false;
  }

  if (ArrayIsArray(val1)) {
    if (
      !ArrayIsArray(val2) ||
      (val1.length !== val2.length &&
        (mode !== kPartial || val1.length < val2.length))
    ) {
      return false;
    }

    const filter = mode !== kLoose
      ? ONLY_ENUMERABLE
      : ONLY_ENUMERABLE | SKIP_SYMBOLS;
    const keys2 = getOwnNonIndexProperties(val2, filter);
    if (
      mode !== kPartial &&
      keys2.length !== getOwnNonIndexProperties(val1, filter).length
    ) {
      return false;
    }
    return keyCheck(val1, val2, mode, memos, valueType.isArray, keys2);
  } else if (val1Tag === "[object Object]") {
    return keyCheck(val1, val2, mode, memos, valueType.noIterator);
  } else if (isDate(val1)) {
    if (!isDate(val2)) {
      return false;
    }
    const time1 = DatePrototypeGetTime(val1);
    const time2 = DatePrototypeGetTime(val2);
    if (time1 !== time2 && !(NumberIsNaN(time1) && NumberIsNaN(time2))) {
      return false;
    }
  } else if (isRegExp(val1)) {
    if (!isRegExp(val2) || !areSimilarRegExps(val1, val2)) {
      return false;
    }
  } else if (isArrayBufferView(val1)) {
    if (
      TypedArrayPrototypeGetSymbolToStringTag(val1) !==
        TypedArrayPrototypeGetSymbolToStringTag(val2)
    ) {
      return false;
    }
    if (mode === kPartial && val1.byteLength !== val2.byteLength) {
      if (!isPartialArrayBufferView(val1, val2)) {
        return false;
      }
    } else if (
      mode === kLoose &&
      (isFloat32Array(val1) || isFloat64Array(val1) || isFloat16Array(val1))
    ) {
      if (!areSimilarFloatArrays(val1, val2)) {
        return false;
      }
    } else if (!areSimilarTypedArrays(val1, val2)) {
      return false;
    }
    // Buffer.compare returns true, so val1.length === val2.length. If they both
    // only contain numeric keys, we don't need to exam further than checking
    // the symbols.
    const filter = mode !== kLoose
      ? ONLY_ENUMERABLE
      : ONLY_ENUMERABLE | SKIP_SYMBOLS;
    const keys2 = getOwnNonIndexProperties(val2, filter);
    if (
      mode !== kPartial &&
      keys2.length !== getOwnNonIndexProperties(val1, filter).length
    ) {
      return false;
    }
    return keyCheck(val1, val2, mode, memos, valueType.noIterator, keys2);
  } else if (isSet(val1)) {
    if (
      !isSet(val2) ||
      (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))
    ) {
      return false;
    }
    return keyCheck(val1, val2, mode, memos, valueType.isSet);
  } else if (isMap(val1)) {
    if (
      !isMap(val2) ||
      (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))
    ) {
      return false;
    }
    return keyCheck(val1, val2, mode, memos, valueType.isMap);
  } else if (isAnyArrayBuffer(val1)) {
    if (!isAnyArrayBuffer(val2)) {
      return false;
    }
    if (mode !== kPartial || val1.byteLength === val2.byteLength) {
      if (!areEqualArrayBuffers(val1, val2)) {
        return false;
      }
    } else if (
      !isPartialUint8Array(new Uint8Array(val1), new Uint8Array(val2))
    ) {
      return false;
    }
  } else if (isError(val1)) {
    // Do not compare the stack as it might differ even though the error itself
    // is otherwise identical.
    if (
      !isError(val2) ||
      !isEnumerableOrIdentical(val1, val2, "message", mode, memos) ||
      !isEnumerableOrIdentical(val1, val2, "name", mode, memos) ||
      !isEnumerableOrIdentical(val1, val2, "cause", mode, memos) ||
      !isEnumerableOrIdentical(val1, val2, "errors", mode, memos)
    ) {
      return false;
    }
    const hasOwnVal2Cause = hasOwn(val2, "cause");
    if (
      (hasOwnVal2Cause !== hasOwn(val1, "cause") &&
        (mode !== kPartial || hasOwnVal2Cause))
    ) {
      return false;
    }
  } else if (isBoxedPrimitive(val1)) {
    if (!isEqualBoxedPrimitive(val1, val2)) {
      return false;
    }
  } else if (
    ArrayIsArray(val2) ||
    isArrayBufferView(val2) ||
    isSet(val2) ||
    isMap(val2) ||
    isDate(val2) ||
    isRegExp(val2) ||
    isAnyArrayBuffer(val2) ||
    isBoxedPrimitive(val2) ||
    isError(val2)
  ) {
    return false;
  } else if (lazyUrl().isURL(val1)) {
    if (!lazyUrl().isURL(val2) || val1.href !== val2.href) {
      return false;
    }
  } else if (isKeyObject(val1)) {
    if (!isKeyObject(val2) || !val1.equals(val2)) {
      return false;
    }
  } else if (isCryptoKey(val1)) {
    if (
      !isCryptoKey(val2) ||
      val1.extractable !== val2.extractable ||
      !innerDeepEqual(val1.algorithm, val2.algorithm, mode, memos) ||
      !innerDeepEqual(val1.usages, val2.usages, mode, memos) ||
      !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos)
    ) {
      return false;
    }
  } else if (isWeakMap(val1) || isWeakSet(val1)) {
    return false;
  } else if (isPromise(val1) && isPromise(val2)) {
    // Native Promises can only be equal by reference.
    return false;
  }

  return keyCheck(val1, val2, mode, memos, valueType.noIterator);
}

function getEnumerables(val: unknown, keys: unknown[]): unknown[] {
  return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key));
}

function partialSymbolEquiv(
  val1: unknown,
  val2: unknown,
  keys2: unknown[],
): boolean {
  const symbolKeys = getOwnSymbols(val2);
  if (symbolKeys.length !== 0) {
    for (const key of symbolKeys) {
      if (hasEnumerable(val2, key)) {
        if (!hasEnumerable(val1, key)) {
          return false;
        }
        ArrayPrototypePush(keys2, key);
      }
    }
  }
  return true;
}

function keyCheck(
  val1: unknown,
  val2: unknown,
  mode: number,
  memos: Memo | null | undefined,
  iterationType: valueType,
  keys2?: string[],
): boolean {
  // For all remaining Object pairs, including Array, objects and Maps,
  // equivalence is determined by having:
  // a) The same number of owned enumerable properties
  // b) The same set of keys/indexes (although not necessarily the same order)
  // c) Equivalent values for every corresponding key/index
  // d) For Sets and Maps, equal contents
  // Note: this accounts for both named and indexed properties on Arrays.
  const isArrayLikeObject = keys2 !== undefined;

  if (keys2 === undefined) {
    keys2 = ObjectKeys(val2);
  }
  let keys1;

  if (!isArrayLikeObject) {
    // The pair must have the same number of owned properties.
    if (mode === kPartial) {
      if (!partialSymbolEquiv(val1, val2, keys2)) {
        return false;
      }
    } else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
      return false;
    } else if (mode === kStrict || mode === kStrictWithoutPrototypes) {
      const symbolKeysA = getOwnSymbols(val1);
      if (symbolKeysA.length !== 0) {
        let count = 0;
        for (const key of symbolKeysA) {
          if (hasEnumerable(val1, key)) {
            if (!hasEnumerable(val2, key)) {
              return false;
            }
            ArrayPrototypePush(keys2, key);
            count++;
          } else if (hasEnumerable(val2, key)) {
            return false;
          }
        }
        const symbolKeysB = getOwnSymbols(val2);
        if (
          symbolKeysA.length !== symbolKeysB.length &&
          getEnumerables(val2, symbolKeysB).length !== count
        ) {
          return false;
        }
      } else {
        const symbolKeysB = getOwnSymbols(val2);
        if (
          symbolKeysB.length !== 0 &&
          getEnumerables(val2, symbolKeysB).length !== 0
        ) {
          return false;
        }
      }
    }
  }

  if (
    keys2.length === 0 &&
    (iterationType === valueType.noIterator ||
      (iterationType === valueType.isArray && val2.length === 0) ||
      val2.size === 0)
  ) {
    return true;
  }

  if (memos === null) {
    return objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
  }
  return handleCycles(val1, val2, mode, keys1, keys2, memos, iterationType);
}

function handleCycles(
  val1: unknown,
  val2: unknown,
  mode: number,
  keys1: string[] | undefined,
  keys2: string[],
  memos: Memo | undefined,
  iterationType: valueType,
): boolean {
  // Use memos to handle cycles.
  if (memos === undefined) {
    memos = {
      set: undefined,
      a: val1,
      b: val2,
      c: undefined,
      d: undefined,
      deep: false,
    };
    return objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
  }

  if (memos.set === undefined) {
    if (memos.deep === false) {
      if (memos.a === val1) {
        return memos.b === val2;
      }
      if (memos.b === val2) {
        return false;
      }
      memos.c = val1;
      memos.d = val2;
      memos.deep = true;
      const result = objEquiv(
        val1,
        val2,
        mode,
        keys1,
        keys2,
        memos,
        iterationType,
      );
      memos.deep = false;
      return result;
    }
    memos.set = new SafeSet();
    memos.set.add(memos.a);
    memos.set.add(memos.b);
    memos.set.add(memos.c);
    memos.set.add(memos.d);
  }

  const { set } = memos;

  const originalSize = set.size;
  set.add(val1);
  set.add(val2);
  if (originalSize !== set.size - 2) {
    return originalSize === set.size;
  }

  const areEq = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);

  set.delete(val1);
  set.delete(val2);

  return areEq;
}

// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using
// Sadly it is not possible to detect corresponding values properly in case the
// type is a string, number, bigint or boolean. The reason is that those values
// can match lots of different string values (e.g., 1n == '+00001').
function findLooseMatchingPrimitives(prim: unknown) {
  switch (typeof prim) {
    case "undefined":
      return null;
    case "object": // Only pass in null as object!
      return undefined;
    case "symbol":
      return false;
    case "string":
      prim = +prim;
      // Loose equal entries exist only if the string is possible to convert to
      // a regular number and not NaN.
      // Fall through
    case "number":
      // Check for NaN
      // eslint-disable-next-line no-self-compare
      if (prim !== prim) {
        return false;
      }
  }
  return true;
}

function setMightHaveLoosePrim(
  a: Set<unknown>,
  b: Set<unknown>,
  prim: unknown,
): boolean {
  const altValue = findLooseMatchingPrimitives(prim);
  if (altValue != null) {
    return altValue;
  }

  return !b.has(altValue) && a.has(altValue);
}

function mapMightHaveLoosePrim(
  a: Map<unknown, unknown>,
  b: Map<unknown, unknown>,
  prim: unknown,
  item2: unknown,
  memo: Memo,
): boolean {
  const altValue = findLooseMatchingPrimitives(prim);
  if (altValue != null) {
    return altValue;
  }
  const item1 = a.get(altValue);
  if (
    (item1 === undefined && !a.has(altValue)) ||
    !innerDeepEqual(item1, item2, kLoose, memo)
  ) {
    return false;
  }
  return !b.has(altValue) && innerDeepEqual(item1, item2, kLoose, memo);
}

function partialObjectSetEquiv(
  array: unknown[],
  a: Set<unknown>,
  b: Set<unknown>,
  mode: number,
  memo: Memo | null,
): boolean {
  let aPos = 0;
  let direction = 1;
  let start = 0;
  let end = array.length - 1;
  for (const val1 of a) {
    aPos++;
    if (!b.has(val1)) {
      let innerStart = start;
      if (direction === 1) {
        if (innerDeepEqual(val1, array[start], mode, memo)) {
          if (start === end) {
            return true;
          }
          start += 1;
          continue;
        }
        if (start === end) {
          // The last element of set b might match a later element in set a.
          continue;
        }
        direction = -1;
        innerStart += 1;
      }
      let matched = true;
      if (!innerDeepEqual(val1, array[end], mode, memo)) {
        direction = 1;
        matched = arrayHasEqualElement(
          array,
          val1,
          mode,
          memo,
          innerDeepEqual,
          innerStart,
          end,
        );
      }
      if (matched) {
        if (start === end) {
          return true;
        }
        end -= 1;
      }
    }
    if (a.size - aPos <= end - start) {
      return false;
    }
  }
  return false;
}

function arrayHasEqualElement(
  array: unknown[],
  val1: unknown,
  mode: number,
  memo: Memo | null,
  comparator: (
    val1: unknown | object,
    val2: unknown | object,
    mode: number,
    memos: Memo | null,
  ) => boolean,
  start: number,
  end: number,
): boolean {
  let matched = false;
  for (let i = end - 1; i >= start; i--) {
    if (comparator(val1, array[i], mode, memo)) {
      // Remove the matching element to make sure we do not check that again.
      array.splice(i, 1);
      matched = true;
      break;
    }
  }
  return matched;
}

function setObjectEquiv(
  array: unknown[],
  a: Set<unknown>,
  b: Set<unknown>,
  mode: number,
  memo: Memo | null,
): boolean {
  let direction = 1;
  let start = 0;
  let end = array.length - 1;
  const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual;
  const extraChecks = mode === kLoose || array.length !== a.size;
  for (const val1 of a) {
    if (extraChecks) {
      if (typeof val1 === "object") {
        if (b.has(val1)) {
          continue;
        }
      } else if (b.has(val1)) {
        continue;
      } else if (mode !== kLoose) {
        return false;
      }
    }

    let innerStart = start;
    if (direction === 1) {
      if (comparator(val1, array[start], mode, memo)) {
        start += 1;
        continue;
      }
      if (start === end) {
        return false;
      }
      direction = -1;
      innerStart += 1;
    }
    if (!comparator(val1, array[end], mode, memo)) {
      direction = 1;
      if (
        !arrayHasEqualElement(
          array,
          val1,
          mode,
          memo,
          comparator,
          innerStart,
          end,
        )
      ) {
        return false;
      }
    }
    end -= 1;
  }
  return true;
}

function compareSmallSets(
  a: Set<unknown>,
  b: Set<unknown>,
  val: unknown,
  iteratorB: Iterator<unknown>,
  mode: number,
  memo: Memo | null,
): boolean {
  const iteratorA = a.values();
  const firstA = iteratorA.next().value;
  const first = innerDeepEqual(firstA, val, mode, memo);
  if (first) {
    if (b.size === 1) { // Partial mode && a.size === 1 || b.size === 1
      return true;
    }
    const secondA = iteratorA.next().value;
    return b.has(secondA) ||
      innerDeepEqual(secondA, iteratorB.next().value, mode, memo);
  }
  return a.size !== 1 &&
    innerDeepEqual(iteratorA.next().value, val, mode, memo) && (
      b.size === 1 || // Partial mode
      b.has(firstA) || // Primitive or reference equal
      innerDeepEqual(firstA, iteratorB.next().value, mode, memo)
    );
}

function setEquiv(
  a: Set<unknown>,
  b: Set<unknown>,
  mode: number,
  memo: Memo | null,
): boolean {
  // This is a lazily initiated Set of entries which have to be compared
  // pairwise.
  let array;

  const iteratorB = b.values();
  for (const val of iteratorB) {
    if (!a.has(val)) {
      if (
        (typeof val !== "object" || val === null) &&
        (mode !== kLoose || !setMightHaveLoosePrim(a, b, val))
      ) {
        return false;
      }

      if (array === undefined) {
        if (a.size < 3) {
          return compareSmallSets(a, b, val, iteratorB, mode, memo);
        }
        array = [];
      }
      // If the specified value doesn't exist in the second set it's a object
      // (or in loose mode: a non-matching primitive). Find the
      // deep-(mode-)equal element in a set copy to reduce duplicate checks.
      array.push(val);
    }
  }

  if (array === undefined) {
    return true;
  }
  if (mode === kPartial) {
    return partialObjectSetEquiv(array, a, b, mode, memo);
  }
  return setObjectEquiv(array, a, b, mode, memo);
}

function partialObjectMapEquiv(
  array: unknown[],
  a: Map<unknown, unknown>,
  b: Map<unknown, unknown>,
  mode: number,
  memo: Memo,
): boolean {
  let aPos = 0;
  let direction = 1;
  let start = 0;
  let end = array.length - 1;
  for (const { 0: key1, 1: item1 } of a) {
    aPos++;
    if (typeof key1 === "object" && key1 !== null) {
      let innerStart = start;
      if (direction === 1) {
        const key2 = array[start];
        if (
          objectComparisonStart(key1, key2, mode, memo) &&
          innerDeepEqual(item1, b.get(key2), mode, memo)
        ) {
          if (start === end) {
            return true;
          }
          start += 1;
          continue;
        }
        if (start === end) {
          // The last element of map b might match a later element in map a.
          continue;
        }
        direction = -1;
        innerStart += 1;
      }
      let matched = true;
      const key2 = array[end];
      if (
        !objectComparisonStart(key1, key2, mode, memo) ||
        !innerDeepEqual(item1, b.get(key2), mode, memo)
      ) {
        direction = 1;
        matched = arrayHasEqualMapElement(
          array,
          key1,
          item1,
          b,
          mode,
          memo,
          objectComparisonStart,
          innerStart,
          end,
        );
      }
      if (matched) {
        if (start === end) {
          return true;
        }
        end -= 1;
      }
    }
    if (a.size - aPos <= end - start) {
      return false;
    }
  }
  return false;
}

function arrayHasEqualMapElement(
  array: unknown[],
  key1: unknown,
  item1: unknown,
  b: Map<unknown, unknown>,
  mode: number,
  memo: Memo,
  comparator: (
    val1: object,
    val2: object,
    mode: number,
    memos: Memo | null,
  ) => boolean,
  start: number,
  end: number,
): boolean {
  let matched = false;
  for (let i = end - 1; i >= start; i--) {
    const key2 = array[i];
    if (
      comparator(key1, key2, mode, memo) &&
      innerDeepEqual(item1, b.get(key2), mode, memo)
    ) {
      // Remove the matching element to make sure we do not check that again.
      array.splice(i, 1);
      matched = true;
      break;
    }
  }
  return matched;
}

function mapObjectEquiv(
  array: unknown[],
  a: Map<unknown, unknown>,
  b: Map<unknown, unknown>,
  mode: number,
  memo: Memo,
): boolean {
  let direction = 1;
  let start = 0;
  let end = array.length - 1;
  const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual;
  const extraChecks = mode === kLoose || array.length !== a.size;

  for (const { 0: key1, 1: item1 } of a) {
    if (extraChecks && (typeof key1 !== "object" || key1 === null)) {
      if (b.has(key1) && innerDeepEqual(item1, b.get(key1), mode, memo)) {
        continue;
      } else if (mode !== kLoose) {
        return false;
      }
    }

    let innerStart = start;
    if (direction === 1) {
      const key2 = array[start];
      if (
        comparator(key1, key2, mode, memo) &&
        innerDeepEqual(item1, b.get(key2), mode, memo)
      ) {
        start += 1;
        continue;
      }
      if (start === end) {
        return false;
      }
      direction = -1;
      innerStart += 1;
    }
    const key2 = array[end];
    if (
      (!comparator(key1, key2, mode, memo) ||
        !innerDeepEqual(item1, b.get(key2), mode, memo))
    ) {
      direction = 1;
      if (
        !arrayHasEqualMapElement(
          array,
          key1,
          item1,
          b,
          mode,
          memo,
          comparator,
          innerStart,
          end,
        )
      ) {
        return false;
      }
    }
    end -= 1;
  }
  return true;
}

function mapEquiv(
  a: Map<unknown, unknown>,
  b: Map<unknown, unknown>,
  mode: number,
  memo: Memo,
): boolean {
  let array;

  for (const { 0: key2, 1: item2 } of b) {
    if (typeof key2 === "object" && key2 !== null) {
      if (array === undefined) {
        if (a.size === 1) {
          const { 0: key1, 1: item1 } = a.entries().next().value;
          return innerDeepEqual(key1, key2, mode, memo) &&
            innerDeepEqual(item1, item2, mode, memo);
        }
        array = [];
      }
      array.push(key2);
    } else {
      // By directly retrieving the value we prevent another b.has(key2) check in
      // almost all possible cases.
      const item1 = a.get(key2);
      if (
        ((item1 === undefined && !a.has(key2)) ||
          !innerDeepEqual(item1, item2, mode, memo))
      ) {
        if (mode !== kLoose) {
          return false;
        }
        // Fast path to detect missing string, symbol, undefined and null
        // keys.
        if (!mapMightHaveLoosePrim(a, b, key2, item2, memo)) {
          return false;
        }
        if (array === undefined) {
          array = [];
        }
        array.push(key2);
      }
    }
  }

  if (array === undefined) {
    return true;
  }

  if (mode === kPartial) {
    return partialObjectMapEquiv(array, a, b, mode, memo);
  }

  return mapObjectEquiv(array, a, b, mode, memo);
}

function partialSparseArrayEquiv(
  a: unknown[],
  b: unknown[],
  mode: number,
  memos: Memo | null,
  startA: number,
  startB: number,
): boolean {
  let aPos = 0;
  const keysA = ObjectKeys(a).slice(startA);
  const keysB = ObjectKeys(b).slice(startB);
  if (keysA.length < keysB.length) {
    return false;
  }
  for (let i = 0; i < keysB.length; i++) {
    const keyB = keysB[i];
    while (!innerDeepEqual(a[keysA[aPos]], b[keyB], mode, memos)) {
      aPos++;
      if (aPos > keysA.length - keysB.length + i) {
        return false;
      }
    }
    aPos++;
  }
  return true;
}

function partialArrayEquiv(
  a: unknown[],
  b: unknown[],
  mode: number,
  memos: Memo | null,
): boolean {
  let aPos = 0;
  for (let i = 0; i < b.length; i++) {
    let isSparse = b[i] === undefined && !hasOwn(b, i);
    if (isSparse) {
      return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
    }
    while (
      !(isSparse = a[aPos] === undefined && !hasOwn(a, aPos)) &&
      !innerDeepEqual(a[aPos], b[i], mode, memos)
    ) {
      aPos++;
      if (aPos > a.length - b.length + i) {
        return false;
      }
    }
    if (isSparse) {
      return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
    }
    aPos++;
  }
  return true;
}

function sparseArrayEquiv(
  a: unknown[],
  b: unknown[],
  mode: number,
  memos: Memo | null,
  i: number,
): boolean {
  // TODO(BridgeAR): Use internal method to only get index properties. The
  // same applies to the partial implementation.
  const keysA = ObjectKeys(a);
  const keysB = ObjectKeys(b);
  if (keysA.length !== keysB.length) {
    return false;
  }
  for (; i < keysB.length; i++) {
    const key = keysB[i];
    if (
      (a[key] === undefined && !hasOwn(a, key)) ||
      !innerDeepEqual(a[key], b[key], mode, memos)
    ) {
      return false;
    }
  }
  return true;
}

function objEquiv(
  a: unknown,
  b: unknown,
  mode: number,
  keys1: string[] | undefined,
  keys2: string[],
  memos: Memo | null,
  iterationType: valueType,
): boolean {
  // The pair must have equivalent values for every corresponding key.
  if (keys2.length > 0) {
    let i = 0;
    // Ordered keys
    if (keys1 !== undefined) {
      for (; i < keys2.length; i++) {
        const key = keys2[i];
        if (keys1[i] !== key) {
          break;
        }
        if (!innerDeepEqual(a[key], b[key], mode, memos)) {
          return false;
        }
      }
    }
    // Unordered keys
    for (; i < keys2.length; i++) {
      const key = keys2[i];
      // It is faster to get the whole descriptor and to check it's enumerable
      // property in V8 13.0 compared to calling Object.propertyIsEnumerable()
      // and accessing the property regularly.
      const descriptor = ObjectGetOwnPropertyDescriptor(a, key);
      if (
        !descriptor?.enumerable ||
        !innerDeepEqual(
          descriptor.value !== undefined ? descriptor.value : a[key],
          b[key],
          mode,
          memos,
        )
      ) {
        return false;
      }
    }
  }

  if (iterationType === valueType.isArray) {
    if (mode === kPartial) {
      return partialArrayEquiv(a, b, mode, memos);
    }
    for (let i = 0; i < a.length; i++) {
      if (b[i] === undefined) {
        if (!hasOwn(b, i)) {
          return sparseArrayEquiv(a, b, mode, memos, i);
        }
        if (mode !== kLoose && (a[i] !== undefined || !hasOwn(a, i))) {
          return false;
        } else if (
          mode === kLoose && !innerDeepEqual(a[i], b[i], mode, memos)
        ) {
          return false;
        }
      } else if (
        (mode !== kLoose && a[i] === undefined) ||
        !innerDeepEqual(a[i], b[i], mode, memos)
      ) {
        return false;
      }
    }
  } else if (iterationType === valueType.isSet) {
    if (!setEquiv(a, b, mode, memos)) {
      return false;
    }
  } else if (iterationType === valueType.isMap) {
    if (!mapEquiv(a, b, mode, memos)) {
      return false;
    }
  }

  return true;
}

// Only handle cycles when they are detected.
// eslint-disable-next-line func-style
let detectCycles = function (
  val1: unknown,
  val2: unknown,
  mode: number,
): boolean {
  try {
    return innerDeepEqual(val1, val2, mode, null);
  } catch {
    detectCycles = innerDeepEqual;
    return innerDeepEqual(val1, val2, mode, undefined);
  }
};

function isDeepEqual(val1: unknown, val2: unknown): boolean {
  return detectCycles(val1, val2, kLoose);
}

function isDeepStrictEqual(
  val1: unknown,
  val2: unknown,
  skipPrototype?: boolean,
): boolean {
  if (skipPrototype) {
    return detectCycles(val1, val2, kStrictWithoutPrototypes);
  }
  return detectCycles(val1, val2, kStrict);
}

function isPartialStrictEqual(val1: unknown, val2: unknown): boolean {
  return detectCycles(val1, val2, kPartial);
}

return {
  isDeepEqual,
  isDeepStrictEqual,
  isPartialStrictEqual,
};
})();
