<?php

/**
 * JCOGS Image Pro - Crop Filter
 * =============================
 * Native EE7 implementation pipeline architecture
 * 
 * @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\Filters;

use Imagine\Image\ImageInterface;
use Imagine\Image\Box;
use Imagine\Image\Point;
use JCOGSDesign\JCOGSImagePro\Service\Pipeline\Context;

/**
 * Crop Transformation Filter
 * 
 * Applies crop transformations to images with support for:
 * - Position-based cropping (center, left, right, top, bottom)
 * - Face detection cropping
 * - Smart scaling with crop
 * - Offset adjustments
 * - Legacy parameter compatibility
 */
class Crop extends ImageAbstractFilter
{
    private Box $target_size;
    private Context $context;

    /**
     * Constructor
     * 
     * @param Box $target_size Target crop dimensions
     * @param Context $context Processing context with crop parameters
     */
    public function __construct(Box $target_size, Context $context)
    {
        parent::__construct(); // Initialize shared services
        $this->target_size = $target_size;
        $this->context = $context;
        
    }

    /**
     * Apply crop transformation to image
     * 
     * @param ImageInterface $image Source image
     * @return ImageInterface Cropped image
     */
    public function apply(ImageInterface $image): ImageInterface
    {
        // For crop operations, we need to calculate the correct target dimensions
        // EXCEPT for face detection mode, which should ignore width/height parameters
        $crop_params = $this->context->get_metadata_value('crop_parsed_params', null);
        $is_face_detect_mode = $crop_params && $crop_params['mode'] === 'f';
        
        $target_size = $this->target_size;

        // Only treat 100x100 as an internal placeholder when no explicit width/height
        // parameters were provided. Legitimate max-constrained outputs can also be 100x100
        // and must not be overridden back to raw width/height tag params.
        $width_param = trim((string) $this->context->get_param('width', ''));
        $height_param = trim((string) $this->context->get_param('height', ''));
        $has_explicit_size = ($width_param !== '' || $height_param !== '');

        if (!$is_face_detect_mode && !$has_explicit_size && $target_size->getWidth() == 100 && $target_size->getHeight() == 100) {
            // This was a placeholder - calculate the real target dimensions for crop
            $target_size = $this->calculate_crop_target_dimensions();
        }

        if ($this->utilities_service) {
            // Detailed technical output only
            $this->utilities_service->debug_message(sprintf(
                'Applying crop transformation to %dx%d',
                $target_size->getWidth(),
                $target_size->getHeight()
            ), null, false, 'detailed');
        }

        try {
            // If a structured crop rectangle is provided (percent-of-source), honour it.
            // This enables the field editor's "close crop" behaviour (zoom + pan).
            $crop_rect = $this->context->get_param('crop_rect');
            $crop_rect_parsed = $this->parse_crop_rect_percent($crop_rect);

            if ($crop_rect_parsed !== null) {
                $current_size = $image->getSize();
                $current_width = (int) $current_size->getWidth();
                $current_height = (int) $current_size->getHeight();

                $left_pct = $crop_rect_parsed['left'];
                $top_pct = $crop_rect_parsed['top'];
                $width_pct = $crop_rect_parsed['width'];
                $height_pct = $crop_rect_parsed['height'];

                // Convert rect to pixel dimensions on the *current* image.
                // crop_rect is defined as percent-of-source (left,top,width,height).
                $rect_w = ($current_width > 0) ? ($current_width * $width_pct / 100.0) : 0.0;
                $rect_h = ($current_height > 0) ? ($current_height * $height_pct / 100.0) : 0.0;

                if ($rect_w > 1.0 && $rect_h > 1.0) {
                    $target_w = (int) $target_size->getWidth();
                    $target_h = (int) $target_size->getHeight();

                    // Crop the selected rectangle from the current (ideally original-resolution)
                    // working image FIRST, then resize the cropped region to the requested output.
                    // This avoids pre-resizing the entire source image to max_width and then
                    // attempting to "zoom" back in, which causes unnecessary pixelation.
                    $crop_x = (int) round($current_width * $left_pct / 100.0);
                    $crop_y = (int) round($current_height * $top_pct / 100.0);
                    $crop_w = (int) round($rect_w);
                    $crop_h = (int) round($rect_h);

                    $crop_w = max(1, min($crop_w, $current_width));
                    $crop_h = max(1, min($crop_h, $current_height));
                    $crop_x = max(0, min($crop_x, $current_width - $crop_w));
                    $crop_y = max(0, min($crop_y, $current_height - $crop_h));

                    if ($this->utilities_service) {
                        $this->utilities_service->debug_message(sprintf(
                            'crop_rect crop region at %d,%d size %dx%d (source %dx%d) → resize to %dx%d',
                            $crop_x,
                            $crop_y,
                            $crop_w,
                            $crop_h,
                            $current_width,
                            $current_height,
                            $target_w,
                            $target_h
                        ), null, false, 'detailed');
                    }

                    $cropped = $image->crop(new Point($crop_x, $crop_y), new Box($crop_w, $crop_h));

                    // Resize cropped region to final output.
                    // When aspect ratio is locked in the UI, this should be a proportional resize.
                    if ($crop_w !== $target_w || $crop_h !== $target_h) {
                        $cropped = $cropped->resize(new Box(max(1, $target_w), max(1, $target_h)));
                    }

                    return $cropped;
                }
            }

            // Smart-scale processing has been moved to pipeline stage before face detection
            // for Legacy compatibility and performance optimization
            
            // Check if preliminary smart-scale has already been applied (no message needed)
            $preliminary_smart_scale_applied = $this->context->get_flag('preliminary_smart_scale_applied', false);
            
            // Check if face detection is needed
            $needs_face_detection = false;
            $face_detection_service = null;
            $face_regions = [];
            
            if ($crop_params) {
                $position = $crop_params['position'] ?? ['center', 'center'];
                $needs_face_detection = in_array('face_detect', $position) || $crop_params['mode'] === 'f';
            }

            // Handle face detection if needed
            if ($needs_face_detection) {
                $face_regions = $this->detect_faces_if_needed($image);
            }

            // Calculate crop position
            $current_size = $image->getSize();
            $crop_position = $this->calculate_crop_position($current_size, $target_size);

            // Crop position details only at detailed verbosity
            if ($this->utilities_service) {
                $this->utilities_service->debug_message(sprintf(
                    'Cropping image from position %d,%d with size %dx%d',
                    $crop_position->getX(),
                    $crop_position->getY(),
                    $target_size->getWidth(),
                    $target_size->getHeight()
                ), null, false, 'detailed');
            }

            // Apply the crop
            return $image->crop($crop_position, $target_size);

        } catch (\Exception $e) {
            if ($this->utilities_service) {
                $this->utilities_service->debug_error('crop_transformation_failed', $e);
            }
            throw new \Exception("Failed to crop image: " . $e->getMessage());
        }
    }

