/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @providesModule enumerate
 *
 */

const KIND_KEYS = 'keys';
const KIND_VALUES = 'values';
const KIND_ENTRIES = 'entries';

/**
 * Specific Array iterators.
 */
const ArrayIterators = (function() {

  let hasNative = hasNativeIterator(Array);
  let ArrayIterator;

  if (!hasNative) {
    ArrayIterator = class ArrayIterator {
      // 22.1.5.1 CreateArrayIterator Abstract Operation
      constructor(array, kind) {
        this._iteratedObject = array;
        this._kind = kind;
        this._nextIndex = 0;
      }

      // 22.1.5.2.1 %ArrayIteratorPrototype%.next()
      next() {
        if (this._iteratedObject == null) {
          return {value: undefined, done: true};
        }

        let array = this._iteratedObject;
        let len = this._iteratedObject.length;
        let index = this._nextIndex;
        let kind = this._kind;

        if (index >= len) {
          this._iteratedObject = undefined;
          return {value: undefined, done: true};
        }

        this._nextIndex = index + 1;

        if (kind === KIND_KEYS) {
          return {value: index, done: false};
        } else if (kind === KIND_VALUES) {
          return {value: array[index], done: false};
        } else if (kind === KIND_ENTRIES) {
          return {value: [index, array[index]], done: false};
        }
      }

      // 22.1.5.2.2 %ArrayIteratorPrototype%[@@iterator]()
      [Symbol.iterator]() {
        return this;
      }
    };
  }

  return {
    keys: hasNative
      ? (array) => array.keys()
      : (array) => new ArrayIterator(array, KIND_KEYS),

    values: hasNative
      ? (array) => array.values()
      : (array) => new ArrayIterator(array, KIND_VALUES),

    entries: hasNative
      ? (array) => array.entries()
      : (array) => new ArrayIterator(array, KIND_ENTRIES),
  };
})();

// -----------------------------------------------------------------

/**
 * Specific String iterators.
 */
const StringIterators = (function() {

  let hasNative = hasNativeIterator(String);
  let StringIterator;

  if (!hasNative) {
    StringIterator = class StringIterator {
      // 21.1.5.1 CreateStringIterator Abstract Operation
      constructor(string) {
        this._iteratedString = string;
        this._nextIndex = 0;
      }

      // 21.1.5.2.1 %StringIteratorPrototype%.next()
      next() {
        if (this._iteratedString == null) {
          return {value: undefined, done: true};
        }

        let index = this._nextIndex;
        let s = this._iteratedString;
        let len = s.length;

        if (index >= len) {
          this._iteratedString = undefined;
          return {value: undefined, done: true};
        }

        let ret;
        let first = s.charCodeAt(index);

        if (first < 0xD800 || first > 0xDBFF || index + 1 === len) {
          ret = s[index];
        } else {
          let second = s.charCodeAt(index + 1);
          if (second < 0xDC00 || second > 0xDFFF) {
            ret = s[index];
          } else {
            ret = s[index] + s[index + 1];
          }
        }

        this._nextIndex = index + ret.length;

        return {value: ret, done: false};
      }

      // 21.1.5.2.2 %StringIteratorPrototype%[@@iterator]()
      [Symbol.iterator]() {
        return this;
      }
    };
  }

  return {
    keys() {
      throw TypeError(`Strings default iterator doesn't implement keys.`);
    },

    values: hasNative
      ? (string) => string[Symbol.iterator]()
      : (string) => new StringIterator(string),

    entries() {
      throw TypeError(`Strings default iterator doesn't implement entries.`);
    },
  };

})();

function hasNativeIterator(classObject) {
  return typeof classObject.prototype[Symbol.iterator] === 'function'
      && typeof classObject.prototype.values === 'function'
      && typeof classObject.prototype.keys === 'function'
      && typeof classObject.prototype.entries === 'function';
}

// -----------------------------------------------------------------

/**
 * Generic object iterator.
 */
class ObjectIterator {
  constructor(object, kind) {
    this._iteratedObject = object;
    this._kind = kind;
    this._keys = Object.keys(object);
    this._nextIndex = 0;
  }

  next() {
    let len = this._keys.length;
    let index = this._nextIndex;
    let kind = this._kind;
    let key = this._keys[index];

    if (index >= len) {
      this._iteratedObject = undefined;
      return {value: undefined, done: true};
    }

    this._nextIndex = index + 1;

    if (kind === KIND_KEYS) {
      return {value: key, done: false};
    } else if (kind === KIND_VALUES) {
      return {value: this._iteratedObject[key], done: false};
    } else if (kind === KIND_ENTRIES) {
      return {value: [key, this._iteratedObject[key]], done: false};
    }
  }

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

/**
 * Generic object iterator, iterates over all own enumerable
 * properties. Used only if if no specific iterator is available,
 * and object don't implement iterator protocol.
 */
const GenericIterators = {
  keys(object) {
    return new ObjectIterator(object, KIND_KEYS);
  },

  values(object) {
    return new ObjectIterator(object, KIND_VALUES);
  },

  entries(object) {
    return new ObjectIterator(object, KIND_ENTRIES);
  },
};

// -----------------------------------------------------------------

/**
 * Main iterator function. Returns default iterator based
 * on the class of an instance.
 */
function enumerate(object, kind) {

  // First check specific iterators.
  if (typeof object === 'string') {
    return StringIterators[kind || KIND_VALUES](object);
  } else if (Array.isArray(object)) {
    return ArrayIterators[kind || KIND_VALUES](object);

  // Then see if an object implements own.
  } else if (object[Symbol.iterator]) {
    return object[Symbol.iterator]();

  // And fallback to generic with entries.
  } else {
    return GenericIterators[kind || KIND_ENTRIES](object);
  }
}

Object.assign(enumerate, {
  /**
   * Export constants
   */

  KIND_KEYS,
  KIND_VALUES,
  KIND_ENTRIES,

  /**
   * Convenient explicit iterators for special kinds.
   */

  keys(object) {
    return enumerate(object, KIND_KEYS);
  },

  values(object) {
    return enumerate(object, KIND_VALUES);
  },

  entries(object) {
    return enumerate(object, KIND_ENTRIES);
  },

  generic: GenericIterators.entries,

});

module.exports = enumerate;
