<?php

/**
 * JCOGS Image Pro - Coordinates Validator
 * ========================================
 * Validates and parses coordinate pairs (x,y format)
 * 
 * @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;

/**
 * Coordinates Validator
 * 
 * Validates and parses coordinate pairs (x,y format) with optional units.
 * 
 * This validator is used for parameters that accept coordinate pairs such as
 * crop offsets, watermark positions, text positions, etc.
 * 
 * Config Options:
 * - allow_negative (bool): Allow negative coordinate values (default: false)
 * - allow_percentage (bool): Allow percentage values (e.g., "50%,25%") (default: true)
 * - allow_units (array): Array of allowed unit suffixes (e.g., ['px', '%']) (default: ['px', '%'])
 * - separator (string): Coordinate separator character (default: ',')
 * - require_both (bool): Require both x and y coordinates (default: true)
 * 
 * Example Usage:
 * 
 * ```php
 * // Crop offset validation (negative allowed, percentages allowed)
 * $validator = new CoordinatesValidator([
 *     'allow_negative' => true,
 *     'allow_percentage' => true
 * ]);
 * $result = $validator->validate('10,20'); // Success: ['x' => 10, 'y' => 20]
 * $result = $validator->validate('-5,10'); // Success: ['x' => -5, 'y' => 10]
 * $result = $validator->validate('50%,25%'); // Success: ['x' => '50%', 'y' => '25%']
 * 
 * // Watermark position validation (no negative, percentages allowed)
 * $validator = new CoordinatesValidator([
 *     'allow_negative' => false,
 *     'allow_percentage' => true
 * ]);
 * $result = $validator->validate('10px,20px'); // Success: ['x' => '10px', 'y' => '20px']
 * $result = $validator->validate('-10,20'); // Error: negative not allowed
 * ```
 * 
 * Return Format:
 * 
 * On success, returns an associative array:
 * - ['x' => mixed, 'y' => mixed] if both coordinates provided
 * - ['x' => mixed] if only x coordinate provided (when require_both = false)
 * 
 * @package JCOGSDesign\JCOGSImagePro\Service\Validation
 * @since 2.0.0
 */
class CoordinatesValidator implements ValidatorInterface
{
    /**
     * Default configuration
     * 
     * @var array
     */
    private array $defaultConfig = [
        'allow_negative' => false,
        'allow_percentage' => true,
        'allow_units' => ['px', '%'],
        'separator' => ',',
        'require_both' => true
    ];
    
    /**
     * Validate coordinate pair
     * 
     * @param mixed $value Value to validate (string like "10,20" or "10px,20px")
     * @param array $config Configuration options
     * 
     * @return ValidationResult Success with parsed coordinates array or error
     */
    public function validate($value, array $config = []): ValidationResult
    {
        // Merge config with defaults
        $config = array_merge($this->defaultConfig, $config);
        
        // Handle null/empty values
        if ($value === null || $value === '') {
            return ValidationResult::error('Value is required', $value);
        }
        
        // Convert to string
        $value_str = (string)$value;
        $value_str = trim($value_str);
        
        // Split by separator
        $parts = explode($config['separator'], $value_str);
        
        // Check for correct number of parts
        if (count($parts) > 2) {
            return ValidationResult::error(
                'Invalid coordinate format. Expected at most two values (x,y)',
                $value
            );
        }
        
        if (count($parts) === 1 && $config['require_both']) {
            return ValidationResult::error(
                'Both x and y coordinates are required',
                $value
            );
        }
        
        // Parse x coordinate
        $x_result = $this->parseCoordinate(trim($parts[0]), 'x', $config, $value);
        if (!$x_result->success) {
            return $x_result;
        }
        
        $coordinates = ['x' => $x_result->sanitizedValue];
        
        // Parse y coordinate if present
        if (count($parts) === 2) {
            $y_result = $this->parseCoordinate(trim($parts[1]), 'y', $config, $value);
            if (!$y_result->success) {
                return $y_result;
            }
            $coordinates['y'] = $y_result->sanitizedValue;
        } elseif (count($parts) === 1 && !$config['require_both']) {
            // Single value: use same value for both x and y
            $coordinates['y'] = $x_result->sanitizedValue;
        }
        
        return ValidationResult::success($coordinates);
    }
    
