const isUndefined = (value) => value === undefined
const isNull = (value) => value === null
const isBoolean = (value) => typeof value === 'boolean'
const isObject = (value) => value === Object(value)
const isArray = (value) => Array.isArray(value)
const isDate = (value) => value instanceof Date

const isBlob = (value) =>
  value &&
  typeof value.size === 'number' &&
  typeof value.type === 'string' &&
  typeof value.slice === 'function'

const isFile = (value) =>
  isBlob(value) &&
  typeof value.name === 'string' &&
  (typeof value.lastModifiedDate === 'object' || typeof value.lastModified === 'number')

export const objectToFormData = (obj, cfg = {}, fd, pre) => {
  const config = {
    indices: isUndefined(cfg.indices) ? true : cfg.indices,
    nullsAsUndefineds: isUndefined(cfg.nullsAsUndefineds) ? false : cfg.nullsAsUndefineds,
    booleansAsIntegers: isUndefined(cfg.booleansAsIntegers) ? false : cfg.booleansAsIntegers,
    allowEmptyArrays: isUndefined(cfg.allowEmptyArrays) ? true : cfg.allowEmptyArrays,
  }

  const formData = fd || new FormData()

  if (isUndefined(obj)) {
    return formData
  }

  if (isNull(obj)) {
    if (!config.nullsAsUndefineds) {
      formData.append(pre, '')
    }
  } else if (isBoolean(obj)) {
    formData.append(pre, config.booleansAsIntegers ? (obj ? 1 : 0) : obj)
  } else if (isArray(obj)) {
    if (obj.length > 0) {
      obj.forEach((value, index) => {
        const key = `${pre}[${config.indices ? index : ''}]`
        objectToFormData(value, config, formData, key)
      })
    } else if (config.allowEmptyArrays) {
      formData.append(pre, [])
    }
  } else if (isDate(obj)) {
    formData.append(pre, obj.toISOString())
  } else if (isObject(obj) && !isFile(obj) && !isBlob(obj)) {
    Object.keys(obj).forEach((rawProp) => {
      const value = obj[rawProp]
      let prop = rawProp
      if (isArray(value)) {
        while (prop.length > 2 && prop.lastIndexOf('[]') === prop.length - 2) {
          prop = prop.slice(0, Math.max(0, prop.length - 2))
        }
      }
      const key = pre ? `${pre}[${prop}]` : prop
      objectToFormData(value, config, formData, key)
    })
  } else {
    formData.append(pre, obj)
  }

  return formData
}
