<?php

/**
 * JCOGS Image Pro - Srcset Parameter Package
 * ===========================================
 * Parameter package for responsive image srcset functionality
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      Parameter Package Implementation
 */

namespace JCOGSDesign\JCOGSImagePro\Service\ParameterPackages;

use JCOGSDesign\JCOGSImagePro\Service\ParameterPackages\AbstractParameterPackage;
use JCOGSDesign\JCOGSImagePro\Service\ParameterRegistry;

/**
 * Srcset Parameter Package
 * 
 * Handles the complex srcset parameter for responsive image variant specification
 * with width descriptors. Supports pipe-separated width values with optional units
 * (px, %) and validates ordering for optimal browser selection.
 */
class SrcsetParameterPackage extends AbstractParameterPackage
{
    private const SRCSET_WIDTH_TOKEN_REGEX = '/^(?:\d+(?:w|px)?|\d+(?:\.\d+)?%)$/i';
    private const SRCSET_DENSITY_TOKEN_REGEX = '/^\d+(?:\.\d+)?x$/i';

    /**
     * Get the package name/identifier
     * 
     * @return string Package identifier
     */
    public function getName(): string
    {
        return 'srcset_parameter_package';
    }

    /**
     * Get the display label for this parameter package
     * 
     * @return string Human-readable package name
     */
    public function getLabel(): string
    {
        return 'Responsive Images (srcset)';
    }

    /**
     * Get package description
     * 
     * @return string Package description
     */
    public function getDescription(): string
    {
        return 'Create responsive variants using width descriptors (w) or density descriptors (x)';
    }

    /**
     * Get the category for this parameter package
     * 
     * @return string Package category
     */
    public function getCategory(): string
    {
        return 'control';
    }

    /**
     * Get the parameters this package handles
     * 
     * @return array List of parameter names handled by this package
     */
    public function getParameters(): array
    {
        return ['srcset'];
    }

    /**
     * Get the priority for this package
     * 
     * Higher numbers indicate higher priority. Set to 15 to have higher 
     * priority than ControlParameterPackage (10) for specialized handling.
     * 
     * @return int Priority level (higher = more priority)
     */
    public function getPriority(): int
    {
        return 15;
    }

    /**
     * Get the form fields for this package
     * 
     * @param array $existingValues Current parameter values for form population
     * @return array Form field definitions for EE control panel
     */
    public function getPackageFormFields(array $existingValues = []): array
    {
        $existing_srcset = trim((string) ($existingValues['srcset'] ?? ''));
        $token_groups = $this->classify_srcset_tokens($existing_srcset);

        $width_value = $token_groups['width'];
        $density_value = $token_groups['density'];
        $mode = !empty($token_groups['width']) ? 'width' : 'density';

        if ($existing_srcset !== '') {
            if ($mode === 'width' && empty($width_value)) {
                $width_value = [$existing_srcset];
            }

            if ($mode === 'density' && empty($density_value)) {
                $density_value = [$existing_srcset];
            }
        }

        return [
            'srcset_mode' => [
                'type' => 'radio',
                'label' => 'Descriptor Mode',
                'desc' => 'Choose how srcset entries are authored. Width mode is viewport/layout-driven; density mode is DPR-driven.',
                'value' => $mode,
                'choices' => [
                    'width' => 'Width descriptors (w)',
                    'density' => 'Pixel density descriptors (x)',
                ],
                'attrs' => [
                    'data-srcset-mode-toggle' => '1'
                ],
                'note' => 'Mixed input remains non-breaking: if width and density descriptors are combined, width descriptors take precedence.'
            ],
            'srcset_width_rows' => [
                'type' => 'html',
                'label' => 'Width Descriptors',
                'desc' => 'Width mode mini-grid. Use one descriptor per row, for example 320, 640, 1024, 50%, or 800w.',
                'content' => $this->build_descriptor_minigrid_html(
                    'srcset_width_rows',
                    'width',
                    'Width descriptor',
                    is_array($width_value) ? $width_value : [],
                    'No width descriptors configured.',
                    'Examples: 320, 480, 768, 1024, 50%, 1200w.',
                    $mode === 'width',
                    true
                )
            ],
            'srcset_density_rows' => [
                'type' => 'html',
                'label' => 'Density Descriptors',
                'desc' => 'Density mode mini-grid. Use one descriptor per row, for example 1x, 1.5x, 2x. Higher densities may be skipped when source dimensions are insufficient unless allow_scale_larger is enabled.',
                'content' => $this->build_descriptor_minigrid_html(
                    'srcset_density_rows',
                    'density',
                    'Density descriptor',
                    is_array($density_value) ? $density_value : [],
                    'No density descriptors configured.',
                    'Examples: 1x, 1.5x, 2x.',
                    $mode === 'density',
                    false
                ),
                'note' => 'In density mode, sizes can be supplied but do not drive x-descriptor candidate selection.'
            ]
        ];
    }

