<?php

namespace DiviBooster\DiviBooster\Modules\Timeline\MarkerNumbers;

if (!defined('ABSPATH')) {
    exit();
}

const TIMELINE_SLUG = 'divi/timeline';
const TIMELINE_ITEM_SLUG = 'divi/timeline-item';
const ATTR_NAME = 'dbTimelineMarkerNumbers';
const FONT_ATTR_NAME = 'dbTimelineMarkerNumberFont';
const DEFAULT_START = 1;
const DEFAULT_STEP = 1;
const DEFAULT_DIRECTION = 'ascending';
const DEFAULT_TEXT_SHADOW_COLOR = 'rgba(0,0,0,0.4)';

if (function_exists('add_action')) {
    add_action('divi_visual_builder_assets_before_enqueue_scripts', __NAMESPACE__ . '\\enqueue_builder_fields');
}

if (function_exists('add_filter')) {
    add_filter('block_type_metadata_settings', __NAMESPACE__ . '\\register_block_attrs');
    add_filter('render_block_' . TIMELINE_SLUG, __NAMESPACE__ . '\\filter_timeline_output', 20, 3);
    add_filter('render_block_' . TIMELINE_ITEM_SLUG, __NAMESPACE__ . '\\filter_timeline_item_output', 20, 3);
    add_filter('divi_module_wrapper_render', __NAMESPACE__ . '\\add_font_styles', 10, 2);
}

function is_divi5_active()
{
    if (!function_exists('et_builder_d5_enabled')) {
        return false;
    }

    try {
        return (bool) \et_builder_d5_enabled();
    } catch (\Throwable $e) {
        return false;
    }
}

function is_divi5_visual_builder()
{
    if (!is_divi5_active() || !function_exists('et_core_is_fb_enabled')) {
        return false;
    }

    try {
        return (bool) \et_core_is_fb_enabled();
    } catch (\Throwable $e) {
        return false;
    }
}

function enqueue_builder_fields()
{
    if (!is_divi5_visual_builder() || !function_exists('sanitize_title') || !function_exists('dbdb_enqueue_inline_script_if_dependencies_available')) {
        return;
    }

    $handle = sanitize_title('divi-booster-' . TIMELINE_SLUG . '-marker-numbers');
    $inline_js = get_builder_inline_js();
    if ($inline_js === '') {
        return;
    }

    \dbdb_enqueue_inline_script_if_dependencies_available($handle, $inline_js, array('lodash', array('divi-vendor-wp-hooks', 'wp-hooks')));
}

