<?php
/**
 * @package Hafenstudios\Leadlotse
 */

namespace Hafenstudios\Leadlotse\Events;

defined( 'ABSPATH' ) || exit;

/**
 * Minimaler QR-Code-Encoder (L-R1): Byte-Mode, Fehlerkorrektur-Level L,
 * Versionen 1 bis 10 (bis 271 Datenbytes; eine Check-in-URL mit Token liegt
 * weit darunter). Bewusst OHNE Dependency und ohne GD: die Ausgabe ist ein
 * schlankes Inline-SVG (druckscharf, skalierbar).
 *
 * Implementiert nach ISO/IEC 18004: Segment-Kodierung, Reed-Solomon-ECC ueber
 * GF(256) (Primpolynom 0x11D), Block-Interleaving, Funktionsmuster,
 * Daten-Zickzack, alle 8 Masken mit Penalty-Bewertung, Format-Info
 * (BCH 15,5, XOR 0x5412) und Versions-Info (BCH 18,6) ab Version 7.
 */
final class QrCode {

	/**
	 * Datenkapazitaet (Codewoerter) je Version bei ECC L.
	 *
	 * @var array<int, int>
	 */
	private const DATA_CODEWORDS = array(
		1  => 19,
		2  => 34,
		3  => 55,
		4  => 80,
		5  => 108,
		6  => 136,
		7  => 156,
		8  => 194,
		9  => 232,
		10 => 274,
	);

	/**
	 * Blockstruktur je Version bei ECC L: [ ecc_pro_block, [ datenlaengen je Block ] ].
	 *
	 * @var array<int, array{0:int, 1:array<int, int>}>
	 */
	private const BLOCKS = array(
		1  => array( 7, array( 19 ) ),
		2  => array( 10, array( 34 ) ),
		3  => array( 15, array( 55 ) ),
		4  => array( 20, array( 80 ) ),
		5  => array( 26, array( 108 ) ),
		6  => array( 18, array( 68, 68 ) ),
		7  => array( 20, array( 78, 78 ) ),
		8  => array( 24, array( 97, 97 ) ),
		9  => array( 30, array( 116, 116 ) ),
		10 => array( 18, array( 68, 68, 69, 69 ) ),
	);

	/**
	 * Alignment-Pattern-Zentren je Version.
	 *
	 * @var array<int, array<int, int>>
	 */
	private const ALIGNMENT = array(
		1  => array(),
		2  => array( 6, 18 ),
		3  => array( 6, 22 ),
		4  => array( 6, 26 ),
		5  => array( 6, 30 ),
		6  => array( 6, 34 ),
		7  => array( 6, 22, 38 ),
		8  => array( 6, 24, 42 ),
		9  => array( 6, 26, 46 ),
		10 => array( 6, 28, 52 ),
	);

	/**
	 * GF(256)-Exponententabelle (erzeugt bei Bedarf).
	 *
	 * @var array<int, int>|null
	 */
	private static ?array $gf_exp = null;

	/**
	 * GF(256)-Logarithmentabelle.
	 *
	 * @var array<int, int>
	 */
	private static array $gf_log = array();

	/**
	 * Erzeugt die Modulmatrix fuer die Nutzdaten.
	 *
	 * @param string $data Nutzdaten (Bytes, z.B. eine URL).
	 * @return array<int, array<int, bool>> Quadratische Matrix, true = dunkel.
	 */
	public static function matrix( string $data ): array {
		$version = self::pick_version( $data );
		$size    = 17 + 4 * $version;

		$codewords = self::codewords( $data, $version );

		// Basis-Matrix mit Funktionsmustern; parallel eine Maske der Funktionsflaechen.
		$modules  = array_fill( 0, $size, array_fill( 0, $size, false ) );
		$reserved = array_fill( 0, $size, array_fill( 0, $size, false ) );
		self::place_function_patterns( $modules, $reserved, $version, $size );

		// Datenbits im Zickzack einsetzen.
		self::place_data( $modules, $reserved, $codewords, $size );

		// Beste Maske per Penalty-Bewertung waehlen.
		$best_mask    = 0;
		$best_penalty = PHP_INT_MAX;
		$best_matrix  = $modules;
		for ( $mask = 0; $mask < 8; $mask++ ) {
			$candidate = self::apply_mask( $modules, $reserved, $mask, $size );
			self::place_format_info( $candidate, $mask, $size );
			$penalty = self::penalty( $candidate, $size );
			if ( $penalty < $best_penalty ) {
				$best_penalty = $penalty;
				$best_mask    = $mask;
				$best_matrix  = $candidate;
			}
		}
		unset( $best_mask );

		return $best_matrix;
	}