    /**
     * Process srcset form values into the persisted srcset parameter string.
     *
     * @param string $parameter_name
     * @param array $form_data
     * @return string
     */
    public function processParameterFromForm(string $parameter_name, array $form_data): string
    {
        if ($parameter_name !== 'srcset') {
            return parent::processParameterFromForm($parameter_name, $form_data);
        }

        $mode = strtolower(trim((string) ($form_data['srcset_mode'] ?? 'width')));
        if (!in_array($mode, ['width', 'density'], true)) {
            $mode = 'width';
        }

        $width_values = $this->extract_grid_values($form_data['srcset_width_rows'] ?? null, 'width', 'width');
        $density_values = $this->extract_grid_values($form_data['srcset_density_rows'] ?? null, 'density', 'density');

        $selected_values = $mode === 'density' ? $density_values : $width_values;

        if (empty($selected_values)) {
            // Backward compatibility for previous UI payload keys.
            $legacy_input = trim((string) ($mode === 'density'
                ? ($form_data['srcset_density_input'] ?? '')
                : ($form_data['srcset_width_input'] ?? '')));

            if ($legacy_input !== '') {
                return $this->normalize_descriptor_string($legacy_input);
            }

            // Backward compatibility for legacy single-value form post payloads.
            return trim((string) ($form_data['parameter_value'] ?? ''));
        }

        return implode('|', $selected_values);
    }

    /**
     * Validate the srcset parameter
     * 
     * Validates srcset parameter format, width specifications, and ordering.
     * Ensures width values are positive, formats are correct, and ordering
     * is optimized for browser selection.
     * 
     * @param string $param_name Parameter name being validated
     * @param mixed $value Parameter value to validate
     * @return bool|string True if valid, error message if invalid
     */
    public function validateParameter(string $param_name, $value)
    {
        if ($param_name !== 'srcset') {
            return true;
        }

        if (empty($value)) {
            return true; // Empty is valid, parameter is optional
        }

        // Split by pipe to classify width and density descriptors.
        $tokens = explode('|', (string) $value);

        if (empty($tokens)) {
            return 'At least one width specification is required';
        }

        $width_specs = [];
        $density_specs = [];

        foreach ($tokens as $index => $token) {
            $token = trim($token);

            if ($token === '') {
                return "Empty width specification found at position " . ($index + 1);
            }

            if (preg_match('/^\d+(?:\.\d+)?x$/i', $token)) {
                $density_specs[] = strtolower($token);
                continue;
            }

            if (preg_match('/^(?:\d+(?:w|px)?|\d+(?:\.\d+)?%)$/i', $token)) {
                $width_specs[] = strtolower($token);
                continue;
            }

            return "Invalid srcset descriptor at position " . ($index + 1) . ": {$token}";
        }

        if (empty($width_specs) && empty($density_specs)) {
            return 'At least one valid srcset descriptor is required';
        }

        // Width descriptors take precedence when mixed syntax is present.
        if (!empty($width_specs)) {
            return $this->validate_width_descriptors($width_specs);
        }

        return $this->validate_density_descriptors($density_specs);
    }

    /**
     * Validate width descriptors for width-mode srcset.
     *
     * @param string[] $width_specs
     * @return bool|string
     */
    private function validate_width_descriptors(array $width_specs)
    {

        $previousWidth = 0;

        foreach ($width_specs as $index => $spec) {
            // Validate width format and value using NumericValidator
            $result = ParameterRegistry::validateValue('numeric', $spec, [
                'allow_negative' => false,
                'allow_zero' => false,
                'allow_float' => false,
                'allow_units' => ['px', '%', 'w'],
                'min' => 1,
                'max' => null // No max limit for pixel values
            ]);
            
            if (!$result->success) {
                return "Invalid width at position " . ($index + 1) . ": " . $result->error;
            }

            $width = (int)$result->sanitizedValue;
            
            // Extract unit from original spec (NumericValidator strips units)
            $unit = '';
            if (preg_match('/(px|%|w)$/i', $spec, $matches)) {
                $unit = strtolower($matches[1]);
            }

            // For percentage values, ensure they're reasonable
            if ($unit === '%') {
                if ($width > 100) {
                    return "Percentage width should not exceed 100% at position " . ($index + 1) . ". Found: {$width}%";
                }
            }

            // Check ordering (values should be in increasing order for optimal processing)
            // Only check pixel values for ordering to avoid unit conversion complexity
            if ($unit !== '%' && $width <= $previousWidth) {
                return "Width values should be in increasing order for optimal processing. Value '$spec' at position " . ($index + 1) . " is not larger than previous value";
            }

            // Update previous width for ordering check (only for pixel values)
            if ($unit !== '%') {
                $previousWidth = $width;
            }
        }

        return true;
    }

