<?php

declare(strict_types=1);

namespace App\Framework\QrCode\Masking;

use App\Framework\QrCode\Structure\AlignmentPattern;
use App\Framework\QrCode\ValueObjects\Module;
use App\Framework\QrCode\ValueObjects\QrCodeMatrix;

/**
 * QR Code Mask Pattern Evaluator
 *
 * Evaluates all 8 mask patterns and selects the best one based on penalty rules:
 * 1. Adjacent modules in row/column (penalty for runs of 5+)
 * 2. Block of modules (penalty for 2x2 blocks of same color)
 * 3. Specific patterns (penalty for patterns resembling finder patterns)
 * 4. Balance (penalty for uneven dark/light ratio)
 */
final readonly class MaskEvaluator
{
    /**
     * Select best mask pattern based on lowest penalty score
     */
    public function selectBestMask(QrCodeMatrix $matrix): MaskPattern
    {
        $bestPattern = MaskPattern::PATTERN_0;
        $lowestPenalty = PHP_INT_MAX;

        foreach (MaskPattern::all() as $pattern) {
            $maskedMatrix = $this->applyMask($matrix, $pattern);
            $penalty = $this->calculatePenalty($maskedMatrix);

            if ($penalty < $lowestPenalty) {
                $lowestPenalty = $penalty;
                $bestPattern = $pattern;
            }
        }

        return $bestPattern;
    }

    /**
     * Apply mask pattern to matrix (without modifying function patterns)
     */
    public function applyMask(QrCodeMatrix $matrix, MaskPattern $pattern): QrCodeMatrix
    {
        $size = $matrix->getSize();
        $maskedMatrix = $matrix;

        for ($row = 0; $row < $size; $row++) {
            for ($col = 0; $col < $size; $col++) {
                // Skip function patterns (finder, timing, alignment, format info, dark module)
                if ($this->isFunctionPattern($matrix, $row, $col)) {
                    continue;
                }

                if ($pattern->shouldInvert($row, $col)) {
                    $currentModule = $matrix->getModuleAt($row, $col);
                    $invertedModule = $currentModule->isDark()
                        ? Module::light()
                        : Module::dark();
                    $maskedMatrix = $maskedMatrix->setModuleAt($row, $col, $invertedModule);
                }
            }
        }

        return $maskedMatrix;
    }

    /**
     * Calculate total penalty score for masked matrix
     */
    private function calculatePenalty(QrCodeMatrix $matrix): int
    {
        return $this->penaltyRule1($matrix)
            + $this->penaltyRule2($matrix)
            + $this->penaltyRule3($matrix)
            + $this->penaltyRule4($matrix);
    }

    /**
     * Rule 1: Adjacent modules in row/column
     * Penalty: (runLength - 5 + 1) * 3 for each run of 5+ same-color modules
     */
    private function penaltyRule1(QrCodeMatrix $matrix): int
    {
        $size = $matrix->getSize();
        $penalty = 0;

        // Check rows
        for ($row = 0; $row < $size; $row++) {
            $runColor = null;
            $runLength = 0;

            for ($col = 0; $col < $size; $col++) {
                $isDark = $matrix->getModuleAt($row, $col)->isDark();

                if ($isDark === $runColor) {
                    $runLength++;
                } else {
                    if ($runLength >= 5) {
                        $penalty += ($runLength - 5 + 1) * 3;
                    }
                    $runColor = $isDark;
                    $runLength = 1;
                }
            }

            if ($runLength >= 5) {
                $penalty += ($runLength - 5 + 1) * 3;
            }
        }

        // Check columns
        for ($col = 0; $col < $size; $col++) {
            $runColor = null;
            $runLength = 0;

            for ($row = 0; $row < $size; $row++) {
                $isDark = $matrix->getModuleAt($row, $col)->isDark();

                if ($isDark === $runColor) {
                    $runLength++;
                } else {
                    if ($runLength >= 5) {
                        $penalty += ($runLength - 5 + 1) * 3;
                    }
                    $runColor = $isDark;
                    $runLength = 1;
                }
            }

            if ($runLength >= 5) {
                $penalty += ($runLength - 5 + 1) * 3;
            }
        }

        return $penalty;
    }

    /**
     * Rule 2: Block of modules
     * Penalty: 3 for each 2x2 block of same color
     */
    private function penaltyRule2(QrCodeMatrix $matrix): int
    {
        $size = $matrix->getSize();
        $penalty = 0;

        for ($row = 0; $row < $size - 1; $row++) {
            for ($col = 0; $col < $size - 1; $col++) {
                $color = $matrix->getModuleAt($row, $col)->isDark();

                if (
                    $matrix->getModuleAt($row, $col + 1)->isDark() === $color &&
                    $matrix->getModuleAt($row + 1, $col)->isDark() === $color &&
                    $matrix->getModuleAt($row + 1, $col + 1)->isDark() === $color
                ) {
                    $penalty += 3;
                }
            }
        }

        return $penalty;
    }

    /**
     * Rule 3: Specific patterns
     * Penalty: 40 for each occurrence of patterns resembling finder patterns
     * Patterns: 1:1:3:1:1 ratio (dark:light:dark:light:dark) with 4 light modules before/after
     */
    private function penaltyRule3(QrCodeMatrix $matrix): int
    {
        $size = $matrix->getSize();
        $penalty = 0;

        // Pattern: dark, light, dark-dark-dark, light, dark
        $pattern1 = [true, false, true, true, true, false, true];
        $pattern2 = [false, true, false, false, false, true, false];

        // Check rows
        for ($row = 0; $row < $size; $row++) {
            for ($col = 0; $col <= $size - 11; $col++) {
                if ($this->matchesPattern($matrix, $row, $col, true, $pattern1) ||
                    $this->matchesPattern($matrix, $row, $col, true, $pattern2)) {
                    $penalty += 40;
                }
            }
        }

        // Check columns
        for ($col = 0; $col < $size; $col++) {
            for ($row = 0; $row <= $size - 11; $row++) {
                if ($this->matchesPattern($matrix, $row, $col, false, $pattern1) ||
                    $this->matchesPattern($matrix, $row, $col, false, $pattern2)) {
                    $penalty += 40;
                }
            }
        }

        return $penalty;
    }

    /**
     * Rule 4: Balance
     * Penalty based on deviation from 50% dark modules
     */
    private function penaltyRule4(QrCodeMatrix $matrix): int
    {
        $size = $matrix->getSize();
        $totalModules = $size * $size;
        $darkModules = 0;

        for ($row = 0; $row < $size; $row++) {
            for ($col = 0; $col < $size; $col++) {
                if ($matrix->getModuleAt($row, $col)->isDark()) {
                    $darkModules++;
                }
            }
        }

        $percent = (int) (($darkModules * 100) / $totalModules);
        $previousMultiple = (int) (floor($percent / 5) * 5);
        $nextMultiple = (int) (ceil($percent / 5) * 5);

        $deviation1 = abs($previousMultiple - 50) / 5;
        $deviation2 = abs($nextMultiple - 50) / 5;

        return (int) (min($deviation1, $deviation2) * 10);
    }

    /**
     * Check if position is a function pattern (finder, timing, alignment, format info, dark module)
     * MUST match QrCodeGenerator::isOccupied() exactly!
     */
    private function isFunctionPattern(QrCodeMatrix $matrix, int $row, int $col): bool
    {
        $size = $matrix->getSize();

        // Finder patterns with 1-module separators (9x9 blocks)
        if (
            ($row <= 8 && $col <= 8) ||              // Top-left
            ($row <= 8 && $col >= $size - 8) ||      // Top-right
            ($row >= $size - 8 && $col <= 8)         // Bottom-left
        ) {
            return true;
        }

        // Timing patterns
        if ($row === 6 || $col === 6) {
            return true;
        }

        // Dark module (row = 4*version + 9, col = 8)
        $darkModuleRow = 4 * $matrix->getVersion()->getVersionNumber() + 9;
        if ($row === $darkModuleRow && $col === 8) {
            return true;
        }

        // Format information areas (15+15 bits, NOT entire row/column 8)
        // Horizontal: row 8, cols 0-5, 7-8 (left) and cols size-8 to size-1 (right)
        if ($row === 8) {
            // Left side: columns 0-5, 7-8 (skip timing column 6)
            if (($col >= 0 && $col <= 5) || $col === 7 || $col === 8) {
                return true;
            }
            // Right side: columns size-8 to size-1
            if ($col >= $size - 8) {
                return true;
            }
        }

        // Vertical: column 8, rows 0-8 (top) and size-7 to size-1 (bottom)
        if ($col === 8) {
            // Top side: rows 0-5, 7-8 (skip timing row 6)
            if (($row >= 0 && $row <= 5) || $row === 7 || $row === 8) {
                return true;
            }
            // Bottom side: rows size-7 to size-1
            if ($row >= $size - 7) {
                return true;
            }
        }

        // Alignment patterns (Version 2+)
        $version = $matrix->getVersion()->getVersionNumber();
        $alignmentLocations = AlignmentPattern::getLocations($version);

        foreach ($alignmentLocations as $alignRow) {
            foreach ($alignmentLocations as $alignCol) {
                // Skip overlaps with finders
                if ($alignRow <= 8 && $alignCol <= 8) {
                    continue;
                }
                if ($alignRow <= 8 && $alignCol >= $size - 8) {
                    continue;
                }
                if ($alignRow >= $size - 8 && $alignCol <= 8) {
                    continue;
                }

                // Check if within 5x5 alignment pattern
                if (abs($row - $alignRow) <= 2 && abs($col - $alignCol) <= 2) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Check if pattern matches at given position
     */
    private function matchesPattern(
        QrCodeMatrix $matrix,
        int $row,
        int $col,
        bool $horizontal,
        array $pattern
    ): bool {
        $size = $matrix->getSize();

        for ($i = 0; $i < count($pattern); $i++) {
            $checkRow = $horizontal ? $row : $row + $i;
            $checkCol = $horizontal ? $col + $i : $col;

            if ($checkRow >= $size || $checkCol >= $size) {
                return false;
            }

            if ($matrix->getModuleAt($checkRow, $checkCol)->isDark() !== $pattern[$i]) {
                return false;
            }
        }

        return true;
    }
}
