deepEq

import _curry2 from '../_internals/_curry2.js'
import values from '../object/values.js'
import eq from './eq.js'
import type from './type.js'

const _functionName = f => {
  const match = String(f).match(/^function (\w*)/)

  return match == null ? '' : match[1]
}

const _containsWith = (pred, x, list) => {
  for (let i = 0, len = list.length; i < len; i++) {
    if (pred(x, list[i])) {
      return true
    }
  }

  return false
}

const _arrFromIter = iter => {
  const list = []
  let next = null

  while (!(next = iter.next()).done) {
    list.push(next.value)
  }

  return list
}

const _uniqContentEquals = (aIterator, bIterator, stackA, stackB) => {
  const a = _arrFromIter(aIterator)
  const b = _arrFromIter(bIterator)

  function _eq (_a, _b) {
    return deepEq(_a, _b, stackA.slice(), stackB.slice())
  }

  // if *a* array contains any element that is not included in *b*
  return !_containsWith(function (b, aItem) {
    return !_containsWith(_eq, aItem, b)
  }, b, a)
}

/**
 * @name deepEq
 * @function
 * @since v0.1.0
 * @category Function
 * @sig a -> b -> Boolean
 * @description Takes and compares two items. Capable of handling cyclical data structures
 * @param {Any} a First item to compare
 * @param {Any} b Second item to compare
 * @return {Boolean} Returns the boolean after running our comparison check
 *
 * @example
 * import { deepEq } from 'kyanite'
 *
 * const q = { a: 1 }
 *
 * deepEq({ a: 1 }, { a: 1 }) // => true
 * deepEq({ a: 1, b: 2 }, { b: 2, a: 1 }) // => true
 * deepEq(/[A-Z]/, new RegExp('[A-Z]') // => true
 * deepEq([1, 2], [1, 2]) // => true
 * deepEq(new Date(), new Date()) // => true
 * deepEq({ a: { q } }, { a: { q } }) // => true
 *
 * deepEq('test', new String('test')) // => false
 * deepEq(false, new Boolean(false)) // => false
 * deepEq(5, new Number(5)) // => false
 * deepEq([1, 2], [2, 1]) // => false
 * deepEq({ a: 1 }, { b: 1 }) // => false
 * deepEq(new Date('11/14/1992'), new Date('11/14/2018')) // => false
 * deepEq([], {}) // => false
 */
const deepEq = (a, b, stackA = [], stackB = []) => {
  if (eq(a, b)) {
    return true
  }

  const aType = type(a)

  if (aType !== type(b) || a == null || b == null) {
    return false
  }

  if (typeof a.equals === 'function' || typeof b.equals === 'function') {
    return typeof a.equals === 'function' && a.equals(b) &&
      typeof b.equals === 'function' && b.equals(a)
  }

  // Using the types certain logic should be called and addressed
  switch (aType) {
    case 'Arguments':
    case 'Array':
    case 'Object':
      if (typeof a.constructor === 'function' &&
        _functionName(a.constructor) === 'Promise') {
        return a === b
      }
      break
    case 'Boolean':
    case 'Number':
    case 'String':
      if (!(typeof a === typeof b && eq(a.valueOf(), b.valueOf()))) {
        return false
      }
      break
    case 'Date':
      if (!eq(a.valueOf(), b.valueOf())) {
        return false
      }
      break
    case 'Error':
      return a.name === b.name && a.message === b.message
    case 'RegExp':
      if (!(a.source === b.source &&
        a.global === b.global &&
        a.ignoreCase === b.ignoreCase &&
        a.multiline === b.multiline &&
        a.sticky === b.sticky &&
        a.unicode === b.unicode)) {
        return false
      }
      break
  }

  for (let i = stackA.length - 1; i >= 0; i--) {
    if (stackA[i] === a) {
      return stackB[i] === b
    }
  }

  switch (aType) {
    case 'Map':
      if (a.size !== b.size) {
        return false
      }

      return _uniqContentEquals(a.entries(), b.entries(), stackA.concat([a]), stackB.concat([b]))
    case 'Set':
      if (a.size !== b.size) {
        return false
      }

      return _uniqContentEquals(a.values(), b.values(), stackA.concat([a]), stackB.concat([b]))
    case 'Arguments':
    case 'Array':
    case 'Object':
    case 'Boolean':
    case 'Number':
    case 'String':
    case 'Date':
    case 'Error':
    case 'RegExp':
    case 'Int8Array':
    case 'Uint8Array':
    case 'Uint8ClampedArray':
    case 'Int16Array':
    case 'Uint16Array':
    case 'Int32Array':
    case 'Uint32Array':
    case 'Float32Array':
    case 'Float64Array':
    case 'ArrayBuffer':
      break
    default:
      return false
  }

  const keysA = Object.keys(a)

  if (keysA.length !== values(b).length) {
    return false
  }

  const extendedStackA = stackA.concat([a])
  const extendedStackB = stackB.concat([b])

  for (let i = keysA.length - 1; i >= 0; i--) {
    const key = keysA[i]

    if (!(Object.prototype.hasOwnProperty.call(b, key) && deepEq(b[key], a[key], extendedStackA, extendedStackB))) {
      return false
    }
  }

  return true
}

export default _curry2(deepEq)