<?php
if (!defined('ABSPATH')) exit;

/**
 * Input-Sanitisierung + Label-Resolver fuer alle Layout-spezifischen Attribute.
 */

/**
 * Bar-Row-Link: nur http/https, trim, esc_url_raw + wp_http_validate_url.
 *
 * @param mixed $raw Raw link value from block attributes.
 */
function ce_pd_sanitize_bar_link_url($raw) {
    $url = trim((string) $raw);
    if ($url === '') {
        return '';
    }
    $url = esc_url_raw($url, ['http', 'https']);
    if ($url === '') {
        return '';
    }
    if (!wp_http_validate_url($url)) {
        return '';
    }
    return $url;
}

function ce_pd_sanitize_bars($bars) {
    if (!is_array($bars)) return [];
    return array_map(function ($bar) {
        $b = is_array($bar) ? $bar : [];
        $gk = isset($b['groupKey']) ? sanitize_text_field((string) $b['groupKey']) : '';
        if (function_exists('mb_strlen') && mb_strlen($gk) > 48) {
            $gk = mb_substr($gk, 0, 48);
        } elseif (strlen($gk) > 48) {
            $gk = substr($gk, 0, 48);
        }
        $out = [
            'value'      => isset($b['value']) ? (float) $b['value'] : 0,
            'unit'       => isset($b['unit']) ? sanitize_text_field((string) $b['unit']) : '',
            'headline'   => isset($b['headline']) ? sanitize_text_field((string) $b['headline']) : '',
            'paragraph'  => isset($b['paragraph']) ? sanitize_textarea_field((string) $b['paragraph']) : '',
            'smallPrint' => isset($b['smallPrint']) ? sanitize_textarea_field((string) $b['smallPrint']) : '',
            'groupKey'   => $gk,
            'color'      => isset($b['color']) && $b['color'] ? sanitize_hex_color((string) $b['color']) : '',
            'link'       => ce_pd_sanitize_bar_link_url($b['link'] ?? ''),
            'linkNewTab' => !empty($b['linkNewTab']),
        ];
        if (isset($b['annotation']) && is_string($b['annotation']) && $b['annotation'] !== '') {
            $out['annotation'] = sanitize_text_field($b['annotation']);
        }
        if (isset($b['annotationDesc']) && is_string($b['annotationDesc']) && $b['annotationDesc'] !== '') {
            $out['annotationDesc'] = sanitize_text_field($b['annotationDesc']);
        }
        /**
         * Erweiterungs-Hook: Add-ons können zusätzliche, von ihnen registrierte Bar-Row-Felder
         * sanitisiert anhaengen (z. B. eigene Layout-Koordinaten oder Annotation-Felder).
         *
         * @param array $out Sanitisiertes Bar-Row-Array (nur Free-Felder).
         * @param array $b   Roh-Eingabe vom Block.
         */
        $out = apply_filters('ce_pd_sanitize_bar_row', $out, $b);
        return is_array($out) ? $out : [];
    }, $bars);
}

/**
 * Legacy: free-form CSS height (older block saves).
 */
function ce_pd_sanitize_diagram_max_height($raw) {
    $s = trim((string) $raw);
    if ($s === '') {
        return '';
    }
    if (strlen($s) > 64) {
        return '';
    }
    if (! preg_match('/^[-+0-9().%,a-zA-Z\s,]+$/', $s)) {
        return '';
    }
    if (preg_match('/url\s*\(/i', $s) || preg_match('/expression\s*\(/i', $s)) {
        return '';
    }
    return $s;
}

function ce_pd_sanitize_diagram_max_height_breakpoint_entry($entry) {
    $defaults = ['value' => '', 'unit' => 'px'];
    if (!is_array($entry)) {
        return $defaults;
    }
    $unit = isset($entry['unit']) ? (string) $entry['unit'] : 'px';
    if (!in_array($unit, ['px', 'em', '%'], true)) {
        $unit = 'px';
    }
    $value = isset($entry['value']) ? trim((string) $entry['value']) : '';
    if ($value === '') {
        return ['value' => '', 'unit' => $unit];
    }
    if (!preg_match('/^\d+(\.\d{1,4})?$/', $value)) {
        return ['value' => '', 'unit' => $unit];
    }
    $f = (float) $value;
    if ($f < 0 || $f > 9999) {
        return ['value' => '', 'unit' => $unit];
    }
    return ['value' => $value, 'unit' => $unit];
}