	/**
	 * Rendert die Nutzdaten als Inline-SVG (dunkle Module als ein Pfad).
	 *
	 * @param string $data   Nutzdaten.
	 * @param int    $module Kantenlaenge eines Moduls in SVG-Einheiten.
	 * @return string SVG-Markup.
	 */
	public static function svg( string $data, int $module = 4 ): string {
		$matrix = self::matrix( $data );
		$size   = count( $matrix );
		$quiet  = 4; // Ruhezone: 4 Module rundum (Spec-Minimum).
		$total  = ( $size + 2 * $quiet ) * $module;

		$path = '';
		for ( $y = 0; $y < $size; $y++ ) {
			for ( $x = 0; $x < $size; $x++ ) {
				if ( $matrix[ $y ][ $x ] ) {
					$px    = ( $x + $quiet ) * $module;
					$py    = ( $y + $quiet ) * $module;
					$path .= 'M' . $px . ' ' . $py . 'h' . $module . 'v' . $module . 'h-' . $module . 'z';
				}
			}
		}

		return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $total . ' ' . $total . '"'
			. ' width="' . $total . '" height="' . $total . '" role="img" aria-label="QR-Code" shape-rendering="crispEdges">'
			. '<rect width="' . $total . '" height="' . $total . '" fill="#ffffff"/>'
			. '<path d="' . $path . '" fill="#000000"/></svg>';
	}

	/**
	 * Format-Info-Bits (15 Bit) fuer ECC L und eine Maske. Oeffentlich fuer den
	 * Known-Answer-Test gegen die Spec-Tabelle.
	 *
	 * @param int $mask Maskennummer 0-7.
	 * @return int 15-Bit-Wert.
	 */
	public static function format_bits( int $mask ): int {
		$data = ( 0b01 << 3 ) | ( $mask & 0b111 ); // ECC L = 01.
		$bch  = $data << 10;
		for ( $i = 14; $i >= 10; $i-- ) {
			if ( ( $bch >> $i ) & 1 ) {
				$bch ^= 0x537 << ( $i - 10 );
			}
		}
		return ( ( $data << 10 ) | $bch ) ^ 0x5412;
	}

	/**
	 * Reed-Solomon-ECC-Codewoerter. Oeffentlich fuer den mathematischen
	 * Selbsttest (Auswertung an den Generator-Nullstellen muss 0 ergeben).
	 *
	 * @param array<int, int> $data Daten-Codewoerter.
	 * @param int             $ecc_count Anzahl ECC-Codewoerter.
	 * @return array<int, int>
	 */
	public static function rs_ecc( array $data, int $ecc_count ): array {
		self::init_gf();

		// Generatorpolynom Grad $ecc_count: Produkt (x - a^i) fuer i = 0..n-1.
		$gen = array( 1 );
		for ( $i = 0; $i < $ecc_count; $i++ ) {
			$next = array_fill( 0, count( $gen ) + 1, 0 );
			foreach ( $gen as $j => $coef ) {
				$next[ $j ]    ^= self::gf_mul( $coef, self::$gf_exp[ $i ] );
				$next[ $j + 1 ] ^= $coef;
			}
			// Hinweis: Koeffizientenreihenfolge hier: Index 0 = hoechster Grad? Wir bauen
			// bewusst mit Index = Grad-Absteigend ueber die Faltung unten konsistent.
			$gen = $next;
		}
		// $gen ist hier in aufsteigender Faltung entstanden; fuer die Division unten
		// brauchen wir absteigende Reihenfolge (Index 0 = x^n). Umdrehen:
		$gen = array_reverse( $gen );

		// Polynomdivision: Rest von data * x^ecc_count durch gen.
		$buffer = array_merge( $data, array_fill( 0, $ecc_count, 0 ) );
		$dlen   = count( $data );
		for ( $i = 0; $i < $dlen; $i++ ) {
			$factor = $buffer[ $i ];
			if ( 0 === $factor ) {
				continue;
			}
			for ( $j = 1; $j < count( $gen ); $j++ ) {
				$buffer[ $i + $j ] ^= self::gf_mul( $gen[ $j ], $factor );
			}
		}

		return array_slice( $buffer, $dlen );
	}

