    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