<?php
namespace Veno\QrCdr\Lib;

use Exception;

/**
 * QR Code Input Item Class (Private Fork for Veno QrCdr)
 *
 * Represents a single segment of data within a QR Code input stream.
 * Handles encoding of specific data modes (Numeric, Alphanumeric, etc.).
 *
 * @package   QrCdr
 * @version   1.1.4-patched
 */

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

// Defines specific to this module, prefixed
if ( ! defined( 'QRCDR_STRUCTURE_HEADER_BITS' ) ) {
	define( 'QRCDR_STRUCTURE_HEADER_BITS', 20 );
}
if ( ! defined( 'QRCDR_MAX_STRUCTURED_SYMBOLS' ) ) {
	define( 'QRCDR_MAX_STRUCTURED_SYMBOLS', 16 );
}

/**
 * Class representing a single data segment.
 */
class QrcdrInputItem {

	/**
	 * Encoding mode (e.g., QRCDR_MODE_NUM).
	 * @var int
	 */
	public $mode;

	/**
	 * Size of data (number of characters).
	 * @var int
	 */
	public $size;

	/**
	 * Data array.
	 * @var array
	 */
	public $data;

	/**
	 * Bitstream generated from this item.
	 * @var QrcdrBitstream
	 */
	public $bstream;

	/**
	 * Constructor.
	 *
	 * @param int         $mode    Encoding mode.
	 * @param int         $size    Data size.
	 * @param array       $data    Data array.
	 * @param QrcdrBitstream $bstream (Optional) Existing bitstream.
	 * @throws Exception If data is invalid for mode/size.
	 */
	public function __construct( $mode, $size, $data, $bstream = null ) {
		$set_data = array_slice( $data, 0, $size );

		if ( count( $set_data ) < $size ) {
			$set_data = array_merge( $set_data, array_fill( 0, $size - count( $set_data ), 0 ) );
		}

		if ( ! QrcdrInput::check( $mode, $size, $set_data ) ) {
			throw new Exception( 'Invalid data provided for mode ' . (int)$mode . ' and size ' . (int)$size );
		}

		$this->mode    = $mode;
		$this->size    = $size;
		$this->data    = $set_data;
		$this->bstream = $bstream;
	}

