/**
 * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module html-support/generalhtmlsupport
 */
import { Plugin } from 'ckeditor5/src/core.js';
import { toArray } from 'ckeditor5/src/utils.js';
import DataFilter from './datafilter.js';
import CodeBlockElementSupport from './integrations/codeblock.js';
import DualContentModelElementSupport from './integrations/dualcontent.js';
import HeadingElementSupport from './integrations/heading.js';
import ImageElementSupport from './integrations/image.js';
import MediaEmbedElementSupport from './integrations/mediaembed.js';
import ScriptElementSupport from './integrations/script.js';
import TableElementSupport from './integrations/table.js';
import StyleElementSupport from './integrations/style.js';
import ListElementSupport from './integrations/list.js';
import CustomElementSupport from './integrations/customelement.js';
import { getHtmlAttributeName, modifyGhsAttribute } from './utils.js';
/**
 * The General HTML Support feature.
 *
 * This is a "glue" plugin which initializes the {@link module:html-support/datafilter~DataFilter data filter} configuration
 * and features integration with the General HTML Support.
 */
export default class GeneralHtmlSupport extends Plugin {
    /**
     * @inheritDoc
     */
    static get pluginName() {
        return 'GeneralHtmlSupport';
    }
    /**
     * @inheritDoc
     */
    static get requires() {
        return [
            DataFilter,
            CodeBlockElementSupport,
            DualContentModelElementSupport,
            HeadingElementSupport,
            ImageElementSupport,
            MediaEmbedElementSupport,
            ScriptElementSupport,
            TableElementSupport,
            StyleElementSupport,
            ListElementSupport,
            CustomElementSupport
        ];
    }
    /**
     * @inheritDoc
     */
    init() {
        const editor = this.editor;
        const dataFilter = editor.plugins.get(DataFilter);
        // Load the allowed empty inline elements' configuration.
        // Note that this modifies DataSchema so must be loaded before registering filtering rules.
        dataFilter.loadAllowedEmptyElementsConfig(editor.config.get('htmlSupport.allowEmpty') || []);
        // Load the filtering configuration.
        dataFilter.loadAllowedConfig(editor.config.get('htmlSupport.allow') || []);
        dataFilter.loadDisallowedConfig(editor.config.get('htmlSupport.disallow') || []);
    }
    /**
     * Returns a GHS model attribute name related to a given view element name.
     *
     * @internal
     * @param viewElementName A view element name.
     */
    getGhsAttributeNameForElement(viewElementName) {
        const dataSchema = this.editor.plugins.get('DataSchema');
        const definitions = Array.from(dataSchema.getDefinitionsForView(viewElementName, false));
        const inlineDefinition = definitions.find(definition => (definition.isInline && !definitions[0].isObject));
        if (inlineDefinition) {
            return inlineDefinition.model;
        }
        return getHtmlAttributeName(viewElementName);
    }
    /**
     * Updates GHS model attribute for a specified view element name, so it includes the given class name.
     *
     * @internal
     * @param viewElementName A view element name.
     * @param className The css class to add.
     * @param selectable The selection or element to update.
     */
    addModelHtmlClass(viewElementName, className, selectable) {
        const model = this.editor.model;
        const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName);
        model.change(writer => {
            for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) {
                modifyGhsAttribute(writer, item, ghsAttributeName, 'classes', classes => {
                    for (const value of toArray(className)) {
                        classes.add(value);
                    }
                });
            }
        });
    }
    /**
     * Updates GHS model attribute for a specified view element name, so it does not include the given class name.
     *
     * @internal
     * @param viewElementName A view element name.
     * @param className The css class to remove.
     * @param selectable The selection or element to update.
     */
    removeModelHtmlClass(viewElementName, className, selectable) {
        const model = this.editor.model;
        const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName);
        model.change(writer => {
            for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) {
                modifyGhsAttribute(writer, item, ghsAttributeName, 'classes', classes => {
                    for (const value of toArray(className)) {
                        classes.delete(value);
                    }
                });
            }
        });
    }
    /**
     * Updates GHS model attribute for a specified view element name, so it includes the given attribute.
     *
     * @param viewElementName A view element name.
     * @param attributes The object with attributes to set.
     * @param selectable The selection or element to update.
     */
    setModelHtmlAttributes(viewElementName, attributes, selectable) {
        const model = this.editor.model;
        const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName);
        model.change(writer => {
            for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) {
                modifyGhsAttribute(writer, item, ghsAttributeName, 'attributes', attributesMap => {
                    for (const [key, value] of Object.entries(attributes)) {
                        attributesMap.set(key, value);
                    }
                });
            }
        });
    }
    /**
     * Updates GHS model attribute for a specified view element name, so it does not include the given attribute.
     *
     * @param viewElementName A view element name.
     * @param attributeName The attribute name (or names) to remove.
     * @param selectable The selection or element to update.
     */
    removeModelHtmlAttributes(viewElementName, attributeName, selectable) {
        const model = this.editor.model;
        const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName);
        model.change(writer => {
            for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) {
                modifyGhsAttribute(writer, item, ghsAttributeName, 'attributes', attributesMap => {
                    for (const key of toArray(attributeName)) {
                        attributesMap.delete(key);
                    }
                });
            }
        });
    }
    /**
     * Updates GHS model attribute for a specified view element name, so it includes a given style.
     *
     * @param viewElementName A view element name.
     * @param styles The object with styles to set.
     * @param selectable The selection or element to update.
     */
    setModelHtmlStyles(viewElementName, styles, selectable) {
        const model = this.editor.model;
        const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName);
        model.change(writer => {
            for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) {
                modifyGhsAttribute(writer, item, ghsAttributeName, 'styles', stylesMap => {
                    for (const [key, value] of Object.entries(styles)) {
                        stylesMap.set(key, value);
                    }
                });
            }
        });
    }
    /**
     * Updates GHS model attribute for a specified view element name, so it does not include a given style.
     *
     * @param viewElementName A view element name.
     * @param properties The style (or styles list) to remove.
     * @param selectable The selection or element to update.
     */
    removeModelHtmlStyles(viewElementName, properties, selectable) {
        const model = this.editor.model;
        const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName);
        model.change(writer => {
            for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) {
                modifyGhsAttribute(writer, item, ghsAttributeName, 'styles', stylesMap => {
                    for (const key of toArray(properties)) {
                        stylesMap.delete(key);
                    }
                });
            }
        });
    }
}
/**
 * Returns an iterator over an items in the selectable that accept given GHS attribute.
 */
function* getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName) {
    if (!selectable) {
        return;
    }
    if (!(Symbol.iterator in selectable) && selectable.is('documentSelection') && selectable.isCollapsed) {
        if (model.schema.checkAttributeInSelection(selectable, ghsAttributeName)) {
            yield selectable;
        }
    }
    else {
        for (const range of getValidRangesForSelectable(model, selectable, ghsAttributeName)) {
            yield* range.getItems({ shallow: true });
        }
    }
}
/**
 * Translates a given selectable to an iterable of ranges.
 */
function getValidRangesForSelectable(model, selectable, ghsAttributeName) {
    if (!(Symbol.iterator in selectable) &&
        (selectable.is('node') ||
            selectable.is('$text') ||
            selectable.is('$textProxy'))) {
        if (model.schema.checkAttribute(selectable, ghsAttributeName)) {
            return [model.createRangeOn(selectable)];
        }
        else {
            return [];
        }
    }
    else {
        return model.schema.getValidRanges(model.createSelection(selectable).getRanges(), ghsAttributeName);
    }
}
