// `window.ParsleyExtend`, like `ParsleyAbstract`, is inherited by `ParsleyField` and `ParsleyForm`
// That way, we could add new methods or redefine some for these both classes. In particular case
// We are adding async validation methods that returns promises, bind them properly to triggered
// Events like onkeyup when field is invalid or on form submit. These validation methods adds an
// Extra `remote` validator which could not be simply added like other `ParsleyExtra` validators
// Because returns promises instead of booleans.
window.ParsleyExtend = window.ParsleyExtend || {};
window.ParsleyExtend = $.extend(window.ParsleyExtend, {
  asyncSupport: true,

  asyncValidators: $.extend({
    default: {
      fn: function (xhr) {
        return 'resolved' === xhr.state();
      },
      url: false
    },
    reverse: {
      fn: function (xhr) {
        // If reverse option is set, a failing ajax request is considered successful
        return 'rejected' === xhr.state();
      },
      url: false
    }
  }, window.ParsleyExtend.asyncValidators),

  addAsyncValidator: function (name, fn, url, options) {
    this.asyncValidators[name.toLowerCase()] = {
      fn: fn,
      url: url || false,
      options: options || {}
    };

    return this;
  },

  asyncValidate: function () {
    if ('ParsleyForm' === this.__class__)
      return this._asyncValidateForm.apply(this, arguments);

    return this._asyncValidateField.apply(this, arguments);
  },

  asyncIsValid: function () {
    if ('ParsleyField' === this.__class__)
      return this._asyncIsValidField.apply(this, arguments);

    return this._asyncIsValidForm.apply(this, arguments);
  },

  onSubmitValidate: function (event) {
    var that = this;

    // This is a Parsley generated submit event, do not validate, do not prevent, simply exit and keep normal behavior
    if (true === event.parsley)
      return;

    // Clone the event object
    this.submitEvent = $.extend(true, {}, event);

    // Prevent form submit and immediately stop its event propagation
    if (event instanceof $.Event) {
      event.stopImmediatePropagation();
      event.preventDefault();
    }

    return this._asyncValidateForm(undefined, event)
      .done(function () {
        // If user do not have prevented the event, re-submit form
        if (!that.submitEvent.isDefaultPrevented())
          that.$element.trigger($.extend($.Event('submit'), { parsley: true }));
      });
  },

  eventValidate: function (event) {
    // For keyup, keypress, keydown.. events that could be a little bit obstrusive
    // do not validate if val length < min threshold on first validation. Once field have been validated once and info
    // about success or failure have been displayed, always validate with this trigger to reflect every yalidation change.
    if (new RegExp('key').test(event.type))
      if (!this._ui.validationInformationVisible  && this.getValue().length <= this.options.validationThreshold)
        return;

    this._ui.validatedOnce = true;
    this.asyncValidate();
  },

  // Returns Promise
  _asyncValidateForm: function (group, event) {
    var
      that = this,
      promises = [];

    this._refreshFields();

    $.emit('parsley:form:validate', this);

    for (var i = 0; i < this.fields.length; i++) {

      // do not validate a field if not the same as given validation group
      if (group && group !== this.fields[i].options.group)
        continue;

      promises.push(this.fields[i]._asyncValidateField());
    }

    return $.when.apply($, promises)
      .always(function () {
        $.emit('parsley:form:validated', that);
      });
  },

  _asyncIsValidForm: function (group, force) {
    var promises = [];
    this._refreshFields();

    for (var i = 0; i < this.fields.length; i++) {

      // do not validate a field if not the same as given validation group
      if (group && group !== this.fields[i].options.group)
        continue;

      promises.push(this.fields[i]._asyncIsValidField(force));
    }

    return $.when.apply($, promises);
  },

  _asyncValidateField: function (force) {
    var that = this;

    $.emit('parsley:field:validate', this);

    return this._asyncIsValidField(force)
      .done(function () {
        $.emit('parsley:field:success', that);
      })
      .fail(function () {
        $.emit('parsley:field:error', that);
      })
      .always(function () {
        $.emit('parsley:field:validated', that);
      });
  },

  _asyncIsValidField: function (force, value) {
    var
      deferred = $.Deferred(),
      remoteConstraintIndex;

    // If regular isValid (matching regular constraints) returns `false`, no need to go further
    // Directly reject promise, do not run remote validator and save server load
    if (false === this.isValid(force, value))
      deferred.rejectWith(this);

    // If regular constraints are valid, and there is a remote validator registered, run it
    else if ('undefined' !== typeof this.constraintsByName.remote)
      this._remote(deferred);

    // Otherwise all is good, resolve promise
    else
      deferred.resolveWith(this);

    // Return promise
    return deferred.promise();
  },

  _remote: function (deferred) {
    var
      that = this,
      data = {},
      ajaxOptions,
      csr,
      validator = this.options.remoteValidator || (true === this.options.remoteReverse ? 'reverse' : 'default');

    validator = validator.toLowerCase();

    if ('undefined' === typeof this.asyncValidators[validator])
      throw new Error('Calling an undefined async validator: `' + validator + '`');

    // Fill data with current value
    data[this.$element.attr('name') || this.$element.attr('id')] = this.getValue();

    // Merge options passed in from the function with the ones in the attribute
    this.options.remoteOptions = $.extend(true, this.options.remoteOptions || {} , this.asyncValidators[validator].options);

    // All `$.ajax(options)` could be overridden or extended directly from DOM in `data-parsley-remote-options`
    ajaxOptions = $.extend(true, {}, {
      url: this.asyncValidators[validator].url || this.options.remote,
      data: data,
      type: 'GET'
    }, this.options.remoteOptions || {});

    // Generate store key based on ajax options
    csr = $.param(ajaxOptions);

    // Initialise querry cache
    if ('undefined' === typeof this._remoteCache)
      this._remoteCache = {};

    // Try to retrieve stored xhr
    if (!this._remoteCache[csr]) {
      // Prevent multi burst xhr queries
      if (this._xhr && 'pending' === this._xhr.state())
        this._xhr.abort();

      // Make ajax call
      this._xhr =  $.ajax(ajaxOptions)

      // Store remote call result to avoid next calls with exact same parameters
      this._remoteCache[csr] = this._xhr;
    }

    this._remoteCache[csr]
      .done(function (data, textStatus, xhr) {
        that._handleRemoteResult(validator, xhr, deferred);
      })
      .fail(function (xhr, status, message) {
        // If we aborted the query, do not handle nothing for this value
        if ('abort' === status)
          return;

        that._handleRemoteResult(validator, xhr, deferred);
      });
  },

  _handleRemoteResult: function (validator, xhr, deferred) {
    // If true, simply resolve and exit
    if ('function' === typeof this.asyncValidators[validator].fn && this.asyncValidators[validator].fn.call(this, xhr)) {
      deferred.resolveWith(this);

      return;
    }

    // Else, create a proper remote validation Violation to trigger right UI
    this.validationResult = [
      new window.ParsleyValidator.Validator.Violation(
        this.constraintsByName.remote,
        this.getValue(),
        null
      )
    ];

    deferred.rejectWith(this);
  }
});

// Remote validator is just an always true sync validator with lowest (-1) priority possible
// It will be overloaded in `validateThroughValidator()` that will do the heavy async work
// This 'hack' is needed not to mess up too much with error messages and stuff in `ParsleyUI`
window.ParsleyConfig = window.ParsleyConfig || {};
window.ParsleyConfig.validators = window.ParsleyConfig.validators || {};
window.ParsleyConfig.validators.remote = {
  fn: function () {
    return true;
  },
  priority: -1
};
