<?php

namespace Grav\Plugin\Form\Captcha;

use Grav\Common\Grav;

class BasicCaptcha
{
    protected $session = null;
    protected $key = 'basic_captcha_value';
    protected $typeKey = 'basic_captcha_type';
    protected $config = null;

    public function __construct($fieldConfig = null)
    {
        $this->session = Grav::instance()['session'];

        // Load global configuration
        $globalConfig = Grav::instance()['config']->get('plugins.form.basic_captcha', []);

        // Merge field-specific config with global config
        if ($fieldConfig && is_array($fieldConfig)) {
            $this->config = array_replace_recursive($globalConfig, $fieldConfig);
        } else {
            $this->config = $globalConfig;
        }
    }

    public function getCaptchaCode($length = null): string
    {
        // Support both 'type' (from global config) and 'captcha_type' (from field config)
        $type = $this->config['captcha_type'] ?? $this->config['type'] ?? 'characters';

        // Store the captcha type in session for validation
        $this->setSession($this->typeKey, $type);

        switch ($type) {
            case 'dotcount':
                return $this->getDotCountCaptcha($this->config);
            case 'position':
                return $this->getPositionCaptcha($this->config);
            case 'math':
                return $this->getMathCaptcha($this->config);
            case 'characters':
            default:
                return $this->getCharactersCaptcha($this->config, $length);
        }
    }

    /**
     * Creates a dot counting captcha - user has to count dots of a specific color
     */
    protected function getDotCountCaptcha($config): string
    {
        // Define colors with names
        $colors = [
            'red' => [255, 0, 0],
            'blue' => [0, 0, 255],
            'green' => [0, 128, 0],
            'yellow' => [255, 255, 0],
            'purple' => [128, 0, 128],
            'orange' => [255, 165, 0]
        ];

        // Pick a random color to count
        $colorNames = array_keys($colors);
        $targetColorName = $colorNames[array_rand($colorNames)];
        $targetColor = $colors[$targetColorName];

        // Generate a random number of dots for the target color (between 5-10)
        $targetCount = mt_rand(5, 10);

        // Store the expected answer
        $this->setSession($this->key, (string) $targetCount);

        // Return description text
        return "count_dots|{$targetColorName}|".implode(',', $targetColor);
    }

    /**
     * Creates a position-based captcha - user has to identify position of a symbol
     */
    protected function getPositionCaptcha($config): string
    {
        // Define possible symbols - using simple ASCII characters
        $symbols = ['*', '+', '$', '#', '@', '!', '?', '%', '&', '='];

        // Define positions - simpler options
        $positions = ['top', 'bottom', 'left', 'right', 'center'];

        // Pick a random symbol and position
        $targetSymbol = $symbols[array_rand($symbols)];
        $targetPosition = $positions[array_rand($positions)];

        // Store the expected answer
        $this->setSession($this->key, $targetPosition);

        // Return the instruction and symbol
        return "position|{$targetSymbol}|{$targetPosition}";
    }

    /**
     * Creates a math-based captcha
     */
    protected function getMathCaptcha($config): string
    {
        $min = $config['math']['min'] ?? 1;
        $max = $config['math']['max'] ?? 12;
        $operators = $config['math']['operators'] ?? ['+', '-', '*'];

        $first_num = random_int($min, $max);
        $second_num = random_int($min, $max);
        $operator = $operators[array_rand($operators)];

        // Calculator
        if ($operator === '-') {
            if ($first_num < $second_num) {
                $result = "$second_num - $first_num";
                $captcha_code = $second_num - $first_num;
            } else {
                $result = "$first_num - $second_num";
                $captcha_code = $first_num - $second_num;
            }
        } elseif ($operator === '*') {
            $result = "{$first_num} x {$second_num}";
            $captcha_code = $first_num * $second_num;
        } elseif ($operator === '+') {
            $result = "$first_num + $second_num";
            $captcha_code = $first_num + $second_num;
        }

        $this->setSession($this->key, (string) $captcha_code);
        return $result;
    }

    /**
     * Creates a character-based captcha
     */
    protected function getCharactersCaptcha($config, $length = null): string
    {
        if ($length === null) {
            $length = $config['chars']['length'] ?? 6;
        }

        // Use more complex character set with mixed case and exclude similar-looking characters
        $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
        $captcha_code = '';

        // Generate random characters
        for ($i = 0; $i < $length; $i++) {
            $captcha_code .= $chars[random_int(0, strlen($chars) - 1)];
        }

        $this->setSession($this->key, $captcha_code);
        return $captcha_code;
    }

    public function setSession($key, $value): void
    {
        $this->session->$key = $value;
    }

