<?php declare(strict_types=1);

/**
 * JCOGS Image Pro - Responsive Image Service
 * ==========================================
 * Comprehensive responsive image processing with HTML5 srcset/sizes support
 * 
 * @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      2.0.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service\Pipeline;

use JCOGSDesign\JCOGSImagePro\Service\Pipeline\Context;
use JCOGSDesign\JCOGSImagePro\Service\Pipeline\AbstractService;

/**
 * Responsive Image Service for JCOGS Image Pro
 * 
 * Provides comprehensive responsive image functionality including:
 * - Legacy srcset parameter parsing and validation
 * - Responsive image variant generation and management
 * - HTML5 srcset and sizes attribute generation
 * - Performance optimization for responsive image workflows
 * - Integration with lazy loading and caching systems
 * 
 * Legacy format support:
 * - srcset parameter: '250|50%|300px|400' (pipe-separated width values)
 * - sizes parameter: Custom sizes attribute or auto-generated from breakpoints
 * - Width descriptors: Standard HTML5 format (e.g., "250w", "300w") 
 * - Integration: Works with lazy loading via data-ji-srcset attributes
 * 
 * Features full backward compatibility with Legacy srcset implementation while
 * providing enhanced performance optimization and modern responsive image best practices.
 * Handles percentage-based widths, explicit pixel values, and optimal breakpoint calculation.
 */
class ResponsiveImageService extends AbstractService
{
    /**
     * Srcset token classification regexes
     */
    private const SRCSET_WIDTH_TOKEN_REGEX = '/^\d+(?:w|px)?$|^\d+(?:\.\d+)?%$/i';
    private const SRCSET_DENSITY_TOKEN_REGEX = '/^\d+(?:\.\d+)?x$/i';

    /**
     * Determine a hard maximum width cap based on tag parameters.
     *
     * This cap is applied to responsive srcset variant widths in addition to any
     * source-image scaling limit.
     *
     * Supports:
     * - max_width (specific)
     * - max (general maximum dimension)
     */
    private function determine_hard_variant_width_cap(Context $context, int $base_width, int $source_width): ?int
    {
        $cap = null;

        $reference_width = $source_width > 0 ? $source_width : $base_width;
        if ($reference_width <= 0) {
            $reference_width = $base_width;
        }

        $max_width_param = trim((string) $context->get_param('max_width', ''));
        if ($max_width_param !== '') {
            $validated = $this->validation_service->validate_dimension($max_width_param, $reference_width);
            if (is_int($validated) && $validated > 0) {
                $cap = $validated;
            }
        }

        $max_param = trim((string) $context->get_param('max', ''));
        if ($max_param !== '') {
            $original_height = (int) $context->get_metadata_value('original_height', 0);
            $reference_max = max($reference_width, $original_height, $base_width);
            $validated = $this->validation_service->validate_dimension($max_param, $reference_max);
            if (is_int($validated) && $validated > 0) {
                $cap = ($cap === null) ? $validated : min($cap, $validated);
            }
        }

        return $cap;
    }

    /**
     * Constructor
     * 
     * All services are now automatically available via parent AbstractService.
     * No need to manually instantiate common services.
     */
    public function __construct()
    {
        parent::__construct('ResponsiveImageService');
        // $this->utilities_service is now available via parent
        // $this->settings_service is now available via parent
        // All other common services are also available
    }
    
    /**
     * Calculate optimal breakpoints from content analysis
     * 
     * This is a placeholder for more sophisticated breakpoint calculation
     * that could analyze content width patterns or use standard responsive breakpoints.
     * 
     * @param int $max_width Maximum image width
     * @return array Array of breakpoint widths
     */
    public function calculate_optimal_breakpoints(int $max_width): array
    {
        // Standard responsive breakpoints commonly used in web design
        $standard_breakpoints = [320, 480, 768, 1024, 1280, 1600];
        
        // Filter to only include breakpoints smaller than max width
        return array_filter($standard_breakpoints, function($width) use ($max_width) {
            return $width <= $max_width;
        });
    }
    