function ce_pd_sanitize_axis_x_label($raw) {
    $s = sanitize_text_field(is_string($raw) ? $raw : '');
    if (function_exists('mb_strlen') && mb_strlen($s) > 48) {
        return mb_substr($s, 0, 48);
    }
    if (strlen($s) > 48) {
        return substr($s, 0, 48);
    }
    return $s;
}

/**
 * Hex color for backgrounds / floors — invalid input falls back to default.
 *
 * @param mixed  $raw      Raw attribute value.
 * @param string $default  Must be a valid hex (e.g. theme default).
 */
function ce_pd_sanitize_hex_color_default($raw, $default) {
    $san = sanitize_hex_color(is_string($raw) ? $raw : '');
    return (is_string($san) && $san !== '') ? $san : $default;
}

/**
 * Background mode for the 3D scene (must match editor AppearancePanel + scene/background.js).
 *
 * @param mixed $raw Raw backgroundType attribute.
 */
function ce_pd_sanitize_background_type($raw) {
    $s = is_string($raw) ? $raw : '';
    return in_array($s, ['grid', 'gradient', 'gridReflective'], true) ? $s : 'grid';
}

/**
 * Prefix/suffix characters on the Y axis (currency, %, etc.) — short plaintext only.
 *
 * @param mixed $raw Raw string.
 */
function ce_pd_sanitize_axis_y_unit_fragment($raw) {
    $s = sanitize_text_field(is_string($raw) ? $raw : '');
    if (function_exists('mb_strlen') && mb_strlen($s) > 16) {
        return mb_substr($s, 0, 16);
    }
    if (strlen($s) > 16) {
        return substr($s, 0, 16);
    }
    return $s;
}

/**
 * axisY block attribute: whitelist keys and numeric range (no arbitrary object passthrough).
 *
 * @param mixed $raw Raw axisY attribute.
 * @return array<string, mixed>
 */
function ce_pd_sanitize_axis_y($raw) {
    $defaults_new = [
        'mode'       => 'numeric',
        'min'        => 0.0,
        'max'        => 100.0,
        'unitPrefix' => '',
        'unitSuffix' => '%',
    ];
    if (!is_array($raw)) {
        return $defaults_new;
    }
    $mode = (isset($raw['mode']) && $raw['mode'] === 'numeric') ? 'numeric' : 'numeric';

    $min = isset($raw['min']) ? (float) $raw['min'] : 0.0;
    $max = isset($raw['max']) ? (float) $raw['max'] : 100.0;
    if (!is_finite($min)) {
        $min = 0.0;
    }
    if (!is_finite($max)) {
        $max = 100.0;
    }
    $min = max(-1e9, min(1e9, $min));
    $max = max(-1e9, min(1e9, $max));
    if ($max <= $min) {
        $max = min(1e9, $min + 1.0);
    }

    $has_new = array_key_exists('unitPrefix', $raw) || array_key_exists('unitSuffix', $raw);
    if ($has_new) {
        return [
            'mode'       => $mode,
            'min'        => $min,
            'max'        => $max,
            'unitPrefix' => ce_pd_sanitize_axis_y_unit_fragment($raw['unitPrefix'] ?? ''),
            'unitSuffix' => ce_pd_sanitize_axis_y_unit_fragment(array_key_exists('unitSuffix', $raw) ? $raw['unitSuffix'] : '%'),
        ];
    }

    $unit = ce_pd_sanitize_axis_y_unit_fragment(isset($raw['unit']) ? $raw['unit'] : '%');
    $pos_raw = isset($raw['unitPosition']) ? (string) $raw['unitPosition'] : '';
    $unit_position = ($pos_raw === 'before') ? 'before' : 'after';

    return [
        'mode'          => $mode,
        'min'           => $min,
        'max'           => $max,
        'unit'          => $unit,
        'unitPosition'  => $unit_position,
    ];
}

/**
 * Example formatted value (matches JS initScene axisYPrefix/Suffix) for bubble-pack legend hint.
 *
 * @param array $axis_y axisY block attribute.
 */
