<?php
namespace Veno\QrCdr\Lib;

use Exception;

/**
 * QR Code Input Management Class (Private Fork for Veno QrCdr)
 *
 * Manages a collection of data segments (QrcdrInputItem) for encoding.
 * Handles version detection, error correction levels, and bitstream assembly.
 *
 * @package   QrCdr
 * @version   1.1.4-patched
 */

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

/**
 * QrcdrInput Class
 */
class QrcdrInput {

	/**
	 * Array of QrcdrInputItem objects.
	 * @var QrcdrInputItem[]
	 */
	public $items;

	/**
	 * QR Code version (0 = autodetect).
	 * @var int
	 */
	private $version;

	/**
	 * Error correction level (QRCDR_ECLEVEL_L, M, Q, H).
	 * @var int
	 */
	private $level;

	/**
	 * Constructor.
	 *
	 * @param int $version Initial version.
	 * @param int $level   Initial ECC level.
	 * @throws Exception
	 */
	public function __construct( $version = 0, $level = QRCDR_ECLEVEL_L ) {
		if ( 0 > $version || QRCDR_QRSPEC_VERSION_MAX < $version || QRCDR_ECLEVEL_H < $level ) {
			throw new Exception( 'Invalid version or error correction level.' );
		}

		$this->version = $version;
		$this->level   = $level;
	}

	/**
	 * Get current version.
	 * @return int
	 */
	public function getVersion() {
		return $this->version;
	}

	/**
	 * Set version.
	 * @param int $version
	 * @return int 0 on success.
	 * @throws Exception
	 */
	public function setVersion( $version ) {
		if ( 0 > $version || QRCDR_QRSPEC_VERSION_MAX < $version ) {
			throw new Exception( 'Invalid version number.' );
		}

		$this->version = $version;
		return 0;
	}

	/**
	 * Get ECC level.
	 * @return int
	 */
	public function getErrorCorrectionLevel() {
		return $this->level;
	}

	/**
	 * Set ECC level.
	 * @param int $level
	 * @return int 0 on success.
	 * @throws Exception
	 */
	public function setErrorCorrectionLevel( $level ) {
		if ( QRCDR_ECLEVEL_H < $level ) {
			throw new Exception( 'Invalid error correction level.' );
		}

		$this->level = $level;
		return 0;
	}

	/**
	 * Append a QrcdrInputItem.
	 * @param QrcdrInputItem $entry
	 */
	public function appendEntry( QrcdrInputItem $entry ) {
		$this->items[] = $entry;
	}

