<?php
/**
 * PBUAIV QR Code Generator
 *
 * Generates standards-compliant QR codes (ISO/IEC 18004:2015) as inline SVG data URIs.
 * Supports byte-mode encoding with Error Correction Level M, versions 1–10.
 * Self-contained — no external libraries.
 *
 * @package PB_Invoicing_System
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class PBUAIV_QR_Code {

	// ── Format information for ECC level M (00), masks 0–7 ──────────────────
	// 15-bit values after XOR with mask pattern 101010000010010.
	// From ISO/IEC 18004:2015 Table C.1.
	private const FORMAT_INFO = [
		0b101010000010010, // M, mask 0
		0b101000100100101, // M, mask 1
		0b101111001111100, // M, mask 2
		0b101101101001011, // M, mask 3
		0b100010111111001, // M, mask 4
		0b100000011001110, // M, mask 5
		0b100111110010111, // M, mask 6
		0b100101010100000, // M, mask 7
	];

	// ── Data codeword counts for ECC-M per version ──────────────────────────
	private const DATA_CW = [
		1 => 16,  2 => 28,  3 => 44,  4 => 64,  5 => 86,
		6 => 108, 7 => 124, 8 => 154, 9 => 182, 10 => 216,
	];

	// ── Byte-mode capacity (characters) for ECC-M per version ───────────────
	private const BYTE_CAP = [
		1 => 14,  2 => 26,  3 => 42,  4 => 62,  5 => 84,
		6 => 106, 7 => 122, 8 => 152, 9 => 180, 10 => 214,
	];

	/**
	 * EC block parameters for ECC-M per version.
	 * [ec_cw_per_block, num_blocks_g1, data_cw_g1, num_blocks_g2, data_cw_g2]
	 */
	private const EC_PARAMS = [
		1  => [10, 1, 16, 0, 0],
		2  => [16, 1, 28, 0, 0],
		3  => [26, 1, 44, 0, 0],
		4  => [18, 2, 32, 0, 0],
		5  => [24, 2, 43, 0, 0],
		6  => [16, 4, 27, 0, 0],
		7  => [18, 4, 31, 0, 0],
		8  => [22, 2, 38, 2, 39],
		9  => [22, 3, 36, 2, 37],
		10 => [26, 4, 43, 1, 44],
	];

	/** Alignment pattern center positions per version (from the spec). */
	private const ALIGN_POS = [
		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],
	];

	// ── GF(256) arithmetic ───────────────────────────────────────────────────

	/** @var int[] */
	private static array $gf_exp = [];
	/** @var int[] */
	private static array $gf_log = [];
	private static bool $gf_ready = false;

	private static function gf_init() : void {
		if ( self::$gf_ready ) 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; // primitive polynomial x^8+x^4+x^3+x^2+1
			}
		}
		for ( $i = 255; $i < 512; $i++ ) {
			self::$gf_exp[ $i ] = self::$gf_exp[ $i - 255 ];
		}
		self::$gf_ready = true;
	}

	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 ] ) % 255 ];
	}

	/** Build the Reed-Solomon generator polynomial for the given degree. */
	private static function rs_generator( int $degree ) : array {
		self::gf_init();
		$g = [1];
		for ( $i = 0; $i < $degree; $i++ ) {
			$next = array_fill( 0, count( $g ) + 1, 0 );
			$ai   = self::$gf_exp[ $i ];
			foreach ( $g as $j => $c ) {
				$next[ $j ]     ^= $c;
				$next[ $j + 1 ] ^= self::gf_mul( $c, $ai );
			}
			$g = $next;
		}
		return $g;
	}

	/** Compute Reed-Solomon error correction codewords. */
	private static function rs_encode( array $data, int $ec_count ) : array {
		self::gf_init();
		$gen = self::rs_generator( $ec_count );
		$buf = array_merge( $data, array_fill( 0, $ec_count, 0 ) );
		$n   = count( $data );
		for ( $i = 0; $i < $n; $i++ ) {
			$c = $buf[ $i ];
			if ( 0 === $c ) continue;
			for ( $j = 1; $j <= $ec_count; $j++ ) {
				$buf[ $i + $j ] ^= self::gf_mul( $gen[ $j ], $c );
			}
		}
		return array_slice( $buf, $n );
	}

	// ── Public API ───────────────────────────────────────────────────────────

	/**
	 * Encode $text as a QR code and return a data:image/svg+xml;base64,… URI.
	 *
	 * @param string $text  The payload to encode (byte mode).
	 * @param int    $size  Output size in CSS pixels (the SVG is square).
	 * @return string       SVG data URI, or empty string on failure.
	 */
	public static function svg_data_uri( string $text, int $size = 90 ) : string {
		if ( '' === $text ) return '';

		$bytes = array_values( unpack( 'C*', $text ) );
		$len   = count( $bytes );

		// Pick the smallest version that fits
		$version = null;
		foreach ( self::BYTE_CAP as $v => $cap ) {
			if ( $len <= $cap ) {
				$version = $v;
				break;
			}
		}
		if ( null === $version ) return ''; // data exceeds max capacity for v10/ECC-M

		// ── 1. Build data bit stream ─────────────────────────────────────────
		$bits = '0100'; // mode = byte
		$bits .= sprintf( '%08b', $len ); // char count (8 bits for versions 1-9)
		foreach ( $bytes as $b ) {
			$bits .= sprintf( '%08b', $b );
		}

		$total_bits = self::DATA_CW[ $version ] * 8;
		// Terminator (up to 4 zeros)
		$bits .= str_repeat( '0', min( 4, $total_bits - strlen( $bits ) ) );
		// Byte-align
		$pad = ( 8 - ( strlen( $bits ) % 8 ) ) % 8;
		$bits .= str_repeat( '0', $pad );
		// Pad to required capacity with 0xEC / 0x11 alternating
		$pad_cw = ['11101100', '00010001'];
		$pi     = 0;
		while ( strlen( $bits ) < $total_bits ) {
			$bits .= $pad_cw[ $pi++ % 2 ];
		}

		// Convert bit string to integer codewords
		$data_cw = [];
		for ( $i = 0; $i < strlen( $bits ); $i += 8 ) {
			$data_cw[] = (int) bindec( substr( $bits, $i, 8 ) );
		}

		// ── 2. Error correction + interleaving ───────────────────────────────
		$all_cw = self::build_codewords( $data_cw, $version );

		// ── 3. Build QR matrix ───────────────────────────────────────────────
		[ $matrix, $func ] = self::build_matrix( $version, $all_cw );

		// ── 4. Choose best mask ──────────────────────────────────────────────
		$best_mask  = 0;
		$best_score = PHP_INT_MAX;
		for ( $m = 0; $m < 8; $m++ ) {
			$test = self::apply_mask( $matrix, $func, $m );
			self::place_format( $test, $m );
			$score = self::penalty( $test );
			if ( $score < $best_score ) {
				$best_score = $score;
				$best_mask  = $m;
			}
		}

		$final = self::apply_mask( $matrix, $func, $best_mask );
		self::place_format( $final, $best_mask );

		$px = max( 60, min( 400, absint( $size ) ) );
		return 'data:image/svg+xml;base64,' . base64_encode( self::to_svg( $final, $px ) );
	}

	// ── Step 2: interleave blocks and add EC ─────────────────────────────────

	private static function build_codewords( array $data_cw, int $version ) : array {
		[ $ec_n, $nb1, $dc1, $nb2, $dc2 ] = self::EC_PARAMS[ $version ];

		$blocks    = [];
		$ec_blocks = [];
		$pos       = 0;

		for ( $b = 0; $b < $nb1; $b++ ) {
			$d           = array_slice( $data_cw, $pos, $dc1 );
			$blocks[]    = $d;
			$ec_blocks[] = self::rs_encode( $d, $ec_n );
			$pos        += $dc1;
		}
		for ( $b = 0; $b < $nb2; $b++ ) {
			$d           = array_slice( $data_cw, $pos, $dc2 );
			$blocks[]    = $d;
			$ec_blocks[] = self::rs_encode( $d, $ec_n );
			$pos        += $dc2;
		}

		// Interleave data
		$out    = [];
		$max_dc = max( $dc1, $dc2 ?: 0 );
		for ( $i = 0; $i < $max_dc; $i++ ) {
			foreach ( $blocks as $blk ) {
				if ( isset( $blk[ $i ] ) ) $out[] = $blk[ $i ];
			}
		}
		// Interleave EC
		for ( $i = 0; $i < $ec_n; $i++ ) {
			foreach ( $ec_blocks as $blk ) {
				if ( isset( $blk[ $i ] ) ) $out[] = $blk[ $i ];
			}
		}
		return $out;
	}

	// ── Step 3: build the QR matrix ──────────────────────────────────────────

	/**
	 * Returns [matrix, func_mask].
	 * matrix: 2D array of bool (dark/light).
	 * func_mask: 2D array of bool (true = function module, do not apply data mask).
	 *
	 * @return array{0: bool[][], 1: bool[][]}
	 */
	private static function build_matrix( int $version, array $codewords ) : array {
		$n = $version * 4 + 17;

		// null = unfilled data slot; bool = placed module
		$mat  = array_fill( 0, $n, array_fill( 0, $n, null ) );
		$func = array_fill( 0, $n, array_fill( 0, $n, false ) );

		// Finder patterns (top-left, top-right, bottom-left) + separators
		self::place_finder( $mat, $func, 0,     0,     $n );
		self::place_finder( $mat, $func, 0,     $n - 7, $n );
		self::place_finder( $mat, $func, $n - 7, 0,     $n );

		// Timing patterns
		for ( $i = 8; $i < $n - 8; $i++ ) {
			$mat[ 6 ][ $i ]  = ( $i % 2 === 0 );
			$func[ 6 ][ $i ] = true;
			$mat[ $i ][ 6 ]  = ( $i % 2 === 0 );
			$func[ $i ][ 6 ] = true;
		}

		// Dark module (always dark)
		$mat[ $n - 8 ][ 8 ]  = true;
		$func[ $n - 8 ][ 8 ] = true;

		// Alignment patterns
		$ap = self::ALIGN_POS[ $version ];
		foreach ( $ap as $ar ) {
			foreach ( $ap as $ac ) {
				// Skip positions overlapping with finder patterns
				if ( $ar <= 8 && $ac <= 8 ) continue;
				if ( $ar <= 8 && $ac >= $n - 8 ) continue;
				if ( $ar >= $n - 8 && $ac <= 8 ) continue;
				self::place_alignment( $mat, $func, $ar, $ac );
			}
		}

		// Reserve format info areas (mark as function, filled later)
		$fi_positions = self::format_info_positions( $n );
		foreach ( $fi_positions as [ $r, $c ] ) {
			if ( null === $mat[ $r ][ $c ] ) $mat[ $r ][ $c ] = false;
			$func[ $r ][ $c ] = true;
		}

		// Place data bits (zig-zag from bottom-right)
		$bits = '';
		foreach ( $codewords as $cw ) {
			$bits .= sprintf( '%08b', $cw );
		}
		$bit_i    = 0;
		$bit_len  = strlen( $bits );
		$col      = $n - 1;
		$going_up = true;

		while ( $col >= 0 ) {
			if ( 6 === $col ) { $col--; continue; } // skip timing column
			for ( $ri = 0; $ri < $n; $ri++ ) {
				$row = $going_up ? ( $n - 1 - $ri ) : $ri;
				foreach ( [0, 1] as $delta ) {
					$c = $col - $delta;
					if ( $c < 0 ) continue;
					if ( null !== $mat[ $row ][ $c ] ) continue; // already placed
					$bit = ( $bit_i < $bit_len ) ? $bits[ $bit_i++ ] : '0';
					$mat[ $row ][ $c ] = ( '1' === $bit );
				}
			}
			$col     -= 2;
			$going_up = ! $going_up;
		}

		// Fill any remaining nulls (remainder bits) as false (light)
		for ( $r = 0; $r < $n; $r++ ) {
			for ( $c = 0; $c < $n; $c++ ) {
				if ( null === $mat[ $r ][ $c ] ) $mat[ $r ][ $c ] = false;
			}
		}

		return [ $mat, $func ];
	}

	/** Place a 7×7 finder pattern + 1-module separator. */
	private static function place_finder( array &$mat, array &$func, int $row, int $col, int $n ) : void {
		for ( $r = -1; $r <= 7; $r++ ) {
			for ( $c = -1; $c <= 7; $c++ ) {
				$pr = $row + $r;
				$pc = $col + $c;
				if ( $pr < 0 || $pc < 0 || $pr >= $n || $pc >= $n ) continue;

				if ( $r === -1 || $r === 7 || $c === -1 || $c === 7 ) {
					// Separator
					$mat[ $pr ][ $pc ]  = false;
					$func[ $pr ][ $pc ] = true;
				} else {
					$edge   = ( 0 === $r || 6 === $r || 0 === $c || 6 === $c );
					$inner  = ( $r >= 2 && $r <= 4 && $c >= 2 && $c <= 4 );
					$mat[ $pr ][ $pc ]  = ( $edge || $inner );
					$func[ $pr ][ $pc ] = true;
				}
			}
		}
	}

	/** Place a 5×5 alignment pattern centred at ($row, $col). */
	private static function place_alignment( array &$mat, array &$func, int $row, int $col ) : void {
		for ( $r = -2; $r <= 2; $r++ ) {
			for ( $c = -2; $c <= 2; $c++ ) {
				$edge   = ( abs( $r ) === 2 || abs( $c ) === 2 );
				$center = ( 0 === $r && 0 === $c );
				$mat[ $row + $r ][ $col + $c ]  = ( $edge || $center );
				$func[ $row + $r ][ $col + $c ] = true;
			}
		}
	}

	/** All (row, col) positions that carry format information. */
	private static function format_info_positions( int $n ) : array {
		$pos = [];
		// Copy 1 — horizontal strip at row 8, around top-left finder
		foreach ( [0,1,2,3,4,5,7,8] as $c ) {
			$pos[] = [8, $c];
		}
		// Copy 1 — vertical strip at col 8, rows 7 down to 0 (skip row 6)
		foreach ( [7,5,4,3,2,1,0] as $r ) {
			$pos[] = [$r, 8];
		}
		// Copy 2 — horizontal at row 8, top-right
		for ( $i = 0; $i < 8; $i++ ) {
			$pos[] = [8, $n - 1 - $i];
		}
		// Copy 2 — vertical at col 8, bottom-left
		for ( $i = 0; $i < 7; $i++ ) {
			$pos[] = [$n - 7 + $i, 8];
		}
		return $pos;
	}

	// ── Step 4: apply data masking ───────────────────────────────────────────

	private static function apply_mask( array $matrix, array $func, int $mask ) : array {
		$n      = count( $matrix );
		$result = $matrix;
		for ( $r = 0; $r < $n; $r++ ) {
			for ( $c = 0; $c < $n; $c++ ) {
				if ( $func[ $r ][ $c ] ) continue; // function module — never masked
				if ( self::mask_condition( $mask, $r, $c ) ) {
					$result[ $r ][ $c ] = ! $matrix[ $r ][ $c ];
				}
			}
		}
		return $result;
	}

	private static function mask_condition( 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;
			case 7: return ( ( ( $r + $c ) % 2 + ( $r * $c ) % 3 ) % 2 ) === 0;
		}
		return false;
	}

	/** Write the 15-bit format string into the two format info copies. */
	private static function place_format( array &$mat, int $mask ) : void {
		$n   = count( $mat );
		$fmt = self::FORMAT_INFO[ $mask ];

		// Extract 15 bits, MSB first (b14 … b0)
		$b = [];
		for ( $i = 14; $i >= 0; $i-- ) {
			$b[] = ( $fmt >> $i ) & 1;
		}
		// $b[0]=b14, $b[1]=b13, …, $b[14]=b0

		// Copy 1 — horizontal (row 8, cols 0-5 then 7-8)
		$hcols = [0,1,2,3,4,5,7,8];
		foreach ( $hcols as $idx => $c ) {
			$mat[8][$c] = (bool) $b[ $idx ];
		}
		// Copy 1 — vertical (col 8, rows 7,5,4,3,2,1,0)
		$vrows = [7,5,4,3,2,1,0];
		foreach ( $vrows as $idx => $r ) {
			$mat[$r][8] = (bool) $b[ 8 + $idx ];
		}

		// Copy 2 — top-right (row 8, cols n-1 … n-8)
		for ( $i = 0; $i < 8; $i++ ) {
			$mat[8][ $n - 1 - $i ] = (bool) $b[ $i ];
		}
		// Copy 2 — bottom-left (col 8, rows n-7 … n-1)
		for ( $i = 0; $i < 7; $i++ ) {
			$mat[ $n - 7 + $i ][8] = (bool) $b[ 8 + $i ];
		}
	}

	// ── Penalty scoring (pick best mask) ─────────────────────────────────────

	private static function penalty( array $mat ) : int {
		$n     = count( $mat );
		$score = 0;

		// Rule 1: 5+ consecutive same-color modules in a row or column
		for ( $r = 0; $r < $n; $r++ ) {
			for ( $c = 1, $run = 1; $c < $n; $c++ ) {
				if ( $mat[$r][$c] === $mat[$r][$c-1] ) { $run++; if ( 5 === $run ) $score += 3; elseif ( $run > 5 ) $score++; }
				else $run = 1;
			}
		}
		for ( $c = 0; $c < $n; $c++ ) {
			for ( $r = 1, $run = 1; $r < $n; $r++ ) {
				if ( $mat[$r][$c] === $mat[$r-1][$c] ) { $run++; if ( 5 === $run ) $score += 3; elseif ( $run > 5 ) $score++; }
				else $run = 1;
			}
		}

		// Rule 2: 2×2 blocks of same color
		for ( $r = 0; $r < $n - 1; $r++ ) {
			for ( $c = 0; $c < $n - 1; $c++ ) {
				$v = $mat[$r][$c];
				if ( $v === $mat[$r][$c+1] && $v === $mat[$r+1][$c] && $v === $mat[$r+1][$c+1] ) $score += 3;
			}
		}

		// Rule 4: proportion of dark modules
		$dark = 0;
		foreach ( $mat as $row ) foreach ( $row as $cell ) if ( $cell ) $dark++;
		$pct   = (int) round( $dark / ( $n * $n ) * 100 );
		$score += 10 * (int) abs( ( intdiv( $pct, 5 ) * 5 ) - 50 ) / 5;

		return $score;
	}

	// ── SVG output ───────────────────────────────────────────────────────────

	private static function to_svg( array $matrix, int $px ) : string {
		$n    = count( $matrix );
		$cell = round( $px / $n, 4 );
		$dim  = round( $cell * $n, 2 );

		$svg  = '<svg xmlns="http://www.w3.org/2000/svg"';
		$svg .= ' width="' . $dim . '" height="' . $dim . '"';
		$svg .= ' viewBox="0 0 ' . $dim . ' ' . $dim . '"';
		$svg .= ' shape-rendering="crispEdges">';
		$svg .= '<rect width="100%" height="100%" fill="#fff"/>';

		foreach ( $matrix as $r => $row ) {
			foreach ( $row as $c => $dark ) {
				if ( ! $dark ) continue;
				$x    = round( $c * $cell, 4 );
				$y    = round( $r * $cell, 4 );
				$size = round( $cell + 0.01, 4 );
				$svg .= '<rect x="' . $x . '" y="' . $y . '" width="' . $size . '" height="' . $size . '" fill="#000"/>';
			}
		}

		$svg .= '</svg>';
		return $svg;
	}
}
