text: import $ from 'jquery';
  import ConstraintFactory from './factory/constraint';
  import ParsleyUI from './ui';
  import ParsleyUtils from './utils';
  
  var ParsleyField = function (field, domOptions, options, parsleyFormInstance) {
      this.__class__ = 'ParsleyField';
  
      this.$element = $(field);
  
      // Set parent if we have one
      if ('undefined' !== typeof parsleyFormInstance) {
          this.parent = parsleyFormInstance;
      }
  
      this.options = options;
      this.domOptions = domOptions;
  
      // Initialize some properties
      this.constraints = [];
      this.constraintsByName = {};
      this.validationResult = true;
  
      // Bind constraints
      this._bindConstraints();
  };
  
  var statusMapping = {pending: null, resolved: true, rejected: false};
  
  ParsleyField.prototype = {
      // # Public API
      // Validate field and trigger some events for mainly `ParsleyUI`
      // @returns `true`, an array of the validators that failed, or
      // `null` if validation is not finished. Prefer using whenValidate
      validate: function (options) {
          if (arguments.length >= 1 && !$.isPlainObject(options)) {
              ParsleyUtils.warnOnce('Calling validate on a parsley field without passing arguments as an object is deprecated.');
              options = {options};
          }
          var promise = this.whenValidate(options);
          if (!promise)  // If excluded with `group` option
              return true;
          switch (promise.state()) {
              case 'pending':
                  return null;
              case 'resolved':
                  return true;
              case 'rejected':
                  return this.validationResult;
          }
      },
  
      // Validate field and trigger some events for mainly `ParsleyUI`
      // @returns a promise that succeeds only when all validations do
      // or `undefined` if field is not in the given `group`.
      whenValidate: function ({force, group} = {}) {
          // do not validate a field if not the same as given validation group
          this.refreshConstraints();
          if (group && !this._isInGroup(group))
              return;
  
          this.value = this.getValue();
  
          // Field Validate event. `this.value` could be altered for custom needs
          this._trigger('validate');
  
          return this.whenValid({force, value: this.value, _refreshed: true})
              .always(() = > {this._reflowUI();
      })
      .
          done(() = > {this._trigger('success');
      })
      .
          fail(() = > {this._trigger('error');
      })
      .
          always(() = > {this._trigger('validated');
      })
      .
          pipe(...this._pipeAccordingToValidationResult()
      )
          ;
      },
  
      hasConstraints: function () {
          return 0 !== this.constraints.length;
      },
  
      // An empty optional field does not need validation
      needsValidation: function (value) {
          if ('undefined' === typeof value)
              value = this.getValue();
  
          // If a field is empty and not required, it is valid
          // Except if `data-parsley-validate-if-empty` explicitely added, useful for some custom validators
          if (!value.length && !this._isRequired() && 'undefined' === typeof this.options.validateIfEmpty)
              return false;
  
          return true;
      },
  
      _isInGroup: function (group) {
          if ($.isArray(this.options.group))
              return -1 !== $.inArray(group, this.options.group);
          return this.options.group === group;
      },
  
      // Just validate field. Do not trigger any event.
      // Returns `true` iff all constraints pass, `false` if there are failures,
      // or `null` if the result can not be determined yet (depends on a promise)
      // See also `whenValid`.
      isValid: function (options) {
          if (arguments.length >= 1 && !$.isPlainObject(options)) {
              ParsleyUtils.warnOnce('Calling isValid on a parsley field without passing arguments as an object is deprecated.');
              var [force, value] = arguments;
              options = {force, value};
          }
          var promise = this.whenValid(options);
          if (!promise) // Excluded via `group`
              return true;
          return statusMapping[promise.state()];
      },
  
      // Just validate field. Do not trigger any event.
      // @returns a promise that succeeds only when all validations do
      // or `undefined` if the field is not in the given `group`.
      // The argument `force` will force validation of empty fields.
      // If a `value` is given, it will be validated instead of the value of the input.
      whenValid: function ({force = false, value, group, _refreshed} = {}) {
          // Recompute options and rebind constraints to have latest changes
          if (!_refreshed)
              this.refreshConstraints();
          // do not validate a field if not the same as given validation group
          if (group && !this._isInGroup(group))
              return;
  
          this.validationResult = true;
  
          // A field without constraint is valid
          if (!this.hasConstraints())
              return $.when();
  
          // Value could be passed as argument, needed to add more power to 'field:validate'
          if ('undefined' === typeof value || null === value)
              value = this.getValue();
  
          if (!this.needsValidation(value) && true !== force)
              return $.when();
  
          var groupedConstraints = this._getGroupedConstraints();
          var promises = [];
          $.each(groupedConstraints, (_, constraints) = > {
              // Process one group of constraints at a time, we validate the constraints
              // and combine the promises together.
              var promise = $.when(
                  ...$.map(constraints, constraint = > this._validateConstraint(value, constraint)
      )
      )
          ;
          promises.push(promise);
          if (promise.state() === 'rejected')
              return false; // Interrupt processing if a group has already failed
      })
          ;
          return $.when.apply($, promises);
      },
  
      // @returns a promise
      _validateConstraint: function (value, constraint) {
          var result = constraint.validate(value, this);
          // Map false to a failed promise
          if (false === result)
              result = $.Deferred().reject();
          // Make sure we return a promise and that we record failures
          return $.when(result).fail(errorMessage = > {
              if(
          !(this.validationResult instanceof Array)
      )
          this.validationResult = [];
          this.validationResult.push({
              assert: constraint,
              errorMessage: 'string' === typeof errorMessage && errorMessage
          });
      })
          ;
      },
  
      // @returns Parsley field computed value that could be overrided or configured in DOM
      getValue: function () {
          var value;
  
          // Value could be overriden in DOM or with explicit options
          if ('function' === typeof this.options.value)
              value = this.options.value(this);
          else if ('undefined' !== typeof this.options.value)
              value = this.options.value;
          else
              value = this.$element.val();
  
          // Handle wrong DOM or configurations
          if ('undefined' === typeof value || null === value)
              return '';
  
          return this._handleWhitespace(value);
      },
  
      // Actualize options that could have change since previous validation
      // Re-bind accordingly constraints (could be some new, removed or updated)
      refreshConstraints: function () {
          return this.actualizeOptions()._bindConstraints();
      },
  
      /**
       * Add a new constraint to a field
       *
       * @param {String}   name
       * @param {Mixed}    requirements      optional
       * @param {Number}   priority          optional
       * @param {Boolean}  isDomConstraint   optional
       */
      addConstraint: function (name, requirements, priority, isDomConstraint) {
  
          if (window.Parsley._validatorRegistry.validators[name]) {
              var constraint = new ConstraintFactory(this, name, requirements, priority, isDomConstraint);
  
              // if constraint already exist, delete it and push new version
              if ('undefined' !== this.constraintsByName[constraint.name])
                  this.removeConstraint(constraint.name);
  
              this.constraints.push(constraint);
              this.constraintsByName[constraint.name] = constraint;
          }
  
          return this;
      },
  
      // Remove a constraint
      removeConstraint: function (name) {
          for (var i = 0; i < this.constraints.length; i++)
              if (name === this.constraints[i].name) {
                  this.constraints.splice(i, 1);
                  break;
              }
          delete this.constraintsByName[name];
          return this;
      },
  
      // Update a constraint (Remove + re-add)
      updateConstraint: function (name, parameters, priority) {
          return this.removeConstraint(name)
              .addConstraint(name, parameters, priority);
      },
  
      // # Internals
  
      // Internal only.
      // Bind constraints from config + options + DOM
      _bindConstraints: function () {
          var constraints = [];
          var constraintsByName = {};
  
          // clean all existing DOM constraints to only keep javascript user constraints
          for (var i = 0; i < this.constraints.length; i++)
              if (false === this.constraints[i].isDomConstraint) {
                  constraints.push(this.constraints[i]);
                  constraintsByName[this.constraints[i].name] = this.constraints[i];
              }
  
          this.constraints = constraints;
          this.constraintsByName = constraintsByName;
  
          // then re-add Parsley DOM-API constraints
          for (var name in this.options)
              this.addConstraint(name, this.options[name], undefined, true);
  
          // finally, bind special HTML5 constraints
          return this._bindHtml5Constraints();
      },
  
      // Internal only.
      // Bind specific HTML5 constraints to be HTML5 compliant
      _bindHtml5Constraints: function () {
          // html5 required
          if (this.$element.hasClass('required') || this.$element.attr('required'))
              this.addConstraint('required', true, undefined, true);
  
          // html5 pattern
          if ('string' === typeof this.$element.attr('pattern'))
              this.addConstraint('pattern', this.$element.attr('pattern'), undefined, true);
  
          // range
          if ('undefined' !== typeof this.$element.attr('min') && 'undefined' !== typeof this.$element.attr('max'))
              this.addConstraint('range', [this.$element.attr('min'), this.$element.attr('max')], undefined, true);
  
          // HTML5 min
          else if ('undefined' !== typeof this.$element.attr('min'))
              this.addConstraint('min', this.$element.attr('min'), undefined, true);
  
          // HTML5 max
          else if ('undefined' !== typeof this.$element.attr('max'))
              this.addConstraint('max', this.$element.attr('max'), undefined, true);
  
  
          // length
          if ('undefined' !== typeof this.$element.attr('minlength') && 'undefined' !== typeof this.$element.attr('maxlength'))
              this.addConstraint('length', [this.$element.attr('minlength'), this.$element.attr('maxlength')], undefined, true);
  
          // HTML5 minlength
          else if ('undefined' !== typeof this.$element.attr('minlength'))
              this.addConstraint('minlength', this.$element.attr('minlength'), undefined, true);
  
          // HTML5 maxlength
          else if ('undefined' !== typeof this.$element.attr('maxlength'))
              this.addConstraint('maxlength', this.$element.attr('maxlength'), undefined, true);
  
  
          // html5 types
          var type = this.$element.attr('type');
  
          if ('undefined' === typeof type)
              return this;
  
          // Small special case here for HTML5 number: integer validator if step attribute is undefined or an integer value, number otherwise
          if ('number' === type) {
              return this.addConstraint('type', ['number', {
                  step: this.$element.attr('step'),
                  base: this.$element.attr('min') || this.$element.attr('value')
              }], undefined, true);
              // Regular other HTML5 supported types
          } else if (/^(email|url|range)$/i.test(type)) {
              return this.addConstraint('type', type, undefined, true);
          }
          return this;
      },
  
      // Internal only.
      // Field is required if have required constraint without `false` value
      _isRequired: function () {
          if ('undefined' === typeof this.constraintsByName.required)
              return false;
  
          return false !== this.constraintsByName.required.requirements;
      },
  
      // Internal only.
      // Shortcut to trigger an event
      _trigger: function (eventName) {
          return this.trigger('field:' + eventName);
      },
  
      // Internal only
      // Handles whitespace in a value
      // Use `data-parsley-whitespace="squish"` to auto squish input value
      // Use `data-parsley-whitespace="trim"` to auto trim input value
      _handleWhitespace: function (value) {
          if (true === this.options.trimValue)
              ParsleyUtils.warnOnce('data-parsley-trim-value="true" is deprecated, please use data-parsley-whitespace="trim"');
  
          if ('squish' === this.options.whitespace)
              value = value.replace(/\s{2,}/g, ' ');
  
          if (('trim' === this.options.whitespace) || ('squish' === this.options.whitespace) || (true === this.options.trimValue))
              value = ParsleyUtils.trimString(value);
  
          return value;
      },
  
      // Internal only.
      // Returns the constraints, grouped by descending priority.
      // The result is thus an array of arrays of constraints.
      _getGroupedConstraints: function () {
          if (false === this.options.priorityEnabled)
              return [this.constraints];
  
          var groupedConstraints = [];
          var index = {};
  
          // Create array unique of priorities
          for (var i = 0; i < this.constraints.length; i++) {
              var p = this.constraints[i].priority;
              if (!index[p])
                  groupedConstraints.push(index[p] = []);
              index[p].push(this.constraints[i]);
          }
          // Sort them by priority DESC
          groupedConstraints.sort(function (a, b) {
              return b[0].priority - a[0].priority;
          });
  
          return groupedConstraints;
      }
  
  };
  
  export default ParsleyField;
  