	/**
	 * Auswertung eines Codewort-Polynoms an einer GF-Stelle (fuer den Selbsttest).
	 *
	 * @param array<int, int> $poly Codewoerter, Index 0 = hoechster Grad.
	 * @param int             $x    Auswertungsstelle.
	 * @return int
	 */
	public static function gf_poly_eval( array $poly, int $x ): int {
		self::init_gf();
		$y = 0;
		foreach ( $poly as $coef ) {
			$y = self::gf_mul( $y, $x ) ^ $coef;
		}
		return $y;
	}

	/**
	 * GF(256)-Exponent (fuer den Selbsttest zugaenglich).
	 *
	 * @param int $power Exponent.
	 * @return int
	 */
	public static function gf_pow( int $power ): int {
		self::init_gf();
		return self::$gf_exp[ $power % 255 ];
	}

	/**
	 * Kleinste Version, deren Datenkapazitaet reicht.
	 *
	 * @param string $data Nutzdaten.
	 * @return int
	 * @throws \InvalidArgumentException Wenn die Daten Version 10 sprengen.
	 */
	private static function pick_version( string $data ): int {
		$length = strlen( $data );
		foreach ( self::DATA_CODEWORDS as $version => $capacity ) {
			// Mode (4 Bit) + Laengenzaehler (8 Bit bis v9, 16 Bit ab v10) + Daten.
			$counter = $version >= 10 ? 16 : 8;
			$bits    = 4 + $counter + 8 * $length;
			if ( $bits <= 8 * $capacity ) {
				return $version;
			}
		}
		throw new \InvalidArgumentException( 'QR-Daten zu lang (max. 271 Bytes bei Version 10, ECC L).' );
	}

