/* *
 *
 *  Grid utilities
 *
 *  (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:
 *  - Dawid Draguła
 *
 * */
import AST from '../../Core/Renderer/HTML/AST.js';
import Globals from './Globals.js';
import { isObject } from '../../Shared/Utilities.js';
AST.allowedAttributes.push('srcset', 'media');
AST.allowedTags.push('picture', 'source');
/* *
*
*  Functions
*
* */
/**
 * Creates a HTML element with the provided options.
 *
 * @param tagName
 * The tag name of the element.
 *
 * @param params
 * The parameters of the element.
 *
 * @param parent
 * The parent element.
 */
export function makeHTMLElement(tagName, params, parent) {
    const element = document.createElement(tagName);
    if (params) {
        const paramsKeys = Object.keys(params);
        for (let i = 0; i < paramsKeys.length; i++) {
            const key = paramsKeys[i];
            const value = params[key];
            if (value !== void 0) {
                if (key === 'style') {
                    Object.assign(element.style, value);
                }
                else {
                    element[key] = value;
                }
            }
        }
    }
    if (parent) {
        parent.appendChild(element);
    }
    return element;
}
/**
 * Creates a div element with the provided class name and id.
 *
 * @param className
 * The class name of the div.
 *
 * @param id
 * The id of the element.
 */
export function makeDiv(className, id) {
    return makeHTMLElement('div', { className, id });
}
/**
 * Measures the horizontal paddings and borders of an element.
 *
 * @param el
 * The element to measure.
 *
 * @returns
 * The overhead in pixels.
 */
export function measureWidthOverhead(el) {
    if (!el) {
        return 0;
    }
    const style = Globals.win.getComputedStyle(el);
    return ((parseFloat(style.paddingLeft) || 0) +
        (parseFloat(style.paddingRight) || 0) +
        (parseFloat(style.borderLeftWidth) || 0) +
        (parseFloat(style.borderRightWidth) || 0));
}
/**
 * Check if there's a possibility that the given string is an HTML
 * (contains '<').
 *
 * @param str
 * Text to verify.
 */
export function isHTML(str) {
    return str.indexOf('<') !== -1;
}
/**
 * Returns a string containing plain text format by removing HTML tags
 *
 * @param text
 * String to be sanitized
 *
 * @returns
 * Sanitized plain text string
 */
export function sanitizeText(text) {
    try {
        return new DOMParser().parseFromString(text, 'text/html')
            .body.textContent || '';
    }
    catch {
        return '';
    }
}
/**
 * Sets an element's content, checking whether it is HTML or plain text.
 * Should be used instead of element.innerText when the content can be HTML.
 *
 * @param element
 * Parent element where the content should be.
 *
 * @param content
 * Content to render.
 */
export function setHTMLContent(element, content) {
    if (isHTML(content)) {
        element.innerHTML = AST.emptyHTML;
        const formattedNodes = new AST(content);
        formattedNodes.addToDOM(element);
    }
    else {
        element.innerText = content;
    }
}
/**
 * Creates a proxy that, when reading a property, first returns the value
 * from the original options of a given entity; if it is not defined, it
 * falls back to the value from the defaults (default options), recursively
 * for nested objects. Setting values on the proxy will change the original
 * options object (1st argument), not the defaults (2nd argument).
 *
 * @param options
 * The specific options object.
 *
 * @param defaultOptions
 * The default options to fall back to.
 *
 * @returns
 * A proxy that provides merged access to options and defaults.
 */
export function createOptionsProxy(options, defaultOptions = {}) {
    const handler = (defaults = {}) => ({
        get(target, prop) {
            const targetValue = target[prop];
            const defaultValue = defaults[prop];
            if (isObject(targetValue, true)) {
                return new Proxy(targetValue, handler(defaultValue ?? {}));
            }
            return targetValue ?? defaultValue;
        },
        set(target, prop, value) {
            target[prop] = value;
            return true;
        },
        deleteProperty(target, prop) {
            delete target[prop];
            return true;
        }
    });
    return new Proxy(options, handler(defaultOptions));
}
/**
 * Format text with placeholders. Used for lang texts.
 *
 * @param template The text template with placeholders
 * @param values Object containing values to replace placeholders
 * @returns Formatted text
 */
