<?php
namespace Veno\QrCdr\Lib;

/**
 * QR Code Specification Class (Private Fork for Veno QrCdr)
 *
 * Contains specifications, constants, and data tables for QR Code generation.
 * Logic based on libqrencode C library.
 *
 * @package   QrCdr
 * @version   1.1.4-patched
 */

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

/**
 * QR Code Specification Constants (Prefixed with QRCDR_)
 */
define( 'QRCDR_QRSPEC_VERSION_MAX', 40 );
define( 'QRCDR_QRSPEC_WIDTH_MAX', 177 );

define( 'QRCDR_QRCAP_WIDTH', 0 );
define( 'QRCDR_QRCAP_WORDS', 1 );
define( 'QRCDR_QRCAP_REMINDER', 2 );
define( 'QRCDR_QRCAP_EC', 3 );

/**
 * QrcdrSpec Class
 *
 * Encapsulates static specifications for QR Codes (Capacity, ECC tables, Alignment patterns).
 */
class QrcdrSpec {

	/**
	 * Capacity table for each QR Code version.
	 * Format: [width, total_words, remainder_bits, [ECC words for L, M, Q, H]]
	 *
	 * @var array
	 */
	public static $capacity = array(
		array( 0, 0, 0, array( 0, 0, 0, 0 ) ),
		array( 21, 26, 0, array( 7, 10, 13, 17 ) ), // 1
		array( 25, 44, 7, array( 10, 16, 22, 28 ) ),
		array( 29, 70, 7, array( 15, 26, 36, 44 ) ),
		array( 33, 100, 7, array( 20, 36, 52, 64 ) ),
		array( 37, 134, 7, array( 26, 48, 72, 88 ) ), // 5
		array( 41, 172, 7, array( 36, 64, 96, 112 ) ),
		array( 45, 196, 0, array( 40, 72, 108, 130 ) ),
		array( 49, 242, 0, array( 48, 88, 132, 156 ) ),
		array( 53, 292, 0, array( 60, 110, 160, 192 ) ),
		array( 57, 346, 0, array( 72, 130, 192, 224 ) ), // 10
		array( 61, 404, 0, array( 80, 150, 224, 264 ) ),
		array( 65, 466, 0, array( 96, 176, 260, 308 ) ),
		array( 69, 532, 0, array( 104, 198, 288, 352 ) ),
		array( 73, 581, 3, array( 120, 216, 320, 384 ) ),
		array( 77, 655, 3, array( 132, 240, 360, 432 ) ), // 15
		array( 81, 733, 3, array( 144, 280, 408, 480 ) ),
		array( 85, 815, 3, array( 168, 308, 448, 532 ) ),
		array( 89, 901, 3, array( 180, 338, 504, 588 ) ),
		array( 93, 991, 3, array( 196, 364, 546, 650 ) ),
		array( 97, 1085, 3, array( 224, 416, 600, 700 ) ), // 20
		array( 101, 1156, 4, array( 224, 442, 644, 750 ) ),
		array( 105, 1258, 4, array( 252, 476, 690, 816 ) ),
		array( 109, 1364, 4, array( 270, 504, 750, 900 ) ),
		array( 113, 1474, 4, array( 300, 560, 810, 960 ) ),
		array( 117, 1588, 4, array( 312, 588, 870, 1050 ) ), // 25
		array( 121, 1706, 4, array( 336, 644, 952, 1110 ) ),
		array( 125, 1828, 4, array( 360, 700, 1020, 1200 ) ),
		array( 129, 1921, 3, array( 390, 728, 1050, 1260 ) ),
		array( 133, 2051, 3, array( 420, 784, 1140, 1350 ) ),
		array( 137, 2185, 3, array( 450, 812, 1200, 1440 ) ), // 30
		array( 141, 2323, 3, array( 480, 868, 1290, 1530 ) ),
		array( 145, 2465, 3, array( 510, 924, 1350, 1620 ) ),
		array( 149, 2611, 3, array( 540, 980, 1440, 1710 ) ),
		array( 153, 2761, 3, array( 570, 1036, 1530, 1800 ) ),
		array( 157, 2876, 0, array( 570, 1064, 1590, 1890 ) ), // 35
		array( 161, 3034, 0, array( 600, 1120, 1680, 1980 ) ),
		array( 165, 3196, 0, array( 630, 1204, 1770, 2100 ) ),
		array( 169, 3362, 0, array( 660, 1260, 1860, 2220 ) ),
		array( 173, 3532, 0, array( 720, 1316, 1950, 2310 ) ),
		array( 177, 3706, 0, array( 750, 1372, 2040, 2430 ) ), // 40
	);

