/* *
 *
 *  (c) 2009-2026 Highsoft AS
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 *  Authors:
 *  - Sebastian Bochan
 *  - Wojciech Chmiel
 *  - Gøran Slettemark
 *  - Sophie Bremer
 *
 * */
'use strict';
import AST from '../../../Core/Renderer/HTML/AST.js';
import Component from '../Component.js';
import HTMLComponentDefaults from './HTMLComponentDefaults.js';
import HTMLSyncs from './HTMLSyncs/HTMLSyncs.js';
import DU from '../../Utilities.js';
import { diffObjects, merge } from '../../../Shared/Utilities.js';
const { deepClone } = DU;
// TODO: This may affect the AST parsing in Highcharts
// should look into adding these as options if possible
// Needs to go in a composition in the Highcharts plugin
AST.allowedTags = [
    ...AST.allowedTags,
    'option',
    'select',
    'label',
    'input',
    'textarea'
];
AST.allowedAttributes = [
    ...AST.allowedAttributes,
    'for',
    'value',
    'checked',
    'src',
    'name',
    'selected'
];
AST.allowedReferences = [
    ...AST.allowedReferences,
    'data:image/'
];
/* *
 *
 *  Class
 *
 * */
/**
 *
 * Class that represents a HTML component.
 *
 */
class HTMLComponent extends Component {
    /* *
     *
     *  Constructor
     *
     * */
    /**
     * Creates a HTML component in the cell.
     *
     * @param cell
     * Instance of cell, where component is attached.
     *
     * @param options
     * The options for the component.
     */
    constructor(cell, options) {
        if (options.className) {
            options.className = `${HTMLComponent.defaultOptions.className} ${options.className}`;
        }
        options = merge(HTMLComponent.defaultOptions, options);
        super(cell, options);
        this.options = options;
        this.type = 'HTML';
        this.elements = [];
    }
    /* *
     *
     *  Functions
     *
     * */
    /** @internal */
    async load() {
        this.emit({
            type: 'load'
        });
        await super.load();
        const options = this.options;
        let isError = false;
        if (options.elements?.length) {
            this.elements = options.elements.map(function (element) {
                if (typeof element === 'string') {
                    return new AST(element).nodes[0];
                }
                if (!element.textContent &&
                    !element.tagName &&
                    element.attributes) {
                    isError = true;
                }
                return element;
            });
        }
        else if (options.html) {
            this.elements = this.getElementsFromString(options.html);
            this.options.elements = this.elements;
        }
        this.constructTree();
        this.emit({ type: 'afterLoad' });
        if (isError) {
            throw new Error(`Missing tagName param in component: ${options.renderTo}`);
        }
        return this;
    }
    render() {
        super.render();
        this.constructTree();
        this.sync.start();
        this.emit({ type: 'afterRender' });
        return this;
    }
    resize(width, height) {
        super.resize(width, height);
        return this;
    }
    /**
     * Handles updating via options.
     *
     * @param options
     * The options to apply.
     */
    async update(options, shouldRerender = true) {
        if (options.html) {
            this.elements = this.getElementsFromString(options.html);
            this.options.elements = this.elements;
            this.constructTree();
        }
        else if (options.elements) {
            this.elements = options.elements;
        }
        await super.update(options, shouldRerender);
        this.emit({ type: 'afterUpdate' });
    }
    getOptionsOnDrop() {
        return {
            type: 'HTML',
            elements: [{
                    tagName: 'span',
                    textContent: '[Your custom HTML here- edit the component]'
                }]
        };
    }
    /**
     * Constructs the HTML tree.
     * @internal
     */
    constructTree() {
        // Remove old tree if rerendering.
        while (this.contentElement.firstChild) {
            this.contentElement.firstChild.remove();
        }
        const parser = new AST(this.options.elements || []);
        parser.addToDOM(this.contentElement);
    }
    /**
     * When HTML definition is a string, it needs to be parsed to AST.
     *
     * @internal
     */
    getElementsFromString(htmlString) {
        return new AST(htmlString).nodes;
    }
    /**
     * Get the HTML component's options.
     * @returns
     * HTML component's options.
     *
     * @internal
     *
     */
    getOptions() {
        return {
            ...diffObjects(this.options, HTMLComponent.defaultOptions),
            type: 'HTML'
        };
    }
    /**
     * Retrieves editable options for the HTML component.
     */
    getEditableOptions() {
        return deepClone(this.options, ['editableOptions']);
    }
    /**
     * Get the value of the editable option by property path. Parse the elements
     * if the HTML options is not set.
     *
     * @param propertyPath
     * The property path of the option.
     */
    getEditableOptionValue(propertyPath) {
        if (!propertyPath) {
            return;
        }
        if (propertyPath[0] === 'html') {
            const result = this.getEditableOptions();
            if (!result.html && result.elements) {
                return this.getStringFromElements(result.elements);
            }
            return result[propertyPath[0]];
        }
        return super.getEditableOptionValue(propertyPath);
    }
    /**
     * Returns the HTML string from the given elements.
     *
     * @param elements
     * The array of elements to serialize.
     */
    getStringFromElements(elements) {
        let html = '';
        for (const element of elements) {
            html += this.serializeNode(element);
        }
        return html;
    }
    /**
     * Serializes the HTML node to string.
     *
     * @param node
     * The HTML node to serialize.
     */
    serializeNode(node) {
        if (!node.tagName || node.tagName === '#text') {
            // Text node
            return node.textContent || '';
        }
        const attributes = node.attributes;
        let html = `<${node.tagName}`;
        if (attributes) {
            for (const key in attributes) {
                if (Object.prototype.hasOwnProperty.call(attributes, key)) {
                    const value = attributes[key];
                    if (value !== void 0) {
                        html += ` ${key}="${value}"`;
                    }
                }
            }
        }
        html += '>';
        html += node.textContent || '';
        (node.children || []).forEach((child) => {
            html += this.serializeNode(child);
        });
        html += `</${node.tagName}>`;
        return html;
    }
    /**
     * @internal
     */
    onTableChanged(e) {
        if (e.detail?.sender !== this.id) {
            this.render();
        }
    }
}
/* *
 *
 *  Static properties
 *
 * */
/**
 * Default options of the HTML component.
 */
HTMLComponent.defaultOptions = merge(Component.defaultOptions, HTMLComponentDefaults);
/**
 * Predefined sync config for HTML component.
 */
HTMLComponent.predefinedSyncConfig = HTMLSyncs;
/* *
 *
 *  Default export
 *
 * */
export default HTMLComponent;