export function formatText(template, values) {
    return template.replace(/\{(\w+)\}/g, (match, key) => (values[key] !== void 0 ? String(values[key]) : match));
}
/**
 * Joins class name parts into a single space-separated string.
 *
 * @param parts
 * Class name parts to join.
 *
 * @returns
 * A space-separated class name string.
 */
export function joinClassNames(...parts) {
    return parts.filter(Boolean).join(' ');
}
/**
 * Replaces previously applied user class tokens on an element without touching
 * other classes (e.g. Core `hcg-*` tokens).
 *
 * @param element
 * The element to update.
 *
 * @param previous
 * Previously applied user class name string.
 *
 * @param next
 * New user class name string.
 *
 * @returns
 * The class name string that was applied, or `undefined` when cleared.
 */
export function applyUserClassNames(element, previous, next) {
    if (previous) {
        const prevTokens = previous.split(/\s+/g).filter(Boolean);
        if (prevTokens.length) {
            element.classList.remove(...prevTokens);
        }
    }
    if (!next) {
        return;
    }
    const nextTokens = next.split(/\s+/g).filter(Boolean);
    if (!nextTokens.length) {
        return;
    }
    element.classList.add(...nextTokens);
    return nextTokens.join(' ');
}
/**
 * Checks whether two objects have the same own keys and values.
 *
 * Supports nested plain objects and arrays. Functions are compared by
 * reference.
 *
 * @param left
 * The first object to compare.
 *
 * @param right
 * The second object to compare.
 *
 * @returns
 * `true` when both objects are equal, otherwise `false`.
 */
export function isDeepEqual(left, right) {
    if (left === right) {
        return true;
    }
    if (left instanceof RegExp || right instanceof RegExp) {
        return (left instanceof RegExp &&
            right instanceof RegExp &&
            left.source === right.source &&
            left.flags === right.flags);
    }
    if (!isObject(left) || !isObject(right)) {
        return false;
    }
    const leftRecord = left;
    const rightRecord = right;
    if ('nodeType' in leftRecord || 'nodeType' in rightRecord) {
        return false;
    }
    if (Array.isArray(left) || Array.isArray(right)) {
        if (!Array.isArray(left) || !Array.isArray(right)) {
            return false;
        }
        if (left.length !== right.length) {
            return false;
        }
    }
    const leftKeys = Object.keys(left).filter(function (key) {
        return key !== '__proto__' && key !== 'constructor';
    });
    const rightKeys = Object.keys(right).filter(function (key) {
        return key !== '__proto__' && key !== 'constructor';
    });
    if (leftKeys.length !== rightKeys.length) {
        return false;
    }
    for (let i = 0, iEnd = leftKeys.length; i < iEnd; ++i) {
        const key = leftKeys[i];
        if (!(key in rightRecord)) {
            return false;
        }
        if (!isDeepEqual(leftRecord[key], rightRecord[key])) {
            return false;
        }
    }
    return true;
}
/**
 * Resolves a style value that can be static or callback based.
 *
 * @param style
 * Style object or callback returning one.
 *
 * @param target
 * Runtime target used as callback context and first argument.
 *
 * @returns
 * A resolved style object or `undefined`.
 */
export function resolveStyleValue(style, target) {
    if (!style) {
        return;
    }
    if (typeof style === 'function') {
        if (!target) {
            return;
        }
        return style.call(target, target);
    }
    return style;
}
/**
 * Resolves and merges style values in order.
 *
 * @param target
 * Runtime target used as callback context and first argument.
 *
 * @param styleValues
 * Style values to merge in order, where latter entries override former.
 *
 * @returns
 * Merged style object.
 */
export function mergeStyleValues(target, ...styleValues) {
    const mergedStyle = {};
    for (const styleValue of styleValues) {
        const resolvedStyle = resolveStyleValue(styleValue, target);
        if (resolvedStyle) {
            Object.assign(mergedStyle, resolvedStyle);
        }
    }
    return mergedStyle;
}
/**
 * Waits for the next animation frame.
 */
export function waitForAnimationFrame() {
    return new Promise((resolve) => {
        requestAnimationFrame(() => resolve());
    });
}
/* *
 *
 *  Default Export
 *
 * */
export default {
    makeHTMLElement,
    makeDiv,
    measureWidthOverhead,
    isHTML,
    sanitizeText,
    setHTMLContent,
    createOptionsProxy,
    formatText,
    joinClassNames,
    applyUserClassNames,
    isDeepEqual,
    resolveStyleValue,
    mergeStyleValues,
    waitForAnimationFrame
};
