<?php
/**
 * BitForgeNet Guest Tickets - Dependency-free QR Code encoder (ISO/IEC 18004).
 *
 * Byte mode, versions 1-20, ECC levels L/M/Q/H, full mask evaluation.
 * No external library, no network, no shell-out. Works on any host with PHP 7.4+.
 * GD is only required for PNG output; SVG output is pure string building.
 *
 * @package BitForgeNet_Guest_Tickets
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * QR Code generator.
 */
class BFNET_QR {

	const ECC_L = 0;
	const ECC_M = 1;
	const ECC_Q = 2;
	const ECC_H = 3;

	/**
	 * ECC block structure.
	 * [version][level] => [ ec_codewords_per_block, blocks_g1, data_cw_g1, blocks_g2, data_cw_g2 ]
	 * Level order: L, M, Q, H.
	 *
	 * @var array
	 */
	private static $ecc_table = array(
		1  => array( array( 7, 1, 19, 0, 0 ), array( 10, 1, 16, 0, 0 ), array( 13, 1, 13, 0, 0 ), array( 17, 1, 9, 0, 0 ) ),
		2  => array( array( 10, 1, 34, 0, 0 ), array( 16, 1, 28, 0, 0 ), array( 22, 1, 22, 0, 0 ), array( 28, 1, 16, 0, 0 ) ),
		3  => array( array( 15, 1, 55, 0, 0 ), array( 26, 1, 44, 0, 0 ), array( 18, 2, 17, 0, 0 ), array( 22, 2, 13, 0, 0 ) ),
		4  => array( array( 20, 1, 80, 0, 0 ), array( 18, 2, 32, 0, 0 ), array( 26, 2, 24, 0, 0 ), array( 16, 4, 9, 0, 0 ) ),
		5  => array( array( 26, 1, 108, 0, 0 ), array( 24, 2, 43, 0, 0 ), array( 18, 2, 15, 2, 16 ), array( 22, 2, 11, 2, 12 ) ),
		6  => array( array( 18, 2, 68, 0, 0 ), array( 16, 4, 27, 0, 0 ), array( 24, 4, 19, 0, 0 ), array( 28, 4, 15, 0, 0 ) ),
		7  => array( array( 20, 2, 78, 0, 0 ), array( 18, 4, 31, 0, 0 ), array( 18, 2, 14, 4, 15 ), array( 26, 4, 13, 1, 14 ) ),
		8  => array( array( 24, 2, 97, 0, 0 ), array( 22, 2, 38, 2, 39 ), array( 22, 4, 18, 2, 19 ), array( 26, 4, 14, 2, 15 ) ),
		9  => array( array( 30, 2, 116, 0, 0 ), array( 22, 3, 36, 2, 37 ), array( 20, 4, 16, 4, 17 ), array( 24, 4, 12, 4, 13 ) ),
		10 => array( array( 18, 2, 68, 2, 69 ), array( 26, 4, 43, 1, 44 ), array( 24, 6, 19, 2, 20 ), array( 28, 6, 15, 2, 16 ) ),
		11 => array( array( 20, 4, 81, 0, 0 ), array( 30, 1, 50, 4, 51 ), array( 28, 4, 22, 4, 23 ), array( 24, 3, 12, 8, 13 ) ),
		12 => array( array( 24, 2, 92, 2, 93 ), array( 22, 6, 36, 2, 37 ), array( 26, 4, 20, 6, 21 ), array( 28, 7, 14, 4, 15 ) ),
		13 => array( array( 26, 4, 107, 0, 0 ), array( 22, 8, 37, 1, 38 ), array( 24, 8, 20, 4, 21 ), array( 22, 12, 11, 4, 12 ) ),
		14 => array( array( 30, 3, 115, 1, 116 ), array( 24, 4, 40, 5, 41 ), array( 20, 11, 16, 5, 17 ), array( 24, 11, 12, 5, 13 ) ),
		15 => array( array( 22, 5, 87, 1, 88 ), array( 24, 5, 41, 5, 42 ), array( 30, 5, 24, 7, 25 ), array( 24, 11, 12, 7, 13 ) ),
		16 => array( array( 24, 5, 98, 1, 99 ), array( 28, 7, 45, 3, 46 ), array( 24, 15, 19, 2, 20 ), array( 30, 3, 15, 13, 16 ) ),
		17 => array( array( 28, 1, 107, 5, 108 ), array( 28, 10, 46, 1, 47 ), array( 28, 1, 22, 15, 23 ), array( 28, 2, 14, 17, 15 ) ),
		18 => array( array( 30, 5, 120, 1, 121 ), array( 26, 9, 43, 4, 44 ), array( 28, 17, 22, 1, 23 ), array( 28, 2, 14, 19, 15 ) ),
		19 => array( array( 28, 3, 113, 4, 114 ), array( 26, 3, 44, 11, 45 ), array( 26, 17, 21, 4, 22 ), array( 26, 9, 13, 16, 14 ) ),
		20 => array( array( 28, 3, 107, 5, 108 ), array( 26, 3, 41, 13, 42 ), array( 30, 15, 24, 5, 25 ), array( 28, 15, 15, 10, 16 ) ),
	);

