类名 common/base/Collection.js
import Evented from './Evented'
import Zondy from './Zondy'
import { CollectionChangeEvent } from './event'

// copy from https://github.com/ecrmnn/collect.js/tree/master
// MIT License

function nestedValue(mainObject, key) {
  try {
    return key.split('.').reduce((obj, property) => obj[property], mainObject)
  } catch (err) {
    // If we end up here, we're not working with an object, and @var mainObject is the value itself
    return mainObject
  }
}

function clone(items) {
  let cloned

  if (Array.isArray(items)) {
    cloned = []

    cloned.push(...items)
  } else {
    cloned = {}

    Object.keys(items).forEach((prop) => {
      cloned[prop] = items[prop]
    })
  }

  return cloned
}

function values(items) {
  const valuesArray = []

  if (Array.isArray(items)) {
    valuesArray.push(...items)
  } else if (items.constructor.name === 'Collection') {
    valuesArray.push(...items.all())
  } else {
    Object.keys(items).forEach((prop) => valuesArray.push(items[prop]))
  }

  return valuesArray
}

/**
 * @returns {boolean}
 */
function isArray(item) {
  return Array.isArray(item)
}

/**
 * @returns {boolean}
 */
function isObject(item) {
  return (
    typeof item === 'object' && Array.isArray(item) === false && item !== null
  )
}

/**
 * @returns {boolean}
 */
function isFunction(item) {
  return typeof item === 'function'
}

function falsyValue(item) {
  if (Array.isArray(item)) {
    if (item.length) {
      return false
    }
  } else if (item !== undefined && item !== null && typeof item === 'object') {
    if (Object.keys(item).length) {
      return false
    }
  } else if (item) {
    return false
  }

  return true
}

function filterObject(func, items) {
  const result = {}
  Object.keys(items).forEach((key) => {
    if (func) {
      if (func(items[key], key)) {
        result[key] = items[key]
      }
    } else if (!falsyValue(items[key])) {
      result[key] = items[key]
    }
  })

  return result
}

function filterArray(func, items) {
  if (func) {
    return items.filter(func)
  }
  const result = []
  for (let i = 0; i < items.length; i += 1) {
    const item = items[i]
    if (!falsyValue(item)) {
      result.push(item)
    }
  }

  return result
}

function variadic(args) {
  if (Array.isArray(args[0])) {
    return args[0]
  }

  return args
}

/**
 * The Collection object.
 * @private
 * @example
 * let collection = new Collection([1, 2, 3]);
 */
class Collection extends Evented {
  /**
   * The collection constructor.
   *
   * @param  {Array} [collection=[]] the array to collect.
   * @return {Collection} A Collection object.
   */
  constructor(collection = []) {
    super()
    this.type = 'collection'
    if (collection instanceof Collection) {
      this.items = collection.all()
    } else if (Array.isArray(collection)) {
      this.items = collection
    } else {
      this.items = []
    }
    this.items = this.items.map((v) => this.createInstance(v))
  }

  /**
   * Gets the collected elements in an array.
   * @private
   * @return {Array} the internal array.
   * @example
   * const collection = new Collection([1, 2, 3]);
   * console.log(collection.all()); // [1, 2, 3]
   */
  all() {
    return this.items
  }

  /**
   * Chunks the collection into a new collection with equal length arrays as its items.
   * @private
   * @param  {number} size the size of each chunk.
   * @return {Collection} the new collection.
   * @example
   * const collection = new Collection([1, 2, 3, 4, 5]).chunk(2);
   * console.log(collection.all()); // [[1, 2], [3, 4], [5]]
   */
  chunk(size) {
    const chunks = []
    let index = 0

    if (Array.isArray(this.items)) {
      do {
        const items = this.items.slice(index, index + size)
        const collection = new this.constructor(items)

        chunks.push(collection)
        index += size
      } while (index < this.items.length)
    } else if (typeof this.items === 'object') {
      const keys = Object.keys(this.items)

      do {
        const keysOfChunk = keys.slice(index, index + size)
        const collection = new this.constructor({})

        keysOfChunk.forEach((key) => collection.put(key, this.items[key]))

        chunks.push(collection)
        index += size
      } while (index < keys.length)
    } else {
      chunks.push(new this.constructor([this.items]))
    }

    return new this.constructor(chunks)
  }