	/**
	 * Get data codewords length for version and ECC level.
	 *
	 * @param int $version QR Code version (1-40).
	 * @param int $level   Error correction level (0-3).
	 * @return int Number of data codewords.
	 */
	public static function getDataLength( $version, $level ) {
		return self::$capacity[ $version ][ QRCDR_QRCAP_WORDS ] - self::$capacity[ $version ][ QRCDR_QRCAP_EC ][ $level ];
	}

	/**
	 * Get ECC codewords length.
	 *
	 * @param int $version QR Code version (1-40).
	 * @param int $level   Error correction level (0-3).
	 * @return int Number of ECC codewords.
	 */
	public static function getECCLength( $version, $level ) {
		return self::$capacity[ $version ][ QRCDR_QRCAP_EC ][ $level ];
	}

	/**
	 * Get width in modules for version.
	 *
	 * @param int $version QR Code version (1-40).
	 * @return int Width in modules.
	 */
	public static function getWidth( $version ) {
		return self::$capacity[ $version ][ QRCDR_QRCAP_WIDTH ];
	}

	/**
	 * Get remainder bits.
	 *
	 * @param int $version QR Code version (1-40).
	 * @return int Remainder bits.
	 */
	public static function getRemainder( $version ) {
		return self::$capacity[ $version ][ QRCDR_QRCAP_REMINDER ];
	}

	/**
	 * Find minimum version that fits the data size.
	 *
	 * @param int $size  Data codewords required.
	 * @param int $level Error correction level (0-3).
	 * @return int Minimum version or -1 if too large.
	 */
	public static function getMinimumVersion( $size, $level ) {
		for ( $i = 1; $i <= QRCDR_QRSPEC_VERSION_MAX; $i++ ) {
			$data_words = self::$capacity[ $i ][ QRCDR_QRCAP_WORDS ] - self::$capacity[ $i ][ QRCDR_QRCAP_EC ][ $level ];
			if ( $size <= $data_words ) {
				return $i;
			}
		}
		return -1;
	}

	/**
	 * Length indicator bits table.
	 * @var array
	 */
	public static $lengthTableBits = array(
		array( 10, 12, 14 ),
		array( 9, 11, 13 ),
		array( 8, 16, 16 ),
		array( 8, 10, 12 ),
	);

	/**
	 * Get length indicator bits.
	 *
	 * @param int $mode    Encoding mode.
	 * @param int $version QR Code version (1-40).
	 * @return int Number of bits.
	 */
	public static function lengthIndicator( $mode, $version ) {
		if ( QRCDR_MODE_STRUCTURE === $mode ) {
			return 0;
		}

		if ( 9 >= $version ) {
			$level_group = 0;
		} elseif ( 26 >= $version ) {
			$level_group = 1;
		} else {
			$level_group = 2;
		}

		return self::$lengthTableBits[ $mode ][ $level_group ];
	}

	/**
	 * Get maximum words allowed.
	 *
	 * @param int $mode    Encoding mode.
	 * @param int $version QR Code version (1-40).
	 * @return int Maximum words.
	 */
	public static function maximumWords( $mode, $version ) {
		if ( QRCDR_MODE_STRUCTURE === $mode ) {
			return 3;
		}

		if ( 9 >= $version ) {
			$level_group = 0;
		} elseif ( 26 >= $version ) {
			$level_group = 1;
		} else {
			$level_group = 2;
		}

		$bits  = self::$lengthTableBits[ $mode ][ $level_group ];
		$words = ( 1 << $bits ) - 1;

		if ( QRCDR_MODE_KANJI === $mode ) {
			$words *= 2; // Byte count required
		}

		return $words;
	}

