<?php

namespace GuzzleHttp\Psr7;

use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;

/**
 * Server-side HTTP request
 *
 * Extends the Request definition to add methods for accessing incoming data,
 * specifically server parameters, cookies, matched path parameters, query
 * string arguments, body parameters, and upload file information.
 *
 * "Attributes" are discovered via decomposing the request (and usually
 * specifically the URI path), and typically will be injected by the application.
 *
 * Requests are considered immutable; all methods that might change state are
 * implemented such that they retain the internal state of the current
 * message and return a new instance that contains the changed state.
 */
class ServerRequest extends Request implements ServerRequestInterface {
	/**
	 * @var array
	 */
	private $attributes = [];

	/**
	 * @var array
	 */
	private $cookieParams = [];

	/**
	 * @var null|array|object
	 */
	private $parsedBody;

	/**
	 * @var array
	 */
	private $queryParams = [];

	/**
	 * @var array
	 */
	private $serverParams;

	/**
	 * @var array
	 */
	private $uploadedFiles = [];

	/**
	 * @param string $method HTTP method
	 * @param string|UriInterface $uri URI
	 * @param array $headers Request headers
	 * @param string|null|resource|StreamInterface $body Request body
	 * @param string $version Protocol version
	 * @param array $serverParams Typically the $_SERVER superglobal
	 */
	public function __construct(
		$method,
		$uri,
		array $headers = [],
		$body = null,
		$version = '1.1',
		array $serverParams = []
	) {
		$this->serverParams = $serverParams;

		parent::__construct( $method, $uri, $headers, $body, $version );
	}

	/**
	 * Return a ServerRequest populated with superglobals:
	 * $_GET
	 * $_POST
	 * $_COOKIE
	 * $_FILES
	 * $_SERVER
	 *
	 * @return ServerRequestInterface
	 */
	public static function fromGlobals() {
		$method   = isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : 'GET';
		$headers  = getallheaders();
		$uri      = self::getUriFromGlobals();
		$body     = new CachingStream( new LazyOpenStream( 'php://input', 'r+' ) );
		$protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? str_replace( 'HTTP/', '', $_SERVER['SERVER_PROTOCOL'] ) : '1.1';

		$serverRequest = new ServerRequest( $method, $uri, $headers, $body, $protocol, $_SERVER );

		return $serverRequest
			->withCookieParams( $_COOKIE )
			->withQueryParams( $_GET )
			->withParsedBody( $_POST )
			->withUploadedFiles( self::normalizeFiles( $_FILES ) );
	}

	/**
	 * Get a Uri populated with values from $_SERVER.
	 *
	 * @return UriInterface
	 */
	public static function getUriFromGlobals() {
		$uri = new Uri( '' );

		$uri = $uri->withScheme( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http' );

		$hasPort = false;
		if ( isset( $_SERVER['HTTP_HOST'] ) ) {
			list( $host, $port ) = self::extractHostAndPortFromAuthority( $_SERVER['HTTP_HOST'] );
			if ( $host !== null ) {
				$uri = $uri->withHost( $host );
			}

			if ( $port !== null ) {
				$hasPort = true;
				$uri     = $uri->withPort( $port );
			}
		} elseif ( isset( $_SERVER['SERVER_NAME'] ) ) {
			$uri = $uri->withHost( $_SERVER['SERVER_NAME'] );
		} elseif ( isset( $_SERVER['SERVER_ADDR'] ) ) {
			$uri = $uri->withHost( $_SERVER['SERVER_ADDR'] );
		}

		if ( ! $hasPort && isset( $_SERVER['SERVER_PORT'] ) ) {
			$uri = $uri->withPort( $_SERVER['SERVER_PORT'] );
		}

		$hasQuery = false;
		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$requestUriParts = explode( '?', $_SERVER['REQUEST_URI'], 2 );
			$uri             = $uri->withPath( $requestUriParts[0] );
			if ( isset( $requestUriParts[1] ) ) {
				$hasQuery = true;
				$uri      = $uri->withQuery( $requestUriParts[1] );
			}
		}

		if ( ! $hasQuery && isset( $_SERVER['QUERY_STRING'] ) ) {
			$uri = $uri->withQuery( $_SERVER['QUERY_STRING'] );
		}

		return $uri;
	}