  /**
   * Concatnates the collection with an array or another collection.
   * @private
   * @param {Array|Collection} collection the array or the collection to be concatenated with.
   * @return {Collection} concatenated collection.
   * @example
   * const collection = new Collection([1, 2, 3]);
   * const array = [4, 5, 6]; // or another collection.
   * const newCollection = collection.concat(array);
   * console.log(newCollection.all()); // [1, 2, 3, 4, 5, 6]
   */
  concat(collectionOrArrayOrObject) {
    let list = collectionOrArrayOrObject

    if (collectionOrArrayOrObject instanceof this.constructor) {
      list = collectionOrArrayOrObject.all()
    } else if (typeof collectionOrArrayOrObject === 'object') {
      list = []
      Object.keys(collectionOrArrayOrObject).forEach((property) => {
        list.push(collectionOrArrayOrObject[property])
      })
    }

    const collection = clone(this.items)

    list.forEach((item) => {
      if (typeof item === 'object') {
        Object.keys(item).forEach((key) => collection.push(item[key]))
      } else {
        collection.push(item)
      }
    })

    return new this.constructor(collection)
  }

  contains(key, value) {
    if (value !== undefined) {
      if (Array.isArray(this.items)) {
        return (
          this.items.filter(
            (items) => items[key] !== undefined && items[key] === value
          ).length > 0
        )
      }

      return this.items[key] !== undefined && this.items[key] === value
    }

    if (isFunction(key)) {
      return this.items.filter((item, index) => key(item, index)).length > 0
    }

    if (Array.isArray(this.items)) {
      return this.items.indexOf(key) !== -1
    }

    const keysAndValues = values(this.items)
    keysAndValues.push(...Object.keys(this.items))

    return keysAndValues.indexOf(key) !== -1
  }

  /**
   * Gets the number of items in the collection.
   * @private
   * @return {number} Number of items in the collection.
   * @example
   * const collection = new Collection([1, 2, 3]);
   * console.log(collection.count()); // 3
   */
  count() {
    let arrayLength = 0

    if (Array.isArray(this.items)) {
      arrayLength = this.items.length
    }

    return Math.max(Object.keys(this.items).length, arrayLength)
  }

  /**
   * Executes a callback for each element in the collection.
   * @private
   * @param  {function} callback the callback to be excuted for each item.
   * @return {Collection} The collection object.
   * @example
   * const collection = new Collection(['this', 'is', 'collectionjs']);
   * collection.each(t => console.log(t)); // this is collectionjs
   */
  each(fn) {
    let stop = false

    if (Array.isArray(this.items)) {
      const { length } = this.items

      for (let index = 0; index < length && !stop; index += 1) {
        stop = fn(this.items[index], index, this.items) === false
      }
    } else {
      const keys = Object.keys(this.items)
      const { length } = keys

      for (let index = 0; index < length && !stop; index += 1) {
        const key = keys[index]

        stop = fn(this.items[key], key, this.items) === false
      }
    }

    return this
  }

  /**
   * Filters the collection using a predicate (callback that returns a boolean).
   * @private
   * @param  {function} callback A function that returns a boolean expression.
   * @return {Collection} Filtered collection.
   * @example
   * const collection = new Collection([
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Jon Snow', age: 14 }
   * ]).filter(stark => stark.age === 14);
   * console.log(collection.all()); // [{ name: 'Jon Snow', age: 14 }]
   */
  filter(fn) {
    const func = fn || false
    let filteredItems = null
    if (Array.isArray(this.items)) {
      filteredItems = filterArray(func, this.items)
    } else {
      filteredItems = filterObject(func, this.items)
    }

    return new this.constructor(filteredItems)
  }

  first(fn, defaultValue) {
    if (isFunction(fn)) {
      const keys = Object.keys(this.items)

      for (let i = 0; i < keys.length; i += 1) {
        const key = keys[i]
        const item = this.items[key]

        if (fn(item, key)) {
          return item
        }
      }

      if (isFunction(defaultValue)) {
        return defaultValue()
      }

      return defaultValue
    }

    if (
      (Array.isArray(this.items) && this.items.length) ||
      Object.keys(this.items).length
    ) {
      if (Array.isArray(this.items)) {
        return this.items[0]
      }

      const firstKey = Object.keys(this.items)[0]

      return this.items[firstKey]
    }

    if (isFunction(defaultValue)) {
      return defaultValue()
    }

    return defaultValue
  }

