/* eslint-disable no-param-reassign */
((global) => {
  /*
   * This class overrides the browser's validation error bubbles, displaying custom
   * error messages for invalid fields instead. To begin validating any form, add the
   * class `gl-show-field-errors` to the form element, and ensure error messages are
   * declared in each inputs' `title` attribute. If no title is declared for an invalid
   * field the user attempts to submit, "This field is required." will be shown by default.
   *
   * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
   *
   * Set a custom error anchor for error message to be injected after with the
   * class `gl-field-error-anchor`
   *
   * Examples:
   *
   * Basic:
   *
   * <form class='gl-show-field-errors'>
   *  <input type='text' name='username' title='Username is required.'/>
   * </form>
   *
   * Ignore specific inputs (e.g. UsernameValidator):
   *
   * <form class='gl-show-field-errors'>
   *   <div class="form-group>
   *     <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
   *   </div>
   *   <div class="form-group">
   *      <input type='text' name='username' title='Username is required.'/>
   *    </div>
   * </form>
   *
   * Custom Error Anchor (allows error message to be injected after specified element):
   *
   * <form class='gl-show-field-errors'>
   *  <div class="form-group gl-field-error-anchor">
   *    <input type='text' name='username' title='Username is required.'/>
   *    // Error message typically injected here
   *  </div>
   *  // Error message now injected here
   * </form>
   *
    * */

  /*
    * Regex Patterns in use:
    *
    * Only alphanumeric: : "[a-zA-Z0-9]+"
    * No special characters : "[a-zA-Z0-9-_]+",
    *
    * */

  const errorMessageClass = 'gl-field-error';
  const inputErrorClass = 'gl-field-error-outline';
  const errorAnchorSelector = '.gl-field-error-anchor';
  const ignoreInputSelector = '.gl-field-error-ignore';

  class GlFieldError {
    constructor({ input, formErrors }) {
      this.inputElement = $(input);
      this.inputDomElement = this.inputElement.get(0);
      this.form = formErrors;
      this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
      this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);

      this.state = {
        valid: false,
        empty: true,
      };

      this.initFieldValidation();
    }

    initFieldValidation() {
      const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
      const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;

      // hidden when injected into DOM
      errorAnchor.after(this.fieldErrorElement);
      this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
      this.scopedSiblings = this.safelySelectSiblings();
    }

    safelySelectSiblings() {
      // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
      const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
      const parentContainer = this.inputElement.parent('.form-group');

      // Only select siblings when they're scoped within a form-group with one input
      const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;

      return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
    }

    renderValidity() {
      this.renderClear();

      if (this.state.valid) {
        this.renderValid();
      } else if (this.state.empty) {
        this.renderEmpty();
      } else if (!this.state.valid) {
        this.renderInvalid();
      }
    }

    handleInvalidSubmit(event) {
      event.preventDefault();
      const currentValue = this.accessCurrentValue();
      this.state.valid = false;
      this.state.empty = currentValue === '';

      this.renderValidity();
      this.form.focusOnFirstInvalid.apply(this.form);
      // For UX, wait til after first invalid submission to check each keyup
      this.inputElement.off('keyup.fieldValidator')
        .on('keyup.fieldValidator', this.updateValidity.bind(this));
    }

    /* Get or set current input value */
    accessCurrentValue(newVal) {
      return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
    }

    getInputValidity() {
      return this.inputDomElement.validity.valid;
    }

    updateValidity() {
      const inputVal = this.accessCurrentValue();
      this.state.empty = !inputVal.length;
      this.state.valid = this.getInputValidity();
      this.renderValidity();
    }

    renderValid() {
      return this.renderClear();
    }

    renderEmpty() {
      return this.renderInvalid();
    }

    renderInvalid() {
      this.inputElement.addClass(inputErrorClass);
      this.scopedSiblings.hide();
      return this.fieldErrorElement.show();
    }

    renderClear() {
      const inputVal = this.accessCurrentValue();
      if (!inputVal.split(' ').length) {
        const trimmedInput = inputVal.trim();
        this.accessCurrentValue(trimmedInput);
      }
      this.inputElement.removeClass(inputErrorClass);
      this.scopedSiblings.hide();
      this.fieldErrorElement.hide();
    }
  }

  global.GlFieldError = GlFieldError;
})(window.gl || (window.gl = {}));
