<?php

namespace Aws\Crypto;

use Aws\Crypto\Cipher\CipherMethod;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use LogicException;
use Psr\Http\Message\StreamInterface;

/**
 * @internal Represents a stream of data to be encrypted with a passed cipher.
 */
class AesEncryptingStream implements AesStreamInterface {
	const BLOCK_SIZE = 16; // 128 bits

	use StreamDecoratorTrait;

	/**
	 * @var string
	 */
	private $buffer = '';

	/**
	 * @var CipherMethod
	 */
	private $cipherMethod;

	/**
	 * @var string
	 */
	private $key;

	/**
	 * @var StreamInterface
	 */
	private $stream;

	/**
	 * @param StreamInterface $plainText
	 * @param string $key
	 * @param CipherMethod $cipherMethod
	 */
	public function __construct(
		StreamInterface $plainText,
		$key,
		CipherMethod $cipherMethod
	) {
		$this->stream       = $plainText;
		$this->key          = $key;
		$this->cipherMethod = clone $cipherMethod;
	}

	public function getOpenSslName() {
		return $this->cipherMethod->getOpenSslName();
	}

	public function getAesName() {
		return $this->cipherMethod->getAesName();
	}

	public function getCurrentIv() {
		return $this->cipherMethod->getCurrentIv();
	}

	public function getSize() {
		$plainTextSize = $this->stream->getSize();

		if ( $this->cipherMethod->requiresPadding() && $plainTextSize !== null ) {
			// PKCS7 padding requires that between 1 and self::BLOCK_SIZE be
			// added to the plaintext to make it an even number of blocks.
			$padding = self::BLOCK_SIZE - $plainTextSize % self::BLOCK_SIZE;

			return $plainTextSize + $padding;
		}

		return $plainTextSize;
	}

	public function isWritable() {
		return false;
	}

	public function seek( $offset, $whence = SEEK_SET ) {
		if ( $whence === SEEK_CUR ) {
			$offset = $this->tell() + $offset;
			$whence = SEEK_SET;
		}

		if ( $whence === SEEK_SET ) {
			$this->buffer = '';
			$wholeBlockOffset
			              = (int) ( $offset / self::BLOCK_SIZE ) * self::BLOCK_SIZE;
			$this->stream->seek( $wholeBlockOffset );
			$this->cipherMethod->seek( $wholeBlockOffset );
			$this->read( $offset - $wholeBlockOffset );
		} else {
			throw new LogicException( 'Unrecognized whence.' );
		}
	}

	public function read( $length ) {
		if ( $length > strlen( $this->buffer ) ) {
			$this->buffer .= $this->encryptBlock(
				self::BLOCK_SIZE * ceil( ( $length - strlen( $this->buffer ) ) / self::BLOCK_SIZE )
			);
		}

		$data         = substr( $this->buffer, 0, $length );
		$this->buffer = substr( $this->buffer, $length );

		return $data ? $data : '';
	}

	private function encryptBlock( $length ) {
		if ( $this->stream->eof() ) {
			return '';
		}

		$plainText = '';
		do {
			$plainText .= $this->stream->read( $length - strlen( $plainText ) );
		} while ( strlen( $plainText ) < $length && ! $this->stream->eof() );

		$options = OPENSSL_RAW_DATA;
		if ( ! $this->stream->eof()
		     || $this->stream->getSize() !== $this->stream->tell()
		) {
			$options |= OPENSSL_ZERO_PADDING;
		}

		$cipherText = openssl_encrypt(
			$plainText,
			$this->cipherMethod->getOpenSslName(),
			$this->key,
			$options,
			$this->cipherMethod->getCurrentIv()
		);

		$this->cipherMethod->update( $cipherText );

		return $cipherText;
	}
}