	/**
	 * Baut die finalen (interleavten) Codewoerter: Segment, Padding, ECC.
	 *
	 * @param string $data    Nutzdaten.
	 * @param int    $version Version.
	 * @return array<int, int>
	 */
	private static function codewords( string $data, int $version ): array {
		$capacity = self::DATA_CODEWORDS[ $version ];

		// Bitstrom: Byte-Mode (0100), Laenge, Datenbytes.
		$bits    = '0100';
		$counter = $version >= 10 ? 16 : 8;
		$bits   .= str_pad( decbin( strlen( $data ) ), $counter, '0', STR_PAD_LEFT );
		foreach ( str_split( $data ) as $char ) {
			$bits .= str_pad( decbin( ord( $char ) ), 8, '0', STR_PAD_LEFT );
		}

		// Terminator (max. 4 Bit), auf Bytegrenze auffuellen.
		$bits .= str_repeat( '0', min( 4, 8 * $capacity - strlen( $bits ) ) );
		if ( 0 !== strlen( $bits ) % 8 ) {
			$bits .= str_repeat( '0', 8 - ( strlen( $bits ) % 8 ) );
		}

		$codewords = array();
		foreach ( str_split( $bits, 8 ) as $byte ) {
			$codewords[] = bindec( $byte );
		}

		// Pad-Bytes 0xEC / 0x11 im Wechsel.
		$pad = array( 0xEC, 0x11 );
		$i   = 0;
		while ( count( $codewords ) < $capacity ) {
			$codewords[] = $pad[ $i % 2 ];
			$i++;
		}

		// Blockaufteilung + ECC je Block.
		list( $ecc_per_block, $block_sizes ) = self::BLOCKS[ $version ];
		$data_blocks = array();
		$ecc_blocks  = array();
		$offset      = 0;
		foreach ( $block_sizes as $block_size ) {
			$block         = array_slice( $codewords, $offset, $block_size );
			$offset       += $block_size;
			$data_blocks[] = $block;
			$ecc_blocks[]  = self::rs_ecc( $block, $ecc_per_block );
		}

		// Interleaving: erst Datenspalten, dann ECC-Spalten.
		$result   = array();
		$max_data = max( $block_sizes );
		for ( $col = 0; $col < $max_data; $col++ ) {
			foreach ( $data_blocks as $block ) {
				if ( isset( $block[ $col ] ) ) {
					$result[] = $block[ $col ];
				}
			}
		}
		for ( $col = 0; $col < $ecc_per_block; $col++ ) {
			foreach ( $ecc_blocks as $block ) {
				$result[] = $block[ $col ];
			}
		}

		return $result;
	}

	/**
	 * Setzt Finder, Separatoren, Timing, Alignment, Dark Module und reserviert
	 * die Format-/Versions-Info-Flaechen.
	 *
	 * @param array<int, array<int, bool>> $modules  Matrix (by reference).
	 * @param array<int, array<int, bool>> $reserved Funktionsflaechen (by reference).
	 * @param int                          $version  Version.
	 * @param int                          $size     Kantenlaenge.
	 * @return void
	 */
	private static function place_function_patterns( array &$modules, array &$reserved, int $version, int $size ): void {
		// Finder-Pattern (3x) inkl. Separator.
		foreach ( array( array( 0, 0 ), array( $size - 7, 0 ), array( 0, $size - 7 ) ) as $pos ) {
			list( $px, $py ) = $pos;
			for ( $y = -1; $y <= 7; $y++ ) {
				for ( $x = -1; $x <= 7; $x++ ) {
					$mx = $px + $x;
					$my = $py + $y;
					if ( $mx < 0 || $my < 0 || $mx >= $size || $my >= $size ) {
						continue;
					}
					$dark = $x >= 0 && $x <= 6 && $y >= 0 && $y <= 6
						&& ( 0 === $x || 6 === $x || 0 === $y || 6 === $y || ( $x >= 2 && $x <= 4 && $y >= 2 && $y <= 4 ) );

					$modules[ $my ][ $mx ]  = $dark;
					$reserved[ $my ][ $mx ] = true;
				}
			}
		}

		// Timing-Pattern.
		for ( $i = 8; $i < $size - 8; $i++ ) {
			$modules[6][ $i ]  = 0 === $i % 2;
			$reserved[6][ $i ] = true;
			$modules[ $i ][6]  = 0 === $i % 2;
			$reserved[ $i ][6] = true;
		}

		// Alignment-Patterns (nicht ueber Finder-Ecken).
		$centers = self::ALIGNMENT[ $version ];
		foreach ( $centers as $cy ) {
			foreach ( $centers as $cx ) {
				// Nur die drei Finder-Ecken aussparen; Ueberlappung mit dem
				// Timing-Pattern ist per Spezifikation vorgesehen (deckungsgleich).
				$in_finder = ( $cy <= 8 && $cx <= 8 )
					|| ( $cy <= 8 && $cx >= $size - 9 )
					|| ( $cy >= $size - 9 && $cx <= 8 );
				if ( $in_finder ) {
					continue;
				}
				for ( $y = -2; $y <= 2; $y++ ) {
					for ( $x = -2; $x <= 2; $x++ ) {
						$dark = 2 === max( abs( $x ), abs( $y ) ) || ( 0 === $x && 0 === $y );

						$modules[ $cy + $y ][ $cx + $x ]  = $dark;
						$reserved[ $cy + $y ][ $cx + $x ] = true;
					}
				}
			}
		}

		// Dark Module.
		$modules[ 4 * $version + 9 ][8]  = true;
		$reserved[ 4 * $version + 9 ][8] = true;

		// Format-Info-Flaechen reservieren (werden je Maske befuellt).
		for ( $i = 0; $i <= 8; $i++ ) {
			if ( 6 !== $i ) {
				$reserved[8][ $i ] = true;
				$reserved[ $i ][8] = true;
			}
		}
		$reserved[8][8] = true;
		for ( $i = 0; $i < 8; $i++ ) {
			$reserved[8][ $size - 1 - $i ] = true;
			$reserved[ $size - 1 - $i ][8] = true;
		}

		// Versions-Info ab Version 7 (2 Bloecke 6x3).
		if ( $version >= 7 ) {
			$bits = self::version_bits( $version );
			for ( $i = 0; $i < 18; $i++ ) {
				$bit = ( $bits >> $i ) & 1;
				$row = intdiv( $i, 3 );
				$col = $size - 11 + ( $i % 3 );

				$modules[ $col ][ $row ]  = 1 === $bit;
				$reserved[ $col ][ $row ] = true;
				$modules[ $row ][ $col ]  = 1 === $bit;
				$reserved[ $row ][ $col ] = true;
			}
		}
	}