	/**
	 * ECC Block Table (Reed-Solomon).
	 * @var array
	 */
	public static $eccTable = array(
		array( array( 0, 0 ), array( 0, 0 ), array( 0, 0 ), array( 0, 0 ) ),
		array( array( 1, 0 ), array( 1, 0 ), array( 1, 0 ), array( 1, 0 ) ), // 1
		array( array( 1, 0 ), array( 1, 0 ), array( 1, 0 ), array( 1, 0 ) ),
		array( array( 1, 0 ), array( 1, 0 ), array( 2, 0 ), array( 2, 0 ) ),
		array( array( 1, 0 ), array( 2, 0 ), array( 2, 0 ), array( 4, 0 ) ),
		array( array( 1, 0 ), array( 2, 0 ), array( 2, 2 ), array( 2, 2 ) ), // 5
		array( array( 2, 0 ), array( 4, 0 ), array( 4, 0 ), array( 4, 0 ) ),
		array( array( 2, 0 ), array( 4, 0 ), array( 2, 4 ), array( 4, 1 ) ),
		array( array( 2, 0 ), array( 2, 2 ), array( 4, 2 ), array( 4, 2 ) ),
		array( array( 2, 0 ), array( 3, 2 ), array( 4, 4 ), array( 4, 4 ) ),
		array( array( 2, 2 ), array( 4, 1 ), array( 6, 2 ), array( 6, 2 ) ), // 10
		array( array( 4, 0 ), array( 1, 4 ), array( 4, 4 ), array( 3, 8 ) ),
		array( array( 2, 2 ), array( 6, 2 ), array( 4, 6 ), array( 7, 4 ) ),
		array( array( 4, 0 ), array( 8, 1 ), array( 8, 4 ), array( 12, 4 ) ),
		array( array( 3, 1 ), array( 4, 5 ), array( 11, 5 ), array( 11, 5 ) ),
		array( array( 5, 1 ), array( 5, 5 ), array( 5, 7 ), array( 11, 7 ) ), // 15
		array( array( 5, 1 ), array( 7, 3 ), array( 15, 2 ), array( 3, 13 ) ),
		array( array( 1, 5 ), array( 10, 1 ), array( 1, 15 ), array( 2, 17 ) ),
		array( array( 5, 1 ), array( 9, 4 ), array( 17, 1 ), array( 2, 19 ) ),
		array( array( 3, 4 ), array( 3, 11 ), array( 17, 4 ), array( 9, 16 ) ),
		array( array( 3, 5 ), array( 3, 13 ), array( 15, 5 ), array( 15, 10 ) ), // 20
		array( array( 4, 4 ), array( 17, 0 ), array( 17, 6 ), array( 19, 6 ) ),
		array( array( 2, 7 ), array( 17, 0 ), array( 7, 16 ), array( 34, 0 ) ),
		array( array( 4, 5 ), array( 4, 14 ), array( 11, 14 ), array( 16, 14 ) ),
		array( array( 6, 4 ), array( 6, 14 ), array( 11, 16 ), array( 30, 2 ) ),
		array( array( 8, 4 ), array( 8, 13 ), array( 7, 22 ), array( 22, 13 ) ), // 25
		array( array( 10, 2 ), array( 19, 4 ), array( 28, 6 ), array( 33, 4 ) ),
		array( array( 8, 4 ), array( 22, 3 ), array( 8, 26 ), array( 12, 28 ) ),
		array( array( 3, 10 ), array( 3, 23 ), array( 4, 31 ), array( 11, 31 ) ),
		array( array( 7, 7 ), array( 21, 7 ), array( 1, 37 ), array( 19, 26 ) ),
		array( array( 5, 10 ), array( 19, 10 ), array( 15, 25 ), array( 23, 25 ) ), // 30
		array( array( 13, 3 ), array( 2, 29 ), array( 42, 1 ), array( 23, 28 ) ),
		array( array( 17, 0 ), array( 10, 23 ), array( 10, 35 ), array( 19, 35 ) ),
		array( array( 17, 1 ), array( 14, 21 ), array( 29, 19 ), array( 11, 46 ) ),
		array( array( 13, 6 ), array( 14, 23 ), array( 44, 7 ), array( 59, 1 ) ),
		array( array( 12, 7 ), array( 12, 26 ), array( 39, 14 ), array( 22, 41 ) ), // 35
		array( array( 6, 14 ), array( 6, 34 ), array( 46, 10 ), array( 2, 64 ) ),
		array( array( 17, 4 ), array( 29, 14 ), array( 49, 10 ), array( 24, 46 ) ),
		array( array( 4, 18 ), array( 13, 32 ), array( 48, 14 ), array( 42, 32 ) ),
		array( array( 20, 4 ), array( 40, 7 ), array( 43, 22 ), array( 10, 67 ) ),
		array( array( 19, 6 ), array( 18, 31 ), array( 34, 34 ), array( 20, 61 ) ),// 40
	);

