<?php

/**
 * Bluz Framework Component
 *
 * @copyright Bluz PHP Team
 * @link      https://github.com/bluzphp/framework
 */

declare(strict_types=1);

namespace Bluz\Proxy;

use Bluz\Common\Exception\ComponentException;
use Bluz\Http\RequestMethod;
use Psr\Http\Message\UriInterface;
use Laminas\Diactoros\ServerRequest as Instance;
use Laminas\Diactoros\UploadedFile;

/**
 * Proxy to Request
 *
 * Example of usage
 * <code>
 *     use Bluz\Proxy\Request;
 *
 *     Request::getParam('foo');
 * </code>
 *
 * @package  Bluz\Proxy
 * @author   Anton Shevchuk
 *
 * @todo     Proxy class should be clean
 *
 * @method   static Instance getInstance()
 *
 * @method   static UriInterface getUri()
 * @see      \Laminas\Diactoros\RequestTrait::getUri()
 */
final class Request
{
    use ProxyTrait;

    /**
     * @const string HTTP content types
     */
    public const TYPE_ANY = '*/*';
    public const TYPE_HTML = 'text/html';
    public const TYPE_JSON = 'application/json';

    /**
     * @var array|null Accepted type
     */
    private static $accept;

    /**
     * @var array|null Accepted languages
     */
    private static $language;

    /**
     * Init instance
     *
     * @throws ComponentException
     */
    private static function initInstance()
    {
        throw new ComponentException('Class `Proxy\\Request` required external initialization');
    }

    /**
     * Retrieve a member of the $_GET super global
     *
     * If no $key is passed, returns the entire $_GET array.
     *
     * @param string|null $key
     * @param string|null $default Default value to use if key not found
     *
     * @return string|array|null Returns null if key does not exist
     */
    public static function getQuery(?string $key = null, ?string $default = null)
    {
        return self::getInstance()->getQueryParams()[$key] ?? $default;
    }

    /**
     * Retrieve a member of the $_POST super global
     *
     * If no $key is passed, returns the entire $_POST array.
     *
     * @param string|null $key
     * @param string|null $default Default value to use if key not found
     *
     * @return string|array|null Returns null if key does not exist
     */
    public static function getPost(?string $key = null, ?string $default = null)
    {
        return self::getInstance()->getParsedBody()[$key] ?? $default;
    }

    /**
     * Retrieve a member of the $_SERVER super global
     *
     * If no $key is passed, returns the entire $_SERVER array.
     *
     * @param string|null $key
     * @param string|null $default Default value to use if key not found
     *
     * @return string Returns null if key does not exist
     */
    public static function getServer(?string $key = null, ?string $default = null)
    {
        return self::getInstance()->getServerParams()[$key] ?? $default;
    }

    /**
     * Retrieve a member of the $_COOKIE super global
     *
     * If no $key is passed, returns the entire $_COOKIE array.
     *
     * @param string|null $key
     * @param string|null $default Default value to use if key not found
     *
     * @return string Returns null if key does not exist
     */
    public static function getCookie(?string $key = null, ?string $default = null)
    {
        return self::getInstance()->getCookieParams()[$key] ?? $default;
    }

    /**
     * Retrieve a member of the $_ENV super global
     *
     * If no $key is passed, returns the entire $_ENV array.
     *
     * @param string|null $key
     * @param string|null $default Default value to use if key not found
     *
     * @return string Returns null if key does not exist
     */
    public static function getEnv(?string $key = null, ?string $default = null)
    {
        return $_ENV[$key] ?? $default;
    }

    /**
     * Search for a header value
     *
     * @param string $header
     * @param mixed  $default
     *
     * @return string
     */
    public static function getHeader(string $header, $default = null)
    {
        $header  = strtolower($header);
        $headers = self::getInstance()->getHeaders();
        $headers = array_change_key_case($headers, CASE_LOWER);
        if (array_key_exists($header, $headers)) {
            $value = is_array($headers[$header]) ? implode(', ', $headers[$header]) : $headers[$header];
            return $value;
        }
        return $default;
    }

    /**
     * Access values contained in the superglobals as public members
     * Order of precedence: 1. GET, 2. POST
     *
     * @param string $key
     * @param null   $default
     *
     * @return string|array|null
     * @link http://msdn.microsoft.com/en-us/library/system.web.httprequest.item.aspx
     */
    public static function getParam(string $key, $default = null)
    {
        return
            self::getQuery($key) ??
            self::getPost($key) ??
            $default;
    }

    /**
     * Get all params from GET and POST or PUT
     *
     * @return array
     */
    public static function getParams(): array
    {
        $body = (array)self::getInstance()->getParsedBody();
        $query = (array)self::getInstance()->getQueryParams();
        return array_merge([], $body, $query);
    }

