<?php

namespace GuzzleHttp\Psr7;

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

/**
 * Reads from multiple streams, one after the other.
 *
 * This is a read-only stream decorator.
 */
class AppendStream implements StreamInterface {
	/** @var StreamInterface[] Streams being decorated */
	private $streams = [];

	private $seekable = true;
	private $current = 0;
	private $pos = 0;

	/**
	 * @param StreamInterface[] $streams Streams to decorate. Each stream must
	 *                                   be readable.
	 */
	public function __construct( array $streams = [] ) {
		foreach ( $streams as $stream ) {
			$this->addStream( $stream );
		}
	}

	/**
	 * Add a stream to the AppendStream
	 *
	 * @param StreamInterface $stream Stream to append. Must be readable.
	 *
	 * @throws InvalidArgumentException if the stream is not readable
	 */
	public function addStream( StreamInterface $stream ) {
		if ( ! $stream->isReadable() ) {
			throw new InvalidArgumentException( 'Each stream must be readable' );
		}

		// The stream is only seekable if all streams are seekable
		if ( ! $stream->isSeekable() ) {
			$this->seekable = false;
		}

		$this->streams[] = $stream;
	}

	public function __toString() {
		try {
			$this->rewind();

			return $this->getContents();
		} catch ( Exception $e ) {
			return '';
		}
	}

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

	/**
	 * Attempts to seek to the given position. Only supports SEEK_SET.
	 *
	 * {@inheritdoc}
	 */
	public function seek( $offset, $whence = SEEK_SET ) {
		if ( ! $this->seekable ) {
			throw new RuntimeException( 'This AppendStream is not seekable' );
		} elseif ( $whence !== SEEK_SET ) {
			throw new RuntimeException( 'The AppendStream can only seek with SEEK_SET' );
		}

		$this->pos = $this->current = 0;

		// Rewind each stream
		foreach ( $this->streams as $i => $stream ) {
			try {
				$stream->rewind();
			} catch ( Exception $e ) {
				throw new RuntimeException( 'Unable to seek stream '
				                             . $i . ' of the AppendStream', 0, $e );
			}
		}

		// Seek to the actual position by reading from each stream
		while ( $this->pos < $offset && ! $this->eof() ) {
			$result = $this->read( min( 8096, $offset - $this->pos ) );
			if ( $result === '' ) {
				break;
			}
		}
	}

	public function eof() {
		return ! $this->streams ||
		       ( $this->current >= count( $this->streams ) - 1 &&
		         $this->streams[ $this->current ]->eof() );
	}

	/**
	 * Reads from all of the appended streams until the length is met or EOF.
	 *
	 * {@inheritdoc}
	 */
	public function read( $length ) {
		$buffer         = '';
		$total          = count( $this->streams ) - 1;
		$remaining      = $length;
		$progressToNext = false;

		while ( $remaining > 0 ) {

			// Progress to the next stream if needed.
			if ( $progressToNext || $this->streams[ $this->current ]->eof() ) {
				$progressToNext = false;
				if ( $this->current === $total ) {
					break;
				}
				$this->current ++;
			}

			$result = $this->streams[ $this->current ]->read( $remaining );

			// Using a loose comparison here to match on '', false, and null
			if ( $result == null ) {
				$progressToNext = true;
				continue;
			}

			$buffer    .= $result;
			$remaining = $length - strlen( $buffer );
		}

		$this->pos += strlen( $buffer );

		return $buffer;
	}

	public function getContents() {
		return copy_to_string( $this );
	}

	/**
	 * Closes each attached stream.
	 *
	 * {@inheritdoc}
	 */
	public function close() {
		$this->pos      = $this->current = 0;
		$this->seekable = true;

		foreach ( $this->streams as $stream ) {
			$stream->close();
		}

		$this->streams = [];
	}

	/**
	 * Detaches each attached stream.
	 *
	 * Returns null as it's not clear which underlying stream resource to return.
	 *
	 * {@inheritdoc}
	 */
	public function detach() {
		$this->pos      = $this->current = 0;
		$this->seekable = true;

		foreach ( $this->streams as $stream ) {
			$stream->detach();
		}

		$this->streams = [];
	}

	public function tell() {
		return $this->pos;
	}

	/**
	 * Tries to calculate the size by adding the size of each stream.
	 *
	 * If any of the streams do not return a valid number, then the size of the
	 * append stream cannot be determined and null is returned.
	 *
	 * {@inheritdoc}
	 */
	public function getSize() {
		$size = 0;

		foreach ( $this->streams as $stream ) {
			$s = $stream->getSize();
			if ( $s === null ) {
				return null;
			}
			$size += $s;
		}

		return $size;
	}

	public function isReadable() {
		return true;
	}

	public function isWritable() {
		return false;
	}

	public function isSeekable() {
		return $this->seekable;
	}

	public function write( $string ) {
		throw new RuntimeException( 'Cannot write to an AppendStream' );
	}

	public function getMetadata( $key = null ) {
		return $key ? null : [];
	}
}
