import { isArray, isObject } from './assertion'
import { Dict } from './types'

/** Removes keys from an object if their value is undefined. */
export function filterUndefined<T = Dict>(object: T) {
  return objectFilter(object as Dict<T>, val => val !== null && val !== undefined)
}

/**
 * Recursively deep merges two objects in an immutable fashion, following these rules:
 * - Key exists in target and both keys are objects => merge
 * - Key exists in target and both keys are arrays => concat (not unique)
 * - Key isn't an object, an array or doesn't exist in target => override
 */
export function merge<S1 extends Dict>(s1: S1): S1
export function merge<S1 extends Dict, S2 extends Dict>(s1: S1, s2: S2): S1 & S2
export function merge<S1 extends Dict, S2 extends Dict, S3 extends Dict>(s1: S1, s2: S2, s3: S3): S1 & S2 & S3
export function merge<S1 extends Dict, S2 extends Dict, S3 extends Dict, S4 extends Dict>(
  s1: S1,
  s2: S2,
  s3: S3,
  s4: S4,
): S1 & S2 & S3 & S4
export function merge(...sources: any[]): any {
  const target = {} as Dict

  sources.forEach(source => {
    if (!isObject(source)) return

    Object.keys(source).forEach(key => {
      if (key === '__proto__') return

      if (isObject(target[key]) && isObject(source[key])) {
        // Merge objects
        target[key] = merge(target[key], source[key])
      } else if (isArray(target[key]) && isArray(source[key])) {
        // Concat arrays
        target[key] = target[key].concat(source[key])
      } else if (source[key] !== undefined) {
        // Pick source if it exists
        target[key] = source[key]
      }
    })
  })

  return target
}

/**
 * Returns the items of an object that meet the condition specified in a callback function.
 */
export function objectFilter<T extends Dict, K extends keyof T>(
  object: T,
  fn: (value: any, key: string, object: T) => boolean,
) {
  const result: Partial<T> = {}

  Object.keys(object).forEach(key => {
    const value = object[key]
    const shouldPass = fn(value, key, object)
    if (shouldPass) {
      result[key as K] = value
    }
  })

  return result
}