    /**
     * Generate responsive image variants information
     * 
     * @param Context $context Processing context
     * @param int $base_width Primary image width
     * @param int $max_width Maximum allowed width (for scale limiting)
     * @param bool $allow_scale_larger Whether to allow scaling larger than original
     * @return array Array of variant information with widths and file paths
     */
    public function generate_variant_info(
        Context $context, 
        int $base_width, 
        int $max_width, 
        bool $allow_scale_larger = false
    ): array {
        $srcset_param = $context->get_param('srcset', '');
        
        if (empty($srcset_param)) {
            return [];
        }
        
        $token_groups = $this->classify_srcset_tokens($srcset_param);

        if (!empty($token_groups['invalid'])) {
            $this->utilities_service->debug_message(
                'Ignoring invalid srcset tokens: ' . implode(', ', $token_groups['invalid']),
                null,
                false,
                'detailed'
            );
        }

        $is_density_mode = empty($token_groups['width']) && !empty($token_groups['density']);
        $requested_widths = [];
        $requested_densities = [];

        if (!empty($token_groups['width'])) {
            if (!empty($token_groups['density'])) {
                $this->utilities_service->debug_message(
                    'Mixed srcset syntax detected; using width descriptors and ignoring density tokens: ' . implode(', ', $token_groups['density']),
                    null,
                    false,
                    'standard'
                );
            }

            $requested_widths = $this->parse_width_tokens_to_pixels($token_groups['width'], $base_width);
        } else {
            $requested_densities = $this->parse_density_tokens($token_groups['density']);
        }

        if (empty($requested_widths) && empty($requested_densities)) {
            return [];
        }

        $variants = [];
        
        // NOTE: $max_width is the source/original width limit supplied by callers.
        // Apply hard caps from max_width/max parameters as well (these should be
        // respected even when allow_scale_larger is enabled).
        $effective_max = $allow_scale_larger ? PHP_INT_MAX : $max_width;
        $hard_cap = $this->determine_hard_variant_width_cap($context, $base_width, $max_width);
        if ($hard_cap !== null && $hard_cap > 0) {
            $effective_max = ($effective_max === PHP_INT_MAX) ? $hard_cap : min($effective_max, $hard_cap);
        }

        if ($is_density_mode) {
            foreach ($requested_densities as $density) {
                $target_width = max(1, (int) round($base_width * $density));

                if ($target_width > $effective_max) {
                    if ($effective_max > 0 && $effective_max < PHP_INT_MAX) {
                        $this->utilities_service->debug_message(
                            sprintf(
                                'Skipping srcset density %sx (%dpx target) - exceeds maximum allowed width of %dpx',
                                $this->format_density_value($density),
                                $target_width,
                                $effective_max
                            ),
                            null,
                            false,
                            'detailed'
                        );
                    }
                    continue;
                }

                $use_main_image = ($target_width === $base_width);

                $variants[] = [
                    'width' => $target_width,
                    'density' => $density,
                    'descriptor' => $this->format_density_descriptor($density),
                    'descriptor_type' => 'density',
                    'cache_suffix' => $use_main_image ? '' : '_' . $this->format_density_cache_suffix($density),
                    'needs_generation' => !$use_main_image,
                    'use_main_image' => $use_main_image,
                ];
            }

            return $variants;
        }

        foreach ($requested_widths as $width) {
            // Skip if width exceeds limits
            if ($width > $effective_max) {
                // Only show message if max_width is meaningful (not 0 or PHP_INT_MAX)
                if ($effective_max > 0 && $effective_max < PHP_INT_MAX) {
                    $this->utilities_service->debug_message(
                        sprintf('Skipping srcset width %dpx - exceeds maximum allowed width of %dpx',
                            $width, $effective_max),
                        null,
                        false,
                        'detailed' // Move to detailed verbosity - technical implementation detail
                    );
                }
                continue;
            }

            $variants[] = [
                'width' => $width,
                'descriptor' => $width . 'w',
                'descriptor_type' => 'width',
                'cache_suffix' => '_' . $width . 'w',
                'needs_generation' => true, // Will be checked against cache later
            ];
        }
        
        return $variants;
    }
    
