var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import ChangeManager from '../change-manager/change-manager';
import { Observer } from '../change-manager/observer/observer';
import { FORM_ELEMENT_VALUE_CHANGE, FORM_FIELD_ELEMENT_DISCONNECTED, FORM_FIELD_PENDING_VALID_CHANGE, FORM_RESET_OPERATION_TRIGGERED, } from '../change-manager/subject/static-subject/static-subject-title';
import { findParent, isElementVisible } from '../../utils/ui';
import { FormFieldValidationRuleHandlerChain, } from './validation-rule-handler/form-field-validation-rule-handler-chain';
import { InjectVegaSlimmer, createDynamicSlimmer, methodPlaceholder } from 'vega-slimmer/core';
import { FieldErrorUIControllerSlimmer } from '../slimmers/field-error-ui-controller-slimmer';
import { PageResizeObserverSlimmer } from '../slimmers/page-resize-observer-slimmer';
import { FormFieldMessageTranslatorSlimmer } from '../slimmers/form-field-message-translator';
import { InternalFormFieldValidationRule } from './rules/internal-form-field-validation-rule.abstract';
import domNodeSubjectFactory from '../change-manager/subject/dom-node-subject/dom-node-subject-factory';
import { VegaInternalFormSubmit } from '../event-manager/event-id/vega-internal-event-id';
/**
 * The FormFieldController class is a class that validates the value of an input elements
 *
 * @implements {Validator<number | string | boolean | null>}
 * @property {FormFieldValidationRule<number | string | boolean>[] = []} defaultRules default rules used for validation
 * @property {FormFieldValidationRule<number | string | boolean>[]} additionalRules additional rules used for validation
 * @property {InputValidatorListener[]} listeners listeners monitoring on the validation result
 * @property {unknown & { value?: number | string | boolean }} hostElement host element used to get the value for validation if input element is not present
 * @property {boolean} isTouched true if an element is touched
 * @property {boolean} isModified true if an element is modified
 * @property {boolean} isVisible true if an element is visible
 * @property {string} touchableArea define the area which listen to the blur event
 * @property {MutationObserver} disabledAttrObserver reference to the MutationObserver
 * @property {ResizeObserver} visibilityObserver reference to the ResizeObserver
 * @property {Observer<boolean>} formResetTriggeredObserver reference to formResetTriggeredObserver
 */