    /**
     * Validate density descriptors for density-mode srcset.
     *
     * @param string[] $density_specs
     * @return bool|string
     */
    private function validate_density_descriptors(array $density_specs)
    {
        foreach ($density_specs as $index => $spec) {
            $density = (float) rtrim($spec, 'x');
            if ($density <= 0) {
                return "Density descriptors must be greater than zero at position " . ($index + 1) . ". Found: {$spec}";
            }
        }

        return true;
    }

    /**
     * Split srcset tokens into width, density, and invalid sets.
     *
     * @param string $srcset_value
     * @return array{width: string[], density: string[], invalid: string[]}
     */
    private function classify_srcset_tokens(string $srcset_value): array
    {
        $groups = [
            'width' => [],
            'density' => [],
            'invalid' => [],
        ];

        if ($srcset_value === '') {
            return $groups;
        }

        $tokens = explode('|', $srcset_value);
        foreach ($tokens as $token) {
            $token = strtolower(trim($token));
            if ($token === '') {
                continue;
            }

            if (preg_match(self::SRCSET_DENSITY_TOKEN_REGEX, $token)) {
                $groups['density'][] = $token;
                continue;
            }

            if (preg_match(self::SRCSET_WIDTH_TOKEN_REGEX, $token)) {
                $groups['width'][] = $token;
                continue;
            }

            $groups['invalid'][] = $token;
        }

        $groups['width'] = array_values(array_unique($groups['width']));
        $groups['density'] = array_values(array_unique($groups['density']));

        return $groups;
    }

    /**
     * Normalize descriptor string to a canonical pipe-separated value.
     *
     * @param string $descriptor_value
     * @return string
     */
    private function normalize_descriptor_string(string $descriptor_value): string
    {
        $tokens = array_filter(array_map('trim', explode('|', $descriptor_value)), static function ($value) {
            return $value !== '';
        });

        return implode('|', $tokens);
    }

    /**
     * Build mini-grid HTML for descriptor editing.
     *
     * @param string $field_name
     * @param string $column_key
     * @param string $column_label
     * @param string[] $values
     * @param string $no_results_text
     * @param string $example_text
     * @param bool $is_visible
     * @param bool $inject_mode_script
     * @return string
     */
    private function build_descriptor_minigrid_html(
        string $field_name,
        string $column_key,
        string $column_label,
        array $values,
        string $no_results_text,
        string $example_text,
        bool $is_visible,
        bool $inject_mode_script
    ): string {
        $display_style = $is_visible ? 'block' : 'none';

        if (!function_exists('ee') || !function_exists('form_input')) {
            return '<div data-srcset-mode-panel="' . htmlspecialchars($inject_mode_script ? 'width' : 'density') . '" style="display:' . $display_style . ';">'
                . '<p style="margin:0 0 8px;">' . htmlspecialchars($no_results_text) . '</p>'
                . '</div>';
        }

        try {
            $grid = ee('CP/MiniGridInput', ['field_name' => $field_name]);
            $grid->loadAssets();
            $grid->setColumns([$column_label]);
            $grid->setNoResultsText($no_results_text, 'Add New');
            $grid->setBlankRow([
                ['html' => form_input($column_key, '')],
            ]);

            $rows = [];
            $i = 1;
            foreach ($values as $value) {
                $rows[] = [
                    'attrs' => ['row_id' => $i],
                    'columns' => [
                        ['html' => form_input($column_key, (string) $value)],
                    ],
                ];
                $i++;
            }

            $grid->setData($rows);
            $grid_html = ee('View')->make('ee:_shared/form/mini_grid')->render($grid->viewData());

            $panel = '<div class="jcogs-img-pro-srcset-settings-box" data-srcset-mode-panel="' . htmlspecialchars($inject_mode_script ? 'width' : 'density') . '" style="max-width:420px;display:' . $display_style . ';">'
                . $grid_html
                . '<div style="margin-top:4px; opacity:.8; font-size:12px;">' . htmlspecialchars($example_text) . '</div>'
                . '</div>';

            if (!$inject_mode_script) {
                return $panel;
            }

            return $panel . $this->build_mode_toggle_script();
        } catch (\Throwable $e) {
            return '<div data-srcset-mode-panel="' . htmlspecialchars($inject_mode_script ? 'width' : 'density') . '" style="display:' . $display_style . ';">'
                . '<p style="margin:0 0 8px;">' . htmlspecialchars($no_results_text) . '</p>'
                . '</div>';
        }
    }

