/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Utilities for working with ES6 iterables.
 *
 * The goal is that this should be a replacement for goog.iter which uses
 * a now non-standard approach to iterables.
 *
 * @see https://goo.gl/Rok5YQ
 */

goog.module('goog.collections.iters');
goog.module.declareLegacyNamespace();

/**
 * Get the iterator for an iterable.
 * @param {!Iterable<VALUE>} iterable
 * @return {!Iterator<VALUE>}
 * @template VALUE
 */
function getIterator(iterable) {
  return iterable[goog.global.Symbol.iterator]();
}
exports.getIterator = getIterator;


/**
 * Call a function with every value of an iterable.
 *
 * Warning: this function will never halt if given an iterable that
 * is never exhausted.
 *
 * @param {!Iterable<VALUE>} iterable
 * @param {function(VALUE) : *} f
 * @template VALUE
 */
exports.forEach = function(iterable, f) {
  for (const elem of iterable) {
    f(elem);
  }
};


/**
 * An Iterable that wraps a child iterable, and maps every element of the child
 * iterator to a new value, using a mapping function. Similar to Array.map, but
 * for Iterable.
 * @template TO,FROM
 * @implements {IteratorIterable<TO>}
 */
class MapIterator {
  /**
   * @param {!Iterable<FROM>} childIter
   * @param {function(FROM, number): TO} mapFn
   */
  constructor(childIter, mapFn) {
    /** @private @const {!Iterator<FROM>} */
    this.childIterator_ = getIterator(childIter);

    /** @private @const {function(FROM, number): TO} */
    this.mapFn_ = mapFn;

    /** @private {number} */
    this.nextIndex_ = 0;
  }

  [Symbol.iterator]() {
    return this;
  }

  /** @override */
  next() {
    const childResult = this.childIterator_.next();
    // Always return a new object, even when childResult.done == true. This is
    // so that we don't accidentally preserve generator return values, which
    // are unlikely to be meaningful in the context of this MapIterator.
    return {
      value: childResult.done ?
          undefined :
          this.mapFn_.call(undefined, childResult.value, this.nextIndex_++),
      done: childResult.done,
    };
  }
}


/**
 * Maps the values of one iterable to create another iterable.
 *
 * When next() is called on the returned iterable, it will call the given
 * function `f` with the next value of the given iterable
 * `iterable` until the given iterable is exhausted.
 *
 * @param {!Iterable<VALUE>} iterable
 * @param {function(VALUE, number): RESULT} f
 * @return {!IteratorIterable<RESULT>} The created iterable that gives the
 *     mapped values.
 * @template VALUE, RESULT
 */
exports.map = function(iterable, f) {
  return new MapIterator(iterable, f);
};


/**
 * An Iterable that wraps a child Iterable and returns a subset of the child's
 * items, based on a filter function. Similar to Array.filter, but for
 * Iterable.
 * @template T
 * @implements {IteratorIterable<T>}
 */
class FilterIterator {
  /**
   * @param {!Iterable<T>} childIter
   * @param {function(T, number): boolean} filterFn
   */
  constructor(childIter, filterFn) {
    /** @private @const {!Iterator<T>} */
    this.childIter_ = getIterator(childIter);

    /** @private @const {function(T, number): boolean} */
    this.filterFn_ = filterFn;

    /** @private {number} */
    this.nextIndex_ = 0;
  }

  [Symbol.iterator]() {
    return this;
  }

  /** @override */
  next() {
    while (true) {
      const childResult = this.childIter_.next();
      if (childResult.done) {
        // Don't return childResult directly, because that would preserve
        // generator return values, and we want to ignore them.
        return {done: true, value: undefined};
      }
      const passesFilter =
          this.filterFn_.call(undefined, childResult.value, this.nextIndex_++);
      if (passesFilter) {
        return childResult;
      }
    }
  }
}


/**
 * Filter elements from one iterator to create another iterable.
 *
 * When next() is called on the returned iterator, it will call next() on the
 * given iterator and call the given function `f` with that value until `true`
 * is returned or the given iterator is exhausted.
 *
 * @param {!Iterable<VALUE>} iterable
 * @param {function(VALUE, number): boolean} f
 * @return {!IteratorIterable<VALUE>} The created iterable that gives the mapped
 *     values.
 * @template VALUE
 */
exports.filter = function(iterable, f) {
  return new FilterIterator(iterable, f);
};


/**
 * @template T
 * @implements {IteratorIterable<T>}
 */
class ConcatIterator {
  /** @param {!Array<!Iterator<T>>} iterators */
  constructor(iterators) {
    /** @private @const {!Array<!Iterator<T>>} */
    this.iterators_ = iterators;

    /** @private {number} */
    this.iterIndex_ = 0;
  }

  [Symbol.iterator]() {
    return this;
  }

  /** @override */
  next() {
    while (this.iterIndex_ < this.iterators_.length) {
      const result = this.iterators_[this.iterIndex_].next();
      if (!result.done) {
        return result;
      }
      this.iterIndex_++;
    }
    return /** @type {!IIterableResult<T>} */ ({done: true});
  }
}


/**
 * Concatenates multiple iterators to create a new iterable.
 *
 * When next() is called on the return iterator, it will call next() on the
 * current passed iterator. When the current passed iterator is exhausted, it
 * will move on to the next iterator until there are no more left.
 *
 * All generator return values will be ignored (i.e. when childIter.next()
 * returns {done: true, value: notUndefined} it will be treated as just
 * {done: true}).
 *
 * @param {...!Iterable<VALUE>} iterables
 * @return {!IteratorIterable<VALUE>}
 * @template VALUE
 */
exports.concat = function(...iterables) {
  return new ConcatIterator(iterables.map(getIterator));
};
