<?php
namespace Veno\QrCdr\Lib;

/**
 * QR Code Masking Class (Private Fork for Veno QrCdr)
 *
 * Handles mask generation and evaluation to find the best mask pattern
 * that minimizes penalties (avoiding large block of colors, etc.).
 *
 * @package   QrCdr
 * @version   1.1.4-patched
 */

if ( ! defined( 'ABSPATH' ) && ! defined( 'QRCDR_OPEN_ACCESS' ) ) {
	exit;
}

/**
 * Masking penalty constants (Prefixed with QRCDR_MASK_)
 */
define( 'QRCDR_MASK_N1', 3 );
define( 'QRCDR_MASK_N2', 3 );
define( 'QRCDR_MASK_N3', 40 );
define( 'QRCDR_MASK_N4', 10 );

/**
 * QrcdrMask Class
 */
class QrcdrMask {

	/**
	 * Run length buffer.
	 * @var array
	 */
	public $runLength = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->runLength = array_fill( 0, QRCDR_QRSPEC_WIDTH_MAX + 1, 0 );
	}

	/**
	 * Write format information to the frame.
	 *
	 * @param int   $width Frame width.
	 * @param array $frame Frame data (reference).
	 * @param int   $mask  Mask number.
	 * @param int   $level ECC level.
	 * @return int Number of black modules added.
	 */
	public function writeFormatInformation( $width, &$frame, $mask, $level ) {
		$blacks = 0;
		$format = QrcdrSpec::getFormatInfo( $mask, $level );

		for ( $i = 0; $i < 8; $i++ ) {
			$value = ( 1 === ( $format & 1 ) ) ? 0x85 : 0x84;
			if ( 0x85 === $value ) {
				$blacks += 2;
			}

			$frame[8][ $width - 1 - $i ] = chr( $value );
			if ( 6 > $i ) {
				$frame[ $i ][8] = chr( $value );
			} else {
				$frame[ $i + 1 ][8] = chr( $value );
			}
			$format = $format >> 1;
		}

		for ( $i = 0; $i < 7; $i++ ) {
			$value = ( 1 === ( $format & 1 ) ) ? 0x85 : 0x84;
			if ( 0x85 === $value ) {
				$blacks += 2;
			}

			$frame[ $width - 7 + $i ][8] = chr( $value );
			if ( 0 === $i ) {
				$frame[8][7] = chr( $value );
			} else {
				$frame[8][ 6 - $i ] = chr( $value );
			}
			$format = $format >> 1;
		}

		return $blacks;
	}

	/**
	 * Mask functions.
	 */
	public function mask0( $x, $y ) { return ( $x + $y ) & 1; }
	public function mask1( $x, $y ) { return $y & 1; }
	public function mask2( $x, $y ) { return $x % 3; }
	public function mask3( $x, $y ) { return ( $x + $y ) % 3; }
	public function mask4( $x, $y ) { return ( (int) ( $y / 2 ) + (int) ( $x / 3 ) ) & 1; }
	public function mask5( $x, $y ) { return ( ( $x * $y ) & 1 ) + ( $x * $y ) % 3; }
	public function mask6( $x, $y ) { return ( ( ( $x * $y ) & 1 ) + ( $x * $y ) % 3 ) & 1; }
	public function mask7( $x, $y ) { return ( ( ( $x * $y ) % 3 ) + ( ( $x + $y ) & 1 ) ) & 1; }

	/**
	 * Generate a bit mask (internal).
	 *
	 * @param int   $mask_number Mask ID (0-7).
	 * @param int   $width       Frame width.
	 * @param array $frame       Source frame.
	 * @return array Bit mask.
	 */
	private function generateMaskNo( $mask_number, $width, $frame ) {
		$bit_mask = array_fill( 0, $width, array_fill( 0, $width, 0 ) );

		for ( $y = 0; $y < $width; $y++ ) {
			for ( $x = 0; $x < $width; $x++ ) {
				if ( ord( $frame[ $y ][ $x ] ) & 0x80 ) {
					$bit_mask[ $y ][ $x ] = 0;
				} else {
					$mask_function_result = call_user_func( array( $this, 'mask' . $mask_number ), $x, $y );
					$bit_mask[ $y ][ $x ]   = ( 0 === $mask_function_result ) ? 1 : 0;
				}
			}
		}

		return $bit_mask;
	}

	/**
	 * Apply mask to frame.
	 *
	 * NOTE: File caching removed for WordPress security compliance.
	 *
	 * @param int   $mask_number       Mask ID.
	 * @param int   $width             Width.
	 * @param array $source_frame      Source.
	 * @param array $destination_frame Destination (ref).
	 * @param bool  $mask_gen_only     Generate only.
	 * @return int|void Black module count.
	 */
	public function makeMaskNo( $mask_number, $width, $source_frame, &$destination_frame, $mask_gen_only = false ) {
		$black_modules = 0;
		
		// Generate mask in memory (faster and safer than disk cache in WP)
		$bit_mask = $this->generateMaskNo( $mask_number, $width, $source_frame );

		if ( $mask_gen_only ) {
			return;
		}

		$destination_frame = $source_frame;

		for ( $y = 0; $y < $width; $y++ ) {
			for ( $x = 0; $x < $width; $x++ ) {
				if ( 1 === $bit_mask[ $y ][ $x ] ) {
					$destination_frame[ $y ][ $x ] = chr( ord( $source_frame[ $y ][ $x ] ) ^ $bit_mask[ $y ][ $x ] );
				}
				$black_modules += (int) ( ord( $destination_frame[ $y ][ $x ] ) & 1 );
			}
		}

		return $black_modules;
	}

	/**
	 * Create full masked frame with format info.
	 *
	 * @param int   $width       Width.
	 * @param array $frame       Source.
	 * @param int   $mask_number Mask ID.
	 * @param int   $level       ECC Level.
	 * @return array Masked frame.
	 */
	public function makeMask( $width, $frame, $mask_number, $level ) {
		$masked_frame = array_fill( 0, $width, str_repeat( "\0", $width ) );
		$this->makeMaskNo( $mask_number, $width, $frame, $masked_frame );
		$this->writeFormatInformation( $width, $masked_frame, $mask_number, $level );

		return $masked_frame;
	}

	/**
	 * Calculate penalty N1 and N3.
	 */
	public function calcN1N3( $length ) {
		$demerit = 0;

		for ( $i = 0; $i < $length; $i++ ) {
			if ( 5 <= $this->runLength[ $i ] ) {
				$demerit += ( QRCDR_MASK_N1 + ( $this->runLength[ $i ] - 5 ) );
			}

			if ( 1 === ( $i & 1 ) ) {
				if ( ( 3 <= $i ) && ( ( $length - 2 ) > $i ) && ( 0 === ( $this->runLength[ $i ] % 3 ) ) ) {
					$fact = (int) ( $this->runLength[ $i ] / 3 );
					if ( ( $this->runLength[ $i - 2 ] === $fact ) &&
						( $this->runLength[ $i - 1 ] === $fact ) &&
						( $this->runLength[ $i + 1 ] === $fact ) &&
						( $this->runLength[ $i + 2 ] === $fact )
					) {
						if ( ( 0 > $this->runLength[ $i - 3 ] ) || ( ( 4 * $fact ) <= $this->runLength[ $i - 3 ] ) ) {
							$demerit += QRCDR_MASK_N3;
						} elseif ( ( ( $i + 3 ) >= $length ) || ( ( 4 * $fact ) <= $this->runLength[ $i + 3 ] ) ) {
							$demerit += QRCDR_MASK_N3;
						}
					}
				}
			}
		}
		return $demerit;
	}

	/**
	 * Evaluate symbol penalties.
	 */
	public function evaluateSymbol( $width, $frame ) {
		$head    = 0;
		$demerit = 0;

		// Rows
		for ( $y = 0; $y < $width; $y++ ) {
			$head                = 0;
			$this->runLength[0] = 1;
			$frame_y             = $frame[ $y ];

			if ( 0 < $y ) {
				$frame_y_minus_1 = $frame[ $y - 1 ];
			}

			for ( $x = 0; $x < $width; $x++ ) {
				if ( ( 0 < $x ) && ( 0 < $y ) ) {
					// Penalty N2
					$b22 = ord( $frame_y[ $x ] ) & ord( $frame_y[ $x - 1 ] ) & ord( $frame_y_minus_1[ $x ] ) & ord( $frame_y_minus_1[ $x - 1 ] );
					$w22 = ord( $frame_y[ $x ] ) | ord( $frame_y[ $x - 1 ] ) | ord( $frame_y_minus_1[ $x ] ) | ord( $frame_y_minus_1[ $x - 1 ] );

					if ( 1 === ( ( $b22 | ( $w22 ^ 1 ) ) & 1 ) ) {
						$demerit += QRCDR_MASK_N2;
					}
				}

				// Run-length N1/N3
				if ( ( 0 === $x ) && ( ord( $frame_y[ $x ] ) & 1 ) ) {
					$this->runLength[0] = -1;
					$head               = 1;
					$this->runLength[ $head ] = 1;
				} elseif ( 0 < $x ) {
					if ( ( ord( $frame_y[ $x ] ) ^ ord( $frame_y[ $x - 1 ] ) ) & 1 ) {
						$head++;
						$this->runLength[ $head ] = 1;
					} else {
						$this->runLength[ $head ]++;
					}
				}
			}
			$demerit += $this->calcN1N3( $head + 1 );
		}

		// Columns
		for ( $x = 0; $x < $width; $x++ ) {
			$head                = 0;
			$this->runLength[0] = 1;

			for ( $y = 0; $y < $width; $y++ ) {
				if ( ( 0 === $y ) && ( ord( $frame[ $y ][ $x ] ) & 1 ) ) {
					$this->runLength[0] = -1;
					$head               = 1;
					$this->runLength[ $head ] = 1;
				} elseif ( 0 < $y ) {
					if ( ( ord( $frame[ $y ][ $x ] ) ^ ord( $frame[ $y - 1 ][ $x ] ) ) & 1 ) {
						$head++;
						$this->runLength[ $head ] = 1;
					} else {
						$this->runLength[ $head ]++;
					}
				}
			}
			$demerit += $this->calcN1N3( $head + 1 );
		}

		return $demerit;
	}

	/**
	 * Find best mask.
	 *
	 * @param int   $width Width.
	 * @param array $frame Frame.
	 * @param int   $level ECC Level.
	 * @return array Best masked frame.
	 */
	public function mask( $width, $frame, $level ) {
		$min_demerit      = PHP_INT_MAX;
		$best_mask_number = 0;
		$best_mask        = array();
		$checked_masks    = array( 0, 1, 2, 3, 4, 5, 6, 7 );

		// if ( false !== QRCDR_FIND_FROM_RANDOM ) {
		// 	$how_many_out = 8 - ( QRCDR_FIND_FROM_RANDOM % 9 );
		// 	for ( $i = 0; $i < $how_many_out; $i++ ) {
		// 		$remove_position = wp_rand( 0, count( $checked_masks ) - 1 );
		// 		unset( $checked_masks[ $remove_position ] );
		// 		$checked_masks = array_values( $checked_masks );
		// 	}
		// }

		$best_mask = $frame;

		foreach ( $checked_masks as $i ) {
			$mask = array_fill( 0, $width, str_repeat( "\0", $width ) );

			$demerit = 0;
			$blacks  = 0;

			$blacks  = $this->makeMaskNo( $i, $width, $frame, $mask );
			$blacks += $this->writeFormatInformation( $width, $mask, $i, $level );

			$blacks  = (int) ( 100 * $blacks / ( $width * $width ) );
			$demerit = (int) ( (int) ( abs( $blacks - 50 ) / 5 ) * QRCDR_MASK_N4 );

			$demerit += $this->evaluateSymbol( $width, $mask );

			if ( $min_demerit > $demerit ) {
				$min_demerit      = $demerit;
				$best_mask        = $mask;
				$best_mask_number = $i;
			}
		}

		return $best_mask;
	}
}