    /**
     * Parse crop_rect param into percent floats.
     * Accepts:
     * - "left,top,width,height" (preferred)
     * - JSON object string: {"left":...,"top":...,"width":...,"height":...}
     */
    private function parse_crop_rect_percent($value): ?array
    {
        if (!is_string($value)) {
            return null;
        }

        $raw = trim($value);
        if ($raw === '') {
            return null;
        }

        $left = null;
        $top = null;
        $width = null;
        $height = null;

        if (str_starts_with($raw, '{')) {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                $left = isset($decoded['left']) ? (float) $decoded['left'] : null;
                $top = isset($decoded['top']) ? (float) $decoded['top'] : null;
                $width = isset($decoded['width']) ? (float) $decoded['width'] : null;
                $height = isset($decoded['height']) ? (float) $decoded['height'] : null;
            }
        } else {
            $parts = array_map('trim', explode(',', $raw));
            if (count($parts) >= 4) {
                $left = is_numeric($parts[0]) ? (float) $parts[0] : null;
                $top = is_numeric($parts[1]) ? (float) $parts[1] : null;
                $width = is_numeric($parts[2]) ? (float) $parts[2] : null;
                $height = is_numeric($parts[3]) ? (float) $parts[3] : null;
            }
        }

        if ($left === null || $top === null || $width === null || $height === null) {
            return null;
        }

        if (!is_finite($left) || !is_finite($top) || !is_finite($width) || !is_finite($height)) {
            return null;
        }

        // Clamp to sane ranges.
        $left = max(0.0, min(100.0, $left));
        $top = max(0.0, min(100.0, $top));
        $width = max(0.1, min(100.0, $width));
        $height = max(0.1, min(100.0, $height));