    /**
     * Generate srcset attribute value
     * 
     * @param array $variants Array of variant information 
     * @param string $base_url Base URL for cache directory
     * @param string $main_image_url URL of the main processed image
     * @param int $main_image_width Width of the main image in pixels
     * @return string Formatted srcset attribute value
     */
    public function generate_srcset_attribute(
        array $variants, 
        string $base_url, 
        string $main_image_url,
        int $main_image_width
    ): string {
        if (empty($variants)) {
            return '';
        }
        
        $srcset_entries = [];
        $is_density_mode = $this->is_density_variant_set($variants);
        
        // Extract base filename and extension from main image URL
        $main_path_info = pathinfo($main_image_url);
        $base_filename = $main_path_info['filename'] ?? ''; // Without extension
        $extension = $main_path_info['extension'] ?? '';
        
        // Add variant entries.
        //
        // Variants generated by generate_variant_info() include:
        // - width, descriptor, cache_suffix
        //
        // Some unit tests (and potential callers) use a simpler shape:
        // - width, cache_path (already includes extension)
        //
        // Be permissive so callers don't need to know internal details.
        foreach ($variants as $variant) {
            $width = (int) ($variant['width'] ?? 0);
            if ($width <= 0 && empty($variant['use_main_image'])) {
                continue;
            }

            $descriptor = (string) ($variant['descriptor'] ?? ($width . 'w'));

            if (!empty($variant['use_main_image'])) {
                $srcset_entries[] = $main_image_url . ' ' . $descriptor;
                continue;
            }

            if (isset($variant['cache_path']) && is_string($variant['cache_path']) && trim($variant['cache_path']) !== '') {
                $variant_url = $base_url . ltrim(basename($variant['cache_path']), '/');
                $srcset_entries[] = $variant_url . ' ' . $descriptor;
                continue;
            }

            $cache_suffix = (string) ($variant['cache_suffix'] ?? ('_' . $width . 'w'));
            if ($base_filename !== '' && $extension !== '') {
                $variant_filename = $base_filename . $cache_suffix . '.' . $extension;
                $variant_url = $base_url . $variant_filename;
                $srcset_entries[] = $variant_url . ' ' . $descriptor;
            }
        }
        
        // Only add main image to srcset if it's larger than all variants
        // (i.e., when variants are capped at source size with allow_scale_larger='no')
        // When allow_scale_larger='yes', largest variant is already the "main" size
        $largest_variant_width = !empty($variants) ? max(array_column($variants, 'width')) : 0;
        if (!$is_density_mode && $main_image_width > $largest_variant_width) {
            $srcset_entries[] = $main_image_url . ' ' . $main_image_width . 'w';
        }
        
        return implode(', ', $srcset_entries);
    }    /**
     * Generate sizes attribute value
     * 
     * @param Context $context Processing context
     * @param array $variants Array of variant information
     * @param int $main_width Main image width
     * @return string Formatted sizes attribute value
     */
    public function generate_sizes_attribute(
        Context $context, 
        array $variants, 
        int $main_width
    ): string {
        $is_density_mode = $this->is_density_variant_set($variants);

        // Check for custom sizes parameter first
        $custom_sizes = $context->get_param('sizes', '');
        if (!empty($custom_sizes)) {
            if ($is_density_mode) {
                $this->utilities_service->debug_message(
                    'sizes parameter provided with density srcset descriptors; sizes is retained but does not influence x-descriptor selection',
                    null,
                    false,
                    'standard'
                );
            }
            return trim((string) $custom_sizes);
        }

        if ($is_density_mode) {
            return '';
        }
        
        if (empty($variants)) {
            return '';
        }
        
        $sizes_entries = [];
        
        // Generate responsive breakpoints from variants
        foreach ($variants as $variant) {
            $width = $variant['width'];
            $sizes_entries[] = "(max-width: {$width}px) {$width}px";
        }
        
        // Add default size - use largest variant width instead of main_width
        // This ensures the browser selects the largest variant for wide viewports
        $largest_variant_width = !empty($variants) ? max(array_column($variants, 'width')) : $main_width;
        $default_size = max($largest_variant_width, $main_width);
        $sizes_entries[] = "{$default_size}px";
        
        return implode(', ', $sizes_entries);
    }
    
    /**
     * Debug information for srcset processing
     * 
     * @param Context $context Processing context
     * @param array $variants Generated variants
     * @return string Debug information
     */
    public function get_debug_info(Context $context, array $variants): string
    {
        $srcset_param = $context->get_param('srcset', '');
        $info = [
            "Srcset parameter: {$srcset_param}",
            "Generated variants: " . count($variants),
        ];
        
        foreach ($variants as $variant) {
            $info[] = "  - {$variant['width']}px ({$variant['descriptor']})";
        }
        
        return implode("\n", $info);
    }
    
    /**
     * Get performance impact of srcset generation
     * 
     * @param array $variants Array of variants to be generated
     * @return array Performance metrics prediction
     */
    public function get_performance_metrics(array $variants): array
    {
        return [
            'variant_count' => count($variants),
            'estimated_generation_time' => count($variants) * 0.1, // Rough estimate
            'cache_storage_multiplier' => count($variants) + 1, // +1 for main image
            'bandwidth_saving_potential' => min(50, count($variants) * 10), // Percentage
        ];
    }
    
    /**
     * Check if responsive images are enabled for the context
     * 
     * @param Context $context Processing context
     * @return bool True if srcset should be generated
     */
    public function is_responsive_enabled(Context $context): bool
    {
        $srcset_param = $context->get_param('srcset', '');
        return !empty($srcset_param);
    }
    
