<?php
namespace Veno\QrCdr\Lib;

use Exception;

/**
 * QR Code Data Splitter Class (Private Fork for Veno QrCdr)
 *
 * Optimizes input data by splitting it into the most efficient encoding modes
 * (Numeric, Alphanumeric, Kanji, Byte).
 *
 * @package   QrCdr
 * @version   1.1.4-patched
 */

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

class QrcdrSplit {

	/**
	 * Data string to process.
	 * @var string
	 */
	public $data_str = '';

	/**
	 * Reference to QrcdrInput object.
	 * @var QrcdrInput
	 */
	public $input;

	/**
	 * Mode hint.
	 * @var int
	 */
	public $mode_hint;

	/**
	 * Constructor.
	 *
	 * @param string  $data_str  Raw data string.
	 * @param QrcdrInput $input     QrcdrInput object reference.
	 * @param int     $mode_hint Mode hint (e.g., QRCDR_MODE_KANJI).
	 */
	public function __construct( $data_str, $input, $mode_hint ) {
		$this->data_str  = $data_str;
		$this->input     = $input;
		$this->mode_hint = $mode_hint;
	}

	/**
	 * Check if character at position is a digit.
	 */
	public static function isdigitat( $string, $position ) {
		if ( $position >= strlen( $string ) ) {
			return false;
		}
		return ( ( ord( $string[ $position ] ) >= ord( '0' ) ) && ( ord( $string[ $position ] ) <= ord( '9' ) ) );
	}

	/**
	 * Check if character at position is alphanumeric.
	 */
	public static function isalnumat( $string, $position ) {
		if ( $position >= strlen( $string ) ) {
			return false;
		}
		return ( QrcdrInput::lookAnTable( ord( $string[ $position ] ) ) >= 0 );
	}

	/**
	 * Identify encoding mode for character at position.
	 *
	 * @param int $position
	 * @return int QRCDR_MODE_* constant.
	 */
	public function identifyMode( $position ) {
		if ( $position >= strlen( $this->data_str ) ) {
			return QRCDR_MODE_NUL;
		}

		$char = $this->data_str[ $position ];

		if ( self::isdigitat( $this->data_str, $position ) ) {
			return QRCDR_MODE_NUM;
		} elseif ( self::isalnumat( $this->data_str, $position ) ) {
			return QRCDR_MODE_AN;
		} elseif ( QRCDR_MODE_KANJI === $this->mode_hint ) {
			if ( $position + 1 < strlen( $this->data_str ) ) {
				$next_char = $this->data_str[ $position + 1 ];
				$word      = ( ord( $char ) << 8 ) | ord( $next_char );
				if ( ( $word >= 0x8140 && $word <= 0x9FFC ) || ( $word >= 0xE040 && $word <= 0xEBBF ) ) {
					return QRCDR_MODE_KANJI;
				}
			}
		}

		return QRCDR_MODE_8;
	}

	/**
	 * Process a sequence of Numeric data.
	 *
	 * @return int Processed length or -1 on error.
	 */
	public function eatNum() {
		$length_indicator = QrcdrSpec::lengthIndicator( QRCDR_MODE_NUM, $this->input->getVersion() );

		$position = 0;
		while ( self::isdigitat( $this->data_str, $position ) ) {
			$position++;
		}

		$run_length = $position;
		$next_mode  = $this->identifyMode( $position );

		if ( QRCDR_MODE_8 === $next_mode ) {
			$bit_difference = QrcdrInput::estimateBitsModeNum( $run_length ) + 4 + $length_indicator
				+ QrcdrInput::estimateBitsMode8( 1 )
				- QrcdrInput::estimateBitsMode8( $run_length + 1 );
			if ( $bit_difference > 0 ) {
				return $this->eat8();
			}
		}

		if ( QRCDR_MODE_AN === $next_mode ) {
			$bit_difference = QrcdrInput::estimateBitsModeNum( $run_length ) + 4 + $length_indicator
				+ QrcdrInput::estimateBitsModeAn( 1 )
				- QrcdrInput::estimateBitsModeAn( $run_length + 1 );
			if ( $bit_difference > 0 ) {
				return $this->eatAn();
			}
		}

		$return_code = $this->input->append( QRCDR_MODE_NUM, $run_length, str_split( $this->data_str ) );
		if ( 0 > $return_code ) {
			return -1;
		}

		return $run_length;
	}

	/**
	 * Process a sequence of Alphanumeric data.
	 *
	 * @return int Processed length or -1 on error.
	 */
	public function eatAn() {
		$length_indicator_an  = QrcdrSpec::lengthIndicator( QRCDR_MODE_AN, $this->input->getVersion() );
		$length_indicator_num = QrcdrSpec::lengthIndicator( QRCDR_MODE_NUM, $this->input->getVersion() );

		$position = 0;

		while ( self::isalnumat( $this->data_str, $position ) ) {
			if ( self::isdigitat( $this->data_str, $position ) ) {
				$sub_position = $position;
				while ( self::isdigitat( $this->data_str, $sub_position ) ) {
					$sub_position++;
				}

				$bit_difference = QrcdrInput::estimateBitsModeAn( $position )
					+ QrcdrInput::estimateBitsModeNum( $sub_position - $position ) + 4 + $length_indicator_num
					- QrcdrInput::estimateBitsModeAn( $sub_position );

				if ( 0 > $bit_difference ) {
					break;
				} else {
					$position = $sub_position;
				}
			} else {
				$position++;
			}
		}

		$run_length = $position;

		if ( ! self::isalnumat( $this->data_str, $position ) ) {
			$bit_difference = QrcdrInput::estimateBitsModeAn( $run_length ) + 4 + $length_indicator_an
				+ QrcdrInput::estimateBitsMode8( 1 )
				- QrcdrInput::estimateBitsMode8( $run_length + 1 );
			if ( $bit_difference > 0 ) {
				return $this->eat8();
			}
		}

		$return_code = $this->input->append( QRCDR_MODE_AN, $run_length, str_split( $this->data_str ) );
		if ( 0 > $return_code ) {
			return -1;
		}

		return $run_length;
	}

