import { createDispatchEvent } from '../helpers/mixinsHelpers';

export const inputEventsDispatchingMixin = (superclass) =>
  class InputEventsDispatchingMixinClass extends superclass {
    init() {
      this.#blockNativeEvents();

      super.init?.();
    }

    // we want to block the native (trusted) events and control when these events are being dispatched
    #blockNativeEvents() {
      ['blur', 'focus', 'focusin', 'focusout'].forEach((event) => {
        this.addEventListener(event, (e) => {
          e.isTrusted && e.target === this && e.stopImmediatePropagation();
        });
      });
    }

    handleFocusEventsDispatching(inputs) {
      let timerId;

      // in order to simulate blur & focusout on root input element
      // we are checking if focus on one of the inner elements happened immediately after blur
      // if not, the component is no longer focused and we should dispatch blur & focusout
      inputs?.forEach((input) => {
        input?.addEventListener('focusout', (e) => {
          e.stopImmediatePropagation();
          timerId = setTimeout(() => {
            timerId = null;

            createDispatchEvent.call(this, 'blur');
            createDispatchEvent.call(this, 'focusout', { bubbles: true });
          });
        });

        // in order to simulate focus & focusin on the root input element
        // we are holding a timer id and clearing it on focusin
        // if there is a timer id, it means that the root input is still focused
        // otherwise, it was not focused before, and we should dispatch focus & focusin
        const onFocus = (e) => {
          e.stopImmediatePropagation();
          clearTimeout(timerId);
          if (!timerId) {
            createDispatchEvent.call(this, 'focus');
            createDispatchEvent.call(this, 'focusin', { bubbles: true });
          }
        };

        // some components are not dispatching focusin but only focus
        input?.addEventListener('focusin', onFocus);
        input?.addEventListener('focus', onFocus);
      });
    }

    // we want to block the input events from propagating in case the value of the root input wasn't change
    // this can happen if we are sanitizing characters on the internal inputs and do not want it to affect the root input element value
    // in this case, on each input event, we are comparing the root input value to the previous one, and only if it does not match, we are allowing the input event to propagate
    handleInputEventDispatching() {
      let previousRootComponentValue = this.value;

      // we are comparing the previous value to the new one,
      // and if they have the same value, we are blocking the input event
      this.addEventListener('input', (e) => {
        // We don't want to block our own event that we fire from handleInputEventDispatching
        if (this !== e.target) {
          e.stopImmediatePropagation();

          if (previousRootComponentValue !== this.value) {
            previousRootComponentValue = this.value;
            createDispatchEvent.call(this, 'input', { bubbles: true, composed: true });
          }
        }
      });
    }
  };
