"use strict";

/**
 * jQuery AJAX transport for receiving Blob and ArrayBuffer data.
 *
 * XMLHttpRequest2 supports receiving binary data represented by
 * :js:class:`Blob` and :js:class:`ArrayBuffer`. This transport enabled binary
 * data support through standard :js:func:`jQuery.ajax` calls.
 */
$.ajaxTransport('arraybuffer blob', function (options, origOptions, jqXHR) {
  if (!window.FormData) {
    return null;
  }
  let xhr;
  return {
    send(headers, completeCB) {
      xhr = options.xhr();
      xhr.addEventListener('load', () => {
        const result = {};
        result[options.dataType] = xhr.response;
        completeCB(xhr.status, xhr.statusText, result, xhr.getAllResponseHeaders());
      });
      if (options.username) {
        xhr.open(options.type, options.url, options.async, options.username, options.password);
      } else {
        xhr.open(options.type, options.url, options.async);
      }
      xhr.responseType = options.dataType;

      /* Apply any custom fields that may be provided. */
      const xhrFields = options.xhrFields;
      if (xhrFields) {
        for (let field in xhrFields) {
          if (xhrFields.hasOwnProperty(field)) {
            xhr[field] = xhrFields[field];
          }
        }
      }
      if (!options.crossDomain && !headers['X-Requested-With']) {
        headers['X-Requested-With'] = 'XMLHttpRequest';
      }

      /*
       * Catch errors with cross-domain requests, like jQuery does.
       */
      try {
        for (let key in headers) {
          if (headers.hasOwnProperty(key)) {
            xhr.setRequestHeader(key, headers[key]);
          }
        }
      } catch (e) {}
      xhr.send(options.hasContent ? options.data : null);
    },
    abort() {
      if (xhr && xhr.readyState !== 4) {
        xhr.abort();
      }
    }
  };
});

/**
 * Enable or disable the activity indicator.
 *
 * Args:
 *     enabled (boolean):
 *         Whether the activity indicator should be enabled.
 *
 *     options (object):
 *         Additional options.
 *
 * Option Args:
 *     noActivityIndicator (boolean):
 *         True if the indicator should be suppressed, even if ``enabled`` is
 *         true.
 *
 *     type (string):
 *         The type of HTTP request that the indicator is representing. This
 *         will be either ``GET`` (loading) or ``POST`` (saving).
 */
RB.setActivityIndicator = function (enabled, options) {
  const $activityIndicator = options._$activityIndicator || $('#activity-indicator');
  if (enabled) {
    if (RB.ajaxOptions.enableIndicator && !options.noActivityIndicator) {
      $activityIndicator.children('.indicator-text').text(options.type && options.type === 'GET' ? gettext("Loading...") : gettext("Saving..."));
      $activityIndicator.removeClass('error').show();
    }
  } else if (RB.ajaxOptions.enableIndicator && !options.noActivityIndicator && !$activityIndicator.hasClass('error')) {
    if (options._activityIndicatorHideImmediately) {
      $activityIndicator.hide();
    } else {
      $activityIndicator.delay(250).fadeOut('fast');
    }
  }
};

/**
 * Make an API request.
 *
 * This will handle any button disabling/enabling, write to the correct path
 * prefix, do form uploading, and display server errors.
 *
 * Args:
 *     options (object):
 *         Options for the API request.
 *
 * Option Args:
 *     buttons (jQuery):
 *         Any buttons to disable while the API request is in flight.
 *
 *     form (jQuery):
 *         A form to submit, if any.
 *
 *     type (string):
 *         The type of HTTP request to make. Defaults to ``POST``.
 *
 *     prefix (string):
 *         The prefix for the API path (after ``SITE_ROOT``, before ``api``).
 *
 *     path (string):
 *         The relative path from the server name to the Review Board API
 *         endpoint.
 *
 *     data (object):
 *         Additional data to send with the request.
 *
 *     success (function):
 *         An optional success callback. If not specified, the default handler
 *         will reload the page.
 *
 *     error (function):
 *         An optional error callback, to be called after the error banner is
 *         displayed.
 *
 *     complete (function):
 *         An optional complete callback, which is called after the success or
 *         error callbacks.
 */