	/**
	 * Versions-Info (18 Bit, BCH 18,6).
	 *
	 * @param int $version Version (>= 7).
	 * @return int
	 */
	private static function version_bits( int $version ): int {
		$bch = $version << 12;
		for ( $i = 17; $i >= 12; $i-- ) {
			if ( ( $bch >> $i ) & 1 ) {
				$bch ^= 0x1F25 << ( $i - 12 );
			}
		}
		return ( $version << 12 ) | $bch;
	}

	/**
	 * Setzt die Datenbits im Zickzack (von rechts unten, Spaltenpaare, Spalte 6 uebersprungen).
	 *
	 * @param array<int, array<int, bool>> $modules   Matrix (by reference).
	 * @param array<int, array<int, bool>> $reserved  Funktionsflaechen.
	 * @param array<int, int>              $codewords Finale Codewoerter.
	 * @param int                          $size      Kantenlaenge.
	 * @return void
	 */
	private static function place_data( array &$modules, array $reserved, array $codewords, int $size ): void {
		$bits = '';
		foreach ( $codewords as $codeword ) {
			$bits .= str_pad( decbin( $codeword ), 8, '0', STR_PAD_LEFT );
		}

		$bit_index = 0;
		$total     = strlen( $bits );
		$upward    = true;

		for ( $col = $size - 1; $col > 0; $col -= 2 ) {
			if ( 6 === $col ) {
				$col--; // Timing-Spalte ueberspringen.
			}
			for ( $i = 0; $i < $size; $i++ ) {
				$row = $upward ? $size - 1 - $i : $i;
				foreach ( array( $col, $col - 1 ) as $c ) {
					if ( $reserved[ $row ][ $c ] ) {
						continue;
					}
					// Restbits (Remainder) bleiben hell (false) - Matrix ist vorbelegt.
					if ( $bit_index < $total ) {
						$modules[ $row ][ $c ] = '1' === $bits[ $bit_index ];
						$bit_index++;
					}
				}
			}
			$upward = ! $upward;
		}
	}