function get_builder_inline_js()
{
    if (!function_exists('wp_json_encode')) {
        return '';
    }

    $timeline_slug = wp_json_encode(TIMELINE_SLUG);
    $attr_name = wp_json_encode(ATTR_NAME);
    $font_attr_name = wp_json_encode(FONT_ATTR_NAME);
    $attr_definitions = wp_json_encode(get_attribute_definitions());
    $breakpoint_settings = wp_json_encode(get_breakpoint_settings());

    if (!is_string($timeline_slug) || !is_string($attr_name) || !is_string($font_attr_name) || !is_string($attr_definitions) || !is_string($breakpoint_settings)) {
        return '';
    }

    return <<<JS
(function() {
    if (!window.vendor || !window.vendor.wp || !window.vendor.wp.hooks || !window.lodash) {
        return;
    }

    var hooks = window.vendor.wp.hooks;
    var WPElement = window.vendor.wp.element;
    var React = (window.vendor && window.vendor.React) || WPElement;
    var get = window.lodash.get;
    var timelineSlug = {$timeline_slug};
    var attrName = {$attr_name};
    var fontAttrName = {$font_attr_name};
    var attrDefinitions = {$attr_definitions};
    var breakpointSettings = {$breakpoint_settings};

    hooks.addFilter('divi.moduleLibrary.moduleAttributes.divi.timeline', 'divibooster.timelineMarkerNumbers', function(attributes) {
        attributes = attributes || {};
        Object.keys(attrDefinitions).forEach(function(name) {
            attributes[name] = attrDefinitions[name];
        });
        applyVisibilityCallbacks(attributes);
        return attributes;
    });

    function applyVisibilityCallbacks(attributes) {
        var items = get(attributes, [attrName, 'settings', 'innerContent', 'items'], {});

        [
            items.dbTimelineMarkerNumberStart,
            items.dbTimelineMarkerNumberDirection,
            items.dbTimelineMarkerNumberStep
        ].forEach(function(item) {
            setItemVisibleWhenNumberingActive(item);
        });
        var fontItem = get(attributes, [fontAttrName, 'settings', 'decoration', 'font', 'item']);
        setItemVisibleWhenNumberingActive(fontItem);
    }

    function setItemVisibleWhenNumberingActive(item) {
        if (!item) {
            return;
        }

        var visible = function(ctx) {
            return isNumberingActiveForContext(ctx);
        };

        item.visible = visible;
        item.component = item.component || {};
        item.component.props = item.component.props || {};
        item.component.props.visible = visible;
    }

    hooks.addFilter('divi.module.wrapper.render', 'divibooster.timelineMarkerNumbers', function(moduleWrapper, param) {
        if (!param || param.name !== timelineSlug) {
            return moduleWrapper;
        }

        if (!React || !WPElement || typeof React.useEffect !== 'function' || typeof React.useRef !== 'function' || typeof WPElement.createElement !== 'function' || typeof WPElement.cloneElement !== 'function') {
            return moduleWrapper;
        }

        var orderClass = getPreviewOrderClass(param);
        if (!orderClass) {
            return moduleWrapper;
        }

        var previewAttrs = toPlainObject(param.attrs || {});
        var css = buildPreviewCss(previewAttrs, orderClass);

        var injectorEl = WPElement.createElement(TimelineMarkerNumberPreviewInjector, {
            key: 'divibooster-timeline-marker-numbers-preview',
            orderClass: orderClass,
            css: css
        });

        return appendPreviewChild(moduleWrapper, injectorEl, WPElement);
    });

    function TimelineMarkerNumberPreviewInjector(props) {
        var ref = React.useRef(null);

        React.useEffect(function() {
            var marker = ref.current;
            var doc = marker && marker.ownerDocument ? marker.ownerDocument : document;
            var timelineEl = findPreviewTimeline(doc, props.orderClass, marker);
            var styleId = 'divibooster-timeline-marker-numbers-preview-' + safeId(props.orderClass);
            var styleEl;

            if (!props.css) {
                removePreviewTimelineNumbers(doc, props.orderClass, marker, styleId);
                return;
            }

            if (!timelineEl) {
                return;
            }

            styleEl = doc.getElementById(styleId);

            timelineEl.classList.add('dbtl-marker-numbers');

            if (!styleEl) {
                styleEl = doc.createElement('style');
                styleEl.id = styleId;
                (doc.head || doc.body || doc.documentElement).appendChild(styleEl);
            }

            styleEl.textContent = props.css;

            return function() {
                // Builder hover renders can transiently unmount this helper.
                // The disabled-state render removes the preview CSS deliberately.
            };
        }, [props.orderClass, props.css]);

        return WPElement.createElement('span', {
            ref: ref,
            'aria-hidden': true,
            style: { display: 'none' }
        });
    }

    function removePreviewTimelineNumbers(doc, orderClass, marker, styleId) {
        var timelineEl = findPreviewTimeline(doc, orderClass, marker);
        var styleEl = styleId ? doc.getElementById(styleId) : null;

        if (timelineEl && timelineEl.classList) {
            timelineEl.classList.remove('dbtl-marker-numbers');
        }
        if (styleEl && styleEl.parentNode) {
            styleEl.parentNode.removeChild(styleEl);
        }
    }

    function appendPreviewChild(moduleWrapper, child, WPElement) {
        if (!moduleWrapper || !moduleWrapper.props) {
            return moduleWrapper;
        }

        var children = moduleWrapper.props.children;
        var nextChildren = Array.isArray(children) ? children.concat([child]) : [children, child].filter(Boolean);

        return WPElement.cloneElement(moduleWrapper, {}, nextChildren);
    }

    function toPlainObject(value) {
        if (!value || typeof value !== 'object') {
            return {};
        }

        try {
            return JSON.parse(JSON.stringify(value));
        } catch (e) {
            return value;
        }
    }

    function getPreviewOrderClass(param) {
        var candidates = [
            get(param, ['elements', 'orderClass']),
            get(param, ['elements', 'order_class']),
            param.orderClass,
            param.order_class
        ];

        for (var i = 0; i < candidates.length; i++) {
            if (typeof candidates[i] !== 'string') {
                continue;
            }

            var match = candidates[i].match(/\\bet_pb_timeline_\\d+(?:_tb_[a-z]+)?\\b/i);
            if (match) {
                return match[0];
            }

            var normalized = candidates[i].replace(/^\\./, '').trim();
            if (/^[a-z0-9_-]+$/i.test(normalized)) {
                return normalized;
            }
        }

        return '';
    }

    function findPreviewTimeline(doc, orderClass, marker) {
        var selector = normalizeOrderSelector(orderClass);
        var timelineEl = selector ? doc.querySelector(selector) : null;

        if (timelineEl && timelineEl.classList && timelineEl.classList.contains('et_pb_timeline')) {
            return timelineEl;
        }

        if (timelineEl) {
            timelineEl = timelineEl.querySelector('.et_pb_timeline');
            if (timelineEl) {
                return timelineEl;
            }
        }

        var node = marker && marker.parentElement;
        while (node && node !== doc.documentElement) {
            if (node.classList && node.classList.contains('et_pb_timeline')) {
                return node;
            }
            node = node.parentElement;
        }

        return null;
    }

    function buildPreviewCss(attrs, orderClass) {
        var configs = getNumberBreakpointConfigs(attrs);
        if (!hasActiveNumberConfig(configs)) {
            return '';
        }

        var baseSelector = normalizeOrderSelector(orderClass);
        var css = '';
        var desktopConfig = configs.desktop || firstConfig(configs);

        if (desktopConfig && desktopConfig.active) {
            css += buildNumberCssRules(baseSelector, desktopConfig, getFontCssFromAttrs(attrs, 'desktop'));
        }

        Object.keys(breakpointSettings || {}).forEach(function(breakpoint) {
            var media;

            if (breakpoint === 'desktop' || !configs[breakpoint] || !configs[breakpoint].active) {
                return;
            }

            media = breakpointMediaQuery(breakpointSettings[breakpoint]);
            if (media) {
                css += '@media ' + media + '{' + buildNumberCssRules(baseSelector, configs[breakpoint], getFontCssFromAttrs(attrs, breakpoint)) + '}';
            }
        });

        return css;
    }

    function getNumberBreakpointConfigs(attrs) {
        var configs = {};
        getNumberBreakpointNames(attrs).forEach(function(breakpoint) {
            configs[breakpoint] = getNumberConfig(attrs, breakpoint);
        });
        return configs;
    }

    function getNumberBreakpointNames(attrs) {
        var names = Object.keys(breakpointSettings || {});
        var attr = attrs && attrs[attrName];

        if (attr && typeof attr === 'object') {
            Object.keys(attr).forEach(function(key) {
                if (attr[key] && typeof attr[key] === 'object' && Object.prototype.hasOwnProperty.call(attr[key], 'value') && names.indexOf(key) === -1) {
                    names.push(key);
                }
            });
        }

        if (names.indexOf('desktop') === -1) {
            names.unshift('desktop');
        }

        return names;
    }

    function getNumberConfig(attrs, breakpoint) {
        var start = sanitizeInt(attrSubValue(attrs, 'start', breakpoint), 1);
        var step = Math.abs(sanitizeInt(attrSubValue(attrs, 'step', breakpoint), 1));
        var direction = attrSubValue(attrs, 'direction', breakpoint) === 'descending' ? 'descending' : 'ascending';
        var increment;

        step = step > 0 ? step : 1;
        increment = direction === 'descending' ? -step : step;

        return {
            active: isEnabled(attrSubValue(attrs, 'enabled', breakpoint)),
            start: start,
            step: step,
            direction: direction,
            increment: increment,
            reset: start - increment
        };
    }

    function hasActiveNumberConfig(configs) {
        return Object.keys(configs || {}).some(function(breakpoint) {
            return !!(configs[breakpoint] && configs[breakpoint].active);
        });
    }

    function firstConfig(configs) {
        var names = Object.keys(configs || {});
        return names.length ? configs[names[0]] : null;
    }

    function buildNumberCssRules(baseSelector, config, fontCss) {
        var reset = typeof config.reset === 'number' ? config.reset : 0;
        var increment = typeof config.increment === 'number' ? config.increment : 1;

        return baseSelector + ' .et_pb_timeline_track{counter-reset:dbtl-timeline-marker-number ' + reset + ';}' +
            baseSelector + ' .et_pb_timeline_item{counter-increment:dbtl-timeline-marker-number ' + increment + ';}' +
            baseSelector + ' .et_pb_timeline_item:not(.dbtl-marker-has-custom-content) .et_pb_timeline_marker::before{' +
                'align-items:center;color:#fff;content:counter(dbtl-timeline-marker-number);content:counter(dbtl-timeline-marker-number)/"";display:flex;font-size:14px;font-weight:700;height:100%;justify-content:center;line-height:1;pointer-events:none;width:100%;' +
                (fontCss || '') +
            '}' +
            baseSelector + ' .et_pb_timeline_item.dbtl-marker-has-custom-content .et_pb_timeline_marker::before{content:none;}' +
            '@supports selector(:has(*)){' + baseSelector + ' .et_pb_timeline_item:has(.et_pb_timeline_marker .et-pb-icon) .et_pb_timeline_marker::before{content:none;}}';
    }

    function breakpointMediaQuery(settings) {
        var parts = [];
        var min = sanitizeCssLength(nestedValue(settings, ['minWidth', 'value']), '');
        var max = sanitizeCssLength(nestedValue(settings, ['maxWidth', 'value']), '');

        if (min) {
            parts.push('(min-width:' + min + ')');
        }
        if (max) {
            parts.push('(max-width:' + max + ')');
        }

        return parts.join(' and ');
    }

    function getFontCssFromAttrs(attrs, breakpoint) {
        var fontValue = getFontValue(attrs, breakpoint);
        var css = '';
        var color = sanitizeCssColor(getFirstFontScalar(fontValue, ['color']));
        var size = sanitizeCssLength(getFirstFontScalar(fontValue, ['size', 'fontSize']), '');
        var weight = getFirstFontScalar(fontValue, ['weight', 'fontWeight']);
        var family = getFirstFontScalar(fontValue, ['family', 'fontFamily']);
        var style = getFirstFontScalar(fontValue, ['style', 'fontStyle']);
        var textDecorationLine = getTextDecorationLineFromFontStyle(style);
        var letterSpacing = sanitizeCssLength(getFirstFontScalar(fontValue, ['letterSpacing']), '');
        var textShadow = sanitizeCssTextShadow(getTextShadowValue(attrs, breakpoint));

        if (color) {
            css += 'color:' + color + ';';
        }
        if (size) {
            css += 'font-size:' + size + ';';
        }
        if (/^(normal|bold|[1-9]00)$/.test(String(weight).trim())) {
            css += 'font-weight:' + String(weight).trim() + ';';
        }
        if (typeof family === 'string' && family.trim() && family.indexOf('|') === -1 && family.indexOf('\$variable(') !== 0) {
            css += 'font-family:"' + cssString(family.trim()) + '";';
        }
        if (typeof style === 'string' && /\\bitalic\\b/i.test(style)) {
            css += 'font-style:italic;';
        }
        if (textDecorationLine) {
            css += 'text-decoration-line:' + textDecorationLine + ';';
        }
        if (letterSpacing) {
            css += 'letter-spacing:' + letterSpacing + ';';
        }
        if (textShadow) {
            css += 'text-shadow:' + textShadow + ';';
        }

        return css;
    }

    function getTextDecorationLineFromFontStyle(style) {
        var lines = [];

        style = typeof style === 'string' ? style : '';
        if (/\\bunderline\\b/i.test(style)) {
            lines.push('underline');
        }
        if (/\\b(?:strike|strikethrough|line-through)\\b/i.test(style)) {
            lines.push('line-through');
        }

        return lines.join(' ');
    }

    function getFontValue(attrs, breakpoint) {
        var paths = [
            [fontAttrName, 'decoration', 'font', 'font', breakpoint, 'value'],
            [fontAttrName, 'decoration', 'font', 'font', 'desktop', 'value'],
            [fontAttrName, 'decoration', 'font', breakpoint, 'value'],
            [fontAttrName, 'decoration', 'font', 'desktop', 'value'],
            [fontAttrName, 'font', breakpoint, 'value'],
            [fontAttrName, 'font', 'desktop', 'value']
        ];
        var value;

        for (var i = 0; i < paths.length; i++) {
            value = nestedValue(attrs, paths[i]);
            if (value && typeof value === 'object') {
                return value;
            }
        }

        value = nestedValue(attrs, [fontAttrName, 'decoration', 'font']);
        return value && typeof value === 'object' ? value : {};
    }

    function getTextShadowValue(attrs, breakpoint) {
        var paths = [
            [fontAttrName, 'decoration', 'font', 'textShadow', breakpoint, 'value'],
            [fontAttrName, 'decoration', 'font', 'textShadow', breakpoint],
            [fontAttrName, 'decoration', 'font', 'textShadow', 'desktop', 'value'],
            [fontAttrName, 'decoration', 'font', 'textShadow', 'desktop'],
            [fontAttrName, 'decoration', 'font', 'textShadow', 'value'],
            [fontAttrName, 'decoration', 'font', 'textShadow'],
            [fontAttrName, 'textShadow', breakpoint, 'value'],
            [fontAttrName, 'textShadow', breakpoint],
            [fontAttrName, 'textShadow', 'desktop', 'value']
        ];
        var value;

        for (var i = 0; i < paths.length; i++) {
            value = nestedValue(attrs, paths[i]);
            if (value && typeof value === 'object') {
                return buildTextShadow(value);
            }
            if (typeof value === 'string' && value.trim()) {
                return value.trim();
            }
        }

        return '';
    }

    function buildTextShadow(value) {
        var shadowStyle = value && typeof value.style === 'string' ? value.style : '';
        var horizontalRaw = getFirstFontScalar(value, ['horizontal', 'horizontalLength', 'x']);
        var verticalRaw = getFirstFontScalar(value, ['vertical', 'verticalLength', 'y']);
        var blurRaw = getFirstFontScalar(value, ['blur', 'blurStrength']);
        var color = getTextShadowColor(value);
        var hasShadowValue = !!(shadowStyle || horizontalRaw || verticalRaw || blurRaw || color);
        var horizontal = sanitizeCssLength(horizontalRaw, '0px');
        var vertical = sanitizeCssLength(verticalRaw, '0px');
        var blur = sanitizeCssLength(blurRaw, '0px');

        if (shadowStyle && shadowStyle.toLowerCase() === 'none') {
            return '';
        }
        if (!hasShadowValue) {
            return '';
        }

        color = color || 'rgba(0,0,0,0.4)';

        return horizontal + ' ' + vertical + ' ' + blur + (color ? ' ' + color : '');
    }

    function getFirstFontScalar(value, keys) {
        var nestedValueResult;

        if (!value || typeof value !== 'object') {
            return '';
        }

        for (var i = 0; i < keys.length; i++) {
            if (Object.prototype.hasOwnProperty.call(value, keys[i]) && value[keys[i]] !== null && value[keys[i]] !== undefined && String(value[keys[i]]).trim() !== '') {
                return String(value[keys[i]]).trim();
            }
        }

        for (var key in value) {
            if (!Object.prototype.hasOwnProperty.call(value, key) || !value[key] || typeof value[key] !== 'object') {
                continue;
            }
            nestedValueResult = getFirstFontScalar(value[key], keys);
            if (nestedValueResult) {
                return nestedValueResult;
            }
        }

        return '';
    }

    function normalizeOrderSelector(orderClass) {
        orderClass = typeof orderClass === 'string' ? orderClass.trim() : '';
        orderClass = orderClass.charAt(0) === '.' ? orderClass.slice(1) : orderClass;

        return /^[a-z0-9_-]+$/i.test(orderClass) ? '.' + orderClass : '';
    }

    function sanitizeInt(value, fallback) {
        var parsed = parseInt(value, 10);
        return isNaN(parsed) ? fallback : parsed;
    }

    function sanitizeCssColor(value) {
        value = typeof value === 'string' ? value.trim() : '';
        if (/^[0-9a-f]{3,8}$/i.test(value)) {
            value = '#' + value;
        }
        return /^(#[0-9a-f]{3,8}|rgba?\\([0-9.,%\\s]+\\)|hsla?\\([0-9.,%\\s]+\\)|[a-z]+)$/i.test(value) ? value : '';
    }

    function getTextShadowColor(value) {
        var colorConfig = getColorConfig(value);
        var color = sanitizeCssColor(colorConfig.color);
        var opacity = parseOpacity(colorConfig.opacity);

        if (!color) {
            return '';
        }

        if (opacity === null && /^#[0-9a-f]{6}$/i.test(color)) {
            opacity = 0.4;
        }

        if (opacity !== null && /^#[0-9a-f]{6}$/i.test(color)) {
            return hexToRgba(color, opacity);
        }

        return color;
    }

    function getColorConfig(value) {
        var result = { color: '', opacity: null };

        findColorConfig(value, result, false);
        return result;
    }

    function findColorConfig(value, result, allowValueColor) {
        if (!value || typeof value !== 'object') {
            return;
        }

        ['color', 'hex', 'textShadowColor', 'shadowColor', 'text-shadow-color'].some(function(key) {
            if (result.color) {
                return true;
            }
            if (isCssColorCandidate(value[key])) {
                result.color = String(value[key]).trim();
                return true;
            }
            if (value[key] && typeof value[key] === 'object') {
                findColorConfig(value[key], result, true);
                return !!result.color;
            }
            return false;
        });

        if (!result.color && allowValueColor && isCssColorCandidate(value.value)) {
            result.color = String(value.value).trim();
        }

        ['opacity', 'alpha'].some(function(key) {
            if (value[key] !== null && value[key] !== undefined && String(value[key]).trim() !== '') {
                result.opacity = value[key];
                return true;
            }
            return false;
        });

        Object.keys(value).some(function(key) {
            if (result.color && result.opacity !== null) {
                return true;
            }
            if (value[key] && typeof value[key] === 'object') {
                findColorConfig(value[key], result, allowValueColor);
            }
            return false;
        });
    }

    function isCssColorCandidate(value) {
        return typeof value === 'string' && !!sanitizeCssColor(value);
    }

    function parseOpacity(value) {
        var parsed;

        if (value === null || value === undefined || String(value).trim() === '') {
            return null;
        }

        parsed = parseFloat(value);
        if (isNaN(parsed)) {
            return null;
        }

        return Math.max(0, Math.min(1, parsed > 1 ? parsed / 100 : parsed));
    }

    function hexToRgba(color, opacity) {
        var hex = color.replace('#', '');
        var r = parseInt(hex.substr(0, 2), 16);
        var g = parseInt(hex.substr(2, 2), 16);
        var b = parseInt(hex.substr(4, 2), 16);

        return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
    }

    function sanitizeCssLength(value, fallback) {
        value = value === null || value === undefined ? '' : String(value).trim();

        if (!value) {
            return fallback;
        }
        if (/^-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem|vw|vh|%)$/i.test(value)) {
            return value;
        }
        if (/^-?(?:\\d+|\\d*\\.\\d+)$/.test(value)) {
            return value + 'px';
        }

        return fallback;
    }

    function sanitizeCssTextShadow(value) {
        value = typeof value === 'string' ? value.trim() : '';

        return /^-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?\\s+-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?(?:\\s+-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?)?(?:\\s+(?:#[0-9a-f]{3,8}|rgba?\\([0-9.,%\\s]+\\)|hsla?\\([0-9.,%\\s]+\\)|[a-z]+))?$/i.test(value) ? value : '';
    }

    function cssString(value) {
        return String(value).replace(/[\\\\"'\\n\\r\\f]/g, function(match) {
            return {
                '\\\\': '\\\\\\\\',
                '"': '\\\\"',
                "'": "\\\\'",
                '\\n': '\\\\a ',
                '\\r': '\\\\d ',
                '\\f': '\\\\c '
            }[match] || '';
        });
    }

    function safeId(value) {
        return String(value || '').replace(/[^a-z0-9_-]/gi, '-');
    }

    function nestedValue(object, path) {
        var value = object;
        for (var i = 0; i < path.length; i++) {
            if (!value || typeof value !== 'object' || !Object.prototype.hasOwnProperty.call(value, path[i])) {
                return null;
            }
            value = value[path[i]];
        }
        return value;
    }

    function hasResponsiveValue(value) {
        if (value === null || value === undefined) {
            return false;
        }
        return !(typeof value === 'string' && value.trim() === '');
    }

    function getResponsiveAttrValue(attr, breakpoint, paths) {
        var orderedPaths = [];
        var i;

        if (breakpoint && breakpoint !== 'desktop') {
            for (i = 0; i < paths.length; i++) {
                orderedPaths.push(paths[i](breakpoint));
            }
        }
        for (i = 0; i < paths.length; i++) {
            orderedPaths.push(paths[i]('desktop'));
        }

        for (i = 0; i < orderedPaths.length; i++) {
            var value = nestedValue(attr, orderedPaths[i]);
            if (hasResponsiveValue(value)) {
                return value;
            }
        }

        return '';
    }

    function attrSubValue(attrs, subName, breakpoint) {
        var attr = attrs && attrs[attrName];
        var value = getResponsiveAttrValue(attr, breakpoint || 'desktop', [
            function(bp) { return [bp, 'value', subName]; },
            function(bp) { return [subName, bp, 'value']; }
        ]);

        if (hasResponsiveValue(value)) {
            return value;
        }

        var legacyPaths = [
            ['value', subName],
            [subName, 'value'],
            [subName]
        ];

        for (var i = 0; i < legacyPaths.length; i++) {
            value = nestedValue(attr, legacyPaths[i]);
            if (hasResponsiveValue(value)) {
                return value;
            }
        }

        return '';
    }

    function contextAttrs(ctx) {
        return (ctx && ctx.attrs) || ctx || {};
    }

    function contextBreakpoint(ctx) {
        var candidates = [
            ctx && ctx.breakpoint,
            ctx && ctx.breakpointName,
            ctx && ctx.activeBreakpoint,
            ctx && ctx.currentBreakpoint,
            ctx && ctx.responsiveState,
            ctx && ctx.device,
            ctx && ctx.state && ctx.state.breakpoint,
            ctx && ctx.state && ctx.state.activeBreakpoint,
            ctx && ctx.context && ctx.context.breakpoint,
            ctx && ctx.context && ctx.context.activeBreakpoint
        ];

        for (var i = 0; i < candidates.length; i++) {
            if (typeof candidates[i] === 'string' && candidates[i]) {
                return candidates[i];
            }
        }

        return getCurrentBreakpointName();
    }

    function getCurrentBreakpointName() {
        var names = Object.keys(breakpointSettings || {});
        var width = window.innerWidth || document.documentElement.clientWidth || 0;
        var fallback = breakpointSettings && breakpointSettings.desktop ? 'desktop' : (names[0] || 'desktop');

        names.sort(function(a, b) {
            return ((breakpointSettings[a] && breakpointSettings[a].order) || 0) - ((breakpointSettings[b] && breakpointSettings[b].order) || 0);
        });

        for (var i = 0; i < names.length; i++) {
            if (breakpointMatchesWidth(breakpointSettings[names[i]], width)) {
                return names[i];
            }
        }

        return fallback;
    }

    function breakpointMatchesWidth(settings, width) {
        if (!settings) {
            return false;
        }

        var min = parsePixelValue(settings.minWidth && settings.minWidth.value);
        var max = parsePixelValue(settings.maxWidth && settings.maxWidth.value);

        if (min !== null && width < min) {
            return false;
        }
        if (max !== null && width > max) {
            return false;
        }

        return true;
    }

    function parsePixelValue(value) {
        value = typeof value === 'number' ? String(value) : (typeof value === 'string' ? value.trim() : '');
        if (!value) {
            return null;
        }
        var match = value.match(/^(-?\\d+(?:\\.\\d+)?)px$/i) || value.match(/^(-?\\d+(?:\\.\\d+)?)$/);
        return match ? parseFloat(match[1]) : null;
    }

    function isNumberingActiveForContext(ctx) {
        return isEnabled(attrSubValue(contextAttrs(ctx), 'enabled', contextBreakpoint(ctx)));
    }

    function isEnabled(value) {
        return value === true || value === 1 || value === '1' || value === 'on' || value === 'yes' || value === 'true';
    }
})();
JS;
}

function register_block_attrs($settings)
{
    if (!is_array($settings)) {
        return $settings;
    }

    if (isset($settings['name']) && $settings['name'] === TIMELINE_SLUG && isset($settings['attributes']) && is_array($settings['attributes'])) {
        $settings['attributes'] = array_replace($settings['attributes'], get_attribute_definitions());
    }

    return $settings;
}

function get_attribute_definitions()
{
    return array(
        ATTR_NAME => get_settings_attribute_definition(),
        FONT_ATTR_NAME => get_font_attribute_definition(),
    );
}

function get_settings_attribute_definition()
{
    return array(
        'type' => 'object',
        'defaultAttr' => array(
            'desktop' => array(
                'value' => array(
                    'enabled' => 'off',
                    'start' => (string) DEFAULT_START,
                    'direction' => DEFAULT_DIRECTION,
                    'step' => (string) DEFAULT_STEP,
                ),
            ),
        ),
        'settings' => array(
            'innerContent' => array(
                'groupType' => 'group-items',
                'items' => array(
                    'dbTimelineMarkerNumbersEnabled' => array(
                        'groupSlug' => 'designMarker',
                        'attrName' => ATTR_NAME,
                        'subName' => 'enabled',
                        'label' => 'Number Markers',
                        'description' => 'Display automatic numbers inside timeline markers.',
                        'category' => 'layout',
                        'priority' => 6,
                        'render' => true,
                        'features' => array(
                            'responsive' => true,
                            'hover' => false,
                            'sticky' => false,
                            'preset' => 'advanced',
                        ),
                        'component' => array(
                            'type' => 'field',
                            'name' => 'divi/toggle',
                        ),
                        'defaultAttr' => array(
                            'desktop' => array(
                                'value' => 'off',
                            ),
                        ),
                    ),
                    'dbTimelineMarkerNumberStart' => array(
                        'groupSlug' => 'designMarker',
                        'attrName' => ATTR_NAME,
                        'subName' => 'start',
                        'label' => 'Start Value',
                        'description' => 'Set the number used by the first timeline marker.',
                        'category' => 'layout',
                        'priority' => 7,
                        'render' => true,
                        'features' => array(
                            'responsive' => true,
                            'hover' => false,
                            'sticky' => false,
                            'dynamicContent' => array(
                                'type' => 'number',
                            ),
                            'preset' => 'advanced',
                        ),
                        'component' => array(
                            'type' => 'field',
                            'name' => 'divi/text',
                            'props' => array(
                                'defaultValue' => (string) DEFAULT_START,
                            ),
                        ),
                        'defaultAttr' => array(
                            'desktop' => array(
                                'value' => (string) DEFAULT_START,
                            ),
                        ),
                    ),
                    'dbTimelineMarkerNumberDirection' => array(
                        'groupSlug' => 'designMarker',
                        'attrName' => ATTR_NAME,
                        'subName' => 'direction',
                        'label' => 'Direction',
                        'description' => 'Choose whether marker numbers increase or decrease.',
                        'category' => 'layout',
                        'priority' => 8,
                        'render' => true,
                        'features' => array(
                            'responsive' => true,
                            'hover' => false,
                            'sticky' => false,
                            'preset' => 'advanced',
                        ),
                        'component' => array(
                            'type' => 'field',
                            'name' => 'divi/select',
                            'props' => array(
                                'defaultValue' => DEFAULT_DIRECTION,
                                'options' => array(
                                    'ascending' => array(
                                        'label' => 'Ascending',
                                    ),
                                    'descending' => array(
                                        'label' => 'Descending',
                                    ),
                                ),
                            ),
                        ),
                        'defaultAttr' => array(
                            'desktop' => array(
                                'value' => DEFAULT_DIRECTION,
                            ),
                        ),
                    ),
                    'dbTimelineMarkerNumberStep' => array(
                        'groupSlug' => 'designMarker',
                        'attrName' => ATTR_NAME,
                        'subName' => 'step',
                        'label' => 'Step',
                        'description' => 'Set how much each timeline marker number changes by.',
                        'category' => 'layout',
                        'priority' => 9,
                        'render' => true,
                        'features' => array(
                            'responsive' => true,
                            'hover' => false,
                            'sticky' => false,
                            'dynamicContent' => array(
                                'type' => 'number',
                            ),
                            'preset' => 'advanced',
                        ),
                        'component' => array(
                            'type' => 'field',
                            'name' => 'divi/text',
                            'props' => array(
                                'defaultValue' => (string) DEFAULT_STEP,
                            ),
                        ),
                        'defaultAttr' => array(
                            'desktop' => array(
                                'value' => (string) DEFAULT_STEP,
                            ),
                        ),
                    ),
                ),
            ),
        ),
    );
}

function get_font_attribute_definition()
{
    return array(
        'type' => 'object',
        'settings' => array(
            'decoration' => array(
                'font' => array(
                    'groupType' => 'group-item',
                    'item' => array(
                        'groupSlug' => 'designMarker',
                        'priority' => 9.5,
                        'render' => true,
                        'component' => array(
                            'name' => 'divi/font',
                            'type' => 'group',
                            'props' => array(
                                'grouped' => false,
                                'groupLabel' => 'Marker Number Text',
                                'fieldLabel' => 'Marker Number',
                                'dynamicSubgroupHost' => true,
                                'fields' => array(
                                    'textAlign' => array(
                                        'render' => false,
                                    ),
                                    'lineHeight' => array(
                                        'render' => false,
                                    ),
                                    'textEffectsGroup' => array(
                                        'render' => false,
                                    ),
                                ),
                                'defaultGroupAttr' => array(
                                    'font' => array(
                                        'desktop' => array(
                                            'value' => array(
                                                'size' => '14px',
                                                'weight' => '700',
                                                'lineHeight' => '1',
                                                'color' => '#ffffff',
                                            ),
                                        ),
                                    ),
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    );
}

function filter_timeline_output($block_content, $parsed_block, $block)
{
    if (!is_divi5_active() || !is_string($block_content) || !is_array($parsed_block)) {
        return $block_content;
    }

    $attrs = isset($parsed_block['attrs']) && is_array($parsed_block['attrs']) ? $parsed_block['attrs'] : array();
    $configs = get_number_breakpoint_configs($attrs, get_breakpoint_settings());

    if (!has_active_number_config($configs)) {
        return $block_content;
    }

    return add_class_to_first_tag_with_class($block_content, 'et_pb_timeline', 'dbtl-marker-numbers');
}

function filter_timeline_item_output($block_content, $parsed_block, $block)
{
    if (!is_divi5_active() || !is_string($block_content) || !is_array($parsed_block)) {
        return $block_content;
    }

    $attrs = isset($parsed_block['attrs']) && is_array($parsed_block['attrs']) ? $parsed_block['attrs'] : array();
    if (!timeline_item_has_custom_marker_content($attrs, $block_content)) {
        return $block_content;
    }

    return add_class_to_first_tag_with_class($block_content, 'et_pb_timeline_item', 'dbtl-marker-has-custom-content');
}

function add_font_styles($module_wrapper, $args)
{
    if (!is_array($args) || ($args['name'] ?? '') !== TIMELINE_SLUG) {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    $module_attrs = isset($args['attrs']) && is_array($args['attrs']) ? $args['attrs'] : array();
    $breakpoint_settings = get_breakpoint_settings();
    $configs = get_number_breakpoint_configs($module_attrs, $breakpoint_settings);
    if (!has_active_number_config($configs)) {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    $module_elements = isset($args['elements']) ? $args['elements'] : null;
    if (!is_object($module_elements) || !is_callable(array($module_elements, 'style'))) {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    if (!is_callable('\\ET\\Builder\\FrontEnd\\Module\\Style::add')) {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    $order_class = get_module_elements_order_class($module_elements);
    $order_selector = normalize_order_selector($order_class);
    if ($order_selector === '') {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    $styles = array();
    $number_style_items = build_number_style_items($order_class, $configs, $module_attrs, $breakpoint_settings);
    if (!empty($number_style_items)) {
        $styles[] = $number_style_items;
    }

    $font_attr = get_nested_array_value($module_attrs, array(FONT_ATTR_NAME, 'decoration', 'font'));
    $text_shadow_attr = get_nested_array_value($module_attrs, array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow'));
    if (!is_array($text_shadow_attr)) {
        $text_shadow_attr = array();
    }

    if (is_array($font_attr)) {
        $selector = $order_selector . ' .et_pb_timeline_item:not(.dbtl-marker-has-custom-content) .et_pb_timeline_marker::before';
        $font_style = get_module_elements_style(
            $module_elements,
            array(
                'attrName' => 'module',
                'styleProps' => array(
                    'advancedStyles' => array(
                        array(
                            'componentName' => 'divi/font',
                            'props' => array(
                                'selector' => $selector,
                                'attr' => $font_attr,
                            ),
                        ),
                        array(
                            'componentName' => 'divi/text-shadow',
                            'props' => array(
                                'selector' => $selector,
                                'attr' => $text_shadow_attr,
                            ),
                        ),
                    ),
                ),
            )
        );
        if (!empty($font_style)) {
            $styles[] = $font_style;
        }
    }

    if (empty($styles)) {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    $style_payload = array(
        'id' => get_scalar_arg($args, 'id', ''),
        'name' => TIMELINE_SLUG,
        'orderIndex' => get_scalar_arg($args, 'orderIndex', 0),
        'storeInstance' => get_scalar_arg($args, 'storeInstance', 0),
        'styles' => $styles,
    );

    try {
        \ET\Builder\FrontEnd\Module\Style::add($style_payload);
    } catch (\Throwable $e) {
        return maybe_et_core_esc_previously($module_wrapper);
    }

    return maybe_et_core_esc_previously($module_wrapper);
}

function get_module_elements_order_class($module_elements)
{
    if (!is_object($module_elements)) {
        return '';
    }

    try {
        $order_class = isset($module_elements->order_class) ? $module_elements->order_class : '';
    } catch (\Throwable $e) {
        return '';
    }

    return is_scalar($order_class) ? (string) $order_class : '';
}

function get_module_elements_style($module_elements, $style_args)
{
    if (!is_object($module_elements) || !is_callable(array($module_elements, 'style'))) {
        return array();
    }

    try {
        $style = $module_elements->style($style_args);
    } catch (\Throwable $e) {
        return array();
    }

    return is_array($style) ? $style : array();
}

function get_scalar_arg($args, $key, $fallback)
{
    if (!is_array($args) || !array_key_exists($key, $args) || !is_scalar($args[$key])) {
        return $fallback;
    }

    return $args[$key];
}

function get_number_breakpoint_configs($attrs, $breakpoint_settings = null)
{
    $configs = array();

    foreach (get_number_breakpoint_names($attrs, $breakpoint_settings) as $breakpoint) {
        $configs[$breakpoint] = get_number_config($attrs, $breakpoint);
    }

    return $configs;
}

function get_number_config($attrs, $breakpoint = 'desktop')
{
    $enabled = is_truthy(get_attr_sub_value($attrs, ATTR_NAME, 'enabled', $breakpoint));
    $start = sanitize_integer(get_attr_sub_value($attrs, ATTR_NAME, 'start', $breakpoint), DEFAULT_START);
    $step = abs(sanitize_integer(get_attr_sub_value($attrs, ATTR_NAME, 'step', $breakpoint), DEFAULT_STEP));
    $step = $step > 0 ? $step : DEFAULT_STEP;
    $direction = sanitize_direction(get_attr_sub_value($attrs, ATTR_NAME, 'direction', $breakpoint));
    $increment = $direction === 'descending' ? -$step : $step;

    return array(
        'active' => $enabled,
        'start' => $start,
        'step' => $step,
        'direction' => $direction,
        'increment' => $increment,
        'reset' => $start - $increment,
    );
}

function get_number_breakpoint_names($attrs, $breakpoint_settings = null)
{
    $names = array_keys(is_array($breakpoint_settings) ? $breakpoint_settings : get_breakpoint_settings());
    $responsive_attrs = array(
        is_array($attrs) && isset($attrs[ATTR_NAME]) ? $attrs[ATTR_NAME] : array(),
    );

    foreach ($responsive_attrs as $attr) {
        if (!is_array($attr)) {
            continue;
        }
        foreach ($attr as $key => $value) {
            if (is_array($value) && isset($value['value']) && !in_array($key, $names, true)) {
                $names[] = $key;
            }
        }
    }

    return in_array('desktop', $names, true) ? $names : array_merge(array('desktop'), $names);
}

function has_active_number_config($configs)
{
    if (!is_array($configs)) {
        return false;
    }

    foreach ($configs as $config) {
        if (is_array($config) && !empty($config['active'])) {
            return true;
        }
    }

    return false;
}

function build_number_style_items($order_class, $configs, $attrs, $breakpoint_settings = null)
{
    if (!is_array($configs)) {
        return array();
    }

    $order_selector = normalize_order_selector($order_class);
    if ($order_selector === '') {
        return array();
    }

    $base_selector = $order_selector . '.dbtl-marker-numbers';
    $style_items = array();
    $desktop_config = isset($configs['desktop']) && is_array($configs['desktop']) ? $configs['desktop'] : reset($configs);
    if (!empty($desktop_config['active'])) {
        $style_items = array_merge(
            $style_items,
            build_number_style_item_rules($base_selector, $desktop_config, get_font_css_from_attrs($attrs), false)
        );
    }

    foreach ((is_array($breakpoint_settings) ? $breakpoint_settings : get_breakpoint_settings()) as $breakpoint => $settings) {
        if ($breakpoint === 'desktop' || empty($configs[$breakpoint]['active'])) {
            continue;
        }
        $at_rules = breakpoint_style_at_rules($settings);
        if ($at_rules === false) {
            continue;
        }
        $style_items = array_merge(
            $style_items,
            build_number_style_item_rules($base_selector, $configs[$breakpoint], get_font_css_from_attrs($attrs, $breakpoint), $at_rules)
        );
    }

    return $style_items;
}

function build_number_style_item_rules($base_selector, $config, $font_css, $at_rules)
{
    if (!is_array($config)) {
        return array();
    }

    $reset = isset($config['reset']) ? (int) $config['reset'] : 0;
    $increment = isset($config['increment']) ? (int) $config['increment'] : 1;
    $font_css = is_string($font_css) ? $font_css : '';

    return array(
        number_style_item($at_rules, $base_selector . ' .et_pb_timeline_track', 'counter-reset:dbtl-timeline-marker-number ' . $reset . ';'),
        number_style_item($at_rules, $base_selector . ' .et_pb_timeline_item', 'counter-increment:dbtl-timeline-marker-number ' . $increment . ';'),
        number_style_item(
            $at_rules,
            $base_selector . ' .et_pb_timeline_item:not(.dbtl-marker-has-custom-content) .et_pb_timeline_marker::before',
            'align-items:center;color:#fff;content:counter(dbtl-timeline-marker-number);content:counter(dbtl-timeline-marker-number)/"";display:flex;font-size:14px;font-weight:700;height:100%;justify-content:center;line-height:1;pointer-events:none;width:100%;' . $font_css
        ),
        number_style_item($at_rules, $base_selector . ' .et_pb_timeline_item.dbtl-marker-has-custom-content .et_pb_timeline_marker::before', 'content:none;'),
        number_style_item($at_rules, $base_selector . ' .et_pb_timeline_item:has(.et_pb_timeline_marker .et-pb-icon) .et_pb_timeline_marker::before', 'content:none;'),
    );
}

function number_style_item($at_rules, $selector, $declaration)
{
    return array(
        'atRules' => $at_rules,
        'selector' => $selector,
        'declaration' => $declaration,
    );
}

function breakpoint_style_at_rules($settings)
{
    $media = breakpoint_media_query($settings);
    return $media === '' ? false : '@media only screen and ' . $media;
}

function timeline_item_has_custom_marker_content($attrs, $html)
{
    if (is_string($html) && preg_match('/<[^>]*class=["\'][^"\']*\\bet_pb_timeline_marker\\b[^"\']*["\'][^>]*>.*?\\bet-pb-icon\\b/s', $html)) {
        return true;
    }

    return attr_has_value(get_nested_array_value($attrs, array('marker', 'innerContent')));
}

function attr_has_value($value)
{
    if (is_array($value)) {
        foreach ($value as $nested_value) {
            if (attr_has_value($nested_value)) {
                return true;
            }
        }
        return false;
    }

    if (is_bool($value)) {
        return $value;
    }

    if (is_numeric($value)) {
        return true;
    }

    return is_string($value) && trim($value) !== '' && strpos(trim($value), '$variable(') !== 0;
}

function get_breakpoint_settings()
{
    $class = '\\ET\\Builder\\Framework\\Breakpoint\\Breakpoint';
    if (class_exists($class) && is_callable(array($class, 'get_style_breakpoint_settings'))) {
        try {
            $settings = call_user_func(array($class, 'get_style_breakpoint_settings'));
        } catch (\Throwable $e) {
            $settings = array();
        }

        if (is_array($settings) && !empty($settings)) {
            return $settings;
        }
    }

    return array(
        'phone' => array(
            'order' => 10,
            'maxWidth' => array('value' => '767px'),
        ),
        'tablet' => array(
            'order' => 30,
            'minWidth' => array('value' => '768px'),
            'maxWidth' => array('value' => '980px'),
        ),
        'desktop' => array(
            'order' => 50,
            'baseDevice' => true,
            'minWidth' => array('value' => '981px'),
        ),
    );
}

function breakpoint_media_query($settings)
{
    if (!is_array($settings)) {
        return '';
    }

    $parts = array();
    $min = sanitize_css_length(get_nested_array_value($settings, array('minWidth', 'value')), '');
    $max = sanitize_css_length(get_nested_array_value($settings, array('maxWidth', 'value')), '');

    if ($min !== '') {
        $parts[] = '(min-width:' . $min . ')';
    }
    if ($max !== '') {
        $parts[] = '(max-width:' . $max . ')';
    }

    return implode(' and ', $parts);
}

function get_nested_array_value($array, $path)
{
    $value = $array;

    foreach ($path as $key) {
        if (!is_array($value) || !array_key_exists($key, $value)) {
            return null;
        }
        $value = $value[$key];
    }

    return $value;
}

function get_attr_sub_value($attrs, $attr_name, $sub_name, $breakpoint = 'desktop')
{
    if (!is_array($attrs)) {
        return '';
    }

    if (!isset($attrs[$attr_name]) || !is_array($attrs[$attr_name])) {
        return '';
    }

    return get_responsive_attr_sub_value($attrs[$attr_name], $sub_name, $breakpoint);
}

function get_responsive_attr_sub_value($attr, $sub_name, $breakpoint = 'desktop', $fallback = '')
{
    if (!is_array($attr)) {
        return $fallback;
    }

    $paths = array(
        array($breakpoint, 'value', $sub_name),
        array('desktop', 'value', $sub_name),
        array('value', $sub_name),
        array($sub_name, $breakpoint, 'value'),
        array($sub_name, 'desktop', 'value'),
        array($sub_name, 'value'),
        array($sub_name),
    );

    foreach ($paths as $path) {
        $value = get_nested_array_value($attr, $path);
        if (is_scalar($value) && trim((string) $value) !== '') {
            return (string) $value;
        }
        if (is_array($value) && !empty($value)) {
            return $value;
        }
    }

    return $fallback;
}

function sanitize_integer($value, $fallback)
{
    if (is_int($value)) {
        return $value;
    }

    if (is_numeric($value)) {
        return (int) $value;
    }

    if (is_string($value) && preg_match('/^-?\\d+/', trim($value), $matches)) {
        return (int) $matches[0];
    }

    return (int) $fallback;
}

function sanitize_direction($value)
{
    $value = is_string($value) ? trim($value) : '';
    return in_array($value, array('ascending', 'descending'), true) ? $value : DEFAULT_DIRECTION;
}

function is_truthy($value)
{
    return in_array($value, array(true, 1, '1', 'on', 'yes', 'true'), true);
}

function sanitize_css_color($value)
{
    $value = is_string($value) ? trim($value) : '';
    if ($value === '') {
        return '';
    }

    if (preg_match('/^[0-9a-f]{3,8}$/i', $value)) {
        $value = '#' . $value;
    }

    if (function_exists('safecss_filter_attr')) {
        $filtered = \safecss_filter_attr('color:' . $value);
        if (preg_match('/(?:^|;)\\s*color\\s*:\\s*([^;]+)\\s*;?$/i', $filtered, $matches)) {
            return trim($matches[1]);
        }
        return '';
    }

    return preg_match('/^(#[0-9a-f]{3,8}|rgba?\\([0-9.,%\\s]+\\)|hsla?\\([0-9.,%\\s]+\\)|[a-z]+)$/i', $value) ? $value : '';
}

function sanitize_css_length($value, $fallback)
{
    $value = is_scalar($value) ? trim((string) $value) : '';
    if ($value === '') {
        return $fallback;
    }

    if (is_numeric($value)) {
        return $value . 'px';
    }

    return preg_match('/^-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem|vw|vh|%)$/i', $value) ? $value : $fallback;
}

function sanitize_css_text_shadow($value)
{
    $value = is_string($value) ? trim($value) : '';

    return preg_match('/^-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?\\s+-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?(?:\\s+-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?)?(?:\\s+(?:#[0-9a-f]{3,8}|rgba?\\([0-9.,%\\s]+\\)|hsla?\\([0-9.,%\\s]+\\)|[a-z]+))?$/i', $value) ? $value : '';
}

function sanitize_css_text_shadow_declaration($declaration, $fallback_color = '')
{
    if (!is_string($declaration) || !preg_match('/(?:^|;)\\s*text-shadow\\s*:\\s*([^;]+)\\s*;?/i', $declaration, $matches)) {
        return '';
    }

    $value = preg_replace('/\\s*!important\\s*$/i', '', trim($matches[1]));
    $value = sanitize_css_text_shadow($value);
    $fallback_color = sanitize_css_color($fallback_color);

    if ($value !== '' && $fallback_color !== '' && !css_text_shadow_has_color($value)) {
        $value .= ' ' . $fallback_color;
    }

    return $value !== '' ? 'text-shadow:' . $value . ';' : '';
}

function css_text_shadow_has_color($value)
{
    return is_string($value) && preg_match('/^-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?\\s+-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?(?:\\s+-?(?:\\d+|\\d*\\.\\d+)(?:px|em|rem)?)?\\s+(?:#[0-9a-f]{3,8}|rgba?\\([0-9.,%\\s]+\\)|hsla?\\([0-9.,%\\s]+\\)|[a-z]+)$/i', trim($value));
}

function get_font_css_from_attrs($attrs, $breakpoint = 'desktop')
{
    $font_value = get_font_value($attrs, $breakpoint);
    if (!is_array($font_value)) {
        return '';
    }

    $css = '';
    $color = sanitize_css_color(get_first_font_scalar($font_value, array('color')));
    $size = sanitize_css_length(get_first_font_scalar($font_value, array('size', 'fontSize')), '');
    $weight = get_first_font_scalar($font_value, array('weight', 'fontWeight'));
    $family = get_first_font_scalar($font_value, array('family', 'fontFamily'));
    $style = get_first_font_scalar($font_value, array('style', 'fontStyle'));
    $text_decoration_line = get_text_decoration_line_from_font_style($style);
    $letter_spacing = sanitize_css_length(get_first_font_scalar($font_value, array('letterSpacing')), '');
    $text_shadow = get_text_shadow_declaration_from_attrs($attrs, $breakpoint);

    if ($color !== '') {
        $css .= 'color:' . $color . ';';
    }
    if ($size !== '') {
        $css .= 'font-size:' . $size . ';';
    }
    if (is_string($weight) && preg_match('/^(normal|bold|[1-9]00)$/', trim($weight))) {
        $css .= 'font-weight:' . trim($weight) . ';';
    }
    if (is_string($family) && trim($family) !== '' && strpos($family, '|') === false && strpos($family, '$variable(') !== 0) {
        $css .= 'font-family:"' . css_string(trim($family)) . '";';
    }
    if (is_string($style) && preg_match('/\\bitalic\\b/i', $style)) {
        $css .= 'font-style:italic;';
    }
    if ($text_decoration_line !== '') {
        $css .= 'text-decoration-line:' . $text_decoration_line . ';';
    }
    if ($letter_spacing !== '') {
        $css .= 'letter-spacing:' . $letter_spacing . ';';
    }
    $css .= $text_shadow;

    return $css;
}

function get_text_decoration_line_from_font_style($style)
{
    $style = is_string($style) ? $style : '';
    $lines = array();

    if (preg_match('/\\bunderline\\b/i', $style)) {
        $lines[] = 'underline';
    }
    if (preg_match('/\\b(?:strike|strikethrough|line-through)\\b/i', $style)) {
        $lines[] = 'line-through';
    }

    return implode(' ', $lines);
}

function get_font_value($attrs, $breakpoint = 'desktop')
{
    $paths = array(
        array(FONT_ATTR_NAME, 'decoration', 'font', 'font', $breakpoint, 'value'),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'font', 'desktop', 'value'),
        array(FONT_ATTR_NAME, 'decoration', 'font', $breakpoint, 'value'),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'desktop', 'value'),
        array(FONT_ATTR_NAME, 'font', $breakpoint, 'value'),
        array(FONT_ATTR_NAME, 'font', 'desktop', 'value'),
    );

    foreach ($paths as $path) {
        $value = get_nested_array_value($attrs, $path);
        if (is_array($value) && !empty($value)) {
            return $value;
        }
    }

    $font_attr = get_nested_array_value($attrs, array(FONT_ATTR_NAME, 'decoration', 'font'));
    return is_array($font_attr) ? $font_attr : array();
}

function get_text_shadow_declaration_from_attrs($attrs, $breakpoint = 'desktop')
{
    $value = get_text_shadow_attr_value($attrs, $breakpoint);

    if (!is_array($value) || empty($value)) {
        return '';
    }

    $declaration = get_divi_text_shadow_declaration($value);
    if ($declaration !== '') {
        return $declaration;
    }

    $fallback = sanitize_css_text_shadow(build_text_shadow($value));
    return $fallback !== '' ? 'text-shadow:' . $fallback . ';' : '';
}

function get_text_shadow_attr_value($attrs, $breakpoint = 'desktop')
{
    $paths = array(
        array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow', $breakpoint, 'value'),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow', $breakpoint),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow', 'desktop', 'value'),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow', 'desktop'),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow', 'value'),
        array(FONT_ATTR_NAME, 'decoration', 'font', 'textShadow'),
        array(FONT_ATTR_NAME, 'textShadow', $breakpoint, 'value'),
        array(FONT_ATTR_NAME, 'textShadow', $breakpoint),
        array(FONT_ATTR_NAME, 'textShadow', 'desktop', 'value'),
    );

    foreach ($paths as $path) {
        $value = get_nested_array_value($attrs, $path);
        if (is_array($value) && !empty($value)) {
            return $value;
        }
    }

    return array();
}

function get_divi_text_shadow_declaration($value)
{
    $class = '\\ET\\Builder\\Packages\\StyleLibrary\\Declarations\\TextShadow\\TextShadow';
    if (!is_callable(array($class, 'style_declaration'))) {
        return '';
    }

    try {
        $declaration = call_user_func(
            array($class, 'style_declaration'),
            array(
                'attrValue' => $value,
                'important' => false,
                'returnType' => 'string',
            )
        );
    } catch (\Throwable $e) {
        return '';
    }

    return sanitize_css_text_shadow_declaration($declaration, get_text_shadow_color($value, true));
}

function build_text_shadow($value)
{
    if (!is_array($value)) {
        return '';
    }

    $shadow_style = isset($value['style']) && is_scalar($value['style']) ? strtolower(trim((string) $value['style'])) : '';
    $horizontal_raw = get_first_font_scalar($value, array('horizontal', 'horizontalLength', 'horizontal_length', 'x'));
    $vertical_raw = get_first_font_scalar($value, array('vertical', 'verticalLength', 'vertical_length', 'y'));
    $blur_raw = get_first_font_scalar($value, array('blur', 'blurStrength', 'blur_strength', 'blur-strength'));
    $color = get_text_shadow_color($value);
    $has_shadow_value = $shadow_style !== '' || $horizontal_raw !== '' || $vertical_raw !== '' || $blur_raw !== '' || $color !== '';
    $horizontal = sanitize_css_length($horizontal_raw, '0em');
    $vertical = sanitize_css_length($vertical_raw, '0em');
    $blur = sanitize_css_length($blur_raw, '0em');

    if ($shadow_style === 'none' || !$has_shadow_value) {
        return '';
    }

    $color = $color !== '' ? $color : DEFAULT_TEXT_SHADOW_COLOR;

    return trim($horizontal . ' ' . $vertical . ' ' . $blur . ($color !== '' ? ' ' . $color : ''));
}

function get_text_shadow_color($value, $default_when_missing = false)
{
    $config = get_text_shadow_color_config($value);
    $color = sanitize_css_color($config['color']);
    $opacity = parse_css_opacity($config['opacity']);

    if ($color === '') {
        return $default_when_missing ? DEFAULT_TEXT_SHADOW_COLOR : '';
    }

    if ($opacity === null && preg_match('/^#[0-9a-f]{6}$/i', $color)) {
        $opacity = 0.4;
    }

    if ($opacity !== null && preg_match('/^#[0-9a-f]{6}$/i', $color)) {
        return hex_color_to_rgba($color, $opacity);
    }

    return $color;
}

function get_text_shadow_color_config($value)
{
    $config = array(
        'color' => '',
        'opacity' => null,
    );

    collect_text_shadow_color_config($value, $config);
    return $config;
}

function collect_text_shadow_color_config($value, &$config, $allow_value_color = false)
{
    if (!is_array($value)) {
        return;
    }

    foreach (array('color', 'hex', 'textShadowColor', 'shadowColor', 'text-shadow-color') as $key) {
        if ($config['color'] === '' && isset($value[$key]) && is_css_color_candidate($value[$key])) {
            $config['color'] = trim((string) $value[$key]);
        }
        if ($config['color'] === '' && isset($value[$key]) && is_array($value[$key])) {
            collect_text_shadow_color_config($value[$key], $config, true);
        }
    }

    if ($config['color'] === '' && $allow_value_color && isset($value['value']) && is_css_color_candidate($value['value'])) {
        $config['color'] = trim((string) $value['value']);
    }

    foreach (array('opacity', 'alpha') as $key) {
        if ($config['opacity'] === null && isset($value[$key]) && is_scalar($value[$key]) && trim((string) $value[$key]) !== '') {
            $config['opacity'] = trim((string) $value[$key]);
        }
    }

    if ($config['color'] !== '' && $config['opacity'] !== null) {
        return;
    }

    foreach ($value as $nested_value) {
        if (is_array($nested_value)) {
            collect_text_shadow_color_config($nested_value, $config, $allow_value_color);
            if ($config['color'] !== '' && $config['opacity'] !== null) {
                return;
            }
        }
    }
}

function is_css_color_candidate($value)
{
    return is_scalar($value) && sanitize_css_color((string) $value) !== '';
}

function parse_css_opacity($value)
{
    if (!is_scalar($value) || trim((string) $value) === '') {
        return null;
    }

    $opacity = (float) $value;
    if (!is_finite($opacity)) {
        return null;
    }

    if ($opacity > 1) {
        $opacity = $opacity / 100;
    }

    return max(0, min(1, $opacity));
}

function hex_color_to_rgba($color, $opacity)
{
    $hex = ltrim((string) $color, '#');
    if (!preg_match('/^[0-9a-f]{6}$/i', $hex)) {
        return '';
    }

    return sprintf(
        'rgba(%d,%d,%d,%s)',
        hexdec(substr($hex, 0, 2)),
        hexdec(substr($hex, 2, 2)),
        hexdec(substr($hex, 4, 2)),
        rtrim(rtrim(sprintf('%.3F', (float) $opacity), '0'), '.')
    );
}

function get_first_font_scalar($value, $keys)
{
    if (!is_array($value)) {
        return '';
    }

    foreach ($keys as $key) {
        if (array_key_exists($key, $value) && is_scalar($value[$key]) && trim((string) $value[$key]) !== '') {
            return trim((string) $value[$key]);
        }
    }

    foreach ($value as $nested_value) {
        if (is_array($nested_value)) {
            $result = get_first_font_scalar($nested_value, $keys);
            if ($result !== '') {
                return $result;
            }
        }
    }

    return '';
}

function add_class_to_first_tag_with_class($html, $required_class, $class_to_add)
{
    if (!is_string($html) || $html === '' || !is_string($required_class) || $required_class === '' || !is_string($class_to_add) || $class_to_add === '') {
        return $html;
    }

    if (class_exists('\\WP_HTML_Tag_Processor')) {
        try {
            $processor = new \WP_HTML_Tag_Processor($html);
            if ($processor->next_tag(array('class_name' => $required_class))) {
                $processor->add_class($class_to_add);
                $updated_html = $processor->get_updated_html();
                return is_string($updated_html) ? $updated_html : $html;
            }
        } catch (\Throwable $e) {
            return $html;
        }
    }

    $tag_pattern = '~<([a-z0-9]+)\\b([^>]*\\bclass=([\\"\'])(?=[^\\"\']*\\b' . preg_quote($required_class, '~') . '\\b)([^\\"\']*)\\3[^>]*)>~i';
    $class_attr_pattern = '~\\bclass=([\\"\'])([^\\"\']*)\\1~i';

    $updated_html = preg_replace_callback(
        $tag_pattern,
        function ($matches) use ($class_to_add, $class_attr_pattern) {
            if (preg_match('/\\b' . preg_quote($class_to_add, '/') . '\\b/', $matches[4])) {
                return $matches[0];
            }
            $updated_tag = preg_replace($class_attr_pattern, 'class=$1$2 ' . escape_html_attr($class_to_add) . '$1', $matches[0], 1);
            return is_string($updated_tag) ? $updated_tag : $matches[0];
        },
        $html,
        1
    );

    return is_string($updated_html) ? $updated_html : $html;
}

function escape_html_attr($value)
{
    if (function_exists('esc_attr')) {
        return \esc_attr($value);
    }

    return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}

function extract_order_class($html)
{
    if (!is_string($html) || $html === '') {
        return '';
    }

    return preg_match('/\\bet_pb_timeline_\\d+(?:_tb_[a-z]+)?\\b/', $html, $matches) ? $matches[0] : '';
}

function normalize_order_selector($order_class)
{
    if (!is_string($order_class)) {
        return '';
    }

    $order_class = trim($order_class);
    if ($order_class === '') {
        return '';
    }

    $order_class = $order_class[0] === '.' ? substr($order_class, 1) : $order_class;

    return preg_match('/^[a-z0-9_-]+$/i', $order_class) ? '.' . $order_class : '';
}

function css_string($value)
{
    return str_replace(
        array('\\', '"', "'", "\n", "\r", "\f"),
        array('\\\\', '\\"', "\\'", '\\a ', '\\d ', '\\c '),
        (string) $value
    );
}

function maybe_et_core_esc_previously($html)
{
    if (function_exists('et_core_esc_previously')) {
        try {
            return \et_core_esc_previously($html);
        } catch (\Throwable $e) {
            return $html;
        }
    }

    return $html;
}