RB.apiCall = function (options) {
  const prefix = options.prefix || '';
  const url = options.url || SITE_ROOT + prefix + 'api' + options.path;
  function showErrorPage(xhr, data) {
    const $iframe = $('<iframe>').width('100%');
    const requestData = options.data ? $.param(options.data) : '(none)';
    $('<div class="server-error-box">').appendTo(document.body).append('<p><b>' + gettext("Error Code:") + '</b> ' + xhr.status + '</p>').append('<p><b>' + gettext("Error Text:") + '</b> ' + xhr.statusText + '</p>').append('<p><b>' + gettext("Request URL:") + '</b> ' + url + '</p>').append('<p><b>' + gettext("Request Data:") + '</b> ' + requestData + '</p>').append('<p class="response-data"><b>' + gettext("Response Data:") + '</b></p>').append(gettext("<p>There may be useful error details below. The following error page may be useful to your system administrator or when <a href=\"https://www.reviewboard.org/bugs/new/\">reporting a bug</a>. To save the page, right-click the error below and choose \"Save Page As,\" if available, or \"View Source\" and save the result as a <tt>.html</tt> file.</p>")).append(gettext("<p><b>Warning:</b> Be sure to remove any sensitive material that may exist in the error page before reporting a bug!</p>")).append($iframe).on('resize', function () {
      $iframe.height($(this).height() - $iframe.position().top);
    }).modalBox({
      stretchX: true,
      stretchY: true,
      title: gettext("Server Error Details")
    });
    const doc = $iframe[0].contentDocument || $iframe[0].contentWindow.document;
    doc.open();
    doc.write(data);
    doc.close();
  }
  function doCall() {
    const $activityIndicator = options._$activityIndicator || $('#activity-indicator');
    if (options.buttons) {
      options.buttons.attr('disabled', true);
    }
    RB.setActivityIndicator(true, options);
    const defaultOptions = {
      url: url,
      data: options.data,
      dataType: options.dataType || 'json',
      error: function (xhr, textStatus, errorThrown) {
        /*
         * This is a fallback error handler, which the caller
         * may override (Backbone.sync always will, for example).
         *
         * This is responsible for trying to determine if what we
         * got was one of:
         *
         * 1. HTTP 204 (No Content -- in response to creating new
         *    objects)
         *
         *    If so, this is considered a successful result.
         *
         * 2. An API error object (in which case it may not be an
         *    "error" per se -- jQuery assumes anything not a 2xx
         *    or 304 is an error, but other things are perfectly
         *    valid responses to requests in our API).
         *
         *    If it has an API error code, we pass it up to the
         *    success() handler for further evaluation.
         *
         *    This is primarily here for legacy reasons, prior to the
         *    usage of Backbone (which should *never* end up
         *    triggering this particular handler, if ordering of
         *    callback registration is correct).
         *
         * 3. An unexpected error (proper HTTP 500, for instance).
         *
         *    This will display an error banner at the top of the
         *    page.
         *
         * In the future, we should consolidate the error handlers
         * in order to benefit from central management of errors and
         * the "A server error has occurred" banner. That is going to
         * require some careful thought, and likely opting into a
         * mode that uses the correct behavior.
         */
        const responseText = xhr.responseText;
        let rsp = null;
        try {
          rsp = JSON.parse(responseText);
        } catch (e) {}
        if (rsp && rsp.stat || xhr.status === 204) {
          /*
           * This either looks like an API error, or it's an HTTP
           * 204, which won't have a body.
           *
           * XXX Historically, we've treated anything with an API
           *     error payload as "success", to avoid triggering
           *     some older error handling behavior.
           *
           *     We'll continue to do so, but this is something we
           *     need to rethink at a later date.
           */
          if (_.isFunction(options.success)) {
            /*
             * Note that this is not the same signature as a
             * standard success call. That would be:
             *
             *     success(rsp, textStatus, xhr);
             *
             * This is a legacy wart, and is covered under unit
             * tests.
             */
            options.success(rsp, xhr.status);
          }
          return;
        }
        $activityIndicator.addClass('error').text(gettext("A server error occurred.")).append($('<a href="#">').text(gettext("Show Details")).click(() => showErrorPage(xhr, responseText))).append($('<a href="#">').text(gettext("Dismiss")).click(function () {
          $activityIndicator.fadeOut('fast');
          return false;
        }));

        /*
         * We actually probably shouldn't ever get here. This is
         * legacy code. It's being kept Just In Case (TM), but if
         * we're in this function right now, then options.error wasn't
         * set.
         *
         * This is a candidate for dead code elimination.
         */
        if (_.isFunction(options.error)) {
          options.error(xhr, textStatus, errorThrown);
        }
      }
    };
    const forcedOptions = {
      complete: function (xhr, status) {
        /*
         * This complete handler will always be called before the
         * caller's handler. This ensures we're able to re-enable
         * any buttons, turn off the loading activity, call the
         * caller's handler, and then begin the next operation in
         * the queue.
         */
        if (options.buttons) {
          options.buttons.attr('disabled', false);
        }
        RB.setActivityIndicator(false, options);
        if (_.isFunction(options.complete)) {
          options.complete(xhr, status);
        }
        $.funcQueue('rbapicall').next();
      }
    };

    /*
     * Please note: Due to complex interactions with jQuery and Backbone,
     * the order in which we build this is very important. Some of this is
     * documented above, but to recap:
     *
     * 1. Options and a default error handler that should be used unless
     *    the caller wants to override behavior.
     *
     * 2. All caller-provided options that may affect this request. This
     *    will often include "error" and "success" handlers.
     *
     * 3. Options we want to ensure take precedence. This includes our
     *    "complete" handler, which must *always* run before the caller's.
     */
    const data = $.extend(true, defaultOptions, options, forcedOptions);
    if (data.data === null || data.data === undefined || data.data instanceof Object && !(window.Blob && data.data instanceof Blob)) {
      data.data = $.extend({
        api_format: 'json'
      }, data.data || {});
    }
    if (options.form) {
      options.form.ajaxSubmit(data);
    } else {
      $.ajax(data);
    }
  }
  options.type = options.type || 'POST';
  if (options.type !== 'GET' && options.type !== 'HEAD' && RB.UserSession.instance.get('readOnly')) {
    console.error('%s request not sent. Site is in read-only mode.', options.type);
    return;
  }

  // We allow disabling the function queue for the sake of unit tests.
  if (RB.ajaxOptions.enableQueuing && options.type !== 'GET') {
    $.funcQueue('rbapicall').add(doCall);
    $.funcQueue('rbapicall').start();
  } else {
    doCall();
  }
};