	/**
	 * Get ECC specification.
	 *
	 * @param int   $version QR Code version (1-40).
	 * @param int   $level   Error correction level (0-3).
	 * @param array $spec    Reference array to populate.
	 */
	public static function getEccSpec( $version, $level, array &$spec ) {
		if ( count( $spec ) < 5 ) {
			$spec = array( 0, 0, 0, 0, 0 );
		}

		$blocks_group1  = self::$eccTable[ $version ][ $level ][0];
		$blocks_group2  = self::$eccTable[ $version ][ $level ][1];
		$data_codewords = self::getDataLength( $version, $level );
		$ecc_codewords  = self::getECCLength( $version, $level );

		if ( 0 === $blocks_group2 ) {
			$spec[0] = $blocks_group1;
			$spec[1] = (int) ( $data_codewords / $blocks_group1 );
			$spec[2] = (int) ( $ecc_codewords / $blocks_group1 );
			$spec[3] = 0;
			$spec[4] = 0;
		} else {
			$spec[0] = $blocks_group1;
			$spec[1] = (int) ( $data_codewords / ( $blocks_group1 + $blocks_group2 ) );
			$spec[2] = (int) ( $ecc_codewords / ( $blocks_group1 + $blocks_group2 ) );
			$spec[3] = $blocks_group2;
			$spec[4] = $spec[1] + 1;
		}
	}

	/**
	 * Alignment patterns positions.
	 * @var array
	 */
	public static $alignmentPattern = array(
		array( 0, 0 ),
		array( 0, 0 ), array( 18, 0 ), array( 22, 0 ), array( 26, 0 ), array( 30, 0 ), // 1- 5
		array( 34, 0 ), array( 22, 38 ), array( 24, 42 ), array( 26, 46 ), array( 28, 50 ), // 6-10
		array( 30, 54 ), array( 32, 58 ), array( 34, 62 ), array( 26, 46 ), array( 26, 48 ), // 11-15
		array( 26, 50 ), array( 30, 54 ), array( 30, 56 ), array( 30, 58 ), array( 34, 62 ), // 16-20
		array( 28, 50 ), array( 26, 50 ), array( 30, 54 ), array( 28, 54 ), array( 32, 58 ), // 21-25
		array( 30, 58 ), array( 34, 62 ), array( 26, 50 ), array( 30, 54 ), array( 26, 52 ), // 26-30
		array( 30, 56 ), array( 34, 60 ), array( 30, 58 ), array( 34, 62 ), array( 30, 54 ), // 31-35
		array( 24, 50 ), array( 28, 54 ), array( 32, 58 ), array( 26, 54 ), array( 30, 58 ), // 35-40
	);

	/**
	 * Put an alignment marker into the frame.
	 *
	 * @param array $frame Frame data (reference).
	 * @param int   $ox    Center X coordinate.
	 * @param int   $oy    Center Y coordinate.
	 */
	public static function putAlignmentMarker( array &$frame, $ox, $oy ) {
		$finder = array(
			"\xa1\xa1\xa1\xa1\xa1",
			"\xa1\xa0\xa0\xa0\xa1",
			"\xa1\xa0\xa1\xa0\xa1",
			"\xa1\xa0\xa0\xa0\xa1",
			"\xa1\xa1\xa1\xa1\xa1",
		);

		$y_start = $oy - 2;
		$x_start = $ox - 2;

		for ( $y = 0; $y < 5; $y++ ) {
			QrcdrStr::set( $frame, $x_start, $y_start + $y, $finder[ $y ] );
		}
	}