	private static function extractHostAndPortFromAuthority( $authority ) {
		$uri   = 'http://' . $authority;
		$parts = parse_url( $uri );
		if ( false === $parts ) {
			return [ null, null ];
		}

		$host = isset( $parts['host'] ) ? $parts['host'] : null;
		$port = isset( $parts['port'] ) ? $parts['port'] : null;

		return [ $host, $port ];
	}

	/**
	 * {@inheritdoc}
	 */
	public function withCookieParams( array $cookies ) {
		$new               = clone $this;
		$new->cookieParams = $cookies;

		return $new;
	}

	/**
	 * Return an UploadedFile instance array.
	 *
	 * @param array $files A array which respect $_FILES structure
	 *
	 * @return array
	 * @throws InvalidArgumentException for unrecognized values
	 */
	public static function normalizeFiles( array $files ) {
		$normalized = [];

		foreach ( $files as $key => $value ) {
			if ( $value instanceof UploadedFileInterface ) {
				$normalized[ $key ] = $value;
			} elseif ( is_array( $value ) && isset( $value['tmp_name'] ) ) {
				$normalized[ $key ] = self::createUploadedFileFromSpec( $value );
			} elseif ( is_array( $value ) ) {
				$normalized[ $key ] = self::normalizeFiles( $value );
				continue;
			} else {
				throw new InvalidArgumentException( 'Invalid value in files specification' );
			}
		}

		return $normalized;
	}

	/**
	 * Create and return an UploadedFile instance from a $_FILES specification.
	 *
	 * If the specification represents an array of values, this method will
	 * delegate to normalizeNestedFileSpec() and return that return value.
	 *
	 * @param array $value $_FILES struct
	 *
	 * @return array|UploadedFileInterface
	 */
	private static function createUploadedFileFromSpec( array $value ) {
		if ( is_array( $value['tmp_name'] ) ) {
			return self::normalizeNestedFileSpec( $value );
		}

		return new UploadedFile(
			$value['tmp_name'],
			(int) $value['size'],
			(int) $value['error'],
			$value['name'],
			$value['type']
		);
	}

	/**
	 * Normalize an array of file specifications.
	 *
	 * Loops through all nested files and returns a normalized array of
	 * UploadedFileInterface instances.
	 *
	 * @param array $files
	 *
	 * @return UploadedFileInterface[]
	 */
	private static function normalizeNestedFileSpec( array $files = [] ) {
		$normalizedFiles = [];

		foreach ( array_keys( $files['tmp_name'] ) as $key ) {
			$spec                    = [
				'tmp_name' => $files['tmp_name'][ $key ],
				'size'     => $files['size'][ $key ],
				'error'    => $files['error'][ $key ],
				'name'     => $files['name'][ $key ],
				'type'     => $files['type'][ $key ],
			];
			$normalizedFiles[ $key ] = self::createUploadedFileFromSpec( $spec );
		}

		return $normalizedFiles;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getServerParams() {
		return $this->serverParams;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getUploadedFiles() {
		return $this->uploadedFiles;
	}

	/**
	 * {@inheritdoc}
	 */
	public function withUploadedFiles( array $uploadedFiles ) {
		$new                = clone $this;
		$new->uploadedFiles = $uploadedFiles;

		return $new;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getCookieParams() {
		return $this->cookieParams;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getQueryParams() {
		return $this->queryParams;
	}

	/**
	 * {@inheritdoc}
	 */
	public function withQueryParams( array $query ) {
		$new              = clone $this;
		$new->queryParams = $query;

		return $new;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getParsedBody() {
		return $this->parsedBody;
	}

	/**
	 * {@inheritdoc}
	 */
	public function withParsedBody( $data ) {
		$new             = clone $this;
		$new->parsedBody = $data;

		return $new;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getAttributes() {
		return $this->attributes;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getAttribute( $attribute, $default = null ) {
		if ( false === array_key_exists( $attribute, $this->attributes ) ) {
			return $default;
		}

		return $this->attributes[ $attribute ];
	}

	/**
	 * {@inheritdoc}
	 */
	public function withAttribute( $attribute, $value ) {
		$new                           = clone $this;
		$new->attributes[ $attribute ] = $value;

		return $new;
	}

	/**
	 * {@inheritdoc}
	 */
	public function withoutAttribute( $attribute ) {
		if ( false === array_key_exists( $attribute, $this->attributes ) ) {
			return $this;
		}

		$new = clone $this;
		unset( $new->attributes[ $attribute ] );

		return $new;
	}
}