/**
 * Parse API error information from a response and stores it.
 *
 * The xhr object provided will be extended with two new attributes:
 * 'errorText' and 'errorPayload'. These represent the response's error
 * message and full error payload, respectively.
 *
 * Args:
 *     xhr (jqXHR):
 *         The XMLHttpRequest object.
 */
RB.storeAPIError = function (xhr) {
  try {
    const rsp = JSON.parse(xhr.responseText);
    xhr.errorPayload = rsp;
    xhr.errorText = rsp.err.msg;
  } catch (e) {
    xhr.errorPayload = null;
    xhr.errorText = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
  }
};
RB.ajaxOptions = {
  enableQueuing: true,
  enableIndicator: true
};

/*
 * Call RB.apiCall instead of $.ajax.
 *
 * We wrap instead of assign for now so that we can hook in/override
 * RB.apiCall with unit tests.
 */
Backbone.ajax = options => RB.apiCall(options);

/**
 * An error class to wrap the error triplet that comes from Backbone calls.
 *
 * Version Added:
 *     5.0
 */
class BackboneError extends Error {
  /**
   * Initialize the error.
   *
   * Args:
   *     modelOrCollection (Backbone.Model or Backbone.Collection):
   *         The model or collection that the call was made on.
   *
   *     xhr (jQuery.XHR):
   *         The XMLHttpRequest wrapper object.
   *
   *     options (object):
   *         Any options that were passed to the call.
   */
  constructor(modelOrCollection, xhr, options) {
    super(xhr.errorText);
    this.modelOrCollection = modelOrCollection;
    this.xhr = xhr;
    this.options = options;
  }
}

/**
 * Adapt promises to old-style callbacks.
 *
 * This is a utility method to wrap a callable that supports returning a
 * promise to continue to support old-style success/ready/error/complete
 * callbacks.
 *
 * Version Added:
 *     5.0
 *
 * Args:
 *     options (object):
 *         Options for the operation, including callbacks.
 *
 *     context (object):
 *         Context to be used when calling callback functions.
 *
 *     callable (function):
 *         The function to call.
 */
RB.promiseToCallbacks = function (options, context, callable) {
  const success = _.isFunction(options.success) ? options.success : _.isFunction(options.ready) ? options.ready : undefined;
  const error = _.isFunction(options.error) ? options.error : undefined;
  const complete = _.isFunction(options.complete) ? options.complete : undefined;
  callable(_.omit(options, ['success', 'ready', 'error', 'complete'])).then(result => {
    if (success) {
      success.call(context, result);
    }
  }).catch(err => {
    if (error) {
      error.call(context, err.modelOrCollection, err.xhr, err.options);
    }
  }).finally(() => {
    if (complete) {
      complete.call(context);
    }
  });
};

// vim: set et:sw=4:

//# sourceMappingURL=apiUtils.js.map