	/**
	 * Put alignment patterns for version.
	 *
	 * @param int   $version QR Code version.
	 * @param array $frame   Frame data (reference).
	 * @param int   $width   Frame width.
	 */
	public static function putAlignmentPattern( $version, &$frame, $width ) {
		if ( 2 > $version ) {
			return;
		}

		$distance = self::$alignmentPattern[ $version ][1] - self::$alignmentPattern[ $version ][0];
		if ( 0 > $distance ) {
			$w = 2;
		} else {
			$w = (int) ( ( $width - self::$alignmentPattern[ $version ][0] ) / $distance + 2 );
		}

		if ( 1 === ( $w * $w - 3 ) ) {
			$x = self::$alignmentPattern[ $version ][0];
			$y = self::$alignmentPattern[ $version ][0];
			self::putAlignmentMarker( $frame, $x, $y );
			return;
		}

		$cx = self::$alignmentPattern[ $version ][0];
		for ( $x = 1; $x < $w - 1; $x++ ) {
			self::putAlignmentMarker( $frame, 6, $cx );
			self::putAlignmentMarker( $frame, $cx, 6 );
			$cx += $distance;
		}

		$cy = self::$alignmentPattern[ $version ][0];
		for ( $y = 0; $y < $w - 1; $y++ ) {
			$cx = self::$alignmentPattern[ $version ][0];
			for ( $x = 0; $x < $w - 1; $x++ ) {
				self::putAlignmentMarker( $frame, $cx, $cy );
				$cx += $distance;
			}
			$cy += $distance;
		}
	}

	/**
	 * Version info pattern table (BCH).
	 * @var array
	 */
	public static $versionPattern = array(
		0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6, 0x0C762, 0x0D847, 0x0E60D,
		0x0F928, 0x10B78, 0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683, 0x168C9,
		0x177EC, 0x18EC4, 0x191E1, 0x1AFAB, 0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75,
		0x1F250, 0x209D5, 0x216F0, 0x228BA, 0x2379F, 0x24B0B, 0x2542E, 0x26A64,
		0x27541, 0x28C69,
	);

	/**
	 * Get version info pattern.
	 *
	 * @param int $version QR Code version.
	 * @return int Pattern or 0.
	 */
	public static function getVersionPattern( $version ) {
		if ( 7 > $version || QRCDR_QRSPEC_VERSION_MAX < $version ) {
			return 0;
		}
		return self::$versionPattern[ $version - 7 ];
	}

	/**
	 * Format info table.
	 * @var array
	 */
	public static $formatInfo = array(
		array( 0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976 ),
		array( 0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0 ),
		array( 0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED ),
		array( 0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, 0x0D0C, 0x083B ),
	);

	/**
	 * Get format info pattern.
	 *
	 * @param int $mask  Mask number (0-7).
	 * @param int $level Error correction level (0-3).
	 * @return int Format info or 0.
	 */
	public static function getFormatInfo( $mask, $level ) {
		if ( 0 > $mask || 7 < $mask ) {
			return 0;
		}
		if ( 0 > $level || 3 < $level ) {
			return 0;
		}
		return self::$formatInfo[ $level ][ $mask ];
	}

	/**
	 * Cache of frames (Runtime memory only).
	 * @var array
	 */
	public static $frames = array();

	/**
	 * Put finder pattern.
	 *
	 * @param array $frame Frame data.
	 * @param int   $ox    X coordinate.
	 * @param int   $oy    Y coordinate.
	 */
	public static function putFinderPattern( &$frame, $ox, $oy ) {
		$finder = array(
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
			"\xc0\xc0\xc0\xc0\xc0\xc0\xc0",
		);

		for ( $y = 0; $y < 7; $y++ ) {
			QrcdrStr::set( $frame, $ox, $oy + $y, $finder[ $y ] );
		}
	}