    public function getSession($key = null): ?string
    {
        if ($key === null) {
            $key = $this->key;
        }
        return $this->session->$key ?? null;
    }

    /**
     * Create captcha image based on the type
     */
    public function createCaptchaImage($captcha_code)
    {
        // Determine image dimensions based on type
        $isCharacterCaptcha = false;
        if (strpos((string) $captcha_code, '|') === false && !preg_match('/[\+\-x]/', (string) $captcha_code)) {
            $isCharacterCaptcha = true;
        }

        // Use box_width/box_height for character captchas if specified, otherwise use default image dimensions
        if ($isCharacterCaptcha && isset($this->config['chars']['box_width'])) {
            $width = $this->config['chars']['box_width'];
        } else {
            $width = $this->config['image']['width'] ?? 135;
        }

        if ($isCharacterCaptcha && isset($this->config['chars']['box_height'])) {
            $height = $this->config['chars']['box_height'];
        } else {
            $height = $this->config['image']['height'] ?? 40;
        }

        // Create a blank image
        $image = imagecreatetruecolor($width, $height);

        // Set background color (support both image.bg and chars.bg for character captchas)
        $bgColor = '#ffffff';
        if ($isCharacterCaptcha && isset($this->config['chars']['bg'])) {
            $bgColor = $this->config['chars']['bg'];
        } elseif (isset($this->config['image']['bg'])) {
            $bgColor = $this->config['image']['bg'];
        }

        $bg = $this->hexToRgb($bgColor);
        $backgroundColor = imagecolorallocate($image, $bg[0], $bg[1], $bg[2]);
        imagefill($image, 0, 0, $backgroundColor);

        // Parse the captcha code to determine type
        if (strpos((string) $captcha_code, '|') !== false) {
            $parts = explode('|', (string) $captcha_code);
            $type = $parts[0];

            switch ($type) {
                case 'count_dots':
                    return $this->createDotCountImage($image, $parts, $this->config);
                case 'position':
                    return $this->createPositionImage($image, $parts, $this->config);
            }
        } else {
            // Assume it's a character or math captcha if no type indicator
            if (preg_match('/[\+\-x]/', (string) $captcha_code)) {
                return $this->createMathImage($image, $captcha_code, $this->config);
            } else {
                return $this->createCharacterImage($image, $captcha_code, $this->config);
            }
        }

        return $image;
    }

    /**
     * Create image for dot counting captcha
     */
    protected function createDotCountImage($image, $parts, $config)
    {
        $colorName = $parts[1];
        $targetColorRGB = explode(',', (string) $parts[2]);

        $width = imagesx($image);
        $height = imagesy($image);

        // Allocate target color
        $targetColor = imagecolorallocate($image, $targetColorRGB[0], $targetColorRGB[1], $targetColorRGB[2]);

        // Create other distraction colors
        $distractionColors = [];
        $colorOptions = [
            [255, 0, 0],    // red
            [0, 0, 255],    // blue
            [0, 128, 0],    // green
            [255, 255, 0],  // yellow
            [128, 0, 128],  // purple
            [255, 165, 0]   // orange
        ];

        foreach ($colorOptions as $rgb) {
            if ($rgb[0] != $targetColorRGB[0] || $rgb[1] != $targetColorRGB[1] || $rgb[2] != $targetColorRGB[2]) {
                $distractionColors[] = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
            }
        }

        // Get target count from session
        $targetCount = (int) $this->getSession();

        // Draw instruction text
        $fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
        $black = imagecolorallocate($image, 0, 0, 0);
        imagettftext($image, 10, 0, 5, 15, $black, $fontPath, "Count {$colorName}:");

        // Simplified approach to prevent overlapping
        // Divide the image into a grid and place one dot per cell
        $gridCells = [];
        $gridRows = 2;
        $gridCols = 4;

        // Build available grid cells
        for ($y = 0; $y < $gridRows; $y++) {
            for ($x = 0; $x < $gridCols; $x++) {
                $gridCells[] = [$x, $y];
            }
        }

        // Shuffle grid cells for random placement
        shuffle($gridCells);

        // Calculate cell dimensions
        $cellWidth = ($width - 20) / $gridCols;
        $cellHeight = ($height - 20) / $gridRows;

        // Dot size for better visibility
        $dotSize = 8;

        // Draw target dots first (taking the first N cells)
        for ($i = 0; $i < $targetCount && $i < count($gridCells); $i++) {
            $cell = $gridCells[$i];
            $gridX = $cell[0];
            $gridY = $cell[1];

            // Calculate center position of cell with small random offset
            $x = 10 + ($gridX + 0.5) * $cellWidth + mt_rand(-2, 2);
            $y = 20 + ($gridY + 0.5) * $cellHeight + mt_rand(-2, 2);

            // Draw the dot
            imagefilledellipse($image, $x, $y, $dotSize, $dotSize, $targetColor);

            // Add a small border for better contrast
            imageellipse($image, $x, $y, $dotSize + 2, $dotSize + 2, $black);
        }

        // Draw distraction dots using remaining grid cells
        $distractionCount = min(mt_rand(8, 15), count($gridCells) - $targetCount);

        for ($i = 0; $i < $distractionCount; $i++) {
            // Get the next available cell
            $cellIndex = $targetCount + $i;

            if ($cellIndex >= count($gridCells)) {
                break; // No more cells available
            }

            $cell = $gridCells[$cellIndex];
            $gridX = $cell[0];
            $gridY = $cell[1];

            // Calculate center position of cell with small random offset
            $x = 10 + ($gridX + 0.5) * $cellWidth + mt_rand(-2, 2);
            $y = 20 + ($gridY + 0.5) * $cellHeight + mt_rand(-2, 2);

            // Draw the dot with a random distraction color
            $color = $distractionColors[array_rand($distractionColors)];
            imagefilledellipse($image, $x, $y, $dotSize, $dotSize, $color);
        }

        // Add subtle grid lines to help with counting
        $lightGray = imagecolorallocate($image, 230, 230, 230);
        for ($i = 1; $i < $gridCols; $i++) {
            imageline($image, 10 + $i * $cellWidth, 20, 10 + $i * $cellWidth, $height - 5, $lightGray);
        }
        for ($i = 1; $i < $gridRows; $i++) {
            imageline($image, 10, 20 + $i * $cellHeight, $width - 10, 20 + $i * $cellHeight, $lightGray);
        }

        // Add minimal noise
        $this->addImageNoise($image, 15);

        return $image;
    }

