<?php

declare(strict_types=1);

namespace Sabre\HTTP;

use LogicException;
use Sabre\Uri;

/**
 * The Request class represents a single HTTP request.
 *
 * You can either simply construct the object from scratch, or if you need
 * access to the current HTTP request, use Sapi::getRequest.
 *
 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
 * @author Evert Pot (http://evertpot.com/)
 * @license http://sabre.io/license/ Modified BSD License
 */
class Request extends Message implements RequestInterface
{
    /**
     * HTTP Method.
     *
     * @var string
     */
    protected $method;

    /**
     * Request Url.
     *
     * @var string
     */
    protected $url;

    /**
     * Creates the request object.
     *
     * @param resource|callable|string $body
     */
    public function __construct(string $method, string $url, array $headers = [], $body = null)
    {
        $this->setMethod($method);
        $this->setUrl($url);
        $this->setHeaders($headers);
        $this->setBody($body);
    }

    /**
     * Returns the current HTTP method.
     */
    public function getMethod(): string
    {
        return $this->method;
    }

    /**
     * Sets the HTTP method.
     */
    public function setMethod(string $method)
    {
        $this->method = $method;
    }

    /**
     * Returns the request url.
     */
    public function getUrl(): string
    {
        return $this->url;
    }

    /**
     * Sets the request url.
     */
    public function setUrl(string $url)
    {
        $this->url = $url;
    }

    /**
     * Returns the list of query parameters.
     *
     * This is equivalent to PHP's $_GET superglobal.
     */
    public function getQueryParameters(): array
    {
        $url = $this->getUrl();
        if (false === ($index = strpos($url, '?'))) {
            return [];
        }

        parse_str(substr($url, $index + 1), $queryParams);

        return $queryParams;
    }

    protected $absoluteUrl;

    /**
     * Sets the absolute url.
     */
    public function setAbsoluteUrl(string $url)
    {
        $this->absoluteUrl = $url;
    }

    /**
     * Returns the absolute url.
     */
    public function getAbsoluteUrl(): string
    {
        if (!$this->absoluteUrl) {
            // Guessing we're a http endpoint.
            $this->absoluteUrl = 'http://'.
                ($this->getHeader('Host') ?? 'localhost').
                $this->getUrl();
        }

        return $this->absoluteUrl;
    }

    /**
     * Base url.
     *
     * @var string
     */
    protected $baseUrl = '/';

    /**
     * Sets a base url.
     *
     * This url is used for relative path calculations.
     */
    public function setBaseUrl(string $url)
    {
        $this->baseUrl = $url;
    }

    /**
     * Returns the current base url.
     */
    public function getBaseUrl(): string
    {
        return $this->baseUrl;
    }

    /**
     * Returns the relative path.
     *
     * This is being calculated using the base url. This path will not start
     * with a slash, so it will always return something like
     * 'example/path.html'.
     *
     * If the full path is equal to the base url, this method will return an
     * empty string.
     *
     * This method will also urldecode the path, and if the url was incoded as
     * ISO-8859-1, it will convert it to UTF-8.
     *
     * If the path is outside of the base url, a LogicException will be thrown.
     */
    public function getPath(): string
    {
        // Removing duplicated slashes.
        $uri = str_replace('//', '/', $this->getUrl());

        $uri = Uri\normalize($uri);
        $baseUri = Uri\normalize($this->getBaseUrl());

        if (0 === strpos($uri, $baseUri)) {
            // We're not interested in the query part (everything after the ?).
            list($uri) = explode('?', $uri);

            return trim(decodePath(substr($uri, strlen($baseUri))), '/');
        }

        if ($uri.'/' === $baseUri) {
            return '';
        }
        // A special case, if the baseUri was accessed without a trailing
        // slash, we'll accept it as well.

        throw new \LogicException('Requested uri ('.$this->getUrl().') is out of base uri ('.$this->getBaseUrl().')');
    }

    /**
     * Equivalent of PHP's $_POST.
     *
     * @var array
     */
    protected $postData = [];

    /**
     * Sets the post data.
     *
     * This is equivalent to PHP's $_POST superglobal.
     *
     * This would not have been needed, if POST data was accessible as
     * php://input, but unfortunately we need to special case it.
     */
    public function setPostData(array $postData)
    {
        $this->postData = $postData;
    }

    /**
     * Returns the POST data.
     *
     * This is equivalent to PHP's $_POST superglobal.
     */
    public function getPostData(): array
    {
        return $this->postData;
    }

    /**
     * An array containing the raw _SERVER array.
     *
     * @var array
     */
    protected $rawServerData;

    /**
     * Returns an item from the _SERVER array.
     *
     * If the value does not exist in the array, null is returned.
     *
     * @return string|null
     */
    public function getRawServerValue(string $valueName)
    {
        return $this->rawServerData[$valueName] ?? null;
    }

    /**
     * Sets the _SERVER array.
     */
    public function setRawServerData(array $data)
    {
        $this->rawServerData = $data;
    }

    /**
     * Serializes the request object as a string.
     *
     * This is useful for debugging purposes.
     */
    public function __toString(): string
    {
        $out = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getHttpVersion()."\r\n";

        foreach ($this->getHeaders() as $key => $value) {
            foreach ($value as $v) {
                if ('Authorization' === $key) {
                    list($v) = explode(' ', $v, 2);
                    $v .= ' REDACTED';
                }
                $out .= $key.': '.$v."\r\n";
            }
        }
        $out .= "\r\n";
        $out .= $this->getBodyAsString();

        return $out;
    }
}
