import { syncAttrs } from '../helpers/componentHelpers';
import {
  applyExternalInputStyles,
  createExternalInputEle,
  createExternalInputSlot,
} from './externalInputHelpers';

export const externalInputMixin =
  ({ inputType, inputName, autocompleteType, includeAttrs = [], noBlurDispatch = false }) =>
  (superclass) =>
    class ExternalInputMixinClass extends superclass {
      #timers = [];

      get isExternalInput() {
        return this.getAttribute('external-input') === 'true';
      }

      createExternalInput() {
        if (!this.isExternalInput || this.isReadOnly || this.isDisabled) {
          return null;
        }

        // use original input element as reference
        const origInput = this.baseElement.querySelector('input');

        if (!origInput) {
          return null;
        }

        // to avoid focus loop between external-input and origInput
        // we set origInput's tabindex to -1
        // otherwise, shift-tab will never leave the component focus
        origInput.setAttribute('tabindex', '-1');

        // create external slot
        const externalInputSlot = createExternalInputSlot('external-input', 'suffix');

        // append external slot to base element
        this.baseElement.appendChild(externalInputSlot);

        this.externalInput = createExternalInputEle(
          'external-input',
          inputType,
          this.getAutocompleteType(),
          inputName
        );

        // apply original input's styles to external input
        setTimeout(() => {
          applyExternalInputStyles(origInput, this.externalInput, this.getAttribute('label-type'));
        });

        // 1Password catches the internal input, so we forward the value to the external input
        this.forwardInputValue(origInput, this.externalInput);

        syncAttrs(origInput, this.externalInput, {
          includeAttrs,
        });

        // We disable Vaadin's original `_setFocused` function, and use handleFocusEvents
        // and handleBlurEvents functions
        this.baseElement
          .querySelector('input')
          .addEventListener('focusout', (e) => e.stopImmediatePropagation(), true);

        // In order to manage focus/blur events when moving between parts of the component
        // we're managing the event dispatching by ourselves, with the following strategy:
        // - If one of the component parts is focused - it means that the component is still focused - so we stop the blur events.
        // - When none of the component parts is focused, we dispatch blur event.
        this.handleFocusEvents();
        this.handleBlurEvents();

        // sync input value
        this.handlelInputEvents(this.externalInput);

        // append external input to component's DOM
        this.appendChild(this.externalInput);

        return this.externalInput;
      }

      clearBlurTimers() {
        this.#timers.forEach((timer) => clearTimeout(timer));
        this.#timers.length = 0;
      }

      dispatchBlur() {
        return setTimeout(() => {
          this.dispatchEvent(new Event('blur', { bubbles: true, composed: true }));
          this.removeAttribute('focused');
        });
      }

      handleFocusEvents() {
        // on baseElement `focus` we forward the focus to the external input.
        // also, in order to avoid any blur within the component, we clear the blur timers.
        this.baseElement.addEventListener('focus', () => {
          this.externalInput.focus();
          this.clearBlurTimers();
        });

        // on `focus` of the external input, we manually set the `focused` attribute
        this.externalInput.addEventListener('focus', () => {
          this.clearBlurTimers();
          setTimeout(() => this.baseElement.setAttribute('focused', 'true'));
        });
      }

      handleBlurEvents() {
        this.baseElement.addEventListener(
          'blur',
          (e) => {
            e.stopImmediatePropagation();
            // some components do not require this synthetic blur dispatch (e.g. Password)
            // so we allow them to override this dispatch
            if (noBlurDispatch) return;
            this.#timers.push(this.dispatchBlur());
          },
          true
        );

        this.externalInput.addEventListener(
          'blur',
          (e) => {
            e.stopImmediatePropagation();
            this.#timers.push(this.dispatchBlur());
          },
          true
        );
      }

      handlelInputEvents(externalInput) {
        // sync value of insible input back to original input
        externalInput.addEventListener('input', (e) => {
          this.value = e.target.value;
        });

        // handle has-value attr
        externalInput.addEventListener('input', (e) => {
          if (!e.target.value) {
            this.removeAttribute('has-value');
          } else {
            this.setAttribute('has-value', 'true');
          }
        });
      }

      getAutocompleteType() {
        return this.getAttribute('autocomplete') || autocompleteType;
      }

      forwardInputValue(source, target) {
        // set internal sync events
        const valueDescriptor = Object.getOwnPropertyDescriptor(
          this.inputElement.constructor.prototype,
          'value'
        );

        Object.defineProperty(source, 'value', {
          ...valueDescriptor,

          set(v) {
            valueDescriptor.set.call(this, v);
            // eslint-disable-next-line no-param-reassign
            target.value = v;
          },
          configurable: true,
        });
      }
    };
