<?php

namespace GuzzleHttp\Psr7;

use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;

/**
 * Stream decorator that can cache previously read bytes from a sequentially
 * read stream.
 */
class CachingStream implements StreamInterface {
	use StreamDecoratorTrait;

	/** @var StreamInterface Stream being wrapped */
	private $remoteStream;

	/** @var int Number of bytes to skip reading due to a write on the buffer */
	private $skipReadBytes = 0;

	/**
	 * We will treat the buffer object as the body of the stream
	 *
	 * @param StreamInterface $stream Stream to cache
	 * @param StreamInterface $target Optionally specify where data is cached
	 */
	public function __construct(
		StreamInterface $stream,
		StreamInterface $target = null
	) {
		$this->remoteStream = $stream;
		$this->stream       = $target ?: new Stream( fopen( 'php://temp', 'r+' ) );
	}

	public function getSize() {
		return max( $this->stream->getSize(), $this->remoteStream->getSize() );
	}

	public function rewind() {
		$this->seek( 0 );
	}

	public function seek( $offset, $whence = SEEK_SET ) {
		if ( $whence == SEEK_SET ) {
			$byte = $offset;
		} elseif ( $whence == SEEK_CUR ) {
			$byte = $offset + $this->tell();
		} elseif ( $whence == SEEK_END ) {
			$size = $this->remoteStream->getSize();
			if ( $size === null ) {
				$size = $this->cacheEntireStream();
			}
			$byte = $size + $offset;
		} else {
			throw new InvalidArgumentException( 'Invalid whence' );
		}

		$diff = $byte - $this->stream->getSize();

		if ( $diff > 0 ) {
			// Read the remoteStream until we have read in at least the amount
			// of bytes requested, or we reach the end of the file.
			while ( $diff > 0 && ! $this->remoteStream->eof() ) {
				$this->read( $diff );
				$diff = $byte - $this->stream->getSize();
			}
		} else {
			// We can just do a normal seek since we've already seen this byte.
			$this->stream->seek( $byte );
		}
	}

	private function cacheEntireStream() {
		$target = new FnStream( [ 'write' => 'strlen' ] );
		copy_to_stream( $this, $target );

		return $this->tell();
	}

	public function read( $length ) {
		// Perform a regular read on any previously read data from the buffer
		$data      = $this->stream->read( $length );
		$remaining = $length - strlen( $data );

		// More data was requested so read from the remote stream
		if ( $remaining ) {
			// If data was written to the buffer in a position that would have
			// been filled from the remote stream, then we must skip bytes on
			// the remote stream to emulate overwriting bytes from that
			// position. This mimics the behavior of other PHP stream wrappers.
			$remoteData = $this->remoteStream->read(
				$remaining + $this->skipReadBytes
			);

			if ( $this->skipReadBytes ) {
				$len                 = strlen( $remoteData );
				$remoteData          = substr( $remoteData, $this->skipReadBytes );
				$this->skipReadBytes = max( 0, $this->skipReadBytes - $len );
			}

			$data .= $remoteData;
			$this->stream->write( $remoteData );
		}

		return $data;
	}

	public function write( $string ) {
		// When appending to the end of the currently read stream, you'll want
		// to skip bytes from being read from the remote stream to emulate
		// other stream wrappers. Basically replacing bytes of data of a fixed
		// length.
		$overflow = ( strlen( $string ) + $this->tell() ) - $this->remoteStream->tell();
		if ( $overflow > 0 ) {
			$this->skipReadBytes += $overflow;
		}

		return $this->stream->write( $string );
	}

	public function eof() {
		return $this->stream->eof() && $this->remoteStream->eof();
	}

	/**
	 * Close both the remote stream and buffer stream
	 */
	public function close() {
		$this->remoteStream->close() && $this->stream->close();
	}
}
