<?php
/**
 * @file
 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
 */

namespace Wikimedia\CSS\Parser;

/**
 * Read data for the CSS parser
 */
class StringDataSource implements DataSource {

	/** @var string */
	protected $string;

	/** @var int */
	protected $len = 0, $pos = 0;

	/** @var string[] */
	protected $putBack = [];

	/**
	 * @param string $string Input string. Must be valid UTF-8 with no BOM.
	 */
	public function __construct( $string ) {
		static $newPHP;

		$this->string = (string)$string;
		$this->len = strlen( $this->string );

		// HHVM 3.4 and older come with an outdated version of libmbfl that
		// incorrectly allows values above U+10FFFF, so we have to check
		// for them separately. (This issue also exists in PHP 5.3 and
		// older, which are no longer supported.)
		// @codeCoverageIgnoreStart
		if ( $newPHP === null ) {
			$newPHP = !mb_check_encoding( "\xf4\x90\x80\x80", 'UTF-8' );
		}
		// @codeCoverageIgnoreEnd

		if ( !mb_check_encoding( $this->string, 'UTF-8' ) ||
			!$newPHP && preg_match( "/\xf4[\x90-\xbf]|[\xf5-\xff]/S", $this->string ) !== 0
		) {
			throw new \InvalidArgumentException( '$string is not valid UTF-8' );
		}
	}

	public function readCharacter() {
		if ( $this->putBack ) {
			return array_pop( $this->putBack );
		}

		if ( $this->pos >= $this->len ) {
			return self::EOF;
		}

		// We already checked that the string is valid UTF-8 in the
		// constructor, so we can do a quick binary "get next character" here.
		$p = $this->pos;
		$c = $this->string[$p];
		$cc = ord( $this->string[$p] );
		if ( $cc <= 0x7f ) {
			$this->pos += 1;
			return $c;
		} elseif ( ( $cc & 0xe0 ) === 0xc0 ) {
			$this->pos += 2;
			return substr( $this->string, $p, 2 );
		} elseif ( ( $cc & 0xf0 ) === 0xe0 ) {
			$this->pos += 3;
			return substr( $this->string, $p, 3 );
		} elseif ( ( $cc & 0xf8 ) === 0xf0 ) {
			$this->pos += 4;
			return substr( $this->string, $p, 4 );
		} else {
			// WTF? Should never get here because it should have failed
			// validation in the constructor.
			// @codeCoverageIgnoreStart
			throw new \UnexpectedValueException(
				sprintf( 'Unexpected byte %02X in string at position %d.', $cc, $this->pos )
			);
			// @codeCoverageIgnoreEnd
		}
	}

	public function putBackCharacter( $char ) {
		if ( $char !== self::EOF ) {
			$this->putBack[] = $char;
		}
	}
}