	/**
	 * Wendet eine Maske auf die Datenmodule an (Funktionsflaechen bleiben unberuehrt).
	 *
	 * @param array<int, array<int, bool>> $modules  Basis-Matrix.
	 * @param array<int, array<int, bool>> $reserved Funktionsflaechen.
	 * @param int                          $mask     Maskennummer 0-7.
	 * @param int                          $size     Kantenlaenge.
	 * @return array<int, array<int, bool>>
	 */
	private static function apply_mask( array $modules, array $reserved, int $mask, int $size ): array {
		for ( $y = 0; $y < $size; $y++ ) {
			for ( $x = 0; $x < $size; $x++ ) {
				if ( $reserved[ $y ][ $x ] ) {
					continue;
				}
				if ( self::mask_bit( $mask, $y, $x ) ) {
					$modules[ $y ][ $x ] = ! $modules[ $y ][ $x ];
				}
			}
		}
		return $modules;
	}

	/**
	 * Maskenbedingung (i = Zeile, j = Spalte).
	 *
	 * @param int $mask Maskennummer.
	 * @param int $i    Zeile.
	 * @param int $j    Spalte.
	 * @return bool
	 */
	private static function mask_bit( int $mask, int $i, int $j ): bool {
		switch ( $mask ) {
			case 0:
				return 0 === ( $i + $j ) % 2;
			case 1:
				return 0 === $i % 2;
			case 2:
				return 0 === $j % 3;
			case 3:
				return 0 === ( $i + $j ) % 3;
			case 4:
				return 0 === ( intdiv( $i, 2 ) + intdiv( $j, 3 ) ) % 2;
			case 5:
				return 0 === ( ( $i * $j ) % 2 ) + ( ( $i * $j ) % 3 );
			case 6:
				return 0 === ( ( ( $i * $j ) % 2 ) + ( ( $i * $j ) % 3 ) ) % 2;
			default:
				return 0 === ( ( ( $i + $j ) % 2 ) + ( ( $i * $j ) % 3 ) ) % 2;
		}
	}

	/**
	 * Schreibt die Format-Info fuer eine Maske an beide Positionen.
	 *
	 * @param array<int, array<int, bool>> $modules Matrix (by reference).
	 * @param int                          $mask    Maskennummer.
	 * @param int                          $size    Kantenlaenge.
	 * @return void
	 */
	private static function place_format_info( array &$modules, int $mask, int $size ): void {
		$bits = self::format_bits( $mask );

		// Bit 14 (MSB) zuerst. Position 1: um das linke obere Finder-Pattern.
		$positions_a = array(
			array( 8, 0 ), array( 8, 1 ), array( 8, 2 ), array( 8, 3 ), array( 8, 4 ), array( 8, 5 ),
			array( 8, 7 ), array( 8, 8 ), array( 7, 8 ), array( 5, 8 ), array( 4, 8 ), array( 3, 8 ),
			array( 2, 8 ), array( 1, 8 ), array( 0, 8 ),
		);
		// Position 2: unter dem rechten oberen bzw. rechts vom linken unteren Finder.
		$positions_b = array(
			array( $size - 1, 8 ), array( $size - 2, 8 ), array( $size - 3, 8 ), array( $size - 4, 8 ),
			array( $size - 5, 8 ), array( $size - 6, 8 ), array( $size - 7, 8 ),
			array( 8, $size - 8 ), array( 8, $size - 7 ), array( 8, $size - 6 ), array( 8, $size - 5 ),
			array( 8, $size - 4 ), array( 8, $size - 3 ), array( 8, $size - 2 ), array( 8, $size - 1 ),
		);

		for ( $i = 0; $i < 15; $i++ ) {
			$bit = ( ( $bits >> ( 14 - $i ) ) & 1 ) === 1;

			list( $ay, $ax )         = $positions_a[ $i ];
			$modules[ $ay ][ $ax ]   = $bit;
			list( $by, $bx )         = $positions_b[ $i ];
			$modules[ $by ][ $bx ]   = $bit;
		}
	}

