/**
 * Dynamic tag template processor.
 *
 * It transforms dynamic tag templates to their final form.
 *
 * @author Quang-Minh DANG <quang-minh@emerya.fr>
 */

/**
 * Defines the prefix which will isolate the template tags and attributes.
 *
 * @type {string}
 */
const namespacePrefix = 'dtt-';

/**
 * Define this to add tag names to process.
 *
 * @type {string[]}
 */
const templateTags = [namespacePrefix + 'dynamic-tag-template'];

/**
 * Builds the template tags as selector.
 *
 * @returns {string}
 */
const getTemplateTagsAsSelector = () => templateTags.join(',');

/**
 * Processes the tag templates.
 *
 * @param {HTMLElement} baseElement
 * @param {Object} [context]
 */
const processTemplates = (baseElement, context = {}) => {
    if (!baseElement) {
        return;
    }

    while (true) {
        const elementToProcess = baseElement.querySelector(getTemplateTagsAsSelector());

        if (!elementToProcess) {
            // No template tag found. All templates have been processed.
            break;
        }

        // Get the metadata of the template to process.
        let metadata = {};

        const originalElementAttributes = elementToProcess.attributes;
        const elementDataset = elementToProcess.dataset;
        const dataset = Object.entries(elementDataset);
        const regex = new RegExp('^' + namespacePrefix.replaceAll('-', '[A-Z]'));

        for (const [datasetKey, datasetValue] of dataset) {
            if (!regex.test(datasetKey)) {
                continue;
            }

            metadata[datasetKey] = datasetValue;
        }

        const {
            dttHtmlTag: htmlTag = null,
            dttFormatAttrs: formatAttrs = null,
        } = metadata;

        if (formatAttrs) {
            // There are attributes to format.
            const attributeToFormatNames = formatAttrs.split(',').map((item) => item.trim());

            for (const attributeToFormatName of attributeToFormatNames) {
                if (!elementToProcess.hasAttribute(attributeToFormatName)) {
                    // The element has no attribute with this name.
                    continue;
                }

                const replacementsAttributeKey = 'data-' + namespacePrefix + 'format-attr-' + attributeToFormatName;

                const replacementsRaw = originalElementAttributes[replacementsAttributeKey]?.value;

                if (null === replacementsRaw) {
                    // The element has no replacements set.
                    continue;
                }

                const replacementVariableNames = replacementsRaw
                    .split(',')
                    .map((item) => item.trim());

                let attributeToFormatValue = elementToProcess.getAttribute(attributeToFormatName);

                for (const replacementVariableName of replacementVariableNames) {
                    const replacementVariableValue = context.hasOwnProperty(replacementVariableName)
                        // Replace by the value given in the arguments.
                        ? context[replacementVariableName]
                        // Replace by an empty string.
                        : '';

                    attributeToFormatValue = attributeToFormatValue.replaceAll('{' + replacementVariableName + '}', replacementVariableValue);
                }

                elementToProcess.setAttribute(attributeToFormatName, attributeToFormatValue);
            }
        }

        // Remove metadata.
        for (const datasetKey of Object.keys(metadata)) {
            if (!regex.test(datasetKey)) {
                continue;
            }

            // Remove the metadata from the element.
            delete elementToProcess.dataset[datasetKey];
        }

        // Replace the tag of this element.
        // Note: this will probably unbind any event on this element and all children.
        // The developer must rebind events after processing.
        const finalHtmlTagReplacement = htmlTag || 'div';

        const templateInner = elementToProcess.innerHTML;
        const templateAttrs = elementToProcess.attributes;
        const parent = elementToProcess.parentNode;

        // This new element will take the content and attributes of the processed element,
        // with the final tag.
        const newElement = document.createElement(finalHtmlTagReplacement);

        // Set the inner HTML.
        newElement.innerHTML = templateInner;

        for (const {nodeName: attrKey, nodeValue: attrValue} of Array.from(templateAttrs)) {
            // Set the attributes like the original.
            newElement.setAttribute(attrKey, attrValue);
        }

        // Swap the original and final elements.
        parent.replaceChild(newElement, elementToProcess);
    }

    // Remove the attribute which marked the elements as templates.
    const templateAttribute = 'data-' + namespacePrefix + 'template';
    const querySelectorToMatch = '[' + templateAttribute + ']';
    const maybeTemplateElements = [baseElement, ...baseElement.querySelectorAll(querySelectorToMatch)];

    const templateElements = maybeTemplateElements.filter(item => item.matches(querySelectorToMatch));

    templateElements.forEach(templateElement => templateElement.removeAttribute(templateAttribute));
};