  flatten(depth) {
    let flattenDepth = depth || Infinity

    let fullyFlattened = false
    let collection = []

    const flat = function flat(items) {
      collection = []

      if (isArray(items)) {
        items.forEach((item) => {
          if (isArray(item)) {
            collection = collection.concat(item)
          } else if (isObject(item)) {
            Object.keys(item).forEach((property) => {
              collection = collection.concat(item[property])
            })
          } else {
            collection.push(item)
          }
        })
      } else {
        Object.keys(items).forEach((property) => {
          if (isArray(items[property])) {
            collection = collection.concat(items[property])
          } else if (isObject(items[property])) {
            Object.keys(items[property]).forEach((prop) => {
              collection = collection.concat(items[property][prop])
            })
          } else {
            collection.push(items[property])
          }
        })
      }

      fullyFlattened = collection.filter((item) => isObject(item))
      fullyFlattened = fullyFlattened.length === 0

      flattenDepth -= 1
    }

    flat(this.items)

    // eslint-disable-next-line no-unmodified-loop-condition
    while (!fullyFlattened && flattenDepth > 0) {
      flat(collection)
    }

    return new this.constructor(collection)
  }

  /**
   * Gets an element at a specified index.
   * @private
   * @param  {number} index the index of the item.
   * @return {*} the item at that index.
   * @example
   * const collection = new Collection([1, 2, 3]);
   * console.log(collection.get(2)); // 3
   */
  get(key, defaultValue = null) {
    if (this.items[key] !== undefined) {
      return this.items[key]
    }

    if (isFunction(defaultValue)) {
      return defaultValue()
    }

    if (defaultValue !== null) {
      return defaultValue
    }

    return null
  }

  has(...args) {
    const properties = variadic(args)

    return (
      properties.filter((key) => Object.hasOwnProperty.call(this.items, key))
        .length === properties.length
    )
  }

  /**
   * Joins the collection elements into a string.
   * @private
   * @param  {string} [seperator=','] The seperator between each element and the next.
   * @return {string} The joined string.
   *
   * @example
   * const collection = new Collection(['Wind', 'Rain', 'Fire']);
   * console.log(collection.join()); // 'Wind,Rain,Fire'
   * console.log(collection.join(', ')); 'Wind, Rain, Fire'
   */
  join(glue, finalGlue) {
    const collection = this.values()

    if (finalGlue === undefined) {
      return collection.implode(glue)
    }

    const count = collection.count()

    if (count === 0) {
      return ''
    }

    if (count === 1) {
      return collection.last()
    }

    const finalItem = collection.pop()

    return collection.implode(glue) + finalGlue + finalItem
  }

  /**
   * Gets the collection elements keys in a new collection.
   * @private
   * @return {Collection} The keys collection.
   * @example <caption>Objects</caption>
   * const keys = new Collection({
   *     arya: 10,
   *     john: 20,
   *     potato: 30
   * }).keys();
   * console.log(keys); // ['arya', 'john', 'potato']
   *
   * @example <caption>Regular Array</caption>
   * const keys = new Collection(['arya', 'john', 'potato']).keys();
   * console.log(keys); // ['0', '1', '2']
   */
  keys() {
    let collection = Object.keys(this.items)

    if (Array.isArray(this.items)) {
      collection = collection.map(Number)
    }

    return new this.constructor(collection)
  }

  /**
   * Gets the last element to satisfy a callback.
   * @private
   * @param  {function} [callback=null] the predicate to be checked on all elements.
   * @return {*} The last element in the collection that satisfies the predicate.
   * @example <caption>Using a callback</caption>
   * const last = new Collection([
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Jon Snow', age: 14 }
   * ]).last(item => item.age > 7);
   *
   * console.log(last); // { name: 'Jon Snow', age: 14 }
   * @example <caption>No Arguments</caption>
   * const last = new Collection([
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Jon Snow', age: 14 }
   * ]).last();
   *
   * console.log(last); // { name: 'Jon Snow', age: 14 }
   */
  last(fn, defaultValue) {
    let { items } = this

    if (isFunction(fn)) {
      items = this.filter(fn).all()
    }

    if ((Array.isArray(items) && !items.length) || !Object.keys(items).length) {
      if (isFunction(defaultValue)) {
        return defaultValue()
      }

      return defaultValue
    }

    if (Array.isArray(items)) {
      return items[items.length - 1]
    }
    const keys = Object.keys(items)

    return items[keys[keys.length - 1]]
  }