        return [
            'left' => $left,
            'top' => $top,
            'width' => $width,
            'height' => $height,
        ];
    }

    /**
     * Calculate crop target dimensions
     * 
     * @return Box Target crop dimensions
     */
    private function calculate_crop_target_dimensions(): Box
    {
        // Get width and height parameters
        $width = $this->context->get_param('width');
        $height = $this->context->get_param('height');

        // Convert to integers
        $target_width = $width ? (int)$width : 100;
        $target_height = $height ? (int)$height : 100;

        return new Box(max(1, $target_width), max(1, $target_height));
    }

    /**
     * Detect faces if needed for crop positioning
     * 
     * @param ImageInterface $image Source image
     * @return array Face regions data
     */
    private function detect_faces_if_needed(ImageInterface $image): array
    {
        // Get existing face regions from context if available
        $face_regions = $this->context->get_metadata_value('face_regions', []);
        
        // If no face regions cached, detect them now
        if (empty($face_regions)) {
            if ($this->utilities_service) {
                $this->utilities_service->debug_message('Detecting faces for crop positioning');
            }
            
            try {
                $face_detection_service = \JCOGSDesign\JCOGSImagePro\Service\ServiceCache::face_detection();
                
                // Get GD resource from Imagine object for face detection
                $gd_resource = imagecreatefromstring($image->__toString());
                if ($gd_resource) {
                    $face_regions = $face_detection_service->detect_faces($gd_resource);
                    
                    // Cache face regions in context for reuse by other operations
                    // Store both the face regions AND the image dimensions when detected
                    $this->context->set_metadata('face_regions', $face_regions);
                    $this->context->set_metadata('face_regions_image_size', [
                        'width' => imagesx($gd_resource),
                        'height' => imagesy($gd_resource)
                    ]);
                    
                    // Also store in a global context for cross-operation sharing
                    $this->context->set_metadata('original_face_regions', $face_regions);
                    $this->context->set_metadata('original_image_size', [
                        'width' => imagesx($gd_resource),
                        'height' => imagesy($gd_resource)
                    ]);
                    
                    if ($this->utilities_service) {
                        $this->utilities_service->debug_message(sprintf(
                            'Stored face regions in context: %d faces at %dx%d',
                            count($face_regions),
                            imagesx($gd_resource),
                            imagesy($gd_resource)
                        ));
                    }

                    if ($this->utilities_service) {
                        $this->utilities_service->debug_message('face_detection_results_count', [count($face_regions)]);
                    }
                }
            } catch (\Exception $e) {
                if ($this->utilities_service) {
                    $this->utilities_service->debug_error('face_detection_failed', $e);
                }
                $face_regions = [];
            }
        }
        
        return $face_regions;
    }

    /**
     * Calculate crop position based on parsed crop parameters
     * 
     * @param Box $current_size Current image size
     * @param Box $target_size Target crop size
     * @return Point Crop starting point
     */
    private function calculate_crop_position(Box $current_size, Box $target_size): Point
    {
        // Get parsed crop parameters, or use defaults if not available
        $crop_params = $this->context->get_metadata_value('crop_parsed_params', [
            'position' => ['center', 'center'],
            'offset' => [0, 0]
        ]);
        
        $position = $crop_params['position'] ?? ['center', 'center'];
        $offset = $crop_params['offset'] ?? [0, 0];
        
        // Check if face detection is needed for positioning
        $needs_face_detection = in_array('face_detect', $position);
        $face_centroid = null;
        
        if ($needs_face_detection) {
            $face_regions = $this->context->get_metadata_value('face_regions', []);
            
            // Get face centroid if faces were detected
            if (!empty($face_regions)) {
                $face_detection_service = \JCOGSDesign\JCOGSImagePro\Service\ServiceCache::face_detection();
                $face_centroid = $face_detection_service->get_centroid($face_regions);
                
                // CRITICAL: Scale face coordinates if image has been resized since detection
                // Face detection runs on source image (e.g., 800×571), but crop may happen
                // on smart-scaled image (e.g., 140×100). Need to scale coordinates accordingly.
                $face_regions_size = $this->context->get_metadata_value('face_regions_image_size', null);
                $current_width = $current_size->getWidth();
                $current_height = $current_size->getHeight();
                
                if ($face_regions_size && 
                    ($face_regions_size['width'] != $current_width || $face_regions_size['height'] != $current_height)) {
                    
                    $scale_x = $current_width / $face_regions_size['width'];
                    $scale_y = $current_height / $face_regions_size['height'];
                    
                    $face_centroid['x'] = (int)round($face_centroid['x'] * $scale_x);
                    $face_centroid['y'] = (int)round($face_centroid['y'] * $scale_y);
                    
                    if ($this->utilities_service) {
                        $this->utilities_service->debug_message(sprintf(
                            'Scaled face centroid from %dx%d to %dx%d: (%d, %d) → (%d, %d)',
                            $face_regions_size['width'], $face_regions_size['height'],
                            $current_width, $current_height,
                            (int)round($face_centroid['x'] / $scale_x), (int)round($face_centroid['y'] / $scale_y),
                            $face_centroid['x'], $face_centroid['y']
                        ));
                    }
                }
                
                if ($this->utilities_service) {
                    $this->utilities_service->debug_message(sprintf(
                        'Face centroid calculated at (%d, %d)',
                        $face_centroid['x'] ?? 0,
                        $face_centroid['y'] ?? 0
                    ));
                }
            } else {
                if ($this->utilities_service) {
                    $this->utilities_service->debug_message('No faces detected for positioning, falling back to center');
                }
            }
        }
        
        $current_width = $current_size->getWidth();
        $current_height = $current_size->getHeight();
        $target_width = $target_size->getWidth();
        $target_height = $target_size->getHeight();
        
        // Calculate base position depending on alignment, with face detection support
        $crop_x = $this->calculate_position_coordinate_with_faces(
            $position[0], $current_width, $target_width, 
            $face_centroid ? $face_centroid['x'] : null
        );
        $crop_y = $this->calculate_position_coordinate_with_faces(
            $position[1], $current_height, $target_height, 
            $face_centroid ? $face_centroid['y'] : null
        );
        
        // Apply offset adjustments
        $crop_x += $offset[0];
        $crop_y += $offset[1];
        
        // Ensure crop position stays within image boundaries
        $crop_x = max(0, min($crop_x, $current_width - $target_width));
        $crop_y = max(0, min($crop_y, $current_height - $target_height));

        // Detailed crop position calculation (only at detailed verbosity)
        if ($this->utilities_service) {
            $this->utilities_service->debug_message(sprintf(
                'Crop position calculation: %s,%s with offset %d,%d = final position %d,%d',
                $position[0], $position[1], $offset[0], $offset[1], $crop_x, $crop_y
            ), null, false, 'detailed');
        }
        
        return new Point($crop_x, $crop_y);
    }

    /**
     * Calculate position coordinate for crop alignment with face detection support
     * 
     * @param string $alignment 'left'/'top', 'center', 'right'/'bottom', 'face_detect'
     * @param int $current_dimension Current image dimension (width or height)
     * @param int $target_dimension Target crop dimension
     * @param int|null $face_coordinate Face centroid coordinate (if available)
     * @return int Position coordinate
     */
    private function calculate_position_coordinate_with_faces(string $alignment, int $current_dimension, int $target_dimension, ?int $face_coordinate): int
    {
        if ($alignment === 'face_detect' && $face_coordinate !== null) {
            // Center the crop on the face coordinate
            $position = $face_coordinate - (int)round($target_dimension / 2);
            // Ensure it stays within image bounds
            return max(0, min($position, $current_dimension - $target_dimension));
        } else if ($alignment === 'face_detect') {
            // Fall back to center if no face coordinate available
            return (int)round(($current_dimension - $target_dimension) / 2);
        } else {
            // Use regular position calculation
            return $this->calculate_position_coordinate($alignment, $current_dimension, $target_dimension);
        }
    }

    /**
     * Calculate position coordinate for crop alignment
     * 
     * @param string $alignment 'left'/'top', 'center', 'right'/'bottom'
     * @param int $current_dimension Current image dimension (width or height)
     * @param int $target_dimension Target crop dimension
     * @return int Position coordinate
     */
    private function calculate_position_coordinate(string $alignment, int $current_dimension, int $target_dimension): int
    {
        switch (strtolower($alignment)) {
            case 'left':
            case 'top':
                return 0;
                
            case 'right':
            case 'bottom':
                return $current_dimension - $target_dimension;
                
            case 'center':
            default:
                return (int)round(($current_dimension - $target_dimension) / 2);
        }
    }
}