type Obj = Record<string, unknown>

/**
 * Creates an object with the same keys as object and values generated by running each own enumerable property of object through iteratee. The iteratee function is invoked with three arguments: (value, key, object).
 *
 * Example:
 *
 * ```
 * const users = {
 *   fred: { user: 'fred', age: 40 },
 *
 *   pebbles: { user: 'pebbles', age: 1 },
 * }
 *
 * mapValues(users, (u) => u.age) // { fred: 40, pebbles: 1 }
 * ```
 *
 */
export const mapValues = (obj: Obj, fn: (value: unknown, key: string, obj?: Obj) => unknown) =>
  Object.keys(obj).reduce((acc: Obj, k) => {
    acc[k] = fn(obj[k], k, obj)
    return acc
  }, {})

export const mapKeys = (obj: Obj, fn: (value: unknown, key: string, obj?: Obj) => string) =>
  Object.keys(obj).reduce((acc: Obj, k) => {
    acc[fn(obj[k], k, obj)] = obj[k]
    return acc
  }, {})

/** Build a new object of only picked props */
export const pick = (obj: Obj, props: string[]) =>
  props.reduce((acc: Obj, prop: string) => ({ ...acc, [prop]: obj[prop] }), {})

/** Builds an array of numbers from (optional) start to end */
export const range = (start: number, end?: number) => {
  if (end === undefined) {
    end = start
    start = 0
  }
  return [...new Array(end - start).keys()].map((i) => i + start)
}

/** Build a new object excluding the omitted props */
export const omit = (obj: Obj, props: string[]) =>
  Object.keys(obj).reduce((acc: Obj, key: string) => {
    if (!props.includes(key)) {
      acc[key] = obj[key]
    }
    return acc
  }, {})

/** Custom type guard to check if a value is not undefined */
export const isNotUndefined = <T>(value: T | undefined): value is T => value !== undefined