  /**
   * Maps each element using a mapping function and collects the mapped items.
   * @private
   * @param  {function} callback the mapping function.
   * @return {Collection} collection containing the mapped items.
   * @example
   * const collection = new Collection([
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Jon Snow', age: 14 }
   * ]).map(stark => stark.name);
   * console.log(collection.all()); ['Bran Stark', 'Arya Stark', 'Jon Snow']
   */
  map(fn) {
    if (Array.isArray(this.items)) {
      return new this.constructor(this.items.map(fn))
    }

    const collection = {}

    Object.keys(this.items).forEach((key) => {
      collection[key] = fn(this.items[key], key)
    })

    return new this.constructor(collection)
  }

  /**
   * Adds an element to the collection.
   * @private
   * @param  {*} item the item to be added.
   * @return {Collection} The collection object.
   * @example
   * const collection = new Collection().push('First');
   * console.log(collection.first()); // "First"
   */
  push(...items) {
    this.items.push(...items)

    return this
  }

  /**
   * Reduces the collection to a single value using a reducing function.
   * @private
   * @param  {function} callback the reducing function.
   * @param  {*} initial  initial value.
   * @return {*} The reduced results.
   * @example
   * const value = new Collection([1, 2, 3]).reduce(
   *     (previous, current) => previous + current,
   *      0
   *  );
   *  console.log(value); // 6
   */
  reduce(fn, carry) {
    let reduceCarry = null

    if (carry !== undefined) {
      reduceCarry = carry
    }

    if (Array.isArray(this.items)) {
      this.items.forEach((item) => {
        reduceCarry = fn(reduceCarry, item)
      })
    } else {
      Object.keys(this.items).forEach((key) => {
        reduceCarry = fn(reduceCarry, this.items[key], key)
      })
    }

    return reduceCarry
  }

  /**
   * Removes the elements that do not satisfy the predicate.
   * @private
   * @param  {function} callback the predicate used on each item.
   * @return {Collection} A collection without the rejected elements.
   * @example
   * const collection = new Collection([
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Jon Snow', age: 14 }
   * ]).reject(stark => stark.age < 14);
   * console.log(collection.all()); // [{ name: 'Jon Snow', age: 14 }]
   */
  reject(fn) {
    return new this.constructor(this.items).filter((item) => !fn(item))
  }

  /**
   * Reverses the collection order.
   * @private
   * @return {Collection} A new collection with the reversed order.
   * @example
   * const collection = new Collection(['one', 'two', 'three']).reverse();
   * console.log(collection.all()); // ['three', 'two', 'one']
   */
  reverse() {
    const collection = [].concat(this.items).reverse()
    return new this.constructor(collection)
  }

  /**
   * Skips a specified number of elements.
   * @private
   * @param  {number} count the number of items to be skipped.
   * @return {Collection} A collection without the skipped items.
   * @example
   * const collection = new Collection(['John', 'Arya', 'Bran', 'Sansa']).skip(2);
   * console.log(collection.all()); // ['Bran', 'Sansa']
   */
  skip(number) {
    if (isObject(this.items)) {
      return new this.constructor(
        Object.keys(this.items).reduce((accumulator, key, index) => {
          if (index + 1 > number) {
            accumulator[key] = this.items[key]
          }

          return accumulator
        }, {})
      )
    }

    return new this.constructor(this.items.slice(number))
  }

  /**
   * Slices the collection starting from a specific index and ending at a specified index.
   * @private
   * @param  {number} start The zero-based starting index.
   * @param  {number} [end=length] The zero-based ending index.
   * @return {Collection} A collection with the sliced items.
   * @example <caption>start and end are specified</caption>
   * const collection = new Collection([0, 1, 2, 3, 4, 5, 6]).slice(2, 4);
   * console.log(collection.all()); // [2, 3]
   *
   * @example <caption>only start is specified</caption>
   * const collection = new Collection([0, 1, 2, 3, 4, 5, 6]).slice(2);
   * console.log(collection.all()); // [2, 3, 4, 5, 6]
   */
  slice(remove, limit) {
    let collection = this.items.slice(remove)

    if (limit !== undefined) {
      collection = collection.slice(0, limit)
    }

    return new this.constructor(collection)
  }