	/**
	 * Create a new frame structure.
	 *
	 * @param int $version QR Code version.
	 * @return array The frame.
	 */
	public static function createFrame( $version ) {
		$width      = self::$capacity[ $version ][ QRCDR_QRCAP_WIDTH ];
		$frame_line = str_repeat( "\0", $width );
		$frame      = array_fill( 0, $width, $frame_line );

		// Finder patterns
		self::putFinderPattern( $frame, 0, 0 );
		self::putFinderPattern( $frame, $width - 7, 0 );
		self::putFinderPattern( $frame, 0, $width - 7 );

		// Separators
		$y_offset = $width - 7;
		for ( $y = 0; $y < 7; $y++ ) {
			$frame[ $y ][7]            = "\xc0";
			$frame[ $y ][ $width - 8 ] = "\xc0";
			$frame[ $y_offset ][7]     = "\xc0";
			$y_offset++;
		}
		$set_pattern = str_repeat( "\xc0", 8 );
		QrcdrStr::set( $frame, 0, 7, $set_pattern );
		QrcdrStr::set( $frame, $width - 8, 7, $set_pattern );
		QrcdrStr::set( $frame, 0, $width - 8, $set_pattern );

		// Format info reserved areas
		$set_pattern = str_repeat( "\x84", 9 );
		QrcdrStr::set( $frame, 0, 8, $set_pattern );
		QrcdrStr::set( $frame, $width - 8, 8, $set_pattern, 8 );

		$y_offset = $width - 8;
		for ( $y = 0; $y < 8; $y++, $y_offset++ ) {
			$frame[ $y ][8]        = "\x84";
			$frame[ $y_offset ][8] = "\x84";
		}

		// Timing pattern
		for ( $i = 1; $i < $width - 15; $i++ ) {
			$frame[6][ 7 + $i ] = chr( 0x90 | ( $i & 1 ) );
			$frame[ 7 + $i ][6] = chr( 0x90 | ( $i & 1 ) );
		}

		// Alignment pattern
		self::putAlignmentPattern( $version, $frame, $width );

		// Version info
		if ( 7 <= $version ) {
			$version_info_pattern = self::getVersionPattern( $version );
			$pattern_bits         = $version_info_pattern;

			for ( $x = 0; $x < 6; $x++ ) {
				for ( $y = 0; $y < 3; $y++ ) {
					$frame[ ( $width - 11 ) + $y ][ $x ] = chr( 0x88 | ( $pattern_bits & 1 ) );
					$pattern_bits                        = $pattern_bits >> 1;
				}
			}

			$pattern_bits = $version_info_pattern;
			for ( $y = 0; $y < 6; $y++ ) {
				for ( $x = 0; $x < 3; $x++ ) {
					$frame[ $y ][ $x + ( $width - 11 ) ] = chr( 0x88 | ( $pattern_bits & 1 ) );
					$pattern_bits                        = $pattern_bits >> 1;
				}
			}
		}

		// Dark module
		$frame[ $width - 8 ][8] = "\x81";

		return $frame;
	}

	/**
	 * Debug helper (disabled).
	 */
	public static function debug( $frame, $binary_mode = false ) {
		// Debug output removed for production security.
	}

	/**
	 * Serialize frame (internal).
	 */
	public static function serial( $frame ) {
		return gzcompress( implode( "\n", $frame ), 9 );
	}

	/**
	 * Unserialize frame (internal).
	 */
	public static function unserial( $code ) {
		return explode( "\n", gzuncompress( $code ) );
	}

	/**
	 * Get a new frame.
	 * Note: File caching has been removed for better security and portability.
	 *
	 * @param int $version QR Code version (1-40).
	 * @return array|null The frame.
	 */
	public static function newFrame( $version ) {
		if ( 1 > $version || QRCDR_QRSPEC_VERSION_MAX < $version ) {
			return null;
		}

		// Check runtime memory cache
		if ( ! isset( self::$frames[ $version ] ) ) {
			// Always recalculate in memory (faster than disk IO on most shared hosting)
			self::$frames[ $version ] = self::createFrame( $version );
		}

		return self::$frames[ $version ];
	}

	/**
	 * Calculate RS blocks number.
	 */
	public static function rsBlockNum( $spec ) {
		return $spec[0] + $spec[3];
	}

	/**
	 * Calculate RS blocks (Group 1).
	 */
	public static function rsBlockNum1( $spec ) {
		return $spec[0];
	}

	/**
	 * Calculate RS data codes (Group 1).
	 */
	public static function rsDataCodes1( $spec ) {
		return $spec[1];
	}

	/**
	 * Calculate RS ECC codes (Group 1).
	 */
	public static function rsEccCodes1( $spec ) {
		return $spec[2];
	}

	/**
	 * Calculate RS blocks (Group 2).
	 */
	public static function rsBlockNum2( $spec ) {
		return $spec[3];
	}

	/**
	 * Calculate RS data codes (Group 2).
	 */
	public static function rsDataCodes2( $spec ) {
		return $spec[4];
	}

	/**
	 * Calculate RS ECC codes (Group 2).
	 */
	public static function rsEccCodes2( $spec ) {
		return $spec[2];
	}

	/**
	 * Calculate total data length.
	 */
	public static function rsDataLength( $spec ) {
		return ( $spec[0] * $spec[1] ) + ( $spec[3] * $spec[4] );
	}

	/**
	 * Calculate total ECC length.
	 */
	public static function rsEccLength( $spec ) {
		return ( $spec[0] + $spec[3] ) * $spec[2];
	}
}