	/**
	 * Encode in Numeric Mode.
	 *
	 * @param int $version QR Code version.
	 * @return int 0 on success, -1 on failure.
	 */
	public function encodeModeNum( $version ) {
		try {
			$words     = (int) ( $this->size / 3 );
			$bitstream = new QrcdrBitstream();

			$bitstream->appendNum( 4, 0x1 );
			$bitstream->appendNum( QrcdrSpec::lengthIndicator( QRCDR_MODE_NUM, $version ), $this->size );

			for ( $i = 0; $i < $words; $i++ ) {
				$value  = ( ord( $this->data[ $i * 3 ] ) - ord( '0' ) ) * 100;
				$value += ( ord( $this->data[ $i * 3 + 1 ] ) - ord( '0' ) ) * 10;
				$value += ( ord( $this->data[ $i * 3 + 2 ] ) - ord( '0' ) );
				$bitstream->appendNum( 10, $value );
			}

			$remaining_chars = $this->size - $words * 3;
			if ( 1 === $remaining_chars ) {
				$value = ord( $this->data[ $words * 3 ] ) - ord( '0' );
				$bitstream->appendNum( 4, $value );
			} elseif ( 2 === $remaining_chars ) {
				$value  = ( ord( $this->data[ $words * 3 ] ) - ord( '0' ) ) * 10;
				$value += ( ord( $this->data[ $words * 3 + 1 ] ) - ord( '0' ) );
				$bitstream->appendNum( 7, $value );
			}

			$this->bstream = $bitstream;
			return 0;

		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Encode in Alphanumeric Mode.
	 *
	 * @param int $version QR Code version.
	 * @return int 0 on success, -1 on failure.
	 */
	public function encodeModeAn( $version ) {
		try {
			$words     = (int) ( $this->size / 2 );
			$bitstream = new QrcdrBitstream();

			$bitstream->appendNum( 4, 0x02 );
			$bitstream->appendNum( QrcdrSpec::lengthIndicator( QRCDR_MODE_AN, $version ), $this->size );

			for ( $i = 0; $i < $words; $i++ ) {
				$value  = (int) QrcdrInput::lookAnTable( ord( $this->data[ $i * 2 ] ) ) * 45;
				$value += (int) QrcdrInput::lookAnTable( ord( $this->data[ $i * 2 + 1 ] ) );
				$bitstream->appendNum( 11, $value );
			}

			if ( $this->size & 1 ) {
				$value = QrcdrInput::lookAnTable( ord( $this->data[ $words * 2 ] ) );
				$bitstream->appendNum( 6, $value );
			}

			$this->bstream = $bitstream;
			return 0;

		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Encode in 8-bit Mode.
	 *
	 * @param int $version QR Code version.
	 * @return int 0 on success, -1 on failure.
	 */
	public function encodeMode8( $version ) {
		try {
			$bitstream = new QrcdrBitstream();

			$bitstream->appendNum( 4, 0x4 );
			$bitstream->appendNum( QrcdrSpec::lengthIndicator( QRCDR_MODE_8, $version ), $this->size );

			for ( $i = 0; $i < $this->size; $i++ ) {
				$bitstream->appendNum( 8, ord( $this->data[ $i ] ) );
			}

			$this->bstream = $bitstream;
			return 0;

		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Encode in Kanji Mode.
	 *
	 * @param int $version QR Code version.
	 * @return int 0 on success, -1 on failure.
	 */
	public function encodeModeKanji( $version ) {
		try {
			$bitstream = new QrcdrBitstream();

			$bitstream->appendNum( 4, 0x8 );
			$bitstream->appendNum( QrcdrSpec::lengthIndicator( QRCDR_MODE_KANJI, $version ), (int) ( $this->size / 2 ) );

			for ( $i = 0; $i < $this->size; $i += 2 ) {
				$value = ( ord( $this->data[ $i ] ) << 8 ) | ord( $this->data[ $i + 1 ] );
				if ( 0x9ffc >= $value ) {
					$value -= 0x8140;
				} else {
					$value -= 0xc140;
				}

				$high_byte = ( $value >> 8 ) * 0xc0;
				$value     = ( $value & 0xff ) + $high_byte;

				$bitstream->appendNum( 13, $value );
			}

			$this->bstream = $bitstream;
			return 0;

		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Encode Structured Append.
	 *
	 * @return int 0 on success, -1 on failure.
	 */
	public function encodeModeStructure() {
		try {
			$bitstream = new QrcdrBitstream();

			$bitstream->appendNum( 4, 0x03 );
			$bitstream->appendNum( 4, ord( $this->data[1] ) - 1 );
			$bitstream->appendNum( 4, ord( $this->data[0] ) - 1 );
			$bitstream->appendNum( 8, ord( $this->data[2] ) );

			$this->bstream = $bitstream;
			return 0;

		} catch ( Exception $e ) {
			return -1;
		}
	}

	/**
	 * Estimate bit stream size.
	 *
	 * @param int $version QR Code version.
	 * @return int Estimated size in bits.
	 */
	public function estimateBitStreamSizeOfEntry( $version ) {
		$bits = 0;

		if ( 0 === $version ) {
			$version = 1;
		}

		switch ( $this->mode ) {
			case QRCDR_MODE_NUM:
				$bits = QrcdrInput::estimateBitsModeNum( $this->size );
				break;
			case QRCDR_MODE_AN:
				$bits = QrcdrInput::estimateBitsModeAn( $this->size );
				break;
			case QRCDR_MODE_8:
				$bits = QrcdrInput::estimateBitsMode8( $this->size );
				break;
			case QRCDR_MODE_KANJI:
				$bits = QrcdrInput::estimateBitsModeKanji( $this->size );
				break;
			case QRCDR_MODE_STRUCTURE:
				return QRCDR_STRUCTURE_HEADER_BITS;
			default:
				return 0;
		}

		$length_indicator = QrcdrSpec::lengthIndicator( $this->mode, $version );
		$group_capacity   = 1 << $length_indicator;
		$num_blocks       = (int) ( ( $this->size + $group_capacity - 1 ) / $group_capacity );

		$bits += $num_blocks * ( 4 + $length_indicator );

		return $bits;
	}

	/**
	 * Encode bitstream from this item.
	 *
	 * @param int $version QR Code version.
	 * @return int Bitstream size or -1 on failure.
	 */
	public function encodeBitStream( $version ) {
		try {
			unset( $this->bstream );
			$max_words = QrcdrSpec::maximumWords( $this->mode, $version );

			if ( $max_words < $this->size ) {
				$entry1 = new QrcdrInputItem( $this->mode, $max_words, $this->data );
				$entry2 = new QrcdrInputItem( $this->mode, $this->size - $max_words, array_slice( $this->data, $max_words ) );

				$entry1->encodeBitStream( $version );
				$entry2->encodeBitStream( $version );

				$this->bstream = new QrcdrBitstream();
				$this->bstream->append( $entry1->bstream );
				$this->bstream->append( $entry2->bstream );

				unset( $entry1 );
				unset( $entry2 );

			} else {
				$return_code = 0;
				switch ( $this->mode ) {
					case QRCDR_MODE_NUM:
						$return_code = $this->encodeModeNum( $version );
						break;
					case QRCDR_MODE_AN:
						$return_code = $this->encodeModeAn( $version );
						break;
					case QRCDR_MODE_8:
						$return_code = $this->encodeMode8( $version );
						break;
					case QRCDR_MODE_KANJI:
						$return_code = $this->encodeModeKanji( $version );
						break;
					case QRCDR_MODE_STRUCTURE:
						$return_code = $this->encodeModeStructure();
						break;
					default:
						break;
				}

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

			return $this->bstream->size();

		} catch ( Exception $e ) {
			return -1;
		}
	}
}