  /**
   * Sorts the elements of a collection and returns a new sorted collection.
   * note that it doesn't change the orignal collection and it creates a
   * shallow copy.
   * @private
   * @param  {function} [compare=undefined] the compare function.
   * @return {Collection} A new collection with the sorted items.
   *
   * @example
   * const collection = new Collection([5, 3, 4, 1, 2]);
   * const sorted = collection.sort();
   * // original collection is intact.
   * console.log(collection.all()); // [5, 3, 4, 1, 2]
   * console.log(sorted.all()); // [1, 2, 3, 4, 5]
   */
  sort(fn) {
    const collection = [].concat(this.items)

    if (fn === undefined) {
      if (this.every((item) => typeof item === 'number')) {
        collection.sort((a, b) => a - b)
      } else {
        collection.sort()
      }
    } else {
      collection.sort(fn)
    }

    return new this.constructor(collection)
  }

  /**
   * Sorts the collection by key value comaprison, given that the items are objects.
   * It creates a shallow copy and retains the order of the orignal collection.
   * @private
   * @param  {string} property the key or the property to be compared.
   * @param  {string} [order='asc'] The sorting order.
   * use 'asc' for ascending or 'desc' for descending, case insensitive.
   * @return {Collection} A new Collection with the sorted items.
   *
   * @example
   * const collection = new Collection([
   *     { name: 'Jon Snow', age: 14 },
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Bran Stark', age: 7 },
   * ]).sortBy('age');
   *
   * console.log(collection.pluck('name').all()); // ['Brand Stark', 'Arya Stark', 'Jon Snow']
   */
  sortBy(valueOrFunction) {
    const collection = [].concat(this.items)
    const getValue = (item) => {
      if (isFunction(valueOrFunction)) {
        return valueOrFunction(item)
      }

      return nestedValue(item, valueOrFunction)
    }

    collection.sort((a, b) => {
      const valueA = getValue(a)
      const valueB = getValue(b)

      if (valueA === null || valueA === undefined) {
        return 1
      }
      if (valueB === null || valueB === undefined) {
        return -1
      }

      if (valueA < valueB) {
        return -1
      }
      if (valueA > valueB) {
        return 1
      }

      return 0
    })

    return new this.constructor(collection)
  }

  sortByDesc(valueOrFunction) {
    return this.sortBy(valueOrFunction).reverse()
  }

  sortDesc() {
    return this.sort().reverse()
  }

  sortKeys() {
    const ordered = {}

    Object.keys(this.items)
      .sort()
      .forEach((key) => {
        ordered[key] = this.items[key]
      })

    return new this.constructor(ordered)
  }

  sortKeysDesc() {
    const ordered = {}

    Object.keys(this.items)
      .sort()
      .reverse()
      .forEach((key) => {
        ordered[key] = this.items[key]
      })

    return new this.constructor(ordered)
  }

  /**
   * Sums the values of the array, or the properties, or the result of the callback.
   * @private
   * @param  {undefined|string|function} [property=null] the property to be summed.
   * @return {*} The sum of values used in the summation.
   * @example <caption>Summing elements</caption>
   * const collection = new Collection([1, 2, 3]);
   * console.log(collection.sum()); // 6
   *
   * @example <caption>Summing a property</caption>
   * const collection = new Collection([
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Jon Snow', age: 14 }
   * ]);
   * console.log(collection.sum('age')); // 30
   *
   * @example <caption>Summing using a callback</caption>
   * const collection = new Collection([
   *     { name: 'Arya Stark', age: 9 },
   *     { name: 'Bran Stark', age: 7 },
   *     { name: 'Jon Snow', age: 14 }
   * ]);
   * console.log(collection.sum(i => i.age + 1)); // 33
   */
  sum(key) {
    const items = values(this.items)

    let total = 0

    if (key === undefined) {
      for (let i = 0, { length } = items; i < length; i += 1) {
        total += parseFloat(items[i])
      }
    } else if (isFunction(key)) {
      for (let i = 0, { length } = items; i < length; i += 1) {
        total += parseFloat(key(items[i]))
      }
    } else {
      for (let i = 0, { length } = items; i < length; i += 1) {
        total += parseFloat(items[i][key])
      }
    }

    return parseFloat(total.toPrecision(12))
  }

