import { createBaseInputClass } from '../../../../baseClasses/createBaseInputClass';
import {
  getComponentName,
  forwardAttrs,
  observeChildren,
} from '../../../../helpers/componentHelpers';

export const componentName = getComponentName('mappings-field-internal');

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

class MappingsFieldInternal extends BaseInputClass {
  #errorItem;

  static get observedAttributes() {
    return [].concat(BaseInputClass.observedAttributes || [], [
      'label-value',
      'label-attr',
      'button-label',
      'invalid',
      'readonly',
      'disabled',
      'st-error-message-icon',
      'st-error-message-icon-size',
      'st-error-message-icon-padding',
    ]);
  }

  isValidDataType(data) {
    try {
      return data.every(
        (obj) =>
          typeof obj === 'object' &&
          !Array.isArray(obj) &&
          Object.getOwnPropertyNames(obj).length === 1 &&
          typeof obj[Object.keys(obj)[0]] === 'string' &&
          obj[Object.keys(obj)[0]].trim() !== ''
      );
    } catch (e) {
      return false;
    }
  }

  get labelValue() {
    return this.getAttribute('label-value') || 'Value';
  }

  get labelAttr() {
    return this.getAttribute('label-attr') || 'Attribute';
  }

  get buttonLabel() {
    return this.getAttribute('button-label') || 'Add mapping';
  }

  get options() {
    return this.getAttribute('options') || [];
  }

  addNewMappingItem(focusNewItem) {
    const newMappingItem = document.createElement('descope-mapping-item');
    newMappingItem.setAttribute('bordered', 'true');
    this.mappingsContainerEle.appendChild(newMappingItem);
    forwardAttrs(this, newMappingItem, {
      includeAttrs: [
        'size',
        'full-width',
        'separator',
        'options',
        'disabled',
        'st-error-message-icon',
        'st-error-message-icon-size',
        'st-error-message-icon-padding',
      ],
    });
    // This needs to be done with the timeout, otherwise the validation is performed
    // before the new item is added and thus returns a wrong result
    setTimeout(() => {
      this.setCustomValidity('');
      newMappingItem.addEventListener('mapping-item-removed', () => {
        // If the removed item was the one that was invalid, we need to reset the invalid indication for the internal
        if (newMappingItem === this.#errorItem) {
          this.resetInvalidIndication();
          this.#errorItem = undefined;
        }
        newMappingItem.remove();
        this.setCustomValidity('');
      });
      this.dispatchEvent(new CustomEvent('mapping-item-added', { bubbles: true, composed: true }));
      if (focusNewItem) {
        newMappingItem.focus();
      }
    }, 0);
    return newMappingItem;
  }

  get items() {
    return Array.from(this.mappingsContainerEle.querySelectorAll('descope-mapping-item'));
  }

  get value() {
    return this.items.reduce((acc, item) => {
      if (!item.value) {
        return acc;
      }

      return [...acc, item.value];
    }, []);
  }

  set value(mappings) {
    if (!this.isValidDataType(mappings)) {
      // eslint-disable-next-line no-console
      console.error(
        'received invalid value to set - should be an array of objects with one key-value pair'
      );
      return;
    }

    const currentItems = this.items;

    // Remove extra mapping items we don't need
    if (currentItems.length > mappings.length) {
      for (let i = mappings.length; i < currentItems.length; i++) {
        this.mappingsContainerEle.removeChild(currentItems[i]);
      }
    }

    // Add or update items
    mappings.forEach((mapping, index) => {
      const mappingItem = currentItems[index];
      if (mappingItem) {
        // Set existing item value
        mappingItem.value = mapping;
      } else {
        // Add new item
        const newMappingItem = this.addNewMappingItem(false);
        // Setting the new item value needs to be done with the timeout,
        // otherwise the value is not set correctly
        setTimeout(() => {
          newMappingItem.value = mapping;
        }, 0);
      }
    });
  }

  constructor() {
    super();

    this.innerHTML = `
      <div class="labels-container" part="labels"></div>
      <div class="mappings-container"></div>
      <div class="button-container"></div>
    `;

    this.labelsEle = this.querySelector('.labels-container');
    this.mappingsContainerEle = this.querySelector('.mappings-container');
    this.buttonContainer = this.querySelector('.button-container');
  }

  initLabels() {
    this.labelsEle.innerHTML = `
      <descope-text variant="body2" part="value-label">${this.labelValue}</descope-text>
      <descope-text variant="body2" part="attr-label">${this.labelAttr}</descope-text>
    `;
  }

  initAddButton() {
    this.buttonContainer.innerHTML = `
      <descope-button variant="link" mode="primary" disabled="${this.isDisabled}">
        <vaadin-icon icon="vaadin:plus"></vaadin-icon>
        ${this.buttonLabel}
      </descope-button>
    `;
    const button = this.querySelector('descope-button');
    button.onclick = () => {
      this.addNewMappingItem(true);
    };
    forwardAttrs(this, button, {
      includeAttrs: ['disabled'],
    });
  }

  #onMappingsContainerChildrenChange() {
    this.labelsEle.style.display = this.items.length ? 'grid' : 'none';
  }

  init() {
    // This event listener needs to be placed before the super.init() call
    this.addEventListener('focus', (e) => {
      // we want to ignore focus events we are dispatching
      if (e.isTrusted) {
        const focusedElement =
          this.#errorItem || this.items[0] || this.querySelector('descope-button');
        focusedElement.focus();
      }
    });

    super.init?.();
    this.initLabels();
    this.initAddButton();

    // This event listener is responsible for removing the invalid attribute
    // from the internal once the invalid item turns valid
    this.addEventListener('input', () => {
      const isErrorItemMounted = this.mappingsContainerEle.contains(this.#errorItem);
      if (isErrorItemMounted && this.#errorItem?.checkValidity()) {
        // Item has changed, it was invalid before and now it's valid
        this.resetInvalidIndication();
        this.#errorItem.removeAttribute('invalid');
        this.#errorItem = undefined;
      }
    });

    observeChildren(this.mappingsContainerEle, this.#onMappingsContainerChildrenChange.bind(this));
  }

  resetInvalidIndication() {
    this.removeAttribute('invalid');
  }

  getValidity() {
    const errorItem = this.items.find((item) => !item.checkValidity());
    if (errorItem) {
      return errorItem.validity;
    }

    return {};
  }

  #handleInvalidMappings(isInvalid) {
    if (isInvalid) {
      this.#errorItem = this.items.find((item) => !item.checkValidity());
      this.#errorItem?.reportValidity();
      this.#errorItem?.setAttribute('invalid', 'true');
    }
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    super.attributeChangedCallback?.(attrName, oldValue, newValue);
    if (attrName === 'label-value' || attrName === 'label-attr') {
      this.initLabels();
    }
    if (attrName === 'button-label') {
      this.initAddButton();
    }
    if (attrName === 'invalid') {
      this.#handleInvalidMappings(newValue === 'true');
    }
    if (attrName === 'readonly') {
      this.toggleAttribute('inert', newValue === 'true');
    }
  }
}

export default MappingsFieldInternal;
