<?php

/**
 * JCOGS Image Pro - Named Parameter Parser
 * Dual Syntax Support for Complex Parameters
 * 
 * @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      Extension Architecture v2.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service\ParameterParsing;

/**
 * Named Parameter Parser
 * 
 * Universal parser supporting both legacy positional and modern named syntax
 * for complex filter and parameter specifications.
 * 
 * Purpose:
 * Enables backwards-compatible parameter evolution. Complex parameters can
 * support self-documenting named syntax while maintaining compatibility with
 * existing positional syntax.
 * 
 * Syntax Detection:
 * - Named: Contains known prefix patterns ('src:', 'placement:', 'mode:', etc.)
 * - Positional: No prefixes, parsed as pipe-delimited indexed array
 * 
 * Edge Cases Handled:
 * - URLs with protocols ('http://example.com/logo.png') - NOT treated as named
 * - Windows paths ('C:\path\to\file.png') - NOT treated as named
 * - Mixed contexts (some named, some positional) - treated as positional for safety
 * - Empty segments ignored
 * 
 * Example Usage:
 * 
 * ```php
 * $parser = new NamedParameterParser();
 * 
 * // Parse legacy positional syntax
 * $params = $parser->parse('logo.png|400,300|70', [
 *     0 => 'src',
 *     1 => 'minimum_dimensions',
 *     2 => 'opacity',
 * ]);
 * // Result: ['src' => 'logo.png', 'minimum_dimensions' => '400,300', 'opacity' => '70']
 * 
 * // Parse modern named syntax
 * $params = $parser->parse('src:logo.png|opacity:70', [
 *     0 => 'src',
 *     1 => 'minimum_dimensions',
 *     2 => 'opacity',
 * ]);
 * // Result: ['src' => 'logo.png', 'opacity' => '70']
 * 
 * // Build named syntax string from array
 * $spec = $parser->build(['src' => 'logo.png', 'opacity' => 70], 'named');
 * // Result: "src:logo.png|opacity:70"
 * ```
 */
class NamedParameterParser
{
    /**
     * Known named parameter prefixes for syntax detection
     * 
     * These prefixes identify named syntax parameters. Must be lowercase.
     * 
     * Categories:
     * - File references: src, source, file, path, image
     * - Positioning: placement, position, align, anchor
     * - Modes/strategies: mode, method, strategy, type, algorithm
     * - Opacity/transparency: opacity, alpha, transparency
     * - Spacing: offset, margin, padding, spacing
     * - Rotation/transformation: rotation, angle, rotate, transform
     * - Sizing: scale, size, dimensions, width, height
     * - Quality: quality, compression, optimize
     * - Focus/targeting: focus, focal, center, target
     * - Ratios: ratio, aspect, proportion
     * - Constraints: min, max, minimum, maximum, limit
     * - Colors: color, colour, fill, stroke
     * - Text: text, font, content, message
     * 
     * @var array
     */
    private const KNOWN_PREFIXES = [
        // File references
        'src', 'source', 'file', 'path', 'image',
        
        // Positioning
        'placement', 'position', 'align', 'anchor',
        
        // Modes/strategies
        'mode', 'method', 'strategy', 'type', 'algorithm',
        
        // Opacity/transparency
        'opacity', 'alpha', 'transparency',
        
        // Spacing
        'offset', 'margin', 'padding', 'spacing',
        
        // Rotation/transformation
        'rotation', 'angle', 'rotate', 'transform',
        
        // Sizing
        'scale', 'size', 'dimensions', 'width', 'height',
        
        // Quality
        'quality', 'compression', 'optimize',
        
        // Focus/targeting
        'focus', 'focal', 'center', 'target',
        
        // Ratios
        'ratio', 'aspect', 'proportion',
        
        // Constraints
        'min', 'max', 'minimum', 'maximum', 'limit',
        
        // Colors
        'color', 'colour', 'fill', 'stroke',
        
        // Text
        'text', 'font', 'content', 'message',
    ];
    
    /**
     * Parse parameter specification
     * 
     * Automatically detects syntax (named vs. positional) and returns
     * normalized associative array.
     * 
     * @param string $spec Parameter specification string (pipe-delimited)
     * @param array $positionalMapping Positional index to key mapping:
     *   [0 => 'src', 1 => 'minimum_dimensions', 2 => 'opacity', ...]
     *   Used when spec is positional syntax or for fallback.
     * @param array $defaults Default values for optional parameters:
     *   ['opacity' => 100, 'position' => 'center,center', ...]
     * 
     * @return array Normalized parameters as associative array
     */
    public function parse(string $spec, array $positionalMapping, array $defaults = []): array
    {
        $spec = trim($spec);
        
        // Empty spec returns defaults
        if (empty($spec)) {
            return $defaults;
        }
        
        // Detect syntax and parse accordingly
        if ($this->isNamedSyntax($spec)) {
            $params = $this->parseNamedSyntax($spec);
        } else {
            $params = $this->parsePositionalSyntax($spec, $positionalMapping);
        }
        
        // Merge with defaults (parsed params take precedence)
        return array_merge($defaults, $params);
    }
    