    /**
     * Build mode-toggle JavaScript for srcset mini-grid panels.
     *
     * @return string
     */
    private function build_mode_toggle_script(): string
    {
        return '<script>(function(){'
            . 'var init=function(){'
            . 'var modeSelect=document.querySelector("select[name=\"srcset_mode\"]");'
            . 'var modeRadios=document.querySelectorAll("input[name=\"srcset_mode\"]");'
            . 'if(!modeSelect&&(!modeRadios||modeRadios.length===0)){return;}'
            . 'var getMode=function(){'
            . 'if(modeSelect){return (modeSelect.value||"width").toLowerCase();}'
            . 'for(var r=0;r<modeRadios.length;r++){if(modeRadios[r].checked){return (modeRadios[r].value||"width").toLowerCase();}}'
            . 'return "width";'
            . '};'
            . 'var sync=function(){'
            . 'var mode=getMode();'
            . 'var panels=document.querySelectorAll("[data-srcset-mode-panel]");'
            . 'for(var i=0;i<panels.length;i++){' 
            . 'var panel=panels[i];'
            . 'panel.style.display=(panel.getAttribute("data-srcset-mode-panel")===mode)?"block":"none";'
            . '}'
            . '};'
            . 'if(modeSelect){modeSelect.addEventListener("change",sync);}'
            . 'if(modeRadios&&modeRadios.length){for(var i=0;i<modeRadios.length;i++){modeRadios[i].addEventListener("change",sync);}}'
            . 'sync();'
            . '};'
            . 'if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init);}else{init();}'
            . '})();</script>';
    }

    /**
     * Extract descriptor values from posted mini-grid data.
     *
     * @param mixed $grid_data
     * @param string $column_key
     * @param string $mode
     * @return string[]
     */
    private function extract_grid_values($grid_data, string $column_key, string $mode): array
    {
        if (!is_array($grid_data)) {
            return [];
        }

        $rows = $grid_data['rows'] ?? $grid_data;
        if (!is_array($rows)) {
            return [];
        }

        $values = [];
        foreach ($rows as $row) {
            if (is_array($row) && array_key_exists($column_key, $row)) {
                $raw = trim((string) $row[$column_key]);
            } elseif (is_scalar($row)) {
                $raw = trim((string) $row);
            } else {
                $raw = '';
            }

            if ($raw === '') {
                continue;
            }

            $normalized = strtolower($raw);
            if ($mode === 'density' && preg_match('/^\d+(?:\.\d+)?$/', $normalized)) {
                $normalized .= 'x';
            }

            $values[] = $normalized;
        }

        return array_values(array_unique($values));
    }

    /**
     * Get the description for this package
     * 
     * @return string Package description for display purposes
     */
    public function getPackageDescription(): string
    {
        return 'Responsive Image Variants (srcset) - Create multiple width variants of images for responsive design';
    }

    /**
     * Get additional help text for this package
     * 
     * @return string Detailed help text explaining usage and examples
     */
    public function getPackageHelp(): string
    {
        return 'The srcset parameter creates responsive image variants with different widths. ' .
               'Width mode: specify pipe-separated width values in increasing order (e.g., "350|450|700"). ' .
               'Values can be numbers (pixels assumed), numbers with "px" or "w" suffix, or percentages with "%" suffix. ' .
               'Density mode: specify pipe-separated density descriptors (e.g., "1x|2x" or "1x|1.5x|2x"). ' .
               'If mixed syntax is provided (for example "1x|320|2x"), width descriptors take precedence and density descriptors are ignored. ' .
               'The browser automatically selects the best variant based on viewport size and pixel density. ' .
               'Works with the sizes parameter to control how variants are selected.';
    }

    /**
     * Get documentation URL for this package
     * 
     * @return string URL to parameter documentation
     */
    public function getDocumentationUrl(): string
    {
        return 'https://jcogs.net/documentation/jcogs_img/jcogs_img-parameters#jcogs-image-parameter-srcset';
    }
}