	/**
	 * Create and append a new item.
	 * @param int   $mode
	 * @param int   $size
	 * @param array $data
	 * @return int 0 on success, -1 on failure.
	 */
	public function append( $mode, $size, $data ) {
		try {
			$entry         = new QrcdrInputItem( $mode, $size, $data );
			$this->items[] = $entry;
			return 0;
		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Insert Structured Append Header.
	 *
	 * @param int $size
	 * @param int $index
	 * @param int $parity
	 * @return int 0 on success, -1 on failure.
	 * @throws Exception
	 */
	public function insertStructuredAppendHeader( $size, $index, $parity ) {
		if ( QRCDR_MAX_STRUCTURED_SYMBOLS < $size ) {
			throw new Exception( 'Invalid size for structured append header.' );
		}

		if ( 1 > $index || QRCDR_MAX_STRUCTURED_SYMBOLS < $index ) {
			throw new Exception( 'Invalid index for structured append header.' );
		}

		$buffer = array( $size, $index, $parity );

		try {
			$entry = new QrcdrInputItem( QRCDR_MODE_STRUCTURE, 3, $buffer );
			array_unshift( $this->items, $entry );
			return 0;
		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Calculate parity for Structured Append.
	 * @return int
	 */
	public function calcParity() {
		$parity = 0;

		foreach ( $this->items as $item ) {
			if ( QRCDR_MODE_STRUCTURE !== $item->mode ) {
				for ( $i = $item->size - 1; 0 <= $i; $i-- ) {
					$parity ^= $item->data[ $i ];
				}
			}
		}

		return $parity;
	}

	/**
	 * Check if data is valid for Numeric Mode.
	 */
	public static function checkModeNum( $size, $data ) {
		for ( $i = 0; $i < $size; $i++ ) {
			if ( ( ord( $data[ $i ] ) < ord( '0' ) ) || ( ord( $data[ $i ] ) > ord( '9' ) ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Estimate bits for Numeric Mode.
	 */
	public static function estimateBitsModeNum( $size ) {
		$words = (int) ( $size / 3 );
		$bits  = $words * 10;

		switch ( $size - $words * 3 ) {
			case 1:
				$bits += 4;
				break;
			case 2:
				$bits += 7;
				break;
			default:
				break;
		}
		return $bits;
	}

	/**
	 * Alphanumeric lookup table.
	 */
	public static $anTable = array(
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43,
		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1,
		-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
		25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	);

	/**
	 * Look up alphanumeric character value.
	 */
	public static function lookAnTable( $char_code ) {
		return ( 127 < $char_code ) ? -1 : self::$anTable[ $char_code ];
	}

	/**
	 * Check if data is valid for Alphanumeric Mode.
	 */
	public static function checkModeAn( $size, $data ) {
		for ( $i = 0; $i < $size; $i++ ) {
			if ( -1 === self::lookAnTable( ord( $data[ $i ] ) ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Estimate bits for Alphanumeric Mode.
	 */
	public static function estimateBitsModeAn( $size ) {
		$words = (int) ( $size / 2 );
		$bits  = $words * 11;

		if ( $size & 1 ) {
			$bits += 6;
		}
		return $bits;
	}

	/**
	 * Estimate bits for 8-bit Mode.
	 */
	public static function estimateBitsMode8( $size ) {
		return $size * 8;
	}

	/**
	 * Estimate bits for Kanji Mode.
	 */
	public function estimateBitsModeKanji( $size ) {
		return (int) ( ( $size / 2 ) * 13 );
	}

	/**
	 * Check if data is valid for Kanji Mode.
	 */
	public static function checkModeKanji( $size, $data ) {
		if ( $size & 1 ) {
			return false;
		}

		for ( $i = 0; $i < $size; $i += 2 ) {
			$value = ( ord( $data[ $i ] ) << 8 ) | ord( $data[ $i + 1 ] );
			if ( 0x8140 > $value
				|| ( 0x9FFC < $value && 0xE040 > $value )
				|| 0xEBBF < $value
			) {
				return false;
			}
		}
		return true;
	}

	/**
	 * General validation check.
	 */
	public static function check( $mode, $size, $data ) {
		if ( 0 >= $size ) {
			return false;
		}

		switch ( $mode ) {
			case QRCDR_MODE_NUM:
				return self::checkModeNum( $size, $data );
			case QRCDR_MODE_AN:
				return self::checkModeAn( $size, $data );
			case QRCDR_MODE_KANJI:
				return self::checkModeKanji( $size, $data );
			case QRCDR_MODE_8:
			case QRCDR_MODE_STRUCTURE:
				return true;
			default:
				break;
		}

		return false;
	}

	/**
	 * Estimate total bitstream size.
	 */
	public function estimateBitStreamSize( $version ) {
		$bits = 0;

		foreach ( $this->items as $item ) {
			$bits += $item->estimateBitStreamSizeOfEntry( $version );
		}

		return $bits;
	}

	/**
	 * Estimate minimum version required.
	 */
	public function estimateVersion() {
		$version          = 0;
		$previous_version = 0;

		do {
			$previous_version = $version;
			$bits             = $this->estimateBitStreamSize( $previous_version );
			$version          = QrcdrSpec::getMinimumVersion( (int) ( ( $bits + 7 ) / 8 ), $this->level );
			if ( 0 > $version ) {
				return -1;
			}
		} while ( $previous_version < $version );

		return $version;
	}

	/**
	 * Calculate max length of data for bits.
	 */
	public static function lengthOfCode( $mode, $version, $bits ) {
		$payload = $bits - 4 - QrcdrSpec::lengthIndicator( $mode, $version );
		$size    = 0;

		switch ( $mode ) {
			case QRCDR_MODE_NUM:
				$chunks = (int) ( $payload / 10 );
				$remain = $payload - $chunks * 10;
				$size   = $chunks * 3;
				if ( 7 <= $remain ) {
					$size += 2;
				} elseif ( 4 <= $remain ) {
					$size += 1;
				}
				break;
			case QRCDR_MODE_AN:
				$chunks = (int) ( $payload / 11 );
				$remain = $payload - $chunks * 11;
				$size   = $chunks * 2;
				if ( 6 <= $remain ) {
					$size++;
				}
				break;
			case QRCDR_MODE_8:
			case QRCDR_MODE_STRUCTURE:
				$size = (int) ( $payload / 8 );
				break;
			case QRCDR_MODE_KANJI:
				$size = (int) ( ( $payload / 13 ) * 2 );
				break;
			default:
				$size = 0;
				break;
		}

		$max_size = QrcdrSpec::maximumWords( $mode, $version );
		if ( 0 > $size ) {
			$size = 0;
		}
		if ( $max_size < $size ) {
			$size = $max_size;
		}

		return $size;
	}

	/**
	 * Create bitstream from items.
	 */
	public function createBitStream() {
		$total_bits = 0;

		foreach ( $this->items as $item ) {
			$bits = $item->encodeBitStream( $this->version );

			if ( 0 > $bits ) {
				return -1;
			}
			$total_bits += $bits;
		}

		return $total_bits;
	}

	/**
	 * Convert data, auto-detecting version.
	 */
	public function convertData() {
		$estimated_version = $this->estimateVersion();
		if ( $this->getVersion() < $estimated_version ) {
			$this->setVersion( $estimated_version );
		}

		for ( ;; ) {
			$bits = $this->createBitStream();

			if ( 0 > $bits ) {
				return -1;
			}

			$required_version = QrcdrSpec::getMinimumVersion( (int) ( ( $bits + 7 ) / 8 ), $this->level );
			if ( 0 > $required_version ) {
				throw new Exception( 'Data is too large for any QR code version.' );
			} elseif ( $this->getVersion() < $required_version ) {
				$this->setVersion( $required_version );
			} else {
				break;
			}
		}

		return 0;
	}

	/**
	 * Append padding and terminator.
	 */
	public function appendPaddingBit( &$bstream ) {
		$bits      = $bstream->size();
		$max_words = QrcdrSpec::getDataLength( $this->version, $this->level );
		$max_bits  = $max_words * 8;

		if ( $max_bits === $bits ) {
			return 0;
		}

		if ( 5 > ( $max_bits - $bits ) ) {
			return $bstream->appendNum( $max_bits - $bits, 0 );
		}

		$bits   += 4;
		$words   = (int) ( ( $bits + 7 ) / 8 );
		$padding = new QrcdrBitstream();
		$return_code = $padding->appendNum( $words * 8 - $bits + 4, 0 );

		if ( 0 > $return_code ) {
			return $return_code;
		}

		$padding_length = $max_words - $words;
		if ( 0 < $padding_length ) {
			$padding_buffer = array();
			for ( $i = 0; $i < $padding_length; $i++ ) {
				$padding_buffer[ $i ] = ( $i & 1 ) ? 0x11 : 0xEC;
			}
			$return_code = $padding->appendBytes( $padding_length, $padding_buffer );
			if ( 0 > $return_code ) {
				return $return_code;
			}
		}

		return $bstream->append( $padding );
	}

	/**
	 * Merge items into one bitstream.
	 */
	public function mergeBitStream() {
		if ( 0 > $this->convertData() ) {
			return null;
		}

		$bstream = new QrcdrBitstream();

		foreach ( $this->items as $item ) {
			$return_code = $bstream->append( $item->bstream );
			if ( 0 > $return_code ) {
				return null;
			}
		}

		return $bstream;
	}

	/**
	 * Get final bitstream with padding.
	 */
	public function getBitStream() {
		$bstream = $this->mergeBitStream();
		if ( null === $bstream ) {
			return null;
		}

		$return_code = $this->appendPaddingBit( $bstream );
		if ( 0 > $return_code ) {
			return null;
		}

		return $bstream;
	}

	/**
	 * Get final byte stream.
	 */
	public function getByteStream() {
		$bstream = $this->getBitStream();
		if ( null === $bstream ) {
			return null;
		}

		return $bstream->toByte();
	}
}