  /**
   * Gets a new collection with the number of specified items from the begining or the end.
   * @private
   * @param  {number} count the number of items to take. Takes from end if negative.
   * @return {Collection} A collection with the taken items.
   * @example <caption>From the beginning</caption>
   * const collection = new Collection([1, 2, 3, 4, 5]).take(3);
   * console.log(collection.all()); // [1, 2, 3]
   *
   * @example <caption>From the end</caption>
   * const collection = new Collection([1, 2, 3, 4, 5]).take(-3);
   * console.log(collection.all()); // [5, 4 ,3]
   */
  take(length) {
    if (!Array.isArray(this.items) && typeof this.items === 'object') {
      const keys = Object.keys(this.items)
      let slicedKeys

      if (length < 0) {
        slicedKeys = keys.slice(length)
      } else {
        slicedKeys = keys.slice(0, length)
      }

      const collection = {}

      keys.forEach((prop) => {
        if (slicedKeys.indexOf(prop) !== -1) {
          collection[prop] = this.items[prop]
        }
      })

      return new this.constructor(collection)
    }

    if (length < 0) {
      return new this.constructor(this.items.slice(length))
    }

    return new this.constructor(this.items.slice(0, length))
  }

  /**
   * Remove duplicate values from the collection.
   * @private
   * @param {function|string} [callback=null] The predicate that returns a value
   * which will be checked for uniqueness, or a string that has the name of the property.
   * @return {Collection} A collection containing ue values.
   * @example <caption>No Arguments</caption>
   * const unique = new Collection([2, 1, 2, 3, 3, 4, 5, 1, 2]).unique();
   * console.log(unique); // [2, 1, 3, 4, 5]
   * @example <caption>Property Name</caption>
   * const students = new Collection([
   *    { name: 'Rick', grade: 'A'},
   *    { name: 'Mick', grade: 'B'},
   *    { name: 'Richard', grade: 'A'},
   * ]);
   * // Students with unique grades.
   * students.unique('grade'); // [{ name: 'Rick', grade: 'A'}, { name: 'Mick', grade: 'B'}]
   * @example <caption>With Callback</caption>
   * const students = new Collection([
   *    { name: 'Rick', grade: 'A'},
   *    { name: 'Mick', grade: 'B'},
   *    { name: 'Richard', grade: 'A'},
   * ]);
   * // Students with unique grades.
   * students.unique(s => s.grade); // [{ name: 'Rick', grade: 'A'}, { name: 'Mick', grade: 'B'}]
   */
  unique(key) {
    let collection

    if (key === undefined) {
      collection = this.items.filter(
        (element, index, self) => self.indexOf(element) === index
      )
    } else {
      collection = []

      const usedKeys = []

      for (
        let iterator = 0, { length } = this.items;
        iterator < length;
        iterator += 1
      ) {
        let uniqueKey
        if (isFunction(key)) {
          uniqueKey = key(this.items[iterator])
        } else {
          uniqueKey = this.items[iterator][key]
        }

        if (usedKeys.indexOf(uniqueKey) === -1) {
          collection.push(this.items[iterator])
          usedKeys.push(uniqueKey)
        }
      }
    }

    return new this.constructor(collection)
  }

  /**
   * Gets the values without preserving the keys.
   * @private
   * @return {Collection} A Collection containing the values.
   * @example
   * const collection = new Collection({
   *     1: 2,
   *     2: 3,
   *     4: 5
   * }).values();
   *
   * console.log(collection.all()); / /[2, 3, 5]
   */
  values() {
    return new this.constructor(values(this.items))
  }

  /**
   * Filters the collection using a callback or equality comparison to a property in each item.
   * @private
   */
  where(key, operator, value) {
    let comparisonOperator = operator
    let comparisonValue = value

    const items = values(this.items)

    if (operator === undefined || operator === true) {
      return new this.constructor(
        items.filter((item) => nestedValue(item, key))
      )
    }

    if (operator === false) {
      return new this.constructor(
        items.filter((item) => !nestedValue(item, key))
      )
    }

    if (value === undefined) {
      comparisonValue = operator
      comparisonOperator = '==='
    }

    const collection = items.filter((item) => {
      switch (comparisonOperator) {
        case '==':
          return (
            nestedValue(item, key) === Number(comparisonValue) ||
            nestedValue(item, key) === comparisonValue.toString()
          )

        default:
        case '===':
          return nestedValue(item, key) === comparisonValue

        case '!=':
        case '<>':
          return (
            nestedValue(item, key) !== Number(comparisonValue) &&
            nestedValue(item, key) !== comparisonValue.toString()
          )

        case '!==':
          return nestedValue(item, key) !== comparisonValue

        case '<':
          return nestedValue(item, key) < comparisonValue

        case '<=':
          return nestedValue(item, key) <= comparisonValue

        case '>':
          return nestedValue(item, key) > comparisonValue

        case '>=':
          return nestedValue(item, key) >= comparisonValue
      }
    })

    return new this.constructor(collection)
  }