	/**
	 * Process a sequence of Kanji data.
	 */
	public function eatKanji() {
		$position = 0;

		while ( QRCDR_MODE_KANJI === $this->identifyMode( $position ) ) {
			$position += 2;
		}

		$return_code = $this->input->append( QRCDR_MODE_KANJI, $position, str_split( $this->data_str ) );
		if ( 0 > $return_code ) {
			return -1;
		}

		return $return_code;
	}

	/**
	 * Process a sequence of 8-bit data.
	 */
	public function eat8() {
		$length_indicator_an  = QrcdrSpec::lengthIndicator( QRCDR_MODE_AN, $this->input->getVersion() );
		$length_indicator_num = QrcdrSpec::lengthIndicator( QRCDR_MODE_NUM, $this->input->getVersion() );

		$position       = 1;
		$data_str_len = strlen( $this->data_str );

		while ( $position < $data_str_len ) {
			$mode = $this->identifyMode( $position );
			if ( QRCDR_MODE_KANJI === $mode ) {
				break;
			}
			if ( QRCDR_MODE_NUM === $mode ) {
				$sub_position = $position;
				while ( self::isdigitat( $this->data_str, $sub_position ) ) {
					$sub_position++;
				}
				$bit_difference = QrcdrInput::estimateBitsMode8( $position )
					+ QrcdrInput::estimateBitsModeNum( $sub_position - $position ) + 4 + $length_indicator_num
					- QrcdrInput::estimateBitsMode8( $sub_position );
				if ( 0 > $bit_difference ) {
					break;
				} else {
					$position = $sub_position;
				}
			} elseif ( QRCDR_MODE_AN === $mode ) {
				$sub_position = $position;
				while ( self::isalnumat( $this->data_str, $sub_position ) ) {
					$sub_position++;
				}
				$bit_difference = QrcdrInput::estimateBitsMode8( $position )
					+ QrcdrInput::estimateBitsModeAn( $sub_position - $position ) + 4 + $length_indicator_an
					- QrcdrInput::estimateBitsMode8( $sub_position );
				if ( 0 > $bit_difference ) {
					break;
				} else {
					$position = $sub_position;
				}
			} else {
				$position++;
			}
		}

		$run_length  = $position;
		$return_code = $this->input->append( QRCDR_MODE_8, $run_length, str_split( $this->data_str ) );

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

		return $run_length;
	}

	/**
	 * Split input string into optimized segments.
	 *
	 * @return int 0 on success, -1 on error.
	 */
	public function splitString() {
		while ( strlen( $this->data_str ) > 0 ) {
			if ( '' === $this->data_str ) {
				return 0;
			}

			$mode = $this->identifyMode( 0 );

			switch ( $mode ) {
				case QRCDR_MODE_NUM:
					$length = $this->eatNum();
					break;
				case QRCDR_MODE_AN:
					$length = $this->eatAn();
					break;
				case QRCDR_MODE_KANJI:
					$length = $this->eatKanji();
					break;
				default:
					$length = $this->eat8();
					break;
			}

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

			$this->data_str = substr( $this->data_str, $length );
		}
	}

	/**
	 * Convert alphabet characters to uppercase (ignores Kanji).
	 *
	 * @return string Uppercased string.
	 */
	public function toUpper() {
		$string_len = strlen( $this->data_str );
		$position   = 0;

		while ( $position < $string_len ) {
			$mode = $this->identifyMode( $position );
			if ( QRCDR_MODE_KANJI === $mode ) {
				$position += 2;
			} else {
				$char_ord = ord( $this->data_str[ $position ] );
				if ( $char_ord >= ord( 'a' ) && $char_ord <= ord( 'z' ) ) {
					$this->data_str[ $position ] = chr( $char_ord - 32 );
				}
				$position++;
			}
		}

		return $this->data_str;
	}

	/**
	 * Static utility to split string and append to QrcdrInput.
	 *
	 * @param string  $string          Input string.
	 * @param QrcdrInput $input           Target QrcdrInput object.
	 * @param int     $mode_hint       Mode hint.
	 * @param bool    $case_sensitive  If false, converts to upper case.
	 * @return int Result code.
	 * @throws Exception If string is empty.
	 */
	public static function splitStringToQRinput( $string, QrcdrInput $input, $mode_hint, $case_sensitive = true ) {
		if ( empty( $string ) || '\0' === $string ) {
			throw new Exception( 'Input string is empty or invalid.' );
		}

		$split = new QrcdrSplit( $string, $input, $mode_hint );

		if ( ! $case_sensitive ) {
			$split->toUpper();
		}

		return $split->splitString();
	}
}
