<?php

/**
 * JCOGS Image Pro - Percentage Validator
 * =======================================
 * Validates and normalizes percentage values
 * 
 * @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\Validation;

/**
 * Percentage Validator
 * 
 * Validates and normalizes percentage values in both 0-100 and 0.0-1.0 formats.
 * 
 * This validator is used for parameters that accept percentage values such as
 * opacity, quality adjustments, scaling factors, etc. It can automatically
 * normalize between the two common percentage formats.
 * 
 * Config Options:
 * - normalize_to (string): Target format - '0-100' or '0-1' (default: '0-100')
 * - min (float): Minimum percentage value in the normalized format (default: 0)
 * - max (float): Maximum percentage value in the normalized format (default: 100 or 1.0 depending on normalize_to)
 * - allow_over_100 (bool): Allow values over 100% (for scaling, etc.) (default: false)
 * 
 * Example Usage:
 * 
 * ```php
 * // Opacity validation (0-100 format)
 * $validator = new PercentageValidator([
 *     'normalize_to' => '0-100',
 *     'min' => 0,
 *     'max' => 100
 * ]);
 * $result = $validator->validate('50'); // Success: 50
 * $result = $validator->validate('0.5'); // Success: 50 (normalized from 0-1 to 0-100)
 * $result = $validator->validate('50%'); // Success: 50
 * 
 * // Scaling factor validation (0-1 format, allow over 100%)
 * $validator = new PercentageValidator([
 *     'normalize_to' => '0-1',
 *     'allow_over_100' => true,
 *     'max' => 2.0
 * ]);
 * $result = $validator->validate('150%'); // Success: 1.5 (normalized)
 * $result = $validator->validate('1.5'); // Success: 1.5
 * ```
 * 
 * @package JCOGSDesign\JCOGSImagePro\Service\Validation
 * @since 2.0.0
 */
class PercentageValidator implements ValidatorInterface
{
    /**
     * Default configuration
     * 
     * @var array
     */
    private array $defaultConfig = [
        'output_format' => null,      // null = preserve input format
        'normalize_to' => null,       // Backward compatibility
        'min' => null,
        'max' => null,
        'allow_over_100' => false
    ];
    
    /**
     * Validate and normalize a percentage value
     * 
     * @param mixed $value Value to validate (string, int, or float)
     * @param array $config Configuration options
     * 
     * @return ValidationResult Success with normalized percentage or error
     */
    public function validate($value, array $config = []): ValidationResult
    {
        // Merge config with defaults
        $config = array_merge($this->defaultConfig, $config);
        
        // Normalize config: output_format takes precedence over normalize_to
        $output_format = $config['output_format'] ?? $config['normalize_to'] ?? null;
        
        // Validate output_format config
        if ($output_format !== null && !in_array($output_format, ['0-100', '0-1'], true)) {
            throw new \InvalidArgumentException('output_format must be "0-100" or "0-1"');
        }
        
        // Handle null/empty values
        if ($value === null || $value === '') {
            return ValidationResult::error('Value is required', $value);
        }
        
        // Convert to string for parsing
        $value_str = (string)$value;
        $value_str = trim($value_str);
        
        // Track if input had decimal point (for type preservation)
        $input_has_decimal = str_contains($value_str, '.');
        
        // Remove % suffix if present
        $has_percent_suffix = str_ends_with($value_str, '%');
        if ($has_percent_suffix) {
            $value_str = substr($value_str, 0, -1);
            $value_str = trim($value_str);
        }
        
        // Check if numeric
        if (!is_numeric($value_str)) {
            return ValidationResult::error('Percentage value must be numeric', $value);
        }
        
        $numeric_value = (float)$value_str;
        
        // Detect input format
        // If value has % suffix → 0-100 format
        // If value > 1 AND is integer (e.g., 2, 50, 150) → 0-100 format  
        // If value > 1 but has decimal (e.g., 1.5, 2.5) → ambiguous, check max
        // If value <= 1 → 0-1 format
        
        $is_input_0_100_format = false;
        
        if ($has_percent_suffix) {
            $is_input_0_100_format = true;
        } elseif ($numeric_value > 1) {
            // Values > 1 without decimals are 0-100 format (e.g., 50, 75)
            // Values > 1 with decimals could be 0-1 format with error (e.g., 1.5 when max is 1.0)
            // Treat as 0-100 only if it's a reasonable percentage (e.g., 2-150, not 1.5)
            if ($numeric_value >= 2) {
                $is_input_0_100_format = true;
            }
            // else: treat as 0-1 format (will fail max check if > 1.0)
        }
        // else: 0-1 format (value <= 1 with no % sign)
        
        // If no output format specified, preserve input format
        $preserve_input_format = ($config['output_format'] === null && $config['normalize_to'] === null);
        if ($output_format === null) {
            $output_format = $is_input_0_100_format ? '0-100' : '0-1';
        }
        
        // Set default min/max based on output_format if not specified
        if ($config['min'] === null) {
            $config['min'] = 0;
        }
        if ($config['max'] === null) {
            $config['max'] = ($output_format === '0-1') ? 1.0 : 100;
        }
        
        // Normalize to target output format
        if ($output_format === '0-100') {
            if (!$is_input_0_100_format) {
                // Convert from 0-1 to 0-100
                $normalized_value = $numeric_value * 100;
            } else {
                $normalized_value = $numeric_value;
            }
        } else {
            // output_format === '0-1'
            if ($is_input_0_100_format) {
                // Convert from 0-100 to 0-1
                $normalized_value = $numeric_value / 100;
            } else {
                $normalized_value = $numeric_value;
            }
        }
        
        // Check for over 100% (in original input format)
        if (!$config['allow_over_100']) {
            $original_max = $is_input_0_100_format ? 100 : 1.0;
            if ($numeric_value > $original_max) {
                return ValidationResult::error(
                    sprintf('Percentage cannot exceed %s', $is_input_0_100_format ? '100%' : '100% (1.0)'),
                    $value
                );
            }
        }
        
        // Check minimum value (in normalized format)
        if ($normalized_value < $config['min']) {
            $min_display = $output_format === '0-100' ? $config['min'] . '%' : $config['min'];
            return ValidationResult::error(
                sprintf('Percentage must be at least %s', $min_display),
                $value
            );
        }
        
        // Check maximum value (in normalized format)
        if ($normalized_value > $config['max']) {
            $max_display = $output_format === '0-100' ? $config['max'] . '%' : $config['max'];
            return ValidationResult::error(
                sprintf('Percentage must be at most %s', $max_display),
                $value
            );
        }
        
        // Return normalized value with appropriate type
        // When preserving input format: return int for whole numbers WITHOUT decimal in input, float if decimal in input
        // When explicitly set to 0-1 format: always return float
        // When explicitly set to 0-100 format: return int for whole numbers
        if ($preserve_input_format) {
            // No format specified, preserve input type
            if (!$input_has_decimal && floor($normalized_value) == $normalized_value) {
                return ValidationResult::success((int)$normalized_value);
            }
            return ValidationResult::success((float)$normalized_value);
        } elseif ($output_format === '0-1') {
            // Explicit 0-1 format: always float
            return ValidationResult::success((float)$normalized_value);
        } else {
            // 0-100 format: integer if whole number
            if (floor($normalized_value) == $normalized_value) {
                return ValidationResult::success((int)$normalized_value);
            }
            return ValidationResult::success($normalized_value);
        }
    }
}