    /**
     * Get uploaded file
     *
     * @param string $name
     *
     * @return UploadedFile
     */
    public static function getFile(string $name)
    {
        return self::getInstance()->getUploadedFiles()[$name] ?? false;
    }

    /**
     * Get the client's IP address
     *
     * @param bool $checkProxy
     *
     * @return string
     */
    public static function getClientIp(bool $checkProxy = true)
    {
        $result = null;
        if ($checkProxy) {
            $result = self::getServer('HTTP_CLIENT_IP') ?? self::getServer('HTTP_X_FORWARDED_FOR') ?? null;
        }
        return $result ?? self::getServer('REMOTE_ADDR');
    }

    /**
     * Get module
     *
     * @return string
     */
    public static function getModule(): string
    {
        return self::getParam('_module', Router::getDefaultModule());
    }

    /**
     * Get controller
     *
     * @return string
     */
    public static function getController(): string
    {
        return self::getParam('_controller', Router::getDefaultController());
    }

    /**
     * Get method
     *
     * @return string
     */
    public static function getMethod(): string
    {
        return self::getParam('_method', self::getInstance()->getMethod());
    }

    /**
     * Get Accept MIME Type
     *
     * @return array
     */
    public static function getAccept(): array
    {
        if (!self::$accept) {
            // get header from request
            self::$accept = self::parseAcceptHeader(self::getHeader('Accept'));
        }
        return self::$accept;
    }

    /**
     * Get Accept MIME Type
     *
     * @return array
     */
    public static function getAcceptLanguage(): array
    {
        if (!self::$language) {
            // get header from request
            self::$language = self::parseAcceptHeader(self::getHeader('Accept-Language'));
        }
        return self::$language;
    }

    /**
     * parseAcceptHeader
     *
     * @param string|null $header
     *
     * @return array
     */
    private static function parseAcceptHeader(?string $header): array
    {
        // empty array
        $accept = [];

        // check empty
        if (!$header || $header === '') {
            return $accept;
        }

        // make array from header
        $values = explode(',', $header);
        $values = array_map('trim', $values);

        foreach ($values as $a) {
            // the default quality is 1.
            $q = 1;
            // check if there is a different quality
            if (strpos($a, ';q=') || strpos($a, '; q=')) {
                // divide "mime/type;q=X" into two parts: "mime/type" i "X"
                [$a, $q] = preg_split('/;([ ]?)q=/', $a);
            }
            // remove other extension
            if (strpos($a, ';')) {
                $a = substr($a, 0, strpos($a, ';'));
            }

            // mime-type $a is accepted with the quality $q
            // WARNING: $q == 0 means, that isn’t supported!
            $accept[$a] = (float)$q;
        }
        arsort($accept);
        return $accept;
    }

    /**
     * Reset accept for tests
     *
     * @return void
     */
    public static function resetAccept(): void
    {
        self::$accept = null;
    }

    /**
     * Check Accept header
     *
     * @param array $allowTypes
     *
     * @return string|false
     */
    public static function checkAccept(array $allowTypes = [])
    {
        $accept = self::getAccept();

        // if no parameter was passed, just return first mime type from parsed data
        if (empty($allowTypes)) {
            return current(array_keys($accept));
        }

        $allowTypes = array_map('strtolower', $allowTypes);

        // let’s check our supported types:
        foreach ($accept as $mime => $quality) {
            if ($quality && in_array($mime, $allowTypes, true)) {
                return $mime;
            }
        }
        // no mime-type found
        return false;
    }

    /**
     * Check CLI
     *
     * @return bool
     */
    public static function isCli(): bool
    {
        return (PHP_SAPI === 'cli');
    }

    /**
     * Check HTTP
     *
     * @return bool
     */
    public static function isHttp(): bool
    {
        return (PHP_SAPI !== 'cli');
    }

    /**
     * Is this a GET method request?
     *
     * @return bool
     */
    public static function isGet(): bool
    {
        return (self::getInstance()->getMethod() === RequestMethod::GET);
    }

    /**
     * Is this a POST method request?
     *
     * @return bool
     */
    public static function isPost(): bool
    {
        return (self::getInstance()->getMethod() === RequestMethod::POST);
    }

    /**
     * Is this a PUT method request?
     *
     * @return bool
     */
    public static function isPut(): bool
    {
        return (self::getInstance()->getMethod() === RequestMethod::PUT);
    }

    /**
     * Is this a DELETE method request?
     *
     * @return bool
     */
    public static function isDelete(): bool
    {
        return (self::getInstance()->getMethod() === RequestMethod::DELETE);
    }

    /**
     * Is the request a Javascript XMLHttpRequest?
     *
     * @return bool
     */
    public static function isXmlHttpRequest(): bool
    {
        return (self::getHeader('X-Requested-With') === 'XMLHttpRequest');
    }
}