function ce_pd_axis_y_format_example_value($axis_y) {
    if (!is_array($axis_y)) {
        return '42%';
    }
    $has_new = array_key_exists('unitPrefix', $axis_y) || array_key_exists('unitSuffix', $axis_y);
    if ($has_new) {
        $prefix = isset($axis_y['unitPrefix']) ? (string) $axis_y['unitPrefix'] : '';
        $suffix = array_key_exists('unitSuffix', $axis_y) ? (string) $axis_y['unitSuffix'] : '%';
    } else {
        $unit   = isset($axis_y['unit']) ? (string) $axis_y['unit'] : '%';
        $before = isset($axis_y['unitPosition']) && $axis_y['unitPosition'] === 'before';
        $prefix = $before ? $unit : '';
        $suffix = $before ? '' : $unit;
    }
    return $prefix . '42' . $suffix;
}

/**
 * Straight layout: captions above/below bars. Empty strings migrate from labelsUnderBars.
 *
 * @param array $attributes Block attributes.
 * @return array{above: string, below: string}
 */
function ce_pd_resolve_straight_bar_labels($attributes) {
    if (!is_array($attributes)) {
        return ['above' => 'off', 'below' => 'off'];
    }
    $above = isset($attributes['straightBarLabelAbove']) ? (string) $attributes['straightBarLabelAbove'] : '';
    $below = isset($attributes['straightBarLabelBelow']) ? (string) $attributes['straightBarLabelBelow'] : '';
    $valid = ['off', 'value', 'headline'];
    if ($above === '' && $below === '') {
        if (!empty($attributes['labelsUnderBars'])) {
            return ['above' => 'off', 'below' => 'headline'];
        }
        return ['above' => 'off', 'below' => 'off'];
    }
    $a = in_array($above, $valid, true) ? $above : 'off';
    $b = in_array($below, $valid, true) ? $below : 'off';
    if ($a === $b && $a !== 'off') {
        $b = 'off';
    }
    return ['above' => $a, 'below' => $b];
}

/**
 * Stacked layout: show total above each stack. Defaults to false.
 *
 * @param array  $attributes Block attributes.
 * @param string $layout     Current layout slug.
 */
function ce_pd_resolve_stacked_bar_sum_above($attributes, $layout) {
    if ('stacked' !== $layout) {
        return false;
    }
    if (is_array($attributes) && array_key_exists('stackedBarSumAbove', $attributes)) {
        return !empty($attributes['stackedBarSumAbove']);
    }
    return false;
}

/**
 * Pie layout: Beschriftung in den Segmenten. Leer → value (wie früher ohne Attribut).
 *
 * @param array $attributes Block attributes.
 * @return string 'off'|'value'|'headline'|'value_callout'
 */
function ce_pd_resolve_pie_slice_label($attributes) {
    if (!is_array($attributes)) {
        return 'value';
    }
    $raw = isset($attributes['pieSliceLabel']) ? (string) $attributes['pieSliceLabel'] : '';
    $valid = ['off', 'value', 'headline', 'value_callout'];
    if ($raw === '') {
        return 'value';
    }
    return in_array($raw, $valid, true) ? $raw : 'value';
}

/**
 * Pyramid layout: Beschriftung in den Segmenten. Leer → off (Block-Default).
 *
 * @param array $attributes Block attributes.
 * @return string 'off'|'value'|'headline'
 */
function ce_pd_resolve_pyramid_slice_label($attributes) {
    if (!is_array($attributes)) {
        return 'off';
    }
    $raw = isset($attributes['pyramidSliceLabel']) ? (string) $attributes['pyramidSliceLabel'] : '';
    if ($raw === '') {
        return 'off';
    }
    if ($raw === 'value_callout') {
        return 'value';
    }
    return in_array($raw, ['off', 'value', 'headline'], true) ? $raw : 'off';
}

/**
 * Pie layout: slice angle basis (share of row sum vs. share of fixed 100).
 *
 * @param array<string,mixed> $attributes Block attributes.
 * @return string 'sum'|'hundred'
 */
