import debounce from 'lodash.debounce';
import { injectStyle } from '@descope-ui/common/components-helpers';
import { createBaseInputClass } from '../../../baseClasses/createBaseInputClass';
import { forwardAttrs, getComponentName } from '../../../helpers/componentHelpers';
import {
  sanitizeStr,
  focusElement,
  toggleMaskVisibility,
  INPUT_MASK_FALLBACK_PROP,
  INPUT_MASK_TEXT_PROP,
  INPUT_MASK_DISPLAY_PROP,
} from './helpers';

export const componentName = getComponentName('passcode-internal');

const observedAttributes = ['digits', 'loading'];

const forwardAttributes = [
  'disabled',
  'bordered',
  'size',
  'invalid',
  'readonly',
  'aria-labelledby',
];

const BaseInputClass = createBaseInputClass({ componentName, baseSelector: 'div' });

class PasscodeInternal extends BaseInputClass {
  static get observedAttributes() {
    return observedAttributes.concat(BaseInputClass.observedAttributes || []);
  }

  constructor() {
    super();

    this.innerHTML = `
		<div class="wrapper"></div>
		<div class="loader-container">
      <descope-loader-radial size="xs" mode="primary"></descope-loader-radial>
    </div>
		`;

    this.wrapperEle = this.querySelector('.wrapper');
    this.loaderContainer = this.querySelector('.loader-container');
  }

  renderInputs() {
    const inputs = [...Array(this.digits).keys()].map(
      (idx) => `
		<descope-text-field
			data-id=${idx}
			type="tel"
			autocomplete="one-time-code"
			inputMode="numeric"
		></descope-text-field>
	`
    );

    this.wrapperEle.innerHTML = inputs.join('');

    this.inputs = Array.from(this.querySelectorAll('descope-text-field'));

    this.initInputs();
  }

  handleLoadingState(isLoading) {
    if (isLoading) {
      this.setAttribute('inert', 'true');
    } else {
      this.removeAttribute('inert');
    }
  }

  get digits() {
    return Number.parseInt(this.getAttribute('digits'), 10) || 6;
  }

  get value() {
    return this.inputs?.map(({ value }) => value).join('') || '';
  }

  set value(val) {
    if (val === this.value) return;

    this.parseInputValue(this.inputs[0], sanitizeStr(val));
  }

  getValidity() {
    if (this.isRequired && !this.value) {
      return { valueMissing: true };
    }
    if (this.pattern && !new RegExp(this.pattern).test(this.value)) {
      return { patternMismatch: true };
    }

    return {};
  }

  init() {
    // we are adding listeners before calling to super because it's stopping the events
    this.addEventListener('focus', (e) => {
      // we want to ignore focus events we are dispatching
      if (e.isTrusted) this.inputs[0].focus();
    });

    super.init?.();

    this.renderInputs();
  }

  getInputIdx(inputEle) {
    return Number.parseInt(inputEle.getAttribute('data-id'), 10);
  }

  getNextInput(currInput) {
    const currentIdx = this.getInputIdx(currInput);
    const newIdx = Math.min(currentIdx + 1, this.inputs.length - 1);
    return this.inputs[newIdx];
  }

  getPrevInput(currInput) {
    const currentIdx = this.getInputIdx(currInput);
    const newIdx = Math.max(currentIdx - 1, 0);
    return this.inputs[newIdx];
  }

  fillDigits(charArr, currentInput) {
    for (let i = 0; i < charArr.length; i += 1) {
      currentInput.value = charArr[i] ?? '';

      const nextInput = this.getNextInput(currentInput);

      if (nextInput === currentInput) break;
      currentInput = nextInput;
    }

    focusElement(currentInput);
  }

  parseInputValue(input, value = '') {
    const charArr = value.split('');

    if (!charArr.length) {
      // if we got an invalid value we want to clear the input
      input.value = '';
    } else this.fillDigits(charArr, input);
  }

  initInputs() {
    this.inputs.forEach((input) => {
      // During debounced input we hide the value with a mask.
      // This allows us to hide the multiple population of the first input
      // in case of an automated input (e.g. Safari OTP Autofill or paste).
      injectStyle(
        `
        vaadin-text-field::part(input-field)::before {
          ${INPUT_MASK_FALLBACK_PROP}: '';
          content: var(${INPUT_MASK_TEXT_PROP}, var(${INPUT_MASK_FALLBACK_PROP}));
          position: absolute;
          width: 100%;
          height: 100%;
          display: var(${INPUT_MASK_DISPLAY_PROP}, none);
          align-items: center;
          justify-content: center;
        }`,
        input
      );

      const handleParseInput = (val) => {
        this.parseInputValue(input, val);
        toggleMaskVisibility(input);
      };

      const debouncedHandleParseInput = debounce(handleParseInput, 0, { trailing: true });

      // sanitize the input
      input.addEventListener('input', (e) => {
        input.value = sanitizeStr(input.value);

        if (input.value) {
          toggleMaskVisibility(input, input.value[0]);
        }

        setTimeout(() => {
          if (e?.inputType === 'deleteContentBackward') {
            focusElement(this.getPrevInput(input));
          }
        });
        debouncedHandleParseInput(input.value);
      });

      // we want backspace to focus on the previous digit
      input.onkeydown = ({ key }) => {
        // when user deletes a digit, we want to focus the previous digit
        if (key === 'Backspace') {
          // if value is empty then the input element does not fire `input` event
          // if this is the case, we focus the element here.
          // otherwise, the focusElement occurs as part of the `input` event listener
          if (!input.value) {
            setTimeout(() => focusElement(this.getPrevInput(input)), 0);
          } else {
            input.setSelectionRange(1, 1);
          }
        } else if (key.length === 1) {
          // we want only characters and not command keys
          input.value = ''; // we are clearing the previous value so we can override it with the new value
        }
      };

      forwardAttrs(this, input, { includeAttrs: forwardAttributes });
      input.shadowRoot.querySelector('input').setAttribute('aria-label', 'passcode digit');
    });

    this.handleFocusEventsDispatching(this.inputs);
    this.handleInputEventDispatching();
  }

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

    // sync attributes to inputs
    if (oldValue !== newValue) {
      if (observedAttributes.includes(attrName)) {
        if (attrName === 'digits') {
          this.renderInputs();
        }
        if (attrName === 'loading') {
          this.handleLoadingState(newValue === 'true');
        }
      }
    }
  }

  get pattern() {
    return `^$|^\\d{${this.digits},}$`;
  }
}

export default PasscodeInternal;
