import defaultValue from "./defaultValue.js";
import defined from "./defined.js";
import formatError from "./formatError.js";

/**
 * Provides details about an error that occurred in an {@link ImageryProvider} or a {@link TerrainProvider}.
 *
 * @alias TileProviderError
 * @constructor
 *
 * @param {ImageryProvider|TerrainProvider} provider The imagery or terrain provider that experienced the error.
 * @param {String} message A message describing the error.
 * @param {Number} [x] The X coordinate of the tile that experienced the error, or undefined if the error
 *        is not specific to a particular tile.
 * @param {Number} [y] The Y coordinate of the tile that experienced the error, or undefined if the error
 *        is not specific to a particular tile.
 * @param {Number} [level] The level of the tile that experienced the error, or undefined if the error
 *        is not specific to a particular tile.
 * @param {Number} [timesRetried=0] The number of times this operation has been retried.
 * @param {Error} [error] The error or exception that occurred, if any.
 */
function TileProviderError(
  provider,
  message,
  x,
  y,
  level,
  timesRetried,
  error
) {
  /**
   * The {@link ImageryProvider} or {@link TerrainProvider} that experienced the error.
   * @type {ImageryProvider|TerrainProvider}
   */
  this.provider = provider;

  /**
   * The message describing the error.
   * @type {String}
   */
  this.message = message;

  /**
   * The X coordinate of the tile that experienced the error.  If the error is not specific
   * to a particular tile, this property will be undefined.
   * @type {Number}
   */
  this.x = x;

  /**
   * The Y coordinate of the tile that experienced the error.  If the error is not specific
   * to a particular tile, this property will be undefined.
   * @type {Number}
   */
  this.y = y;

  /**
   * The level-of-detail of the tile that experienced the error.  If the error is not specific
   * to a particular tile, this property will be undefined.
   * @type {Number}
   */
  this.level = level;

  /**
   * The number of times this operation has been retried.
   * @type {Number}
   * @default 0
   */
  this.timesRetried = defaultValue(timesRetried, 0);

  /**
   * True if the failed operation should be retried; otherwise, false.  The imagery or terrain provider
   * will set the initial value of this property before raising the event, but any listeners
   * can change it.  The value after the last listener is invoked will be acted upon.
   * @type {Boolean}
   * @default false
   */
  this.retry = false;

  /**
   * The error or exception that occurred, if any.
   * @type {Error}
   */
  this.error = error;
}

/**
 * Handles an error in an {@link ImageryProvider} or {@link TerrainProvider} by raising an event if it has any listeners, or by
 * logging the error to the console if the event has no listeners.  This method also tracks the number
 * of times the operation has been retried and will automatically retry if requested to do so by the
 * event listeners.
 *
 * @param {TileProviderError} previousError The error instance returned by this function the last
 *        time it was called for this error, or undefined if this is the first time this error has
 *        occurred.
 * @param {ImageryProvider|TerrainProvider} provider The imagery or terrain provider that encountered the error.
 * @param {Event} event The event to raise to inform listeners of the error.
 * @param {String} message The message describing the error.
 * @param {Number} x The X coordinate of the tile that experienced the error, or undefined if the
 *        error is not specific to a particular tile.
 * @param {Number} y The Y coordinate of the tile that experienced the error, or undefined if the
 *        error is not specific to a particular tile.
 * @param {Number} level The level-of-detail of the tile that experienced the error, or undefined if the
 *        error is not specific to a particular tile.
 * @param {TileProviderError.RetryFunction} retryFunction The function to call to retry the operation.  If undefined, the
 *        operation will not be retried.
 * @param {Error} [errorDetails] The error or exception that occurred, if any.
 * @returns {TileProviderError} The error instance that was passed to the event listeners and that
 *          should be passed to this function the next time it is called for the same error in order
 *          to track retry counts.
 */
TileProviderError.handleError = function (
  previousError,
  provider,
  event,
  message,
  x,
  y,
  level,
  retryFunction,
  errorDetails
) {
  var error = previousError;
  if (!defined(previousError)) {
    error = new TileProviderError(
      provider,
      message,
      x,
      y,
      level,
      0,
      errorDetails
    );
  } else {
    error.provider = provider;
    error.message = message;
    error.x = x;
    error.y = y;
    error.level = level;
    error.retry = false;
    error.error = errorDetails;
    ++error.timesRetried;
  }

  if (event.numberOfListeners > 0) {
    event.raiseEvent(error);
  } else {
    console.log(
      'An error occurred in "' +
        provider.constructor.name +
        '": ' +
        formatError(message)
    );
  }

  if (error.retry && defined(retryFunction)) {
    retryFunction();
  }

  return error;
};

/**
 * Handles success of an operation by resetting the retry count of a previous error, if any.  This way,
 * if the error occurs again in the future, the listeners will be informed that it has not yet been retried.
 *
 * @param {TileProviderError} previousError The previous error, or undefined if this operation has
 *        not previously resulted in an error.
 */
TileProviderError.handleSuccess = function (previousError) {
  if (defined(previousError)) {
    previousError.timesRetried = -1;
  }
};

/**
 * A function that will be called to retry the operation.
 * @callback TileProviderError.RetryFunction
 */
export default TileProviderError;
