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

/**
 * @fileoverview Wrapper for an IndexedDB index.
 */


goog.provide('goog.db.Index');

goog.require('goog.async.Deferred');
goog.require('goog.db.Cursor');
goog.require('goog.db.Error');
goog.require('goog.db.KeyRange');
goog.require('goog.debug');



/**
 * Creates an IDBIndex wrapper object. Indexes are associated with object
 * stores and provide methods for looking up objects based on their non-key
 * properties. Should not be created directly, access through the object store
 * it belongs to.
 * @see goog.db.ObjectStore#getIndex
 *
 * @param {!IDBIndex} index Underlying IDBIndex object.
 * @constructor
 * @final
 */
goog.db.Index = function(index) {
  'use strict';
  /**
   * Underlying IndexedDB index object.
   *
   * @type {!IDBIndex}
   * @private
   */
  this.index_ = index;
};


/**
 * @return {string} Name of the index.
 */
goog.db.Index.prototype.getName = function() {
  'use strict';
  return this.index_.name;
};


/**
 * @return {*} Key path of the index.
 */
goog.db.Index.prototype.getKeyPath = function() {
  'use strict';
  return this.index_.keyPath;
};


/**
 * @return {boolean} True if the index enforces that there is only one object
 *     for each unique value it indexes on.
 */
goog.db.Index.prototype.isUnique = function() {
  'use strict';
  return this.index_.unique;
};


/**
 * Helper function for get and getKey.
 *
 * @param {string} fn Function name to call on the index to get the request.
 * @param {string} msg Message to give to the error.
 * @param {!IDBKeyType} key The key to look up in the index.
 * @return {!goog.async.Deferred} The resulting deferred object.
 * @private
 */
goog.db.Index.prototype.get_ = function(fn, msg, key) {
  'use strict';
  const d = new goog.async.Deferred();
  let request;
  try {
    request = this.index_[fn](key);
  } catch (err) {
    msg += ' with key ' + goog.debug.deepExpose(key);
    d.errback(goog.db.Error.fromException(err, msg));
    return d;
  }
  request.onsuccess = function(ev) {
    'use strict';
    d.callback(ev.target.result);
  };
  request.onerror = function(ev) {
    'use strict';
    msg += ' with key ' + goog.debug.deepExpose(key);
    d.errback(goog.db.Error.fromRequest(ev.target, msg));
  };
  return d;
};


/**
 * Fetches a single object from the object store. Even if there are multiple
 * objects that match the given key, this method will get only one of them.
 *
 * @param {!IDBKeyType} key Key to look up in the index.
 * @return {!goog.async.Deferred} The deferred object for the given record.
 */
goog.db.Index.prototype.get = function(key) {
  'use strict';
  return this.get_('get', 'getting from index ' + this.getName(), key);
};


/**
 * Looks up a single object from the object store and gives back the key that
 * it's listed under in the object store. Even if there are multiple records
 * that match the given key, this method returns the first.
 *
 * @param {!IDBKeyType} key Key to look up in the index.
 * @return {!goog.async.Deferred} The deferred key for the record that matches
 *     the key.
 */
goog.db.Index.prototype.getKey = function(key) {
  'use strict';
  return this.get_('getKey', 'getting key from index ' + this.getName(), key);
};


/**
 * Returns the values matching `opt_key` up to `opt_count`.
 *
 * If `obt_key` is a `KeyRange`, returns all keys in that range. If it is
 * `undefined`, returns all known keys.
 *
 * @param {!IDBKeyType|!goog.db.KeyRange=} opt_key Key or KeyRange to look up in
 *     the index.
 * @param {number=} opt_count The number records to return
 * @return {!goog.async.Deferred} A deferred array of objects that match the
 *     key.
 */
goog.db.Index.prototype.getAll = function(opt_key, opt_count) {
  'use strict';
  return this.getAll_(
      'getAll', 'getting all from index ' + this.getName(), opt_key, opt_count);
};


/**
 * Returns the keys matching `opt_key` up to `opt_count`.
 *
 * If `obt_key` is a `KeyRange`, returns all keys in that range. If it is
 * `undefined`, returns all known keys.
 *
 * @param {!IDBKeyType|!goog.db.KeyRange=} opt_key Key or KeyRange to look up in
 *     the index.
 * @param {number=} opt_count The number records to return
 * @return {!goog.async.Deferred} A deferred array of keys for objects that
 *     match the key.
 */
goog.db.Index.prototype.getAllKeys = function(opt_key, opt_count) {
  'use strict';
  return this.getAll_(
      'getAllKeys', 'getting all keys index ' + this.getName(), opt_key,
      opt_count);
};


/**
 * Helper function for native `getAll` and `getAllKeys` on `IDBObjectStore` that
 * takes in `IDBKeyRange` as params.
 *
 * Returns the result of the native method in a `Deferred` object.
 *
 * @param {string} fn Function name to call on the index to get the request.
 * @param {string} msg Message to give to the error.
 * @param {!IDBKeyType|!goog.db.KeyRange|undefined} keyOrRange
 *     Key or KeyRange to look up in the index.
 * @param {number|undefined} count The number records to return
 * @return {!goog.async.Deferred} The resulting deferred array of objects.
 * @private
 */
goog.db.Index.prototype.getAll_ = function(fn, msg, keyOrRange, count) {
  'use strict';
  let nativeRange;
  if (keyOrRange === undefined) {
    nativeRange = undefined;
  } else if (keyOrRange instanceof goog.db.KeyRange) {
    nativeRange = keyOrRange.range();
  } else {
    nativeRange = goog.db.KeyRange.only(keyOrRange).range();
  }

  const d = new goog.async.Deferred();
  let request;
  try {
    request = this.index_[fn](nativeRange, count);
  } catch (err) {
    msg += ' for range ' +
        (nativeRange ? goog.debug.deepExpose(nativeRange) : '<all>');
    d.errback(goog.db.Error.fromException(err, msg));
    return d;
  }
  request.onsuccess = function() {
    'use strict';
    d.callback(request.result);
  };
  request.onerror = function(ev) {
    'use strict';
    msg += ' for range ' +
        (nativeRange ? goog.debug.deepExpose(nativeRange) : '<all>');
    d.errback(goog.db.Error.fromRequest(ev.target, msg));
  };
  return d;
};


/**
 * Opens a cursor over the specified key range. Returns a cursor object which is
 * able to iterate over the given range.
 *
 * Example usage:
 *
 * <code>
 *  var cursor = index.openCursor(goog.db.KeyRange.bound('a', 'c'));
 *
 *  var key = goog.events.listen(
 *      cursor, goog.db.Cursor.EventType.NEW_DATA,
 *      function() {
 *        // Do something with data.
 *        cursor.next();
 *      });
 *
 *  goog.events.listenOnce(
 *      cursor, goog.db.Cursor.EventType.COMPLETE,
 *      function() {
 *        // Clean up listener, and perform a finishing operation on the data.
 *        goog.events.unlistenByKey(key);
 *      });
 * </code>
 *
 * @param {!goog.db.KeyRange=} opt_range The key range. If undefined iterates
 *     over the whole object store.
 * @param {!goog.db.Cursor.Direction=} opt_direction The direction. If undefined
 *     moves in a forward direction with duplicates.
 * @return {!goog.db.Cursor} The cursor.
 * @throws {!goog.db.Error} If there was a problem opening the cursor.
 */
goog.db.Index.prototype.openCursor = function(opt_range, opt_direction) {
  'use strict';
  return goog.db.Cursor.openCursor(this.index_, opt_range, opt_direction);
};