    /**
     * Create image for position captcha
     */
    protected function createPositionImage($image, $parts, $config)
    {
        $symbol = $parts[1];
        $position = $parts[2];

        $width = imagesx($image);
        $height = imagesy($image);

        // Allocate colors
        $black = imagecolorallocate($image, 0, 0, 0);
        $red = imagecolorallocate($image, 255, 0, 0);

        // Draw instruction text
        $fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
        imagettftext($image, 9, 0, 5, 15, $black, $fontPath, "Position of symbol?");

        // Determine symbol position based on the target position
        $symbolX = $width / 2;
        $symbolY = $height / 2;

        switch ($position) {
            case 'top':
                $symbolX = $width / 2;
                $symbolY = 20;
                break;
            case 'bottom':
                $symbolX = $width / 2;
                $symbolY = $height - 10;
                break;
            case 'left':
                $symbolX = 20;
                $symbolY = $height / 2;
                break;
            case 'right':
                $symbolX = $width - 20;
                $symbolY = $height / 2;
                break;
            case 'center':
                $symbolX = $width / 2;
                $symbolY = $height / 2;
                break;
        }

        // Draw the symbol - make it larger and in red for visibility
        imagettftext($image, 20, 0, $symbolX - 8, $symbolY + 8, $red, $fontPath, (string) $symbol);

        // Draw a grid to make positions clearer
        $gray = imagecolorallocate($image, 200, 200, 200);
        imageline($image, $width / 2, 15, $width / 2, $height - 5, $gray);
        imageline($image, 5, $height / 2, $width - 5, $height / 2, $gray);

        // Add minimal noise
        $this->addImageNoise($image, 10);

        return $image;
    }

    /**
     * Create image for math captcha
     */
    protected function createMathImage($image, $mathExpression, $config)
    {
        $width = imagesx($image);
        $height = imagesy($image);

        // Get font and colors
        $fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
        $textColor = imagecolorallocate($image, 0, 0, 0);

        // Draw the math expression
        $fontSize = 16;
        $textBox = imagettfbbox($fontSize, 0, $fontPath, (string) $mathExpression);
        $textWidth = $textBox[2] - $textBox[0];
        $textHeight = $textBox[1] - $textBox[7];
        $textX = ($width - $textWidth) / 2;
        $textY = ($height + $textHeight) / 2;

        imagettftext($image, $fontSize, 0, $textX, $textY, $textColor, $fontPath, (string) $mathExpression);

        // Add visual noise and distortions to prevent OCR
        $this->addImageNoise($image, 25);
        $this->addWaveDistortion($image);

        return $image;
    }

