<?php
/**
 * Samowystarczalny enkoder kodów QR (model 2, tryb bajtowy) — BEZ zależności
 * (GD / Imagick / sieć). Wynik renderowany jako inline SVG, gotowy do osadzenia
 * w HTML/PDF (np. wizualizacja faktury KSeF — kod weryfikacyjny KOD I).
 *
 * Implementacja własna wg specyfikacji ISO/IEC 18004 (algorytm zgodny z referencją
 * Project Nayuki). Wspiera wersje 1–10 oraz poziomy korekcji L/M/Q/H — z zapasem
 * dla linku weryfikacyjnego KSeF (~100 znaków).
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

final class QR_Encoder {

	/** Bity poziomu korekcji w słowie format-info. */
	private const ECC_FORMAT_BITS = [ 'L' => 1, 'M' => 0, 'Q' => 3, 'H' => 2 ];

	/**
	 * Tablica korekcji błędów dla wersji 1–10 i poziomów L/M/Q/H.
	 * Format: [ wersja => [ poziom => [ ecPerBlock, [ [ liczbaBloków, danychNaBlok ], ... ] ] ] ].
	 */
	private const ECC = [
		1  => [ 'L' => [ 7,  [ [ 1, 19 ] ] ],            'M' => [ 10, [ [ 1, 16 ] ] ],            'Q' => [ 13, [ [ 1, 13 ] ] ],            'H' => [ 17, [ [ 1, 9 ] ] ] ],
		2  => [ 'L' => [ 10, [ [ 1, 34 ] ] ],            'M' => [ 16, [ [ 1, 28 ] ] ],            'Q' => [ 22, [ [ 1, 22 ] ] ],            'H' => [ 28, [ [ 1, 16 ] ] ] ],
		3  => [ 'L' => [ 15, [ [ 1, 55 ] ] ],            'M' => [ 26, [ [ 1, 44 ] ] ],            'Q' => [ 18, [ [ 2, 17 ] ] ],            'H' => [ 22, [ [ 2, 13 ] ] ] ],
		4  => [ 'L' => [ 20, [ [ 1, 80 ] ] ],            'M' => [ 18, [ [ 2, 32 ] ] ],            'Q' => [ 26, [ [ 2, 24 ] ] ],            'H' => [ 16, [ [ 4, 9 ] ] ] ],
		5  => [ 'L' => [ 26, [ [ 1, 108 ] ] ],           'M' => [ 24, [ [ 2, 43 ] ] ],            'Q' => [ 18, [ [ 2, 15 ], [ 2, 16 ] ] ], 'H' => [ 22, [ [ 2, 11 ], [ 2, 12 ] ] ] ],
		6  => [ 'L' => [ 18, [ [ 2, 68 ] ] ],            'M' => [ 16, [ [ 4, 27 ] ] ],            'Q' => [ 24, [ [ 4, 19 ] ] ],            'H' => [ 28, [ [ 4, 15 ] ] ] ],
		7  => [ 'L' => [ 20, [ [ 2, 78 ] ] ],            'M' => [ 18, [ [ 4, 31 ] ] ],            'Q' => [ 18, [ [ 2, 14 ], [ 4, 15 ] ] ], 'H' => [ 26, [ [ 4, 13 ], [ 1, 14 ] ] ] ],
		8  => [ 'L' => [ 24, [ [ 2, 97 ] ] ],            'M' => [ 22, [ [ 2, 38 ], [ 2, 39 ] ] ], 'Q' => [ 22, [ [ 4, 18 ], [ 2, 19 ] ] ], 'H' => [ 26, [ [ 4, 14 ], [ 2, 15 ] ] ] ],
		9  => [ 'L' => [ 30, [ [ 2, 116 ] ] ],           'M' => [ 22, [ [ 3, 36 ], [ 2, 37 ] ] ], 'Q' => [ 20, [ [ 4, 16 ], [ 4, 17 ] ] ], 'H' => [ 24, [ [ 4, 12 ], [ 4, 13 ] ] ] ],
		10 => [ 'L' => [ 18, [ [ 2, 68 ], [ 2, 69 ] ] ], 'M' => [ 26, [ [ 4, 43 ], [ 1, 44 ] ] ], 'Q' => [ 24, [ [ 6, 19 ], [ 2, 20 ] ] ], 'H' => [ 28, [ [ 6, 15 ], [ 2, 16 ] ] ] ],
	];

	/** Pozycje środków wzorców wyrównania (alignment) per wersja. */
	private const ALIGN = [
		1 => [], 2 => [ 6, 18 ], 3 => [ 6, 22 ], 4 => [ 6, 26 ], 5 => [ 6, 30 ],
		6 => [ 6, 34 ], 7 => [ 6, 22, 38 ], 8 => [ 6, 24, 42 ], 9 => [ 6, 26, 46 ], 10 => [ 6, 28, 50 ],
	];

	/** Tablice GF(256): exp[0..254], log[1..255]. Prymityw 0x11d. */
	private static array $exp = [];
	private static array $log = [];

	/**
	 * Zwraca inline SVG z kodem QR dla podanego tekstu (UTF-8).
	 *
	 * @param string $text  Dane do zakodowania.
	 * @param int    $px    Rozmiar (px) boku SVG.
	 * @param string $ecl   Poziom korekcji 'L'|'M'|'Q'|'H'.
	 * @param int    $quiet Margines (quiet zone) w modułach (spec: 4).
	 * @return string SVG albo '' gdy kodowanie się nie powiodło.
	 */
	public static function svg( string $text, int $px = 150, string $ecl = 'M', int $quiet = 4 ): string {
		try {
			$m = self::encode( $text, $ecl );
		} catch ( \Throwable $e ) {
			return '';
		}
		$n   = count( $m );
		$dim = $n + 2 * $quiet;
		$path = '';
		for ( $y = 0; $y < $n; $y++ ) {
			$x = 0;
			while ( $x < $n ) {
				if ( 1 === $m[ $y ][ $x ] ) {
					$run = 1;
					while ( $x + $run < $n && 1 === $m[ $y ][ $x + $run ] ) {
						$run++;
					}
					$path .= 'M' . ( $x + $quiet ) . ' ' . ( $y + $quiet ) . 'h' . $run . 'v1h-' . $run . 'z';
					$x    += $run;
				} else {
					$x++;
				}
			}
		}
		return '<svg xmlns="http://www.w3.org/2000/svg" width="' . $px . '" height="' . $px . '" viewBox="0 0 ' . $dim . ' ' . $dim . '" shape-rendering="crispEdges">'
			. '<rect width="' . $dim . '" height="' . $dim . '" fill="#ffffff"/>'
			. '<path d="' . $path . '" fill="#000000"/></svg>';
	}

	/**
	 * Zwraca PNG (raw bytes) z kodem QR. Bez GD/Imagick — ręczne kodowanie PNG
	 * (truecolor RGB) z kompresją zlib (gzcompress). Najpewniejsze do osadzenia
	 * w dompdf (render rastrowy) oraz w przeglądarce jako data-URI.
	 *
	 * @param int $scale Piksele na moduł.
	 * @param int $quiet Margines w modułach.
	 * @return string PNG albo '' gdy kodowanie się nie powiodło.
	 */
	public static function png( string $text, int $scale = 4, string $ecl = 'M', int $quiet = 4 ): string {
		try {
			$m = self::encode( $text, $ecl );
		} catch ( \Throwable $e ) {
			return '';
		}
		if ( ! function_exists( 'gzcompress' ) ) {
			return '';
		}
		$n     = count( $m );
		$mods  = $n + 2 * $quiet;
		$w     = $mods * $scale;
		$black = "\x00\x00\x00";
		$white = "\xff\xff\xff";
		$raw   = '';
		for ( $py = 0; $py < $w; $py++ ) {
			$raw .= "\x00"; // filtr: brak.
			$my   = intdiv( $py, $scale ) - $quiet;
			$vy   = ( $my >= 0 && $my < $n );
			for ( $mx = -$quiet; $mx < $n + $quiet; $mx++ ) {
				$dark = ( $vy && $mx >= 0 && $mx < $n && 1 === $m[ $my ][ $mx ] );
				$raw .= str_repeat( $dark ? $black : $white, $scale );
			}
		}
		$ihdr = pack( 'N', $w ) . pack( 'N', $w ) . "\x08\x02\x00\x00\x00"; // 8-bit, truecolor.
		return "\x89PNG\r\n\x1a\n"
			. self::png_chunk( 'IHDR', $ihdr )
			. self::png_chunk( 'IDAT', gzcompress( $raw, 9 ) )
			. self::png_chunk( 'IEND', '' );
	}

	/** Kod QR jako data-URI PNG (gotowy do atrybutu src). */
	public static function png_data_uri( string $text, int $scale = 4, string $ecl = 'M', int $quiet = 4 ): string {
		$png = self::png( $text, $scale, $ecl, $quiet );
		return '' === $png ? '' : 'data:image/png;base64,' . base64_encode( $png );
	}

	private static function png_chunk( string $type, string $data ): string {
		return pack( 'N', strlen( $data ) ) . $type . $data . pack( 'N', crc32( $type . $data ) );
	}

	/**
	 * Koduje tekst do macierzy modułów QR (0=jasny, 1=ciemny), bez quiet zone.
	 *
	 * @return array<int,array<int,int>>
	 */
	public static function encode( string $text, string $ecl = 'M' ): array {
		$ecl   = isset( self::ECC_FORMAT_BITS[ $ecl ] ) ? $ecl : 'M';
		$bytes = '' === $text ? [] : array_values( unpack( 'C*', $text ) );
		$len   = count( $bytes );

		$version = self::pick_version( $len, $ecl );
		$cw      = self::add_ecc_interleave( $bytes, $version, $ecl );

		// Bity do umieszczenia (każde słowo kodowe = 8 bitów, MSB first).
		$bits = [];
		foreach ( $cw as $byte ) {
			for ( $b = 7; $b >= 0; $b-- ) {
				$bits[] = ( $byte >> $b ) & 1;
			}
		}

		return self::draw( $version, $ecl, $bits );
	}

	/** Najmniejsza wersja 1–10 mieszcząca dane w trybie bajtowym. */
	private static function pick_version( int $len, string $ecl ): int {
		foreach ( self::ECC as $v => $levels ) {
			$cc     = $v <= 9 ? 8 : 16;
			$data   = self::data_codewords( $v, $ecl );
			$needed = 4 + $cc + 8 * $len;
			if ( $needed <= $data * 8 ) {
				return $v;
			}
		}
		throw new \RuntimeException( 'QR: dane za dlugie' );
	}

	/** Liczba słów danych (bez EC) dla wersji+poziomu. */
	private static function data_codewords( int $version, string $ecl ): int {
		[ , $groups ] = self::ECC[ $version ][ $ecl ];
		$sum = 0;
		foreach ( $groups as [ $nb, $dpb ] ) {
			$sum += $nb * $dpb;
		}
		return $sum;
	}

	/** Buduje słowa danych + EC, z przeplotem bloków (final codeword sequence). */
	private static function add_ecc_interleave( array $bytes, int $version, string $ecl ): array {
		[ $ec_per_block, $groups ] = self::ECC[ $version ][ $ecl ];
		$total_data = self::data_codewords( $version, $ecl );

		// Strumień bitów: tryb bajtowy (0100) + licznik + dane + terminator + padding.
		$cc = $version <= 9 ? 8 : 16;
		$bs = '0100';
		$bs .= str_pad( decbin( count( $bytes ) ), $cc, '0', STR_PAD_LEFT );
		foreach ( $bytes as $bv ) {
			$bs .= str_pad( decbin( $bv ), 8, '0', STR_PAD_LEFT );
		}
		$cap_bits = $total_data * 8;
		$bs      .= str_repeat( '0', min( 4, $cap_bits - strlen( $bs ) ) );
		while ( strlen( $bs ) % 8 !== 0 ) {
			$bs .= '0';
		}
		$pad = [ '11101100', '00010001' ];
		$pi  = 0;
		while ( strlen( $bs ) < $cap_bits ) {
			$bs .= $pad[ $pi % 2 ];
			$pi++;
		}
		$data_cw = [];
		for ( $i = 0; $i < strlen( $bs ); $i += 8 ) {
			$data_cw[] = bindec( substr( $bs, $i, 8 ) );
		}

		// Podział na bloki wg grup.
		$blocks = [];
		$idx    = 0;
		foreach ( $groups as [ $nb, $dpb ] ) {
			for ( $b = 0; $b < $nb; $b++ ) {
				$blocks[] = array_slice( $data_cw, $idx, $dpb );
				$idx     += $dpb;
			}
		}
		// EC dla każdego bloku.
		$ec_blocks = [];
		foreach ( $blocks as $blk ) {
			$ec_blocks[] = self::rs_encode( $blk, $ec_per_block );
		}
		// Przeplot słów danych, potem słów EC.
		$out      = [];
		$max_data = 0;
		foreach ( $blocks as $b ) {
			$max_data = max( $max_data, count( $b ) );
		}
		for ( $i = 0; $i < $max_data; $i++ ) {
			foreach ( $blocks as $b ) {
				if ( $i < count( $b ) ) {
					$out[] = $b[ $i ];
				}
			}
		}
		for ( $i = 0; $i < $ec_per_block; $i++ ) {
			foreach ( $ec_blocks as $e ) {
				$out[] = $e[ $i ];
			}
		}
		return $out;
	}

	/* ---------------------------------------------------------------------- *
	 * Reed–Solomon nad GF(256)
	 * ---------------------------------------------------------------------- */

	private static function init_gf(): void {
		if ( ! empty( self::$exp ) ) {
			return;
		}
		$exp = array_fill( 0, 255, 0 );
		$log = array_fill( 0, 256, 0 );
		$x   = 1;
		for ( $i = 0; $i < 255; $i++ ) {
			$exp[ $i ]   = $x;
			$log[ $x ]   = $i;
			$x         <<= 1;
			if ( $x & 0x100 ) {
				$x ^= 0x11d;
			}
		}
		self::$exp = $exp;
		self::$log = $log;
	}

	/** Wielomian generujący stopnia $n (n+1 współczynników, g[0]=1). */
	private static function rs_generator( int $n ): array {
		self::init_gf();
		$g = [ 1 ];
		for ( $i = 0; $i < $n; $i++ ) {
			$factor = [ 1, self::$exp[ $i ] ];
			$res    = array_fill( 0, count( $g ) + 1, 0 );
			foreach ( $g as $a => $ga ) {
				if ( 0 === $ga ) {
					continue;
				}
				foreach ( $factor as $b => $fb ) {
					if ( 0 === $fb ) {
						continue;
					}
					$res[ $a + $b ] ^= self::$exp[ ( self::$log[ $ga ] + self::$log[ $fb ] ) % 255 ];
				}
			}
			$g = $res;
		}
		return $g;
	}

	/** Słowa EC (remainder) dla bloku danych. */
	private static function rs_encode( array $data, int $n ): array {
		self::init_gf();
		$g   = self::rs_generator( $n );
		$res = array_merge( $data, array_fill( 0, $n, 0 ) );
		$dlen = count( $data );
		for ( $i = 0; $i < $dlen; $i++ ) {
			$coef = $res[ $i ];
			if ( 0 === $coef ) {
				continue;
			}
			$lc = self::$log[ $coef ];
			for ( $j = 0; $j <= $n; $j++ ) {
				$res[ $i + $j ] ^= self::$exp[ ( self::$log[ $g[ $j ] ] + $lc ) % 255 ];
			}
		}
		return array_slice( $res, $dlen, $n );
	}

	/* ---------------------------------------------------------------------- *
	 * Rozmieszczenie modułów + maskowanie
	 * ---------------------------------------------------------------------- */

	private static function draw( int $version, string $ecl, array $bits ): array {
		$size = 17 + 4 * $version;
		$m    = array_fill( 0, $size, array_fill( 0, $size, 0 ) );
		$f    = array_fill( 0, $size, array_fill( 0, $size, false ) );

		// Wzorce wyszukiwania (finder) + separatory.
		foreach ( [ [ 0, 0 ], [ 0, $size - 7 ], [ $size - 7, 0 ] ] as [ $r0, $c0 ] ) {
			for ( $r = -1; $r <= 7; $r++ ) {
				for ( $c = -1; $c <= 7; $c++ ) {
					$rr = $r0 + $r;
					$cc = $c0 + $c;
					if ( $rr < 0 || $rr >= $size || $cc < 0 || $cc >= $size ) {
						continue;
					}
					$dark = ( $r >= 0 && $r <= 6 && $c >= 0 && $c <= 6 )
						&& ( 0 === $r || 6 === $r || 0 === $c || 6 === $c || ( $r >= 2 && $r <= 4 && $c >= 2 && $c <= 4 ) );
					$m[ $rr ][ $cc ] = $dark ? 1 : 0;
					$f[ $rr ][ $cc ] = true;
				}
			}
		}

		// Wzorce taktujące (timing).
		for ( $i = 8; $i < $size - 8; $i++ ) {
			$v = ( $i % 2 === 0 ) ? 1 : 0;
			if ( ! $f[ 6 ][ $i ] ) {
				$m[ 6 ][ $i ] = $v;
				$f[ 6 ][ $i ] = true;
			}
			if ( ! $f[ $i ][ 6 ] ) {
				$m[ $i ][ 6 ] = $v;
				$f[ $i ][ 6 ] = true;
			}
		}

		// Wzorce wyrównania (alignment).
		$pos   = self::ALIGN[ $version ];
		$na    = count( $pos );
		for ( $i = 0; $i < $na; $i++ ) {
			for ( $j = 0; $j < $na; $j++ ) {
				if ( ( 0 === $i && 0 === $j ) || ( 0 === $i && $j === $na - 1 ) || ( $i === $na - 1 && 0 === $j ) ) {
					continue;
				}
				$ar = $pos[ $i ];
				$ac = $pos[ $j ];
				for ( $dr = -2; $dr <= 2; $dr++ ) {
					for ( $dc = -2; $dc <= 2; $dc++ ) {
						$dark            = max( abs( $dr ), abs( $dc ) ) !== 1;
						$m[ $ar + $dr ][ $ac + $dc ] = $dark ? 1 : 0;
						$f[ $ar + $dr ][ $ac + $dc ] = true;
					}
				}
			}
		}

		// Moduł ciemny.
		$m[ 4 * $version + 9 ][ 8 ] = 1;
		$f[ 4 * $version + 9 ][ 8 ] = true;

		// Rezerwacja obszaru format-info (wypełniane po wyborze maski).
		for ( $i = 0; $i <= 8; $i++ ) {
			$f[ 8 ][ $i ]        = true;
			$f[ $i ][ 8 ]        = true;
		}
		for ( $i = 0; $i < 8; $i++ ) {
			$f[ 8 ][ $size - 1 - $i ] = true;
			$f[ $size - 1 - $i ][ 8 ] = true;
		}

		// Informacja o wersji (v>=7) — moduły funkcyjne.
		if ( $version >= 7 ) {
			$code = $version << 12;
			for ( $i = 17; $i >= 12; $i-- ) {
				if ( ( $code >> $i ) & 1 ) {
					$code ^= 0x1F25 << ( $i - 12 );
				}
			}
			$vbits = ( $version << 12 ) | ( $code & 0xFFF );
			for ( $i = 0; $i < 18; $i++ ) {
				$bit = ( $vbits >> $i ) & 1;
				$a   = $size - 11 + ( $i % 3 );
				$b   = intdiv( $i, 3 );
				$m[ $b ][ $a ] = $bit;
				$m[ $a ][ $b ] = $bit;
				$f[ $b ][ $a ] = true;
				$f[ $a ][ $b ] = true;
			}
		}

		// Rozmieszczenie danych (zygzak od prawego-dolnego rogu).
		$bit_idx = 0;
		$total   = count( $bits );
		for ( $right = $size - 1; $right >= 1; $right -= 2 ) {
			if ( 6 === $right ) {
				$right = 5;
			}
			for ( $vert = 0; $vert < $size; $vert++ ) {
				for ( $jj = 0; $jj < 2; $jj++ ) {
					$col    = $right - $jj;
					$upward = ( ( $right + 1 ) & 2 ) === 0;
					$row    = $upward ? $size - 1 - $vert : $vert;
					if ( ! $f[ $row ][ $col ] ) {
						$m[ $row ][ $col ] = ( $bit_idx < $total ) ? $bits[ $bit_idx ] : 0;
						$bit_idx++;
					}
				}
			}
		}

		// Wybór najlepszej maski (najniższa kara).
		$best     = null;
		$best_pen = PHP_INT_MAX;
		for ( $mask = 0; $mask < 8; $mask++ ) {
			$mm = $m;
			for ( $y = 0; $y < $size; $y++ ) {
				for ( $x = 0; $x < $size; $x++ ) {
					if ( ! $f[ $y ][ $x ] && self::mask_cond( $mask, $y, $x ) ) {
						$mm[ $y ][ $x ] ^= 1;
					}
				}
			}
			self::draw_format( $mm, $size, $ecl, $mask );
			$pen = self::penalty( $mm, $size );
			if ( $pen < $best_pen ) {
				$best_pen = $pen;
				$best     = $mm;
			}
		}
		return $best;
	}

	private static function draw_format( array &$m, int $size, string $ecl, int $mask ): void {
		$data = ( self::ECC_FORMAT_BITS[ $ecl ] << 3 ) | $mask;
		$code = $data << 10;
		for ( $i = 14; $i >= 10; $i-- ) {
			if ( ( $code >> $i ) & 1 ) {
				$code ^= 0x537 << ( $i - 10 );
			}
		}
		$bits = ( ( $data << 10 ) | ( $code & 0x3FF ) ) ^ 0x5412;

		$gb = static fn ( int $i ): int => ( $bits >> $i ) & 1;
		// Kopia 1.
		for ( $i = 0; $i <= 5; $i++ ) {
			$m[ $i ][ 8 ] = $gb( $i );
		}
		$m[ 7 ][ 8 ] = $gb( 6 );
		$m[ 8 ][ 8 ] = $gb( 7 );
		$m[ 8 ][ 7 ] = $gb( 8 );
		for ( $i = 9; $i < 15; $i++ ) {
			$m[ 8 ][ 14 - $i ] = $gb( $i );
		}
		// Kopia 2.
		for ( $i = 0; $i < 8; $i++ ) {
			$m[ 8 ][ $size - 1 - $i ] = $gb( $i );
		}
		for ( $i = 8; $i < 15; $i++ ) {
			$m[ $size - 15 + $i ][ 8 ] = $gb( $i );
		}
	}

	private static function mask_cond( int $mask, int $r, int $c ): bool {
		switch ( $mask ) {
			case 0: return ( $r + $c ) % 2 === 0;
			case 1: return $r % 2 === 0;
			case 2: return $c % 3 === 0;
			case 3: return ( $r + $c ) % 3 === 0;
			case 4: return ( intdiv( $r, 2 ) + intdiv( $c, 3 ) ) % 2 === 0;
			case 5: return ( ( $r * $c ) % 2 ) + ( ( $r * $c ) % 3 ) === 0;
			case 6: return ( ( ( $r * $c ) % 2 ) + ( ( $r * $c ) % 3 ) ) % 2 === 0;
			default: return ( ( ( $r + $c ) % 2 ) + ( ( $r * $c ) % 3 ) ) % 2 === 0;
		}
	}

	private static function penalty( array $m, int $size ): int {
		$pen = 0;

		// Reguła 1: serie >=5 tego samego koloru (wiersze i kolumny).
		for ( $y = 0; $y < $size; $y++ ) {
			$run_c = 1;
			$run_r = 1;
			for ( $x = 1; $x < $size; $x++ ) {
				$run_c = ( $m[ $y ][ $x ] === $m[ $y ][ $x - 1 ] ) ? $run_c + 1 : 1;
				if ( 5 === $run_c ) {
					$pen += 3;
				} elseif ( $run_c > 5 ) {
					$pen += 1;
				}
				$run_r = ( $m[ $x ][ $y ] === $m[ $x - 1 ][ $y ] ) ? $run_r + 1 : 1;
				if ( 5 === $run_r ) {
					$pen += 3;
				} elseif ( $run_r > 5 ) {
					$pen += 1;
				}
			}
		}

		// Reguła 2: bloki 2x2 jednego koloru.
		for ( $y = 0; $y < $size - 1; $y++ ) {
			for ( $x = 0; $x < $size - 1; $x++ ) {
				$v = $m[ $y ][ $x ];
				if ( $v === $m[ $y ][ $x + 1 ] && $v === $m[ $y + 1 ][ $x ] && $v === $m[ $y + 1 ][ $x + 1 ] ) {
					$pen += 3;
				}
			}
		}

		// Reguła 3: wzorzec 1011101 z 4 jasnymi modułami (wiersze i kolumny).
		$pat1 = [ 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0 ];
		$pat2 = [ 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1 ];
		for ( $y = 0; $y < $size; $y++ ) {
			for ( $x = 0; $x <= $size - 11; $x++ ) {
				$hit1 = true;
				$hit2 = true;
				$vt1  = true;
				$vt2  = true;
				for ( $k = 0; $k < 11; $k++ ) {
					$hr = $m[ $y ][ $x + $k ];
					$vr = $m[ $x + $k ][ $y ];
					if ( $hr !== $pat1[ $k ] ) { $hit1 = false; }
					if ( $hr !== $pat2[ $k ] ) { $hit2 = false; }
					if ( $vr !== $pat1[ $k ] ) { $vt1 = false; }
					if ( $vr !== $pat2[ $k ] ) { $vt2 = false; }
				}
				if ( $hit1 ) { $pen += 40; }
				if ( $hit2 ) { $pen += 40; }
				if ( $vt1 ) { $pen += 40; }
				if ( $vt2 ) { $pen += 40; }
			}
		}

		// Reguła 4: proporcja ciemnych modułów.
		$dark = 0;
		foreach ( $m as $row ) {
			$dark += array_sum( $row );
		}
		$total   = $size * $size;
		$percent = ( $dark * 100 ) / $total;
		$k       = (int) ( abs( $percent - 50 ) / 5 );
		$pen    += $k * 10;

		return $pen;
	}
}
