<?php
/**
 * Class ECI
 *
 * @created      20.11.2020
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2020 smiley
 * @license      MIT
 */
declare(strict_types=1);

namespace chillerlan\QRCode\Data;

use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf;

/**
 * Adds an ECI Designator
 *
 * ISO/IEC 18004:2000 8.4.1.1
 *
 * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
 */
final class ECI extends QRDataModeAbstract{

	public const DATAMODE = Mode::ECI;

	/**
	 * The current ECI encoding id (default to UTF-8)
	 */
	private int $encoding = ECICharset::ISO_IEC_10646_UTF_8;

	/**
	 * @inheritDoc
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
	 * @noinspection PhpMissingParentConstructorInspection
	 */
	public function __construct(int|null $encoding = null){

		if($encoding !== null){

			if($encoding < 0 || $encoding > 999999){
				throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding));
			}

			$this->encoding = $encoding;
		}

	}

	public function getLengthInBits():int{

		if($this->encoding < 128){
			return 8;
		}

		if($this->encoding < 16384){
			return 16;
		}

		return 24;
	}

	/**
	 * Writes an ECI designator to the bitbuffer
	 *
	 * @inheritDoc
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
	 */
	public function write(BitBuffer $bitBuffer, int $versionNumber):static{
		$bitBuffer->put(self::DATAMODE, 4);

		if($this->encoding < 128){
			$bitBuffer->put($this->encoding, 8);
		}
		elseif($this->encoding < 16384){
			$bitBuffer->put(($this->encoding | 0x8000), 16);
		}
		elseif($this->encoding < 1000000){
			$bitBuffer->put(($this->encoding | 0xC00000), 24);
		}
		else{
			throw new QRCodeDataException('invalid ECI ID'); // @codeCoverageIgnore
		}

		return $this;
	}

	/**
	 * Reads and parses the value of an ECI designator
	 *
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
	 */
	public function parseValue(BitBuffer $bitBuffer):ECICharset{
		$firstByte = $bitBuffer->read(8);

		// just one byte
		if(($firstByte & 0b10000000) === 0){
			$id = ($firstByte & 0b01111111);
		}
		// two bytes
		elseif(($firstByte & 0b11000000) === 0b10000000){
			$id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8));
		}
		// three bytes
		elseif(($firstByte & 0b11100000) === 0b11000000){
			$id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16));
		}
		else{
			throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte));// @codeCoverageIgnore
		}

		return new ECICharset($id);
	}

	/**
	 * @codeCoverageIgnore Unused, but required as per interface
	 */
	public static function validateString(string $string):bool{
		return true;
	}

	/**
	 * Reads and decodes the ECI designator including the following byte sequence
	 *
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
	 */
	public function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
		$eciCharset = $this->parseValue($bitBuffer);
		$nextMode   = $bitBuffer->read(4);
		$encoding   = $eciCharset->getName();

		// this is definitely weird, but there are QR Codes out in the wild
		// that have ECI followed by numeric and alphanum segments
		// @see https://github.com/chillerlan/php-qrcode/discussions/289
		$dataMode = match($nextMode){
			Mode::NUMBER   => new Number,
			Mode::ALPHANUM => new AlphaNum,
			Mode::BYTE     => new Byte,
			default        => throw new QRCodeDataException(
				sprintf('ECI designator followed by invalid mode: "%04b"', $nextMode),
			),
		};

		$data = $dataMode->decodeSegment($bitBuffer, $versionNumber);

		if($encoding === null){
			// The spec isn't clear on this mode; see
			// section 6.4.5: it does not say which encoding to assuming
			// upon decoding. I have seen ISO-8859-1 used as well as
			// Shift_JIS -- without anything like an ECI designator to
			// give a hint.
			$encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);

			if($encoding === false){
				throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore
			}
		}

		$encoded = mb_convert_encoding($data, mb_internal_encoding(), $encoding);

		if($encoded === false){
			throw new QRCodeDataException('mb_convert_encoding() error'); // @codeCoverageIgnore
		}

		return $encoded;
	}

}