    /**
     * Detect if specification uses named syntax
     * 
     * Algorithm:
     * 1. Split on pipe delimiter
     * 2. Check each segment for "key:value" pattern
     * 3. If key matches known prefix (not URL/Windows path), treat as named
     * 4. Return true if ANY segment matches named pattern
     * 
     * Edge Cases:
     * - "http://example.com/logo.png" → NOT named (URL protocol)
     * - "C:\path\to\file.png" → NOT named (Windows drive letter)
     * - "src:logo.png|70" → Named (has known prefix)
     * - "logo.png|70|center" → Positional (no known prefixes)
     * 
     * @param string $spec Specification string
     * @return bool True if named syntax detected
     */
    public function isNamedSyntax(string $spec): bool
    {
        $segments = explode('|', $spec);
        
        foreach ($segments as $segment) {
            $segment = trim($segment);
            
            // Skip empty segments
            if (empty($segment)) {
                continue;
            }
            
            // Check for colon separator
            if (strpos($segment, ':') === false) {
                continue;
            }
            
            // Extract potential key (everything before first colon)
            $parts = explode(':', $segment, 2);
            $key = strtolower(trim($parts[0]));
            
            // Skip URL protocols (http, https, ftp, etc.)
            if (in_array($key, ['http', 'https', 'ftp', 'ftps', 'file', 'ssh', 'sftp'])) {
                continue;
            }
            
            // Skip Windows drive letters (single character followed by colon)
            if (strlen($key) === 1 && ctype_alpha($key)) {
                continue;
            }
            
            // Check if key matches known prefix
            if (in_array($key, self::KNOWN_PREFIXES)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Parse named syntax specification
     * 
     * Format: "key:value|key:value|..."
     * 
     * Examples:
     * - "src:logo.png|opacity:70"
     * - "mode:smart|ratio:16:9|focus:center"  (note: value can contain colons)
     * - "placement:bottom-right|offset:10,10"
     * 
     * @param string $spec Named specification string
     * @return array Parsed parameters as associative array
     */
    public function parseNamedSyntax(string $spec): array
    {
        $params = [];
        $segments = explode('|', $spec);
        
        foreach ($segments as $segment) {
            $segment = trim($segment);
            
            // Skip empty segments
            if (empty($segment)) {
                continue;
            }
            
            // Split on first colon only (value may contain colons)
            $parts = explode(':', $segment, 2);
            
            if (count($parts) === 2) {
                $key = strtolower(trim($parts[0]));
                $value = trim($parts[1]);
                $params[$key] = $value;
            }
        }
        
        return $params;
    }
    
    /**
     * Parse positional syntax specification (legacy)
     * 
     * Format: "value1|value2|value3|..."
     * 
     * Maps indexed array to named keys using positionalMapping.
     * 
     * Examples:
     * - "logo.png|400,300|70" with mapping [0=>'src', 1=>'minimum_dimensions', 2=>'opacity']
     *   → ['src' => 'logo.png', 'minimum_dimensions' => '400,300', 'opacity' => '70']
     * 
     * @param string $spec Positional specification string
     * @param array $positionalMapping Index to key mapping
     * @return array Parsed parameters as associative array
     */
    public function parsePositionalSyntax(string $spec, array $positionalMapping): array
    {
        $params = [];
        $segments = explode('|', $spec);
        
        foreach ($segments as $index => $segment) {
            $segment = trim($segment);
            
            // Map positional index to named key
            if (isset($positionalMapping[$index])) {
                $key = $positionalMapping[$index];
                $params[$key] = $segment;
            }
        }
        
        return $params;
    }
    
    /**
     * Build specification string from parameter array
     * 
     * Used by preset editor to convert form input back to spec string.
     * Supports both named and positional output formats.
     * 
     * @param array $params Parameter array (associative)
     * @param string $syntax Desired syntax ('named' or 'positional')
     * @param array $positionalMapping For positional syntax, key to index mapping:
     *   ['src' => 0, 'minimum_dimensions' => 1, 'opacity' => 2, ...]
     * 
     * @return string Specification string
     */
    public function build(array $params, string $syntax = 'named', array $positionalMapping = []): string
    {
        if ($syntax === 'named') {
            return $this->buildNamedSyntax($params);
        } else {
            return $this->buildPositionalSyntax($params, $positionalMapping);
        }
    }
    
    /**
     * Build named syntax string
     * 
     * Format: "key:value|key:value|..."
     * 
     * Skips parameters with null or empty string values.
     * 
     * @param array $params Parameter array
     * @return string Named syntax string
     */
    private function buildNamedSyntax(array $params): string
    {
        $segments = [];
        
        foreach ($params as $key => $value) {
            // Skip null and empty string values
            if ($value !== null && $value !== '') {
                $segments[] = $key . ':' . $value;
            }
        }
        
        return implode('|', $segments);
    }
    
    /**
     * Build positional syntax string (legacy)
     * 
     * Format: "value1|value2|value3|..."
     * 
     * Uses positionalMapping to determine value order.
     * Fills gaps with empty strings to maintain position integrity.
     * 
     * @param array $params Parameter array (associative)
     * @param array $positionalMapping Key to index mapping
     * @return string Positional syntax string
     */
    private function buildPositionalSyntax(array $params, array $positionalMapping): string
    {
        // Reverse mapping (key => index)
        $keyToIndex = array_flip($positionalMapping);
        
        // Build indexed array
        $indexed = [];
        foreach ($params as $key => $value) {
            if (isset($keyToIndex[$key])) {
                $indexed[$keyToIndex[$key]] = $value;
            }
        }
        
        // If no indexed values, return empty string
        if (empty($indexed)) {
            return '';
        }
        
        // Fill gaps with empty strings to maintain position
        ksort($indexed);
        $maxIndex = max(array_keys($indexed));
        for ($i = 0; $i <= $maxIndex; $i++) {
            if (!isset($indexed[$i])) {
                $indexed[$i] = '';
            }
        }
        
        return implode('|', $indexed);
    }
}
