import 'element-internals-polyfill'; // this is needed in order to support older browsers

const observedAttributes = ['required', 'pattern'];

const errorAttributes = {
  valueMissing: 'data-errormessage-value-missing',
  patternMismatch: 'data-errormessage-pattern-mismatch',
  tooShort: 'data-errormessage-pattern-mismatch-too-short',
  tooLong: 'data-errormessage-pattern-mismatch-too-long',
  rangeUnderflow: 'data-errormessage-pattern-range-underflow',
  rangeOverflow: 'data-errormessage-pattern-range-overflow',
  typeMismatch: 'data-errormessage-type-mismatch',
};

const validationTargetSymbol = Symbol('validationTarget');

export const inputValidationMixin = (superclass) =>
  class InputValidationMixinClass extends superclass {
    #validationTarget = validationTargetSymbol;

    static get observedAttributes() {
      return [...(superclass.observedAttributes || []), ...observedAttributes];
    }

    static get formAssociated() {
      return true;
    }

    #internals;

    get internals() {
      return this.#internals;
    }

    set internals(value) {
      this.#internals = value;
    }

    constructor() {
      super();

      this.#internals = this.attachInternals();
    }

    // eslint-disable-next-line class-methods-use-this
    get defaultErrorMsgValueMissing() {
      return 'Please fill out this field.';
    }

    // eslint-disable-next-line class-methods-use-this
    get defaultErrorMsgPatternMismatch() {
      return 'Please match the requested format.';
    }

    get defaultErrorMsgTooShort() {
      return `Minimum length is ${this.getAttribute('minlength')}.`;
    }

    get defaultErrorMsgTooLong() {
      return `Maximum length is ${this.getAttribute('maxlength')}.  `;
    }

    get defaultErrorMsgRangeUnderflow() {
      return `At least ${this.getAttribute('min-items-selection')} items are required.`;
    }

    get defaultErrorMsgRangeOverflow() {
      return `At most ${this.getAttribute('max-items-selection')} items are allowed.`;
    }

    // eslint-disable-next-line class-methods-use-this
    get defaultErrorMsgTypeMismatch() {
      return `Please match the requested type.`;
    }

    getCustomErrorMessage(attr, defaultMsg) {
      return this.hasAttribute(attr) ? this.getAttribute(attr) || ' ' : defaultMsg;
    }

    getErrorMessage(flags) {
      const {
        valueMissing,
        patternMismatch,
        typeMismatch,
        stepMismatch,
        tooShort,
        tooLong,
        rangeOverflow,
        rangeUnderflow,
        badInput,
        customError,
      } = flags;

      switch (true) {
        case valueMissing:
          return this.getCustomErrorMessage(
            errorAttributes.valueMissing,
            this.defaultErrorMsgValueMissing
          );
        case patternMismatch || stepMismatch || badInput:
          return this.getCustomErrorMessage(
            errorAttributes.patternMismatch,
            this.defaultErrorMsgPatternMismatch
          );
        case typeMismatch:
          return this.getCustomErrorMessage(
            errorAttributes.typeMismatch,
            this.defaultErrorMsgTypeMismatch
          );
        case tooShort:
          return this.getCustomErrorMessage(errorAttributes.tooShort, this.defaultErrorMsgTooShort);
        case tooLong:
          return this.getCustomErrorMessage(errorAttributes.tooLong, this.defaultErrorMsgTooLong);
        case rangeUnderflow:
          return this.getCustomErrorMessage(
            errorAttributes.rangeUnderflow,
            this.defaultErrorMsgRangeUnderflow
          );
        case rangeOverflow:
          return this.getCustomErrorMessage(
            errorAttributes.rangeOverflow,
            this.defaultErrorMsgRangeOverflow
          );
        case customError:
          return this.validationMessage;
        default:
          return '';
      }
    }

    #setValidity() {
      const validity = this.isReadOnly ? {} : this.getValidity();
      this.#internals.setValidity(validity, this.getErrorMessage(validity), this.validationTarget);
    }

    get validationMessage() {
      return this.#internals.validationMessage;
    }

    // eslint-disable-next-line class-methods-use-this
    getValidity() {
      // eslint-disable-next-line no-console
      console.warn('getValidity', 'is not implemented');
    }

    checkValidity() {
      return this.#internals.validity.valid;
    }

    reportValidity() {
      return this.#internals.reportValidity();
    }

    get validity() {
      return this.#internals.validity;
    }

    get validationTarget() {
      return this.#validationTarget === validationTargetSymbol
        ? this.inputElement
        : this.#validationTarget;
    }

    set validationTarget(val) {
      this.#validationTarget = val;
    }

    setCustomValidity(errorMessage) {
      if (errorMessage) {
        this.#internals.setValidity({ customError: true }, errorMessage, this.validationTarget);
      } else {
        this.#internals.setValidity({});
        this.#setValidity();
      }
    }

    get isRequired() {
      return this.hasAttribute('required') && this.getAttribute('required') !== 'false';
    }

    get isReadOnly() {
      return this.hasAttribute('readonly') && this.getAttribute('readonly') !== 'false';
    }

    get isDisabled() {
      return this.hasAttribute('disabled') && this.getAttribute('disabled') !== 'false';
    }

    get pattern() {
      return this.getAttribute('pattern');
    }

    get form() {
      return this.#internals.form;
    }

    attributeChangedCallback(attrName, oldValue, newValue) {
      super.attributeChangedCallback?.(attrName, oldValue, newValue);

      if (observedAttributes.includes(attrName)) {
        this.#setValidity();
      }
    }

    init() {
      super.init?.();
      this.addEventListener('change', this.#setValidity);
      this.addEventListener('invalid', (e) => e.stopPropagation());
      this.addEventListener('input', this.#setValidity);

      // The attribute sync takes time, so getValidity returns valid,
      // even when it shouldn't. This way allows all attributes to sync
      // after render, before checking the validity.
      setTimeout(() => this.#setValidity());
    }
  };
