// TODO: delete when this is merged: https://github.com/pmndrs/zustand/pull/1825/files
export function isEqual<T>(objA: T, objB: T) {
  return Object.is(objA, objB);
}

interface Keyed {
  id: string;
}

function instanceChecks<T>(objA: T, objB: T, checkId = false) {
  if (objA instanceof Map && objB instanceof Map) {
    if (objA.size !== objB.size) return false;

    for (const [key, value] of objA) {
      if (!shallow(value, objB.get(key), checkId)) {
        return false;
      }
    }
    return true;
  }

  if (objA instanceof Set && objB instanceof Set) {
    if (objA.size !== objB.size) return false;

    for (const value of objA) {
      if (!objB.has(value)) {
        return false;
      }
    }
    return true;
  }
  if (objA instanceof Date && objB instanceof Date) {
    return objA.getTime() === objB.getTime();
  }
  if (
    checkId &&
    (objA as unknown as Keyed)?.id &&
    (objB as unknown as Keyed)?.id &&
    (objA as unknown as Keyed)?.id === (objB as unknown as Keyed)?.id
  ) {
    return true;
  }
  return null;
}

export function shallow<T>(objA: T, objB: T, checkId = false) {
  if (Object.is(objA, objB)) {
    return true;
  }
  if (
    typeof objA !== "object" ||
    objA === null ||
    typeof objB !== "object" ||
    objB === null
  ) {
    return false;
  }

  const known = instanceChecks(objA, objB, checkId);
  if (known !== null) {
    return known;
  }

  const keysA = Object.keys(objA);
  if (keysA.length !== Object.keys(objB).length) {
    return false;
  }
  for (let i = 0; i < keysA.length; i++) {
    const key = keysA[i] as keyof T;
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
      !shallow(objA[key], objB[key], checkId)
    ) {
      return false;
    }
  }
  return true;
}

// this function checks to see if the subject is at least equal with the base but permits the base to have additional fields
export function partialShallow<T>(
  subject: Partial<T>,
  base: T,
  checkId = false,
) {
  if (Object.is(subject, base)) {
    return true;
  }
  if (
    typeof subject !== "object" ||
    subject === null ||
    typeof base !== "object" ||
    base === null
  ) {
    return false;
  }

  const known = instanceChecks(subject, base, checkId);
  if (known !== null) {
    return known;
  }

  const keysSubject = Object.keys(subject);
  if (keysSubject.length > Object.keys(base as Record<string, any>).length) {
    return false;
  }
  for (let i = 0; i < keysSubject.length; i++) {
    const key = keysSubject[i] as keyof T;
    if (
      !Object.prototype.hasOwnProperty.call(base, keysSubject[i] as string) ||
      !shallow(subject[key], base[key], checkId)
    ) {
      return false;
    }
  }
  return true;
}