    /**
     * Create image for character captcha
     */
    protected function createCharacterImage($image, $captcha_code, $config)
    {
        $width = imagesx($image);
        $height = imagesy($image);

        // Get font settings with support for custom box dimensions, position, and colors
        $fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
        $fontSize = $config['chars']['size'] ?? 16;

        // Support custom text color (defaults to black)
        $textColorHex = $config['chars']['text'] ?? '#000000';
        $textRgb = $this->hexToRgb($textColorHex);
        $textColor = imagecolorallocate($image, $textRgb[0], $textRgb[1], $textRgb[2]);

        // Support custom start position (useful for fine-tuning text placement)
        $startX = $config['chars']['start_x'] ?? ($width / (strlen((string) $captcha_code) + 2));
        $baseY = $config['chars']['start_y'] ?? ($height / 2 + 5);

        // Draw each character with random rotation and position
        $charWidth = $width / (strlen((string) $captcha_code) + 2);

        for ($i = 0; $i < strlen((string) $captcha_code); $i++) {
            $char = $captcha_code[$i];
            $angle = mt_rand(-15, 15); // Random rotation

            // Random vertical position with custom base Y
            $y = $baseY + mt_rand(-5, 5);

            imagettftext($image, $fontSize, $angle, $startX, $y, $textColor, $fontPath, (string) $char);

            // Move to next character position with some randomness
            $startX += $charWidth + mt_rand(-5, 5);
        }

        // Add visual noise and distortions
        $this->addImageNoise($image, 25);
        $this->addWaveDistortion($image);

        return $image;
    }

    /**
     * Add random noise to the image
     */
    protected function addImageNoise($image, $density = 100)
    {
        $width = imagesx($image);
        $height = imagesy($image);

        // For performance, reduce density
        $density = min($density, 30);

        // Add random dots
        for ($i = 0; $i < $density; $i++) {
            $x = mt_rand(0, $width - 1);
            $y = mt_rand(0, $height - 1);
            $shade = mt_rand(150, 200);
            $color = imagecolorallocate($image, $shade, $shade, $shade);
            imagesetpixel($image, $x, $y, $color);
        }

        // Add a few random lines
        $lineCount = min(3, mt_rand(2, 3));
        for ($i = 0; $i < $lineCount; $i++) {
            $x1 = mt_rand(0, $width / 4);
            $y1 = mt_rand(0, $height - 1);
            $x2 = mt_rand(3 * $width / 4, $width - 1);
            $y2 = mt_rand(0, $height - 1);
            $shade = mt_rand(150, 200);
            $color = imagecolorallocate($image, $shade, $shade, $shade);
            imageline($image, $x1, $y1, $x2, $y2, $color);
        }
    }

    /**
     * Add wave distortion to the image
     */
    protected function addWaveDistortion($image)
    {
        $width = imagesx($image);
        $height = imagesy($image);

        // Create temporary image
        $temp = imagecreatetruecolor($width, $height);
        $bg = imagecolorallocate($temp, 255, 255, 255);
        imagefill($temp, 0, 0, $bg);

        // Copy original to temp
        imagecopy($temp, $image, 0, 0, 0, 0, $width, $height);

        // Clear original image
        $bg = imagecolorallocate($image, 255, 255, 255);
        imagefill($image, 0, 0, $bg);

        // Apply simplified wave distortion
        $amplitude = mt_rand(1, 2);
        $period = mt_rand(10, 15);

        // Process only every 2nd pixel for better performance
        for ($x = 0; $x < $width; $x += 2) {
            $wave = sin($x / $period) * $amplitude;

            for ($y = 0; $y < $height; $y += 2) {
                $yp = $y + $wave;

                if ($yp >= 0 && $yp < $height) {
                    $color = imagecolorat($temp, $x, $yp);
                    imagesetpixel($image, $x, $y, $color);

                    // Fill adjacent pixel for better performance
                    if ($x + 1 < $width && $y + 1 < $height) {
                        imagesetpixel($image, $x + 1, $y, $color);
                    }
                }
            }
        }

        imagedestroy($temp);
    }

    public function renderCaptchaImage($imageData): void
    {
        header("Content-type: image/jpeg");
        imagejpeg($imageData);
    }

    public function validateCaptcha($formData): bool
    {
        $isValid = false;
        $capchaSessionData = $this->getSession();

        // Make validation case-insensitive
        if (strtolower((string) $capchaSessionData) == strtolower((string) $formData)) {
            $isValid = true;
        }

        // Debug validation if enabled
        $grav = Grav::instance();
        if ($grav['config']->get('plugins.form.basic_captcha.debug', false)) {
            $grav['log']->debug("Captcha Validation - Expected: '{$capchaSessionData}', Got: '{$formData}', Result: ".
                ($isValid ? 'valid' : 'invalid'));
        }

        // Regenerate a new captcha after validation
        $this->setSession($this->key, null);

        return $isValid;
    }

    private function hexToRgb($hex): array
    {
        return sscanf($hex, "#%02x%02x%02x");
    }
}