	/**
	 * Penalty-Bewertung einer maskierten Matrix (Regeln N1-N4).
	 *
	 * @param array<int, array<int, bool>> $modules Matrix.
	 * @param int                          $size    Kantenlaenge.
	 * @return int
	 */
	private static function penalty( array $modules, int $size ): int {
		$score = 0;

		// N1: Laeufe gleicher Farbe >= 5 in Zeile/Spalte.
		for ( $dir = 0; $dir < 2; $dir++ ) {
			for ( $a = 0; $a < $size; $a++ ) {
				$run  = 1;
				$last = null;
				for ( $b = 0; $b < $size; $b++ ) {
					$value = 0 === $dir ? $modules[ $a ][ $b ] : $modules[ $b ][ $a ];
					if ( $value === $last ) {
						$run++;
					} else {
						if ( $run >= 5 ) {
							$score += 3 + ( $run - 5 );
						}
						$run  = 1;
						$last = $value;
					}
				}
				if ( $run >= 5 ) {
					$score += 3 + ( $run - 5 );
				}
			}
		}

		// N2: 2x2-Bloecke gleicher Farbe.
		for ( $y = 0; $y < $size - 1; $y++ ) {
			for ( $x = 0; $x < $size - 1; $x++ ) {
				$v = $modules[ $y ][ $x ];
				if ( $v === $modules[ $y ][ $x + 1 ] && $v === $modules[ $y + 1 ][ $x ] && $v === $modules[ $y + 1 ][ $x + 1 ] ) {
					$score += 3;
				}
			}
		}

		// N3: Finder-aehnliche Muster 1011101 mit 4 hellen Modulen davor/danach.
		$patterns = array( array( true, false, true, true, true, false, true, false, false, false, false ) );
		$patterns[] = array_reverse( $patterns[0] );
		for ( $dir = 0; $dir < 2; $dir++ ) {
			for ( $a = 0; $a < $size; $a++ ) {
				for ( $b = 0; $b <= $size - 11; $b++ ) {
					foreach ( $patterns as $pattern ) {
						$hit = true;
						for ( $k = 0; $k < 11; $k++ ) {
							$value = 0 === $dir ? $modules[ $a ][ $b + $k ] : $modules[ $b + $k ][ $a ];
							if ( $value !== $pattern[ $k ] ) {
								$hit = false;
								break;
							}
						}
						if ( $hit ) {
							$score += 40;
						}
					}
				}
			}
		}

		// N4: Abweichung des Dunkelanteils von 50 % in 5%-Schritten.
		$dark = 0;
		for ( $y = 0; $y < $size; $y++ ) {
			for ( $x = 0; $x < $size; $x++ ) {
				if ( $modules[ $y ][ $x ] ) {
					$dark++;
				}
			}
		}
		$percent = (int) ( 100 * $dark / ( $size * $size ) );
		$score  += 10 * intdiv( abs( $percent - 50 ), 5 );

		return $score;
	}

	/**
	 * Initialisiert die GF(256)-Tabellen (Primpolynom 0x11D).
	 *
	 * @return void
	 */
	private static function init_gf(): void {
		if ( null !== self::$gf_exp ) {
			return;
		}
		self::$gf_exp = array();
		$x            = 1;
		for ( $i = 0; $i < 255; $i++ ) {
			self::$gf_exp[ $i ]  = $x;
			self::$gf_log[ $x ]  = $i;
			$x                 <<= 1;
			if ( $x & 0x100 ) {
				$x ^= 0x11D;
			}
		}
		for ( $i = 255; $i < 512; $i++ ) {
			self::$gf_exp[ $i ] = self::$gf_exp[ $i - 255 ];
		}
	}

	/**
	 * GF(256)-Multiplikation.
	 *
	 * @param int $a Faktor.
	 * @param int $b Faktor.
	 * @return int
	 */
	private static function gf_mul( int $a, int $b ): int {
		if ( 0 === $a || 0 === $b ) {
			return 0;
		}
		return self::$gf_exp[ self::$gf_log[ $a ] + self::$gf_log[ $b ] ];
	}
}