    /**
     * Parse a single coordinate value
     * 
     * @param string $coord_str Coordinate string (e.g., "10", "10px", "50%", "-5")
     * @param string $axis Axis name ('x' or 'y') for error messages
     * @param array $config Configuration options
     * @param mixed $original_value Original value for error reporting
     * 
     * @return ValidationResult Success with parsed coordinate or error
     */
    private function parseCoordinate(string $coord_str, string $axis, array $config, mixed $original_value): ValidationResult
    {
        if ($coord_str === '') {
            return ValidationResult::error(
                sprintf('%s coordinate cannot be empty', strtoupper($axis)),
                $original_value
            );
        }
        
        // Check for percentage - preserve % in output
        if (str_ends_with($coord_str, '%')) {
            if (!$config['allow_percentage']) {
                return ValidationResult::error(
                    sprintf('%s coordinate cannot be a percentage (%%)', strtoupper($axis)),
                    $original_value
                );
            }
            
            $numeric_part = substr($coord_str, 0, -1);
            if (!is_numeric($numeric_part)) {
                return ValidationResult::error(
                    sprintf('Invalid %s coordinate: must be numeric', strtoupper($axis)),
                    $original_value
                );
            }
            
            $numeric_value = (float)$numeric_part;
            if ($numeric_value < 0 && !$config['allow_negative']) {
                return ValidationResult::error(
                    sprintf('%s coordinate cannot be negative', strtoupper($axis)),
                    $original_value
                );
            }
            
            // Return with % sign preserved
            return ValidationResult::success($coord_str);
        }
        
        // Check for other unit suffixes (strip units from output, return numeric)
        foreach ($config['allow_units'] as $unit) {
            if ($unit === '%') {
                continue; // Already handled above
            }
            
            // Case-insensitive unit matching
            if (preg_match('/(' . preg_quote($unit, '/') . ')$/i', $coord_str, $matches)) {
                $found_unit = $matches[1];
                $numeric_part = substr($coord_str, 0, -strlen($found_unit));
                $numeric_part = trim($numeric_part);
                
                if (!is_numeric($numeric_part)) {
                    return ValidationResult::error(
                        sprintf('Invalid %s coordinate: must be numeric', strtoupper($axis)),
                        $original_value
                    );
                }
                
                $numeric_value = (float)$numeric_part;
                if ($numeric_value < 0 && !$config['allow_negative']) {
                    return ValidationResult::error(
                        sprintf('%s coordinate cannot be negative', strtoupper($axis)),
                        $original_value
                    );
                }
                
                // Strip unit, return numeric value
                return ValidationResult::success((int)$numeric_value);
            }
        }
        
        // No unit suffix - must be pure numeric
        if (!is_numeric($coord_str)) {
            // Check if it's entirely letters (no numeric part at all)
            if (preg_match('/^[a-z]+$/i', $coord_str)) {
                return ValidationResult::error(
                    sprintf('Invalid %s coordinate: must be numeric', strtoupper($axis)),
                    $original_value
                );
            }
            
            // Has letters at end - disallowed unit
            if (preg_match('/([a-z]+)$/i', $coord_str, $matches)) {
                $found_unit = $matches[1];
                return ValidationResult::error(
                    sprintf('Invalid %s coordinate: unit "%s" not allowed', strtoupper($axis), $found_unit),
                    $original_value
                );
            }
            
            // Some other invalid format
            return ValidationResult::error(
                sprintf('Invalid %s coordinate: must be numeric', strtoupper($axis)),
                $original_value
            );
        }
        
        $numeric_value = (float)$coord_str;
        
        if ($numeric_value < 0 && !$config['allow_negative']) {
            return ValidationResult::error(
                sprintf('%s coordinate cannot be negative', strtoupper($axis)),
                $original_value
            );
        }
        
        // Return as int if whole number, float otherwise
        if (floor($numeric_value) == $numeric_value) {
            return ValidationResult::success((int)$numeric_value);
        }
        
        return ValidationResult::success($numeric_value);
    }
}
