/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @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;