    /**
     * Validate srcset parameter format
     * 
     * @param string $srcset_param Raw srcset parameter
     * @return bool True if parameter format is valid
     */
    public function is_valid_srcset_parameter(string $srcset_param): bool
    {
        if (empty($srcset_param)) {
            return false;
        }

        $token_groups = $this->classify_srcset_tokens($srcset_param);
        if (!empty($token_groups['invalid'])) {
            return false;
        }

        return !empty($token_groups['width']) || !empty($token_groups['density']);
    }
    
    /**
     * Parse srcset parameter from Legacy format
     * 
     * Legacy format: '250|50%|300px|400'
     * Supports: numeric pixels, percentages, and explicit px values
     * 
     * @param string $srcset_param Raw srcset parameter value
     * @param int $base_width Base image width for percentage calculations
     * @return array Array of width values in pixels
     */
    public function parse_srcset_parameter(string $srcset_param, int $base_width): array
    {
        if (empty($srcset_param)) {
            return [];
        }

        $token_groups = $this->classify_srcset_tokens($srcset_param);
        return $this->parse_width_tokens_to_pixels($token_groups['width'], $base_width);
    }

    /**
     * Parse srcset density descriptors from pipe-separated srcset tokens.
     *
     * @param string $srcset_param Raw srcset parameter value
     * @return float[] Array of density values (e.g. [1.0, 1.5, 2.0])
     */
    public function parse_srcset_density_parameter(string $srcset_param): array
    {
        if (empty($srcset_param)) {
            return [];
        }

        $token_groups = $this->classify_srcset_tokens($srcset_param);
        return $this->parse_density_tokens($token_groups['density']);
    }

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

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

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

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

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

        return $groups;
    }

    /**
     * Convert width-like srcset tokens into pixel widths.
     *
     * @param string[] $width_tokens
     * @param int $base_width
     * @return int[]
     */
    private function parse_width_tokens_to_pixels(array $width_tokens, int $base_width): array
    {
        $widths = [];

        foreach ($width_tokens as $token) {
            if (str_ends_with($token, '%')) {
                $percentage = (float) rtrim($token, '%');
                $width = (int) round(($percentage / 100) * $base_width);
            } elseif (str_ends_with($token, 'px')) {
                $width = (int) rtrim($token, 'px');
            } elseif (str_ends_with($token, 'w')) {
                $width = (int) rtrim($token, 'w');
            } else {
                $width = (int) $token;
            }

            if ($width > 0) {
                $widths[] = $width;
            }
        }

        $widths = array_values(array_unique($widths));
        sort($widths, SORT_NUMERIC);

        return $widths;
    }

    /**
     * Convert density tokens into normalized float values.
     *
     * @param string[] $density_tokens
     * @return float[]
     */
    private function parse_density_tokens(array $density_tokens): array
    {
        $densities = [];

        foreach ($density_tokens as $token) {
            $density = (float) rtrim(strtolower($token), 'x');
            if ($density > 0) {
                $densities[] = $density;
            }
        }

        $densities = array_values(array_unique($densities, SORT_NUMERIC));
        sort($densities, SORT_NUMERIC);

        return $densities;
    }

    /**
     * Determine whether variants are using density descriptors.
     *
     * @param array $variants
     * @return bool
     */
    private function is_density_variant_set(array $variants): bool
    {
        foreach ($variants as $variant) {
            if (($variant['descriptor_type'] ?? '') === 'density') {
                return true;
            }

            $descriptor = (string) ($variant['descriptor'] ?? '');
            if ($descriptor !== '' && str_ends_with(strtolower($descriptor), 'x')) {
                return true;
            }
        }

        return false;
    }

    /**
     * Format a numeric density value for descriptor output.
     *
     * @param float $density
     * @return string
     */
    private function format_density_value(float $density): string
    {
        $formatted = rtrim(rtrim(number_format($density, 3, '.', ''), '0'), '.');
        return $formatted === '' ? '1' : $formatted;
    }

    /**
     * Format a density descriptor value with x suffix.
     *
     * @param float $density
     * @return string
     */
    private function format_density_descriptor(float $density): string
    {
        return $this->format_density_value($density) . 'x';
    }

    /**
     * Create normalized cache suffix fragment for a density descriptor.
     *
     * @param float $density
     * @return string
     */
    private function format_density_cache_suffix(float $density): string
    {
        return str_replace('.', '-', $this->format_density_value($density)) . 'x';
    }
    
    /**
     * Check if image format supports responsive images
     * 
     * @param Context $context Processing context
     * @return bool True if format supports srcset
     */
    public function supports_responsive_images(Context $context): bool
    {
        // SVG and animated GIF should not have srcset generated
        $is_svg = $context->get_metadata_value('is_svg', false);
        $is_animated_gif = $context->get_metadata_value('is_animated_gif', false);
        
        return !$is_svg && !$is_animated_gif;
    }
}
