const toString = Object.prototype.toString
const isEnumerable = Object.prototype.propertyIsEnumerable
function isFunction(data) {
  return typeof data === 'function'
}
function isBigInt(data) {
  return typeof data === 'bigint'
}
function isDate(data) {
  return toString.call(data).includes('Date')
}
function isRegExp(data) {
  return toString.call(data).includes('RegExp')
}
function isMap(data) {
  return toString.call(data).includes('Map')
}
function isSet(data) {
  return toString.call(data).includes('Set')
}
function cloneArray(data) {
  return data.reduce((result, value) => {
    return result.concat(cloneDeep(value))
  }, [])
}
function getObjectKeys(data) {
  const keys = []
  
  for (const key in data) {
    keys.push(key)
  }
  
  while (data) {
    const symbols = Object.getOwnPropertySymbols(data).filter((s) =>
      isEnumerable.call(data, s),
    )
    keys.push(...symbols)
    data = Object.getPrototypeOf(data)
  }
  return keys
}
function cloneObject(data) {
  const result = {}
  const keys = getObjectKeys(data)
  keys.forEach((key) => {
    result[key] = cloneDeep(data[key])
  })
  return result
}
function cloneMap(data) {
  const result = new Map()
  data.forEach((val, key) => {
    result.set(key, cloneDeep(val))
  })
  return result
}
function cloneSet(data) {
  const set = new Set()
  data.forEach((val) => {
    set.add(cloneDeep(val))
  })
  return set
}
function saveAndReturn(stack, key, value) {
  stack.set(key, value)
  return value
}
export default function cloneDeep(data) {
  const stack = cloneDeep.stack
  if (stack.has(data)) {
    return stack.get(data)
  }
  
  if (isFunction(data)) {
    return {}
  }
  
  if (isBigInt(data)) {
    return BigInt(data)
  }
  
  if (typeof data !== 'object' || data === null) {
    return data
  }
  
  if (isDate(data)) {
    return new Date(data.valueOf())
  }
  
  if (isRegExp(data)) {
    const reg = new RegExp(data.source, data.flags)
    reg.lastIndex = data.lastIndex
    return reg
  }
  
  if (Array.isArray(data)) {
    return saveAndReturn(stack, data, cloneArray(data))
  }
  
  if (isMap(data)) {
    return saveAndReturn(stack, data, cloneMap(data))
  }
  
  if (isSet(data)) {
    return saveAndReturn(stack, data, cloneSet(data))
  }
  
  return saveAndReturn(stack, data, cloneObject(data))
}
cloneDeep.stack = new Map()