export class FormFieldController {
  /**
   * This function is called when the component is created, and it sets up the observers that will be used to detect changes to the component's state.
   * The first thing the constructor does is set the defaultRules, hostElement, touchableArea, isTouched, isModified, and isDisabled properties
   *
   * @param {FormFieldValidationRule<number | string | boolean>[]} defaultRules - FormFieldValidationRule<T>[]
   * @param {FormFieldElement<number | string | boolean>} hostElement - The form field element that this validation manager is attached to.
   * @param {FormFieldDefaultValueType}  defaultValue - This is defualt value for value property of component.
   * @param {FormFieldOptionForSlimmer} options - The selector for the element that contains the input element.
   */
  constructor(defaultRules, hostElement, defaultValue, options) {
    var _a, _b;
    this.formFieldMessageTranslatorSlimmer = new FormFieldMessageTranslatorSlimmer();
    this.fieldErrorUIController = new FieldErrorUIControllerSlimmer();
    this.resizeObserverSlimmer = createDynamicSlimmer(FormFieldController, PageResizeObserverSlimmer, [], [{ mapFrom: 'connectResizeObserver', mapTo: 'connectResizeObserver' }], [
      {
        // eslint-disable-next-line jsdoc/require-jsdoc
        callback: (entries) => {
          const prevVisible = this.isVisible;
          const currentVisible = entries[0].contentRect.height > 0;
          this.isVisible = currentVisible;
          if (this.isAutoValidationEnabled() && findParent(this.host, 'vega-form')) {
            prevVisible !== currentVisible && this.valid(['visible']);
          }
        },
        // eslint-disable-next-line jsdoc/require-jsdoc
        target: () => this.host,
      },
    ]);
    this.defaultRules = [];
    this.additionalRules = [];
    this.listeners = [];
    this.isTouched = false;
    this.isModified = false;
    this.isVisible = false;
    this.formFieldValidationRuleHandlerChain = new FormFieldValidationRuleHandlerChain();
    this.hostChildAddedBlurEventMap = new WeakMap();
    /**
     * The function handles the validation result and updates the UI accordingly.
     *
     * @param {boolean | 'rule'} showError - which indicates that the error message should only be shown if the `shouldShowError` property of the
     * `validationResult` object is `true` or `rule`
     * @param {ValidationResult} invalidationResult - The `validationResult` parameter is an object that
     * represents the result of a validation process. It contains information about whether the validation
     * was successful or not, and an optional error message.
     * @returns {boolean} a boolean value, which represents whether the validation result is valid or not.
     */
    this.handleValidationResult = (showError, invalidationResult) => {
      let isValid = true;
      if (invalidationResult) {
        if (showError === 'rule') {
          invalidationResult.shouldShowError
            ? this.fieldErrorUIController.applyInputInvalidUI(invalidationResult.message)
            : this.fieldErrorUIController.resetInputInvalidUI();
        }
        else {
          showError
            ? this.fieldErrorUIController.applyInputInvalidUI(invalidationResult.message)
            : this.fieldErrorUIController.resetInputInvalidUI();
        }
        isValid = false;
      }
      else {
        this.fieldErrorUIController.resetInputInvalidUI();
      }
      this.invokeListeners(isValid);
      //change pending valid should be in last to solve the vegaValidate event emit issue
      ChangeManager.notify(FORM_FIELD_PENDING_VALID_CHANGE, {
        host: this.host,
        detail: isValid,
      });
      return isValid;
    };
    /**
     * It adds a blur event listener to all elements that match the selector in the touchableArea property
     */
    this.setupTouchedListener = () => {
      this.getTouchableAreaElements()
        .filter((item) => item && this.hostChildAddedBlurEventMap.get(item) !== true)
        .forEach((item) => {
        item.addEventListener('blur', () => {
          this.updateIsTouched(true);
        });
        this.hostChildAddedBlurEventMap.set(item, true);
      });
    };
    this.defaultRules = defaultRules || [];
    this.host = hostElement;
    this.defaultValue = defaultValue;
    this.options = options;
    this.initDisabledAttrObserver();
    this.connectResizeObserver();
    this.initFormResetObserver();
    (_b = (_a = this.host).componentOnReady) === null || _b === void 0 ? void 0 : _b.call(_a).then(() => {
      this.didRender();
    });
    this.setupModifiedListener();
    this.listenHostElementKeydown();
    this.updateValidationHandlers();
  }
  /**
   * It validates the input against the rules and returns a boolean indicating whether the input is valid or not
   *
   * @param {boolean} showError whether the validation result should be reflected in UI
   * @returns {Promise<EvaluateResultBase>} A promise value.
   */
  async validate(showError) {
    return await this.valid([], showError);
  }
  /**
   * This function adds a listener to the listeners array.
   *
   * @param {InputValidatorListener} listener - The listener to add.
   */
  addListener(listener) {
    this.listeners.push(listener);
  }
  /**
   * It returns an array of all the validation rules for the field
   *
   * @typedef T generic type
   * @returns {FormFieldValidationRule<T>[]} An array of FormFieldValidationRule<T>
   */
  getRules() {
    const additionalRules = [...this.additionalRules];
    const overriddenDefaultRules = this.defaultRules.map((rule) => {
      return this.overrideDefaultRuleByAdditionalRule(rule, additionalRules);
    });
    return [...overriddenDefaultRules, ...additionalRules];
  }
  /**
   * Set the default rules for the form field.
   *
   * @param {FormFieldValidationRule<number | string | boolean>[]} rules - FormFieldValidationRule<T>[]
   */
  setDefaultRules(rules) {
    this.defaultRules = rules;
    this.updateValidationHandlers();
  }
  /**
   * This function sets the additional rules for the form field
   *
   * @param {FormFieldValidationRule<number | string | boolean>[]} rules - The rules that are required for the field to be valid.
   */
  setAdditionalRules(rules) {
    this.additionalRules = rules;
    this.updateValidationHandlers();
  }
  /**
   * The destroy function unregister the form reset observer
   */
  destroy() {
    ChangeManager.notify(FORM_FIELD_ELEMENT_DISCONNECTED, {
      host: this.host,
    });
    this.unregisterFormResetObserver();
    ChangeManager.unregister(FORM_ELEMENT_VALUE_CHANGE, this.hostValueChangeObserver);
    if (this.hostChildrenObserver) {
      this.hostChildrenObserver.disconnect();
    }
  }
  /**
   * It creates an observer that will watch the host element and set the isVisible property to true if
   * the element is visible.
   */
  connectResizeObserver() {
    methodPlaceholder();
  }
  /**
   * Updates the `disableBlurValidation.
   *
   * @param {boolean} disableBlurValidation - From parent vega form.
   */
  updateDisableBlurValidation(disableBlurValidation) {
    this.disableBlurValidation = disableBlurValidation;
  }
  /**
   * The function "updateValidationHandlers" initializes the validation rule handler chain with the rules obtained from the "getRules" function.
   */
  updateValidationHandlers() {
    this.formFieldValidationRuleHandlerChain.setHandlersByRules(this.getRules());
  }
  /**
   * valid function to validate the rules
   */
  async valid(triggerStatus, showError = 'rule') {
    const status = this.getStatus();
    const input = this.host.value;
    const hasAsyncRules = this.formFieldValidationRuleHandlerChain.checkIncludesAsyncRuleHandler(input, status, triggerStatus);
    if (hasAsyncRules) {
      ChangeManager.notify(FORM_FIELD_PENDING_VALID_CHANGE, { host: this.host, detail: null });
    }
    const { isValid, message } = await this.formFieldValidationRuleHandlerChain.process(input, status, triggerStatus, showError, this.handleValidationResult);
    const translatedMessage = this.formFieldMessageTranslatorSlimmer.getTranslatedMessage(message);
    return { isValid, message: translatedMessage };
  }
  /**
   * Invoke all listeners with the given isValid value
   *
   * @param {boolean} isValid - boolean
   */
  invokeListeners(isValid) {
    this.listeners.forEach((listener) => listener.callback(isValid));
  }
  /**
   * It returns an object with the current status of the form field
   *
   * @returns {FormFieldStatusMeta} The status of the form field.
   */
  getStatus() {
    return {
      touched: this.isTouched,
      modified: this.isModified,
      disabled: this.host['disabled'],
      visible: this.isVisible,
    };
  }
  /**
   * It updates the `isTouched` property and if auto validation is enabled, it validates the input
   *
   * @param {boolean} touched - boolean - whether the input is touched or not
   */
  updateIsTouched(touched) {
    const prevIsTouched = this.isTouched;
    this.isTouched = touched;
    if (!this.disableBlurValidation && this.isAutoValidationEnabled()) {
      void this.valid(prevIsTouched === touched ? [] : ['touched']);
    }
  }
  /**
   * "If the value of the `isModified` property has changed, then update the `isModified` property and if auto-validation is enabled, then validate the control."
   *
   * @param {boolean} modified - boolean - whether the value has been modified or not
   */
  updateIsModified(modified) {
    const prevIsModified = this.isModified;
    this.isModified = modified;
    if (this.isAutoValidationEnabled()) {
      void this.valid(prevIsModified === modified ? [] : ['modified']);
    }
  }
  /**
   * It creates a new MutationObserver that watches for changes to the disabled attribute of the host
   * element, and when it detects a change, it validates the host element's value
   */
  initDisabledAttrObserver() {
    this.disabledAttrObserver = new MutationObserver(() => {
      if (this.isAutoValidationEnabled()) {
        void this.valid(['disabled']);
      }
    });
    this.disabledAttrObserver.observe(this.host, {
      attributes: true,
      attributeFilter: ['disabled'],
    });
  }
  /**
   * If the input form is a child of the current form, then the current form can accept the input form's reset operation
   *
   * @param {ResetOperationPayload} input - VegaForm - The form that is being reset.
   * @returns {boolean} A boolean value.
   */
  canAcceptFormResetOperation(input) {
    return findParent(this.host, 'vega-form') === input.host;
  }
  /**
   * Unregister the form reset observer.
   */
  unregisterFormResetObserver() {
    ChangeManager.unregister(FORM_RESET_OPERATION_TRIGGERED, this.formResetTriggeredObserver);
  }
  /**
   * monitor the host component touchable area elements update
   */
  monitorTouchedListener() {
    const inputContainer = this.getInputContainerElements();
    if (inputContainer) {
      this.hostChildrenObserver = new MutationObserver(() => {
        this.setupTouchedListener();
      });
      this.hostChildrenObserver.observe(inputContainer, {
        childList: true,
        subtree: true,
      });
    }
    this.setupTouchedListener();
  }
  /**
   * Get an array of HTMLElements that represent the touchable area elements based on the host's tag name.
   *
   * @returns {Nullable<HTMLElement>[]} an array of HTMLElements.
   */
  getTouchableAreaElements() {
    return this.options && this.options.touchableAreaGetter ? this.options.touchableAreaGetter() : [];
  }
  /**
   * Get the input container element based on the options provided.
   *
   * @returns {Nullable<HTMLElement>} the form field element input container element
   */
  getInputContainerElements() {
    return this.options && this.options.inputContainerGetter ? this.options.inputContainerGetter() : null;
  }
  /**
   * It adds an event listener to the host element that listens for a custom event called `vegaChange`.
   * When that event is fired, the `updateIsModified` function is called with the argument `true`
   */
  setupModifiedListener() {
    this.hostValueChangeObserver = new Observer(this.isCurrentTarget.bind(this), () => this.updateIsModified(true));
    ChangeManager.register(FORM_ELEMENT_VALUE_CHANGE, this.hostValueChangeObserver);
  }
  /**
   * The function checks if the host of the input matches the host of the current target.
   *
   * @param {NotifyObserverPayload<unknown>} input - The `input` parameter is of type `NotifyObserverPayload<unknown>`.
   * @returns {boolean} a boolean value, which indicates whether the `host` property of the current object is
   * equal to the `host` property of the `input` object.
   */
  isCurrentTarget(input) {
    return this.host === input.host;
  }
  /**
   * It resets the form field's status and then validates the form field with the current value
   *
   * @param {ResetOperationPayload} payload - ResetOperationPayload
   */
  async reset(payload) {
    var _a, _b;
    if (this.shouldVisitElement(this.host, (_a = payload.option) === null || _a === void 0 ? void 0 : _a.skipDisabled, (_b = payload.option) === null || _b === void 0 ? void 0 : _b.skipInvisible)) {
      const defaultValueMap = payload.defaultValueMap;
      const fieldName = this.host.dataset['vegaForm'];
      const defaultValue = defaultValueMap[fieldName] === undefined || defaultValueMap[fieldName] === null
        ? this.getDefaultValue()
        : defaultValueMap[fieldName];
      typeof this.host['setValue'] === 'function'
        ? await this.host['setValue'](defaultValue)
        : (this.host.value = defaultValue);
      this.isTouched = false;
      this.isModified = false;
      this.fieldErrorUIController.resetInputInvalidUI();
    }
  }
  /**
   * If the default value is a function, call it with the host element as the argument, otherwise return
   * the default value
   */
  getDefaultValue() {
    return this.defaultValue instanceof Function ? this.defaultValue(this.host) : this.defaultValue;
  }
  /**
   * This function is called after the component has been rendered. It re-attaches the invalid UI to
   * the input element, and sets up the listeners for the `touched` and `modified` events
   */
  didRender() {
    this.monitorTouchedListener();
  }
  /**
   * This function registers an observer with the ChangeManager that will be notified when a form
   * reset operation is triggered
   */
  initFormResetObserver() {
    this.formResetTriggeredObserver = new Observer(this.canAcceptFormResetOperation.bind(this), this.reset.bind(this));
    ChangeManager.register(FORM_RESET_OPERATION_TRIGGERED, this.formResetTriggeredObserver);
  }
  /**
   * "If the host element's autoValidation property is not false, then return true."
   * The host element is the element that the directive is attached to. In this case, the host element is the input element
   *
   * @returns {boolean} A boolean value.
   */
  isAutoValidationEnabled() {
    return this.host.autoValidation !== false;
  }
  /**
   * Return true if the HTMLElement has data-vega-form property and it's not disabled and not invisible if these two properties have to be checked.
   *
   * @param {HTMLElement} element - The HTMLElement to check.
   * @param {boolean} skipDisabled - If true, then disabled form fields will be skipped.
   * @param {boolean} skipInvisible - If true, only visible elements will be visited.
   * @returns {boolean} a boolean value.
   */
  shouldVisitElement(element, skipDisabled, skipInvisible) {
    return !!(element.dataset['vegaForm'] &&
      (!skipDisabled || !element['disabled']) &&
      (!skipInvisible || isElementVisible(element)));
  }
  /**
   * It listens for the keydown event on the host element, and if the key pressed is the enter key, it
   * notifies the ChangeManager that the form has been submitted
   */
  listenHostElementKeydown() {
    this.host.addEventListener('keydown', 
    /* istanbul ignore next */ (event) => {
      if (this.canSubmitFormByEvent(event)) {
        /**
         * The below method is e2e-test covered in
         *
         * @see {module:emit-vegaFormSubmit-when-press-enter-nested-forms-input}
         */
        const outerForm = findParent(this.host, 'vega-form', {
          outerMost: true,
        });
        if (outerForm) {
          ChangeManager.notify(domNodeSubjectFactory.getSubject(outerForm, VegaInternalFormSubmit), { host: this.host });
        }
      }
    });
  }
  /* istanbul ignore next */
  /**
   * The function determines whether a form can be submitted based on the event type and the element's
   * tag name.
   *
   * @param {KeyboardEvent} event - The event parameter is of type KeyboardEvent. It represents the
   * keyboard event that occurred.
   * @returns {boolean} a boolean value.
   */
  canSubmitFormByEvent(event) {
    const isEnterKey = event.key === 'Enter' || event.keyCode === 13;
    const isSubmittedKey = isEnterKey && event.shiftKey;
    switch (this.host.tagName) {
      case 'VEGA-TEXTAREA':
      case 'VEGA-DATE-PICKER':
      case 'VEGA-TIME-PICKER':
        if (isSubmittedKey)
          event.preventDefault();
        return isSubmittedKey;
      case 'VEGA-RICH-TEXT-EDITOR':
        return false;
      default:
        return isEnterKey;
    }
  }
  /**
   * It overrides the default rule by checking if the additional rules contain a custom rule that overrides.
   * If it does, it returns that additional rule, and the rule will be removed from the additional rules array.
   * otherwise, it returns the default rule.
   *
   * @typedef T generic type
   * @param {InternalFormFieldValidationRule} defaultRule - The rule to be overridden.
   * @param {FormFieldValidationRule[]} additionalRules - The additional rules to be applied.
   * @returns {FormFieldValidationRule<T>} The overridden rule.
   */
  overrideDefaultRuleByAdditionalRule(defaultRule, additionalRules) {
    if (defaultRule && additionalRules) {
      const customRuleIndex = additionalRules.findIndex((customRule) => {
        return (customRule instanceof InternalFormFieldValidationRule &&
          customRule instanceof defaultRule.constructor &&
          customRule.overrideDefault);
      });
      if (customRuleIndex > -1) {
        return additionalRules.splice(customRuleIndex, 1)[0];
      }
    }
    return defaultRule;
  }
}
__decorate([
  InjectVegaSlimmer()
], FormFieldController.prototype, "formFieldMessageTranslatorSlimmer", void 0);
__decorate([
  InjectVegaSlimmer()
], FormFieldController.prototype, "fieldErrorUIController", void 0);
__decorate([
  InjectVegaSlimmer()
], FormFieldController.prototype, "resizeObserverSlimmer", void 0);
