/* eslint-disable import/no-extraneous-dependencies */
// cSpell:words datafilter eventinfo downcastdispatcher generalhtmlsupport
import { Plugin } from 'ckeditor5/src/core';
import { setViewAttributes } from '@ckeditor/ckeditor5-html-support/src/utils';

/**
 * View-to-model conversion helper for Entity Embed.
 * Used for preserving allowed attributes on the Entity Embed model.
 *
 * @param {module:html-support/datafilter~DataFilter} dataFilter
 *   The General HTML support data filter.
 *
 * @return {function}
 *   Function that adds an event listener to upcastDispatcher.
 */
function viewToModelDrupalEntityAttributeConverter(dataFilter) {
  return (dispatcher) => {
    dispatcher.on(
      'element:drupal-entity',
      (evt, data, conversionApi) => {
        function preserveElementAttributes(viewElement, attributeName) {
          const viewAttributes = dataFilter.processViewAttributes(
            viewElement,
            conversionApi
          );

          if (viewAttributes) {
            conversionApi.writer.setAttribute(
              attributeName,
              viewAttributes,
              data.modelRange
            );
          }
        }
        function preserveLinkAttributes(linkElement) {
          preserveElementAttributes(linkElement, 'htmlLinkAttributes');
        }

        const viewEntityEmbedElement = data.viewItem;
        const viewContainerElement = viewEntityEmbedElement.parent;

        preserveElementAttributes(viewEntityEmbedElement, 'htmlAttributes');

        if (viewContainerElement.is('element', 'a')) {
          preserveLinkAttributes(viewContainerElement);
        }
      },
      { priority: 'low' }
    );
  };
}

/**
 * Gets descendant element from a container.
 *
 * @param {module:engine/model/writer~Writer} writer
 *   The writer.
 * @param {module:engine/view/element~Element} containerElement
 *   The container element.
 * @param {string} elementName
 *   The element name.
 * @return {module:engine/view/element~Element|undefined}
 *   The descendant element matching element name or undefined if not found.
 */
function getDescendantElement(writer, containerElement, elementName) {
  const range = writer.createRangeOn(containerElement);

  // eslint-disable-next-line no-restricted-syntax
  for (const { item } of range.getWalker()) {
    if (item.is('element', elementName)) {
      return item;
    }
  }
}

/**
 * Model to view converter for the Entity Embed wrapper attributes.
 *
 * @param {module:utils/eventinfo~EventInfo} evt
 *   An object containing information about the fired event.
 * @param {Object} data
 *   Additional information about the change.
 * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher} conversionApi
 *   Conversion interface to be used by the callback.
 */
function modelToDataAttributeConverter(evt, data, conversionApi) {
  if (!conversionApi.consumable.consume(data.item, evt.name)) {
    return;
  }

  const viewElement = conversionApi.mapper.toViewElement(data.item);

  setViewAttributes(conversionApi.writer, data.attributeNewValue, viewElement);
}

/**
 * Model to editing view attribute converter.
 *
 * @return {function}
 *   A function that adds an event listener to downcastDispatcher.
 */
function modelToEditingViewAttributeConverter() {
  return (dispatcher) => {
    dispatcher.on(
      'attribute:linkHref:drupalEntity',
      (evt, data, conversionApi) => {
        if (
          !conversionApi.consumable.consume(
            data.item,
            'attribute:htmlLinkAttributes:drupalEntity'
          )
        ) {
          return;
        }

        const containerElement = conversionApi.mapper.toViewElement(data.item);
        const viewElement = getDescendantElement(
          conversionApi.writer,
          containerElement,
          'a'
        );

        setViewAttributes(
          conversionApi.writer,
          data.item.getAttribute('htmlLinkAttributes'),
          viewElement
        );
      },
      { priority: 'low' }
    );
  };
}

/**
 * Model to data view attribute converter.
 *
 * @return {function}
 *   Function that adds an event listener to downcastDispatcher.
 */
function modelToDataViewAttributeConverter() {
  return (dispatcher) => {
    dispatcher.on(
      'attribute:linkHref:drupalEntity',
      (evt, data, conversionApi) => {
        if (
          !conversionApi.consumable.consume(
            data.item,
            'attribute:htmlLinkAttributes:drupalEntity'
          )
        ) {
          return;
        }

        const entityEmbedElement = conversionApi.mapper.toViewElement(
          data.item
        );
        const linkElement = entityEmbedElement.parent;

        setViewAttributes(
          conversionApi.writer,
          data.item.getAttribute('htmlLinkAttributes'),
          linkElement
        );
      },
      { priority: 'low' }
    );

    dispatcher.on(
      'attribute:htmlAttributes:drupalEntity',
      modelToDataAttributeConverter,
      { priority: 'low' }
    );
  };
}

/**
 * Integrates Entity Embed with General HTML Support.
 *
 * @private
 */
export default class EntityEmbedGeneralHtmlSupport extends Plugin {
  /**
   * @inheritdoc
   */
  constructor(editor) {
    super(editor);

    // This plugin is only needed if General HTML Support plugin is loaded.
    if (!editor.plugins.has('GeneralHtmlSupport')) {
      return;
    }
    // This plugin works only if `DataFilter` and `DataSchema` plugins are
    // loaded. These plugins are dependencies of `GeneralHtmlSupport` meaning
    // that these should be available always when `GeneralHtmlSupport` is
    // enabled.
    if (
      !editor.plugins.has('DataFilter') ||
      !editor.plugins.has('DataSchema')
    ) {
      console.error(
        'DataFilter and DataSchema plugins are required for Entity Embed to integrate with General HTML Support plugin.'
      );
    }

    const { schema } = editor.model;
    const { conversion } = editor;
    const dataFilter = this.editor.plugins.get('DataFilter');
    const dataSchema = this.editor.plugins.get('DataSchema');

    // This needs to be initialized in ::constructor() to ensure this runs
    // before the General HTML Support has been initialized.
    // @see module:html-support/generalhtmlsupport~GeneralHtmlSupport
    dataSchema.registerBlockElement({
      model: 'drupalEntity',
      view: 'drupal-entity',
    });

    dataFilter.on('register:drupal-entity', (evt, definition) => {
      if (definition.model !== 'drupalEntity') {
        return;
      }

      schema.extend('drupalEntity', {
        allowAttributes: ['htmlLinkAttributes', 'htmlAttributes'],
      });

      conversion
        .for('upcast')
        .add(viewToModelDrupalEntityAttributeConverter(dataFilter));
      conversion
        .for('editingDowncast')
        .add(modelToEditingViewAttributeConverter());
      conversion.for('dataDowncast').add(modelToDataViewAttributeConverter());

      evt.stop();
    });
  }

  /**
   * @inheritdoc
   */
  static get pluginName() {
    return 'EntityEmbedGeneralHtmlSupport';
  }
}
