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

/**
 * @fileoverview Wrapper for a IndexedDB cursor.
 */


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

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



/**
 * Creates a new IDBCursor wrapper object. Should not be created directly,
 * access cursor through object store.
 * @see goog.db.ObjectStore#openCursor
 *
 * @constructor
 * @extends {goog.events.EventTarget}
 * @final
 */
goog.db.Cursor = function() {
  'use strict';
  goog.db.Cursor.base(this, 'constructor');
};
goog.inherits(goog.db.Cursor, goog.events.EventTarget);


/**
 * Underlying IndexedDB cursor object.
 *
 * @type {?IDBCursor}
 * @private
 */
goog.db.Cursor.prototype.cursor_ = null;


/**
 * Advances the cursor to the next position along its direction. When new data
 * is available, the NEW_DATA event will be fired. If the cursor has reached the
 * end of the range it will fire the COMPLETE event. If opt_key is specified it
 * will advance to the key it matches in its direction.
 *
 * This wraps the native #continue method on the underlying object.
 *
 * @param {IDBKeyType=} opt_key The optional key to advance to.
 */
goog.db.Cursor.prototype.next = function(opt_key) {
  'use strict';
  if (opt_key) {
    this.cursor_['continue'](opt_key);
  } else {
    this.cursor_['continue']();
  }
};


/**
 * Updates the value at the current position of the cursor in the object store.
 * If the cursor points to a value that has just been deleted, a new value is
 * created.
 *
 * @param {*} value The value to be stored.
 * @return {!goog.async.Deferred} The resulting deferred request.
 */
goog.db.Cursor.prototype.update = function(value) {
  'use strict';
  let msg = 'updating via cursor with value ';
  const d = new goog.async.Deferred();
  let request;

  try {
    request = this.cursor_.update(value);
  } catch (err) {
    msg += goog.debug.deepExpose(value);
    d.errback(goog.db.Error.fromException(err, msg));
    return d;
  }
  request.onsuccess = function(ev) {
    'use strict';
    d.callback();
  };
  request.onerror = function(ev) {
    'use strict';
    msg += goog.debug.deepExpose(value);
    d.errback(goog.db.Error.fromRequest(ev.target, msg));
  };
  return d;
};


/**
 * Deletes the value at the cursor's position, without changing the cursor's
 * position. Once the value is deleted, the cursor's value is set to null.
 *
 * @return {!goog.async.Deferred} The resulting deferred request.
 */
goog.db.Cursor.prototype.remove = function() {
  'use strict';
  const msg = 'deleting via cursor';
  const d = new goog.async.Deferred();
  let request;

  try {
    request = this.cursor_['delete']();
  } catch (err) {
    d.errback(goog.db.Error.fromException(err, msg));
    return d;
  }
  request.onsuccess = function(ev) {
    'use strict';
    d.callback();
  };
  request.onerror = function(ev) {
    'use strict';
    d.errback(goog.db.Error.fromRequest(ev.target, msg));
  };
  return d;
};


/**
 * @return {*} The value for the value at the cursor's position. Undefined
 *     if no current value, or null if value has just been deleted.
 */
goog.db.Cursor.prototype.getValue = function() {
  'use strict';
  return this.cursor_['value'];
};


/**
 * @return {IDBKeyType} The key for the value at the cursor's position. If
 *     the cursor is outside its range, this is undefined.
 */
goog.db.Cursor.prototype.getKey = function() {
  'use strict';
  return this.cursor_.key;
};


/**
 * Opens a value cursor from IDBObjectStore or IDBIndex over the specified key
 * range. Returns a cursor object which is able to iterate over the given range.
 * @param {!(IDBObjectStore|IDBIndex)} source Data source to open cursor.
 * @param {!goog.db.KeyRange=} opt_range The key range. If undefined iterates
 *     over the whole data source.
 * @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.Cursor.openCursor = function(source, opt_range, opt_direction) {
  'use strict';
  const cursor = new goog.db.Cursor();
  let request;

  try {
    const range = opt_range ? opt_range.range() : null;
    if (opt_direction) {
      request = source.openCursor(range, opt_direction);
    } else {
      request = source.openCursor(range);
    }
  } catch (ex) {
    cursor.dispose();
    throw goog.db.Error.fromException(ex, source.name);
  }
  request.onsuccess = function(e) {
    'use strict';
    cursor.cursor_ = e.target.result || null;
    if (cursor.cursor_) {
      cursor.dispatchEvent(goog.db.Cursor.EventType.NEW_DATA);
    } else {
      cursor.dispatchEvent(goog.db.Cursor.EventType.COMPLETE);
    }
  };
  request.onerror = function(e) {
    'use strict';
    cursor.dispatchEvent(goog.db.Cursor.EventType.ERROR);
  };
  return cursor;
};


/**
 * Possible cursor directions.
 * @see http://www.w3.org/TR/IndexedDB/#idl-def-IDBCursor
 *
 * @enum {string}
 */
goog.db.Cursor.Direction = {
  NEXT: 'next',
  NEXT_NO_DUPLICATE: 'nextunique',
  PREV: 'prev',
  PREV_NO_DUPLICATE: 'prevunique'
};


/**
 * Event types that the cursor can dispatch. COMPLETE events are dispatched when
 * a cursor is depleted of values, a NEW_DATA event if there is new data
 * available, and ERROR if an error occurred.
 *
 * @enum {string}
 */
goog.db.Cursor.EventType = {
  COMPLETE: 'c',
  ERROR: 'e',
  NEW_DATA: 'n'
};
