is_enumerable := func(x) {
  return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x)
}

is_array_like := func(x) {
  return is_array(x) || is_immutable_array(x)
}

export {
  // all returns true if the given function `fn` evaluates to a truthy value on
  // all of the items in `x`. It returns undefined if `x` is not enumerable.
  all: func(x, fn) {
    if !is_enumerable(x) { return undefined }

    for k, v in x {
      if !fn(k, v) { return false }
    }

    return true
  },
  // any returns true if the given function `fn` evaluates to a truthy value on
  // any of the items in `x`. It returns undefined if `x` is not enumerable.
  any: func(x, fn) {
    if !is_enumerable(x) { return undefined }

    for k, v in x {
      if fn(k, v) { return true }
    }

    return false
  },
  // chunk returns an array of elements split into groups the length of size.
  // If `x` can't be split evenly, the final chunk will be the remaining elements.
  // It returns undefined if `x` is not array.
  chunk: func(x, size) {
    if !is_array_like(x) || !size { return undefined }

    numElements := len(x)
    if !numElements { return [] }

    res := []
    idx := 0
    for idx < numElements {
      res = append(res, x[idx:idx+size])
      idx += size
    }

    return res
  },
  // at returns an element at the given index (if `x` is array) or
  // key (if `x` is map). It returns undefined if `x` is not enumerable.
  at: func(x, key) {
    if !is_enumerable(x) { return undefined }

    if is_array_like(x) {
        if !is_int(key) { return undefined }
    } else {
        if !is_string(key) { return undefined }
    }

    return x[key]
  },
  // each iterates over elements of `x` and invokes `fn` for each element. `fn` is
  // invoked with two arguments: `key` and `value`. `key` is an int index
  // if `x` is array. `key` is a string key if `x` is map. It does not iterate
  // and returns undefined if `x` is not enumerable.
  each: func(x, fn) {
    if !is_enumerable(x) { return undefined }

    for k, v in x {
      fn(k, v)
    }
  },
  // filter iterates over elements of `x`, returning an array of all elements `fn`
  // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.
  // `key` is an int index if `x` is array. `key` is a string key if `x` is map.
  // It returns undefined if `x` is not enumerable.
  filter: func(x, fn) {
    if !is_array_like(x) { return undefined }

    dst := []
    for k, v in x {
      if fn(k, v) { dst = append(dst, v) }
    }

    return dst
  },
  // find iterates over elements of `x`, returning value of the first element `fn`
  // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.
  // `key` is an int index if `x` is array. `key` is a string key if `x` is map.
  // It returns undefined if `x` is not enumerable.
  find: func(x, fn) {
    if !is_enumerable(x) { return undefined }

    for k, v in x {
      if fn(k, v) { return v }
    }
  },
  // find_key iterates over elements of `x`, returning key or index of the first
  // element `fn` returns truthy for. `fn` is invoked with two arguments: `key`
  // and `value`. `key` is an int index if `x` is array. `key` is a string key if
  // `x` is map. It returns undefined if `x` is not enumerable.
  find_key: func(x, fn) {
    if !is_enumerable(x) { return undefined }

    for k, v in x {
      if fn(k, v) { return k }
    }
  },
  // map creates an array of values by running each element in `x` through `fn`.
  // `fn` is invoked with two arguments: `key` and `value`. `key` is an int index
  // if `x` is array. `key` is a string key if `x` is map. It returns undefined
  // if `x` is not enumerable.
  map: func(x, fn) {
    if !is_enumerable(x) { return undefined }

    dst := []
    for k, v in x {
      dst = append(dst, fn(k, v))
    }

    return dst
  },
  // key returns the first argument.
  key: func(k, _) { return k },
  // value returns the second argument.
  value: func(_, v) { return v }
}