	/**
	 * Alignment pattern centre coordinates per version.
	 *
	 * @var array
	 */
	private static $align_positions = 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, 50 ),
		11 => array( 6, 30, 54 ),
		12 => array( 6, 32, 58 ),
		13 => array( 6, 34, 62 ),
		14 => array( 6, 26, 46, 66 ),
		15 => array( 6, 26, 48, 70 ),
		16 => array( 6, 26, 50, 74 ),
		17 => array( 6, 30, 54, 78 ),
		18 => array( 6, 30, 56, 82 ),
		19 => array( 6, 30, 58, 86 ),
		20 => array( 6, 34, 62, 90 ),
	);

	/** @var array GF(256) exponent table. */
	private static $gf_exp = array();
	/** @var array GF(256) log table. */
	private static $gf_log = array();

	/**
	 * Build the QR module matrix for a payload.
	 *
	 * @param string $data       Payload (binary safe).
	 * @param int    $ecc_level  One of the ECC_* constants.
	 * @param int    $min_version Minimum symbol version to consider.
	 * @return array 2D array [row][col] of 0|1.
	 * @throws InvalidArgumentException When the payload does not fit.
	 */
	public static function matrix( $data, $ecc_level = self::ECC_M, $min_version = 1 ) {
		self::init_gf();

		$len = strlen( $data );
		if ( 0 === $len ) {
			throw new InvalidArgumentException( 'QR payload is empty.' );
		}

		$version = 0;
		for ( $v = max( 1, (int) $min_version ); $v <= 20; $v++ ) {
			$spec     = self::$ecc_table[ $v ][ $ecc_level ];
			$capacity = ( $spec[1] * $spec[2] ) + ( $spec[3] * $spec[4] );
			$cc_bits  = ( $v < 10 ) ? 8 : 16;
			$needed   = (int) ceil( ( 4 + $cc_bits + ( $len * 8 ) ) / 8 );
			if ( $needed <= $capacity ) {
				$version = $v;
				break;
			}
		}
		if ( 0 === $version ) {
			throw new InvalidArgumentException( 'QR payload too large for versions 1-20.' );
		}

		$codewords = self::build_codewords( $data, $version, $ecc_level );
		return self::build_matrix( $codewords, $version, $ecc_level );
	}

	/* --------------------------------------------------------------------- */
	/* Galois field                                                          */
	/* --------------------------------------------------------------------- */

	/**
	 * Initialise GF(256) log/exp tables (primitive polynomial 0x11D).
	 *
	 * @return void
	 */
	private static function init_gf() {
		if ( ! empty( self::$gf_exp ) ) {
			return;
		}
		self::$gf_exp = array_fill( 0, 512, 0 );
		self::$gf_log = array_fill( 0, 256, 0 );
		$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 ];
		}
	}

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

	/**
	 * Build the Reed-Solomon generator polynomial of a given degree.
	 *
	 * @param int $degree Number of EC codewords.
	 * @return array
	 */
	private static function rs_generator( $degree ) {
		$poly = array( 1 );
		for ( $i = 0; $i < $degree; $i++ ) {
			$next = array_fill( 0, count( $poly ) + 1, 0 );
			foreach ( $poly as $k => $coef ) {
				$next[ $k ]     ^= self::gf_mul( $coef, self::$gf_exp[ $i ] );
				$next[ $k + 1 ] ^= $coef;
			}
			$poly = $next;
		}
		return $poly;
	}

	/**
	 * Compute EC codewords for a data block.
	 *
	 * @param array $block  Data codewords.
	 * @param int   $degree Number of EC codewords.
	 * @return array
	 */
	private static function rs_remainder( array $block, $degree ) {
		// rs_generator() returns ascending powers with a leading 1 at index $degree.
		// The synthetic-division loop below needs descending powers without that leading term.
		$gen       = array_reverse( array_slice( self::rs_generator( $degree ), 0, $degree ) );
		$remainder = array_fill( 0, $degree, 0 );
		foreach ( $block as $byte ) {
			$factor      = $byte ^ array_shift( $remainder );
			$remainder[] = 0;
			for ( $i = 0; $i < $degree; $i++ ) {
				$remainder[ $i ] ^= self::gf_mul( $gen[ $i ], $factor );
			}
		}
		return $remainder;
	}

	/* --------------------------------------------------------------------- */
	/* Data encoding                                                         */
	/* --------------------------------------------------------------------- */

	/**
	 * Encode payload into the final interleaved codeword stream.
	 *
	 * @param string $data      Payload.
	 * @param int    $version   Symbol version.
	 * @param int    $ecc_level ECC level constant.
	 * @return array Codeword integers.
	 */
	private static function build_codewords( $data, $version, $ecc_level ) {
		$spec       = self::$ecc_table[ $version ][ $ecc_level ];
		list( $ec_per_block, $blocks1, $data1, $blocks2, $data2 ) = $spec;
		$total_data = ( $blocks1 * $data1 ) + ( $blocks2 * $data2 );

		// Bit stream: mode 0100 + count + payload.
		$bits    = '0100';
		$cc_bits = ( $version < 10 ) ? 8 : 16;
		$bits   .= str_pad( decbin( strlen( $data ) ), $cc_bits, '0', STR_PAD_LEFT );
		$len     = strlen( $data );
		for ( $i = 0; $i < $len; $i++ ) {
			$bits .= str_pad( decbin( ord( $data[ $i ] ) ), 8, '0', STR_PAD_LEFT );
		}

		// Terminator (max 4 zero bits) then pad to byte boundary.
		$capacity_bits = $total_data * 8;
		$bits         .= str_repeat( '0', min( 4, $capacity_bits - strlen( $bits ) ) );
		if ( strlen( $bits ) % 8 !== 0 ) {
			$bits .= str_repeat( '0', 8 - ( strlen( $bits ) % 8 ) );
		}

		$dcw = array();
		foreach ( str_split( $bits, 8 ) as $byte ) {
			$dcw[] = bindec( $byte );
		}
		// Pad codewords.
		$pad = array( 0xEC, 0x11 );
		$p   = 0;
		while ( count( $dcw ) < $total_data ) {
			$dcw[] = $pad[ $p % 2 ];
			$p++;
		}

		// Split into blocks.
		$data_blocks = array();
		$ec_blocks   = array();
		$offset      = 0;
		for ( $b = 0; $b < $blocks1 + $blocks2; $b++ ) {
			$size          = ( $b < $blocks1 ) ? $data1 : $data2;
			$block         = array_slice( $dcw, $offset, $size );
			$offset       += $size;
			$data_blocks[] = $block;
			$ec_blocks[]   = self::rs_remainder( $block, $ec_per_block );
		}

		// Interleave data then EC.
		$result   = array();
		$max_data = max( $data1, $data2 );
		for ( $i = 0; $i < $max_data; $i++ ) {
			foreach ( $data_blocks as $block ) {
				if ( isset( $block[ $i ] ) ) {
					$result[] = $block[ $i ];
				}
			}
		}
		for ( $i = 0; $i < $ec_per_block; $i++ ) {
			foreach ( $ec_blocks as $block ) {
				$result[] = $block[ $i ];
			}
		}

		return $result;
	}

	/* --------------------------------------------------------------------- */
	/* Matrix construction                                                   */
	/* --------------------------------------------------------------------- */

	/**
	 * Number of remainder bits for a version.
	 *
	 * @param int $version Symbol version.
	 * @return int
	 */
	private static function remainder_bits( $version ) {
		if ( 1 === $version ) {
			return 0;
		}
		if ( $version <= 6 ) {
			return 7;
		}
		if ( $version <= 13 ) {
			return 0;
		}
		if ( $version <= 20 ) {
			return 3;
		}
		return 0;
	}

	/**
	 * Assemble, mask and finalise the module matrix.
	 *
	 * @param array $codewords Interleaved codewords.
	 * @param int   $version   Symbol version.
	 * @param int   $ecc_level ECC level constant.
	 * @return array
	 */
	private static function build_matrix( array $codewords, $version, $ecc_level ) {
		$size     = ( $version * 4 ) + 17;
		$matrix   = array_fill( 0, $size, array_fill( 0, $size, 0 ) );
		$reserved = array_fill( 0, $size, array_fill( 0, $size, false ) );

		// Finder patterns + separators.
		foreach ( array( array( 0, 0 ), array( 0, $size - 7 ), array( $size - 7, 0 ) ) as $pos ) {
			self::place_finder( $matrix, $reserved, $pos[0], $pos[1], $size );
		}

		// Timing patterns.
		for ( $i = 8; $i < $size - 8; $i++ ) {
			$bit                 = ( 0 === $i % 2 ) ? 1 : 0;
			$matrix[6][ $i ]     = $bit;
			$reserved[6][ $i ]   = true;
			$matrix[ $i ][6]     = $bit;
			$reserved[ $i ][6]   = true;
		}

		// Alignment patterns.
		$centres = self::$align_positions[ $version ];
		$count   = count( $centres );
		for ( $a = 0; $a < $count; $a++ ) {
			for ( $b = 0; $b < $count; $b++ ) {
				$r = $centres[ $a ];
				$c = $centres[ $b ];
				// Skip the three cells overlapping finder patterns.
				if ( ( 0 === $a && 0 === $b ) || ( 0 === $a && $b === $count - 1 ) || ( $a === $count - 1 && 0 === $b ) ) {
					continue;
				}
				self::place_alignment( $matrix, $reserved, $r, $c );
			}
		}

		// Dark module.
		$matrix[ $size - 8 ][8]   = 1;
		$reserved[ $size - 8 ][8] = true;

		// Reserve format information areas.
		for ( $i = 0; $i < 9; $i++ ) {
			if ( ! $reserved[8][ $i ] ) {
				$reserved[8][ $i ] = true;
			}
			if ( ! $reserved[ $i ][8] ) {
				$reserved[ $i ][8] = true;
			}
		}
		for ( $i = 0; $i < 8; $i++ ) {
			$reserved[8][ $size - 1 - $i ] = true;
			$reserved[ $size - 1 - $i ][8] = true;
		}

		// Reserve version information areas (version >= 7).
		if ( $version >= 7 ) {
			for ( $i = 0; $i < 6; $i++ ) {
				for ( $j = 0; $j < 3; $j++ ) {
					$reserved[ $i ][ $size - 11 + $j ] = true;
					$reserved[ $size - 11 + $j ][ $i ] = true;
				}
			}
		}

		// Build the data bit string.
		$bits = '';
		foreach ( $codewords as $cw ) {
			$bits .= str_pad( decbin( $cw ), 8, '0', STR_PAD_LEFT );
		}
		$bits .= str_repeat( '0', self::remainder_bits( $version ) );

		// Zigzag placement, right to left, skipping the vertical timing column.
		$idx = 0;
		$len = strlen( $bits );
		$up  = true;
		for ( $col = $size - 1; $col > 0; $col -= 2 ) {
			if ( 6 === $col ) {
				$col = 5;
			}
			for ( $n = 0; $n < $size; $n++ ) {
				$row = $up ? ( $size - 1 - $n ) : $n;
				for ( $k = 0; $k < 2; $k++ ) {
					$c = $col - $k;
					if ( $reserved[ $row ][ $c ] ) {
						continue;
					}
					$matrix[ $row ][ $c ] = ( $idx < $len && '1' === $bits[ $idx ] ) ? 1 : 0;
					$idx++;
				}
			}
			$up = ! $up;
		}

		// Evaluate all eight masks.
		$best      = null;
		$best_mask = 0;
		$best_pen  = PHP_INT_MAX;
		for ( $mask = 0; $mask < 8; $mask++ ) {
			$candidate = self::apply_mask( $matrix, $reserved, $size, $mask );
			self::write_format( $candidate, $size, $ecc_level, $mask );
			if ( $version >= 7 ) {
				self::write_version( $candidate, $size, $version );
			}
			$penalty = self::penalty( $candidate, $size );
			if ( $penalty < $best_pen ) {
				$best_pen  = $penalty;
				$best      = $candidate;
				$best_mask = $mask;
			}
		}
		unset( $best_mask );

		return $best;
	}

	/**
	 * Draw a finder pattern and its separator.
	 *
	 * @param array $matrix   Matrix by reference.
	 * @param array $reserved Reserved map by reference.
	 * @param int   $row      Top row.
	 * @param int   $col      Left column.
	 * @param int   $size     Matrix size.
	 * @return void
	 */
	private static function place_finder( array &$matrix, array &$reserved, $row, $col, $size ) {
		for ( $r = -1; $r <= 7; $r++ ) {
			for ( $c = -1; $c <= 7; $c++ ) {
				$rr = $row + $r;
				$cc = $col + $c;
				if ( $rr < 0 || $cc < 0 || $rr >= $size || $cc >= $size ) {
					continue;
				}
				$in_ring   = ( 0 === $r || 6 === $r || 0 === $c || 6 === $c ) && ( $r >= 0 && $r <= 6 && $c >= 0 && $c <= 6 );
				$in_centre = ( $r >= 2 && $r <= 4 && $c >= 2 && $c <= 4 );
				$matrix[ $rr ][ $cc ]   = ( $in_ring || $in_centre ) ? 1 : 0;
				$reserved[ $rr ][ $cc ] = true;
			}
		}
	}

	/**
	 * Draw a 5x5 alignment pattern centred on the given cell.
	 *
	 * @param array $matrix   Matrix by reference.
	 * @param array $reserved Reserved map by reference.
	 * @param int   $row      Centre row.
	 * @param int   $col      Centre column.
	 * @return void
	 */
	private static function place_alignment( array &$matrix, array &$reserved, $row, $col ) {
		for ( $r = -2; $r <= 2; $r++ ) {
			for ( $c = -2; $c <= 2; $c++ ) {
				$dark = ( 2 === max( abs( $r ), abs( $c ) ) ) || ( 0 === $r && 0 === $c );
				$matrix[ $row + $r ][ $col + $c ]   = $dark ? 1 : 0;
				$reserved[ $row + $r ][ $col + $c ] = true;
			}
		}
	}

	/**
	 * Return a masked copy of the matrix.
	 *
	 * @param array $matrix   Source matrix.
	 * @param array $reserved Reserved map.
	 * @param int   $size     Matrix size.
	 * @param int   $mask     Mask pattern 0-7.
	 * @return array
	 */
	private static function apply_mask( array $matrix, array $reserved, $size, $mask ) {
		for ( $r = 0; $r < $size; $r++ ) {
			for ( $c = 0; $c < $size; $c++ ) {
				if ( $reserved[ $r ][ $c ] ) {
					continue;
				}
				switch ( $mask ) {
					case 0:
						$flip = ( 0 === ( $r + $c ) % 2 );
						break;
					case 1:
						$flip = ( 0 === $r % 2 );
						break;
					case 2:
						$flip = ( 0 === $c % 3 );
						break;
					case 3:
						$flip = ( 0 === ( $r + $c ) % 3 );
						break;
					case 4:
						$flip = ( 0 === ( intdiv( $r, 2 ) + intdiv( $c, 3 ) ) % 2 );
						break;
					case 5:
						$flip = ( 0 === ( ( $r * $c ) % 2 ) + ( ( $r * $c ) % 3 ) );
						break;
					case 6:
						$flip = ( 0 === ( ( ( $r * $c ) % 2 ) + ( ( $r * $c ) % 3 ) ) % 2 );
						break;
					default:
						$flip = ( 0 === ( ( ( $r + $c ) % 2 ) + ( ( $r * $c ) % 3 ) ) % 2 );
						break;
				}
				if ( $flip ) {
					$matrix[ $r ][ $c ] ^= 1;
				}
			}
		}
		return $matrix;
	}

	/**
	 * Write the 15-bit format information.
	 *
	 * @param array $matrix    Matrix by reference.
	 * @param int   $size      Matrix size.
	 * @param int   $ecc_level ECC level constant.
	 * @param int   $mask      Mask pattern.
	 * @return void
	 */
	private static function write_format( array &$matrix, $size, $ecc_level, $mask ) {
		$level_bits = array( self::ECC_L => 1, self::ECC_M => 0, self::ECC_Q => 3, self::ECC_H => 2 );
		$data       = ( $level_bits[ $ecc_level ] << 3 ) | $mask;
		$rem        = $data;
		for ( $i = 0; $i < 10; $i++ ) {
			$rem = ( $rem << 1 ) ^ ( ( ( $rem >> 9 ) & 1 ) * 0x537 );
		}
		$format = ( ( ( $data << 10 ) | $rem ) ^ 0x5412 ) & 0x7FFF;

		for ( $i = 0; $i < 15; $i++ ) {
			$bit = ( $format >> $i ) & 1;
			// Copy 1: around the top-left finder.
			if ( $i < 6 ) {
				$matrix[ $i ][8] = $bit;
			} elseif ( 6 === $i ) {
				$matrix[7][8] = $bit;
			} elseif ( 7 === $i ) {
				$matrix[8][8] = $bit;
			} elseif ( 8 === $i ) {
				$matrix[8][7] = $bit;
			} else {
				$matrix[8][ 14 - $i ] = $bit;
			}
			// Copy 2: split between top-right and bottom-left.
			if ( $i < 8 ) {
				$matrix[8][ $size - 1 - $i ] = $bit;
			} else {
				$matrix[ $size - 15 + $i ][8] = $bit;
			}
		}
		$matrix[ $size - 8 ][8] = 1;
	}

	/**
	 * Write the 18-bit version information (versions 7+).
	 *
	 * @param array $matrix  Matrix by reference.
	 * @param int   $size    Matrix size.
	 * @param int   $version Symbol version.
	 * @return void
	 */
	private static function write_version( array &$matrix, $size, $version ) {
		$rem = $version;
		for ( $i = 0; $i < 12; $i++ ) {
			$rem = ( $rem << 1 ) ^ ( ( ( $rem >> 11 ) & 1 ) * 0x1F25 );
		}
		$bits = ( $version << 12 ) | $rem;

		for ( $i = 0; $i < 18; $i++ ) {
			$bit = ( $bits >> $i ) & 1;
			$a   = intdiv( $i, 3 );
			$b   = $size - 11 + ( $i % 3 );
			$matrix[ $a ][ $b ] = $bit;
			$matrix[ $b ][ $a ] = $bit;
		}
	}

	/**
	 * Compute the total mask penalty score.
	 *
	 * @param array $m    Matrix.
	 * @param int   $size Matrix size.
	 * @return int
	 */
	private static function penalty( array $m, $size ) {
		$score = 0;

		// Rule 1: runs of five or more identical modules.
		for ( $r = 0; $r < $size; $r++ ) {
			$run_h = 1;
			$run_v = 1;
			for ( $c = 1; $c < $size; $c++ ) {
				$run_h = ( $m[ $r ][ $c ] === $m[ $r ][ $c - 1 ] ) ? $run_h + 1 : 1;
				if ( 5 === $run_h ) {
					$score += 3;
				} elseif ( $run_h > 5 ) {
					$score++;
				}
				$run_v = ( $m[ $c ][ $r ] === $m[ $c - 1 ][ $r ] ) ? $run_v + 1 : 1;
				if ( 5 === $run_v ) {
					$score += 3;
				} elseif ( $run_v > 5 ) {
					$score++;
				}
			}
		}

		// Rule 2: 2x2 blocks of the same colour.
		for ( $r = 0; $r < $size - 1; $r++ ) {
			for ( $c = 0; $c < $size - 1; $c++ ) {
				$v = $m[ $r ][ $c ];
				if ( $v === $m[ $r ][ $c + 1 ] && $v === $m[ $r + 1 ][ $c ] && $v === $m[ $r + 1 ][ $c + 1 ] ) {
					$score += 3;
				}
			}
		}

		// Rule 3: finder-like patterns.
		$p1 = array( 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0 );
		$p2 = array( 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1 );
		for ( $r = 0; $r < $size; $r++ ) {
			for ( $c = 0; $c <= $size - 11; $c++ ) {
				$row_ok1 = true;
				$row_ok2 = true;
				$col_ok1 = true;
				$col_ok2 = true;
				for ( $k = 0; $k < 11; $k++ ) {
					if ( $m[ $r ][ $c + $k ] !== $p1[ $k ] ) {
						$row_ok1 = false;
					}
					if ( $m[ $r ][ $c + $k ] !== $p2[ $k ] ) {
						$row_ok2 = false;
					}
					if ( $m[ $c + $k ][ $r ] !== $p1[ $k ] ) {
						$col_ok1 = false;
					}
					if ( $m[ $c + $k ][ $r ] !== $p2[ $k ] ) {
						$col_ok2 = false;
					}
				}
				$score += ( $row_ok1 ? 40 : 0 ) + ( $row_ok2 ? 40 : 0 ) + ( $col_ok1 ? 40 : 0 ) + ( $col_ok2 ? 40 : 0 );
			}
		}

		// Rule 4: dark module ratio.
		$dark = 0;
		foreach ( $m as $row ) {
			$dark += array_sum( $row );
		}
		$total   = $size * $size;
		$percent = ( $dark * 100 ) / $total;
		$score  += intdiv( (int) abs( $percent - 50 ), 5 ) * 10;

		return $score;
	}

	/* --------------------------------------------------------------------- */
	/* Rendering                                                             */
	/* --------------------------------------------------------------------- */

	/**
	 * Render a QR code as a GD image resource.
	 *
	 * @param string $data       Payload.
	 * @param int    $target_px  Desired image edge size in pixels (approximate).
	 * @param int    $ecc_level  ECC level constant.
	 * @param int    $quiet      Quiet zone in modules (4 is the standard minimum).
	 * @param array  $dark_rgb   Foreground colour.
	 * @param array  $light_rgb  Background colour.
	 * @return resource|GdImage
	 */
	public static function image( $data, $target_px = 600, $ecc_level = self::ECC_M, $quiet = 4, $dark_rgb = array( 0, 0, 0 ), $light_rgb = array( 255, 255, 255 ) ) {
		$matrix  = self::matrix( $data, $ecc_level );
		$modules = count( $matrix );
		$total   = $modules + ( $quiet * 2 );
		$scale   = max( 1, (int) floor( $target_px / $total ) );
		$px      = $total * $scale;

		$img = imagecreatetruecolor( $px, $px );
		imagealphablending( $img, false );
		imagesavealpha( $img, true );
		$light = imagecolorallocate( $img, $light_rgb[0], $light_rgb[1], $light_rgb[2] );
		$dark  = imagecolorallocate( $img, $dark_rgb[0], $dark_rgb[1], $dark_rgb[2] );
		imagefilledrectangle( $img, 0, 0, $px, $px, $light );

		for ( $r = 0; $r < $modules; $r++ ) {
			for ( $c = 0; $c < $modules; $c++ ) {
				if ( 1 !== $matrix[ $r ][ $c ] ) {
					continue;
				}
				$x = ( $c + $quiet ) * $scale;
				$y = ( $r + $quiet ) * $scale;
				imagefilledrectangle( $img, $x, $y, $x + $scale - 1, $y + $scale - 1, $dark );
			}
		}

		return $img;
	}

	/**
	 * Render a QR code as an SVG string (for admin previews and emails).
	 *
	 * @param string $data      Payload.
	 * @param int    $size_px   SVG viewport size.
	 * @param int    $ecc_level ECC level constant.
	 * @param int    $quiet     Quiet zone in modules.
	 * @param string $dark      Foreground colour (CSS).
	 * @param string $light     Background colour (CSS), or 'none'.
	 * @return string
	 */
	public static function svg( $data, $size_px = 240, $ecc_level = self::ECC_M, $quiet = 4, $dark = '#000000', $light = '#FFFFFF' ) {
		$matrix  = self::matrix( $data, $ecc_level );
		$modules = count( $matrix );
		$total   = $modules + ( $quiet * 2 );

		$path = '';
		for ( $r = 0; $r < $modules; $r++ ) {
			for ( $c = 0; $c < $modules; $c++ ) {
				if ( 1 === $matrix[ $r ][ $c ] ) {
					$path .= 'M' . ( $c + $quiet ) . ' ' . ( $r + $quiet ) . 'h1v1h-1z';
				}
			}
		}

		$bg = ( 'none' === $light ) ? '' : '<rect width="' . $total . '" height="' . $total . '" fill="' . $light . '"/>';

		return '<svg xmlns="http://www.w3.org/2000/svg" width="' . (int) $size_px . '" height="' . (int) $size_px . '" viewBox="0 0 ' . $total . ' ' . $total . '" shape-rendering="crispEdges" role="img" aria-label="QR code">' . $bg . '<path d="' . $path . '" fill="' . $dark . '"/></svg>';
	}

	/**
	 * Render a QR code as a base64 data URI PNG.
	 *
	 * @param string $data      Payload.
	 * @param int    $target_px Approximate pixel size.
	 * @param int    $ecc_level ECC level constant.
	 * @return string
	 */
	public static function data_uri( $data, $target_px = 400, $ecc_level = self::ECC_M ) {
		$img = self::image( $data, $target_px, $ecc_level );
		ob_start();
		imagepng( $img );
		$raw = ob_get_clean();
		imagedestroy( $img );
		return 'data:image/png;base64,' . base64_encode( $raw );
	}
}
