Source: Common/ReedSolomonEncoder.js

/**
 * @created      11.07.2022
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2022 smiley
 * @license      MIT
 */

import PHPJS from './PHPJS.js';
import GenericGFPoly from './GenericGFPoly.js';
import GF256 from './GF256.js';

/**
 * ISO/IEC 18004:2000 Section 8.5 ff
 *
 * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
 */
export default class ReedSolomonEncoder{

	/**
	 * @type {Object<{}>}
	 * @private
	 */
	interleavedData;

	/**
	 * @type {number|int}
	 * @private
	 */
	interleavedDataIndex;

	/**
	 * @type {Version} $version
	 */
	version;

	/**
	 * @type {EccLevel} $eccLevel
	 */
	eccLevel;

	/**
	 * @param {Version} $version
	 * @param {EccLevel} $eccLevel
	 */
	constructor($version, $eccLevel){
		this.version  = $version;
		this.eccLevel = $eccLevel;
	}

	/**
	 * ECC interleaving
	 *
	 * @param {BitBuffer} $bitBuffer
	 *
	 * @returns {Object<{}>}
	 * @throws QRCodeException
	 */
	interleaveEcBytes($bitBuffer){
		let $rsblockData     = this.version.getRSBlocks(this.eccLevel);
		let $numEccCodewords = $rsblockData[0];
		let $l1              = $rsblockData[1][0][0];
		let $b1              = $rsblockData[1][0][1];
		let $l2              = $rsblockData[1][1][0];
		let $b2              = $rsblockData[1][1][1];
		let $rsBlocks        = PHPJS.array_fill($l1, [$numEccCodewords + $b1, $b1]);

		if($l2 > 0){
			$rsBlocks = $rsBlocks.concat(PHPJS.array_fill($l2, [$numEccCodewords + $b2, $b2]));
		}

		let $bitBufferData  = $bitBuffer.getBuffer();
		let $dataBytes      = [];
		let $ecBytes        = [];
		let $maxDataBytes   = 0;
		let $maxEcBytes     = 0;
		let $dataByteOffset = 0;

		for(let $key in $rsBlocks){
			let $rsBlockTotal  = $rsBlocks[$key][0];
			let $dataByteCount = $rsBlocks[$key][1];

			$dataBytes[$key]   = [];

			for(let $i = 0; $i < $dataByteCount; $i++){
				$dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff;
			}

			let $ecByteCount = $rsBlockTotal - $dataByteCount;
			$ecBytes[$key]   = this.encode($dataBytes[$key], $ecByteCount);
			$maxDataBytes    = Math.max($maxDataBytes, $dataByteCount);
			$maxEcBytes      = Math.max($maxEcBytes, $ecByteCount);
			$dataByteOffset += $dataByteCount;
		}

		this.interleavedData      = PHPJS.array_fill(this.version.getTotalCodewords(), 0);
		this.interleavedDataIndex = 0;
		let $numRsBlocks          = $l1 + $l2;

		this.interleave($dataBytes, $maxDataBytes, $numRsBlocks);
		this.interleave($ecBytes, $maxEcBytes, $numRsBlocks);

		return this.interleavedData;
	}

	/**
	 * @param {Array} $dataBytes
	 * @param {number|int} $ecByteCount
	 *
	 * @returns {Object<{}>}
	 * @private
	 */
	encode($dataBytes, $ecByteCount){
		let $rsPoly = new GenericGFPoly([1]);

		for(let $i = 0; $i < $ecByteCount; $i++){
			$rsPoly = $rsPoly.multiply(new GenericGFPoly([1, GF256.exp($i)]));
		}

		let $rsPolyDegree    = $rsPoly.getDegree();
		let $modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree))
			.mod($rsPoly)
			.getCoefficients()
		;

		let $ecBytes = PHPJS.array_fill($rsPolyDegree, 0);
		let $count   = $modCoefficients.length - $rsPolyDegree;

		for(let $i = 0; $i < $ecBytes.length; $i++){
			let $modIndex = $i + $count;
			$ecBytes[$i]  = $modIndex >= 0 ? $modCoefficients[$modIndex] : 0;
		}

		return $ecBytes;
	}

	/**
	 * @param {Object<{}>} $byteArray
	 * @param {number|int} $maxBytes
	 * @param {number|int} $numRsBlocks
	 *
	 * @returns {void}
	 * @private
	 */
	interleave($byteArray, $maxBytes, $numRsBlocks){
		for(let $x = 0; $x < $maxBytes; $x++){
			for(let $y = 0; $y < $numRsBlocks; $y++){
				if($x < $byteArray[$y].length){
					this.interleavedData[this.interleavedDataIndex++] = $byteArray[$y][$x];
				}
			}
		}
	}

}