  /**
   * Pairs each item in the collection with another array item in the same index.
   * @private
   * @param  {Array|Collection} array the array to be paired with.
   * @return {Collection} A collection with the paired items.
   * @example
   * const array = ['a', 'b', 'c']; // or a collection.
   * const collection = new Collection([1, 2, 3]).zip(array);
   * console.log(collection.all()); // [[1, 'a'], [2, 'b'], [3, 'c']]
   */
  zip(array) {
    let values = array

    if (values instanceof this.constructor) {
      values = values.all()
    }

    const collection = this.items.map(
      (item, index) => new this.constructor([item, values[index]])
    )

    return new this.constructor(collection)
  }

  every(fn) {
    const items = values(this.items)
    return items.every(fn)
  }

  findIndex(callback) {
    return this.items.findIndex(callback)
  }

  forEach(callback) {
    return this.each(callback)
  }

  /**
   * 创建实例
   * @private
   * @return {any} any object
   */
  createInstance(item) {
    return item
  }

  /**
   * Adds an item to the collection.
   * @private
   * @param {*} item the item to be added.
   * @return {Collection} the collection object.
   * @example
   * const collection = new Collection();
   * collection.add('Arya');
   * console.log(collection.first()); //outputs 'Arya'
   */
  add(item) {
    const instance = this.createInstance(item)
    this.items.push(instance)
    this.fire(
      'change',
      new CollectionChangeEvent({
        removed: [],
        added: [instance]
      })
    )
    return this
  }

  /**
   * Removes an item from the collection.
   * @private
   * @param  {*} item the item to be searched and removed, first occurance will be removed.
   * @return {boolean} True if the element was removed, false otherwise.
   * @example
   * const collection = new Collection(['john', 'arya', 'bran']);
   * collection.remove('john');
   * console.log(collection.all()); // ['arya', 'bran']
   */
  remove(item) {
    const index = this.items.indexOf(item)
    if (index > -1) {
      this.items.splice(index, 1)
      this.fire(
        'change',
        new CollectionChangeEvent({
          removed: [item],
          added: []
        })
      )
      return true
    }
    return false
  }

  /**
   * 创建实例
   * @private
   * @return {any} any object
   */
  addMany(items) {
    if (Array.isArray(items)) {
      const instances = items.map((v) => this.createInstance(v))
      this.items = this.items.concat(instances)
      this.fire(
        'change',
        new CollectionChangeEvent({
          removed: [],
          added: instances
        })
      )
    }
  }

  /**
   * 删除实例
   * @private
   * @return {any} any object
   */
  removeMany(items) {
    if (Array.isArray(items)) {
      const removed = []
      for (let i = 0; i < items.length; i++) {
        const flag = this.remove(items[i])
        if (flag) {
          removed.push(items[i])
        }
      }
      this.fire(
        'change',
        new CollectionChangeEvent({
          removed,
          added: []
        })
      )
    }
  }

  /**
   * 删除实例
   * @private
   * @return {any} any object
   */
  removeAll() {
    const removed = this.items.map((v) => v)
    this.fire(
      'change',
      new CollectionChangeEvent({
        removed,
        added: []
      })
    )
    this.items = []
  }

  /**
   * Static constructor.
   * cool if you don't like using the 'new' keyword.
   * @private
   * @param  {Array} collectable the array or the string to wrapped in a collection.
   * @return {Collection} A collection that wraps the collectable items.
   * @example
   * const collection = Collection.collect([1, 2, 3]);
   * console.log(collection.all()); // [1, 2, 3]
   */
  static collect(collectable) {
    return new Collection(collectable)
  }
}

Zondy.Collection = Collection
export default Collection
构造函数
成员变量
方法
事件