function ce_pd_resolve_pie_angle_basis($attributes) {
    if (!is_array($attributes)) {
        return 'sum';
    }
    $raw = isset($attributes['pieAngleBasis']) ? (string) $attributes['pieAngleBasis'] : '';
    $valid = ['sum', 'hundred'];
    if ($raw === '') {
        return 'sum';
    }
    return in_array($raw, $valid, true) ? $raw : 'sum';
}

function ce_pd_sanitize_diagram_max_height_by_breakpoint($raw) {
    $out = [
        'desktop' => ['value' => '', 'unit' => 'px'],
        'tablet'  => ['value' => '', 'unit' => 'px'],
        'mobile'  => ['value' => '', 'unit' => 'px'],
    ];
    if (!is_array($raw)) {
        return $out;
    }
    foreach (['desktop', 'tablet', 'mobile'] as $k) {
        if (isset($raw[$k])) {
            $out[$k] = ce_pd_sanitize_diagram_max_height_breakpoint_entry($raw[$k]);
        }
    }
    return $out;
}

/**
 * Theme color palette (list of #rrggbb) for the "Theme colors" scheme.
 *
 * Reads the active theme's editor color palette via wp_get_global_settings().
 * Used by both the frontend (render.php) and the editor preview
 * (editor-assets.php) so both render the same colors in the same order.
 *
 * Design decision (intentional, NOT a hidden fallback): we iterate the
 * 'theme', 'default' and 'custom' origins and merge them (deduplicated).
 * Including the 'default' origin means a theme that defines no palette of its
 * own still yields the WordPress core default palette — otherwise the
 * "Theme colors" option would render nothing on many (classic) themes. Block
 * themes that disable the core default palette (defaultPalette: false) return
 * an empty 'default' origin, so for those only the theme's own colors are used.
 * To restrict strictly to the theme's palette, drop 'default' and 'custom' here.
 *
 * @return string[] Unique list of #rrggbb colors (may be empty).
 */
function ce_pd_get_theme_palette_hex() {
    if (!function_exists('wp_get_global_settings')) {
        return [];
    }
    $out  = [];
    $seen = [];
    foreach (['theme', 'default', 'custom'] as $origin) {
        $palette = wp_get_global_settings(['color', 'palette', $origin]);
        if (!is_array($palette)) {
            continue;
        }
        foreach ($palette as $entry) {
            if (!is_array($entry) || !isset($entry['color'])) {
                continue;
            }
            $hex = sanitize_hex_color((string) $entry['color']);
            if (!is_string($hex) || $hex === '') {
                continue;
            }
            $key = strtolower($hex);
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;
            $out[]      = $hex;
        }
    }
    return $out;
}

/**
 * WordPress locale (e.g. de_DE) → BCP 47 tag for Intl.NumberFormat in the viewer (de-DE).
 */
function ce_pd_normalize_bcp47_locale($locale) {
    if (!is_string($locale) || $locale === '') {
        return 'en-US';
    }
    return str_replace('_', '-', $locale);
}

/**
 * Wave: gradient direction under the curve/ribbon.
 *
 * @param mixed $raw Raw waveGradientDirection attribute.
 */
function ce_pd_sanitize_wave_gradient_direction($raw) {
    $valid = ['horizontal', 'vertical', 'radial', 'diagonal'];
    $s = is_string($raw) ? $raw : '';
    return in_array($s, $valid, true) ? $s : 'horizontal';
}

/**
 * Wave: baseline Y value for area fill (unbounded numeric, finite only).
 *
 * @param mixed $raw Raw waveBaselineValue attribute.
 */
function ce_pd_sanitize_wave_baseline_value($raw) {
    if (!is_numeric($raw)) {
        return 0.0;
    }
    $v = (float) $raw;
    if (!is_finite($v)) {
        return 0.0;
    }
    return max(-1e9, min(1e9, $v));
}

/**
 * Wave / bar label slot: off|value|headline (empty string → default).
 *
 * @param mixed  $raw     Raw attribute value.
 * @param array  $allowed Allowed slugs.
 * @param string $default Fallback when empty or invalid.
 */
function ce_pd_resolve_label_mode($raw, array $allowed, $default) {
    $s = is_string($raw) ? $raw : '';
    if ($s === '') {
        return $default;
    }
    return in_array($s, $allowed, true) ? $s : $default;
}

