<?php


namespace Mnv\Http;

use ArrayAccess;
use Mnv\Core\Collections\Arr;
use Mnv\Core\Collections\Contracts\Arrayable;
use Mnv\Core\Collections\Str;
use Mnv\Http\Concerns\InteractsWithContentTypes;
use Mnv\Http\Concerns\InteractsWithFlashData;
use Mnv\Http\Concerns\InteractsWithInput;
use Mnv\Core\Collections\Traits\Macroable;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

/**
 * Class Request
 * @package Mnv\Core\Http
 */
class Request extends SymfonyRequest  implements Arrayable, ArrayAccess
{
    use InteractsWithContentTypes,
        InteractsWithFlashData,
        InteractsWithInput,
        Macroable;
    /**
     *Декодированное содержимое JSON для запроса.
     *
     * @var \Symfony\Component\HttpFoundation\ParameterBag|null
     */
    protected $json;

    /**
     * Все преобразованные файлы для запроса.
     *
     * @var array
     */
    protected $convertedFiles;

    /**
     * Обратный вызов пользовательского преобразователя.
     *
     * @var \Closure
     */
    protected $userResolver;

    /**
     * Обратный вызов преобразователя маршрута.
     *
     * @var \Closure
     */
    protected $routeResolver;

    /**
     * Создайте новый HTTP-запрос MNV из переменных сервера.
     *
     * @return static
     */
    public static function capture()
    {
        static::enableHttpMethodParameterOverride();

        return static::createFromBase(SymfonyRequest::createFromGlobals());
    }

    /**
     * Вернем экземпляр запроса.
     *
     * @return $this
     */
    public function instance()
    {
        return $this;
    }

    /**
     * Получить метод запроса.
     *
     * @return string
     */
    public function method()
    {
        return $this->getMethod();
    }

    /**
     * Получить корневой URL-адрес приложения.
     *
     * @return string
     */
    public function root()
    {
        return rtrim($this->getSchemeAndHttpHost().$this->getBaseUrl(), '/');
    }

    /**
     * Получить URL-адрес (без строки запроса) для запроса.
     *
     * @return string
     */
    public function url()
    {
        return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/');
    }

    /**
     * Получить полный URL-адрес для запроса.
     *
     * @return string
     */
    public function fullUrl()
    {
        $query = $this->getQueryString();

        $question = $this->getBaseUrl().$this->getPathInfo() === '/' ? '/?' : '?';

        return $query ? $this->url().$question.$query : $this->url();
    }

    /**
     * Получить полный URL-адрес запроса с добавленными параметрами строки запроса.
     *
     * @param  array  $query
     * @return string
     */
    public function fullUrlWithQuery(array $query)
    {
        $question = $this->getBaseUrl().$this->getPathInfo() === '/' ? '/?' : '?';

        return count($this->query()) > 0
            ? $this->url().$question.Arr::query(array_merge($this->query(), $query))
            : $this->fullUrl().$question.Arr::query($query);
    }

    /**
     * Получить информацию о текущем пути для запроса.
     *
     * @return string
     */
    public function path()
    {
        $pattern = trim($this->getPathInfo(), '/');

        return $pattern == '' ? '/' : $pattern;
    }

    /**
     * Получить текущую информацию о декодированном пути для запроса.
     *
     * @return string
     */
    public function decodedPath()
    {
        return rawurldecode($this->path());
    }

    /**
     * Получить сегмент из URI (индекс на основе 1).
     *
     * @param  int  $index
     * @param  string|null  $default
     * @return string|null
     */
    public function segment($index, $default = null)
    {
        return Arr::get($this->segments(), $index - 1, $default);
    }

    /**
     * Получить все сегменты для пути запроса.
     *
     * @return array
     */
    public function segments()
    {
        $segments = explode('/', $this->decodedPath());

        return array_values(array_filter($segments, function ($value) {
            return $value !== '';
        }));
    }

    /**
     * Определить, соответствует ли текущий URI запроса шаблону.
     *
     * @param  mixed  ...$patterns
     * @return bool
     */
    public function is(...$patterns)
    {
        $path = $this->decodedPath();

        foreach ($patterns as $pattern) {
            if (Str::is($pattern, $path)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Определите, соответствует ли название маршрута заданному шаблону.
     *
     * @param  mixed  ...$patterns
     * @return bool
     */
    public function routeIs(...$patterns)
    {
        return $this->route() && $this->route()->named(...$patterns);
    }

    /**
     * Определите, соответствует ли текущий URL-адрес запроса и строка запроса шаблону.
     *
     * @param  mixed  ...$patterns
     * @return bool
     */
    public function fullUrlIs(...$patterns)
    {
        $url = $this->fullUrl();

        foreach ($patterns as $pattern) {
            if (Str::is($pattern, $url)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Определите, является ли запрос результатом вызова AJAX.
     *
     * @return bool
     */
    public function ajax()
    {
        return $this->isXmlHttpRequest();
    }

    /**
     * Определите, является ли запрос результатом вызова PJAX.
     *
     * @return bool
     */
    public function pjax()
    {
        return $this->headers->get('X-PJAX') == true;
    }

    /**
     * Определите, является ли запрос результатом вызова предварительной выборки.
     *
     * @return bool
     */
    public function prefetch()
    {
        return strcasecmp($this->server->get('HTTP_X_MOZ'), 'prefetch') === 0 ||
            strcasecmp($this->headers->get('Purpose'), 'prefetch') === 0;
    }

    /**
     * Определите, передается ли запрос по протоколу HTTPS.
     *
     * @return bool
     */
    public function secure()
    {
        return $this->isSecure();
    }

    /**
     * Получите IP-адрес клиента.
     *
     * @return string|null
     */
    public function ip()
    {
        return $this->getClientIp();
    }

    /**
     * Получите IP-адреса клиентов.
     *
     * @return array
     */
    public function ips()
    {
        return $this->getClientIps();
    }

    /**
     * Получите клиентский пользовательский агент.
     *
     * @return string|null
     */
    public function userAgent()
    {
        return $this->headers->get('User-Agent');
    }

    /**
     * Объедините новые входные данные с входным массивом текущего запроса.
     *
     * @param  array  $input
     * @return $this
     */
    public function merge(array $input)
    {
        $this->getInputSource()->add($input);

        return $this;
    }

    /**
     * Замените входные данные для текущего запроса.
     *
     * @param  array  $input
     * @return $this
     */
    public function replace(array $input)
    {
        $this->getInputSource()->replace($input);

        return $this;
    }

    /**
     * Этот метод принадлежит Symfony HttpFoundation и обычно не требуется при использовании.
     *
     * Вместо этого вы можете использовать метод "input".
     *
     * @param  string  $key
     * @param  mixed  $default
     * @return mixed
     */
    public function get(string $key, $default = null)
    {
        return parent::get($key, $default);
    }

    /**
     * Получите полезную нагрузку JSON для запроса.
     *
     * @param  string|null  $key
     * @param  mixed  $default
     * @return \Symfony\Component\HttpFoundation\ParameterBag|mixed
     */
    public function json($key = null, $default = null)
    {
        if (! isset($this->json)) {
            $this->json = new ParameterBag((array) json_decode($this->getContent(), true));
        }

        if (is_null($key)) {
            return $this->json;
        }

        return data_get($this->json->all(), $key, $default);
    }

    /**
     * Получите источник входных данных для запроса.
     *
     * @return \Symfony\Component\HttpFoundation\ParameterBag
     */
    protected function getInputSource()
    {
        if ($this->isJson()) {
            return $this->json();
        }

        return in_array($this->getRealMethod(), ['GET', 'HEAD']) ? $this->query : $this->request;
    }

    /**
     * Создайте новый экземпляр запроса на основе данного запроса.
     *
     * @param  \Mnv\Http\Request  $from
     * @param  \Mnv\Http\Request|null  $to
     * @return static
     */
    public static function createFrom(self $from, $to = null)
    {
        $request = $to ?: new static;

        $files = $from->files->all();

        $files = is_array($files) ? array_filter($files) : $files;

        $request->initialize(
            $from->query->all(),
            $from->request->all(),
            $from->attributes->all(),
            $from->cookies->all(),
            $files,
            $from->server->all(),
            $from->getContent()
        );

        $request->headers->replace($from->headers->all());

        $request->setJson($from->json());

        if ($session = $from->getSession()) {
            $request->setSession($session);
        }

        $request->setUserResolver($from->getUserResolver());

        $request->setRouteResolver($from->getRouteResolver());

        return $request;
    }

    /**
     *Создайте запрос MNV из экземпляра Symfony.
     *
     * @param  \Symfony\Component\HttpFoundation\Request  $request
     * @return static
     */
    public static function createFromBase(SymfonyRequest $request)
    {
        $newRequest = (new static)->duplicate(
            $request->query->all(), $request->request->all(), $request->attributes->all(),
            $request->cookies->all(), $request->files->all(), $request->server->all()
        );

        $newRequest->headers->replace($request->headers->all());

        $newRequest->content = $request->content;

        $newRequest->request = $newRequest->getInputSource();

        return $newRequest;
    }

    /**
     * {@inheritdoc}
     */
    public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
    {
        return parent::duplicate($query, $request, $attributes, $cookies, $this->filterFiles($files), $server);
    }

    /**
     * Отфильтруйте данный массив файлов, удалив все пустые значения.
     *
     * @param  mixed  $files
     * @return mixed
     */
    protected function filterFiles($files)
    {
        if (! $files) {
            return;
        }

        foreach ($files as $key => $file) {
            if (is_array($file)) {
                $files[$key] = $this->filterFiles($files[$key]);
            }

            if (empty($files[$key])) {
                unset($files[$key]);
            }
        }

        return $files;
    }

    /**
     * Получите `session`, связанный с запросом.
     *
     * @return \Mnv\Core\Collections\Store
     *
     * @throws \RuntimeException
     */
    public function session()
    {
        if (! $this->hasSession()) {
            throw new \RuntimeException('Session store not set on request.');
        }

        return $this->session;
    }

    /**
     * Получите `session`, связанный с запросом.
     *
     * @return \Mnv\Core\Collections\Store|null
     */
    public function getSession(): SessionInterface
    {
        return $this->session;
    }

    /**
     *Установите экземпляр `session` в запросе.
     *
     * @param  \Mnv\Core\Collections\Contracts\Session\Session  $session
     * @return void
     */
    public function setSession($session)
    {
        $this->session = $session;
    }

    /**
     * Получите пользователя, отправляющего запрос.
     *
     * @param  string|null  $guard
     * @return mixed
     */
    public function user($guard = null)
    {
        return call_user_func($this->getUserResolver(), $guard);
    }

    /**
     * Получите маршрут, обрабатывающий запрос.
     *
     * @param string|null $param
     * @param null $default
     * @return false|mixed
     */
    public function route($param = null, $default = null)
    {
        $route = call_user_func($this->getRouteResolver());

        if (is_null($route) || is_null($param)) {
            return $route;
        }

        return $route->parameter($param, $default);
    }

    /**
     * Get a unique fingerprint for the request / route / IP address.
     * Получите уникальный отпечаток пальца для запроса / маршрута / IP-адреса.
     *
     * @return string
     *
     * @throws \RuntimeException
     */
    public function fingerprint()
    {
        if (! $route = $this->route()) {
            throw new \RuntimeException('Unable to generate fingerprint. Route unavailable.');
        }

        return sha1(implode('|', array_merge($route->methods(), [$route->getDomain(), $route->uri(), $this->ip()])));
    }

    /**
     * Установите полезную нагрузку JSON для запроса.
     *
     * @param  \Symfony\Component\HttpFoundation\ParameterBag  $json
     * @return $this
     */
    public function setJson($json)
    {
        $this->json = $json;

        return $this;
    }

    /**
     * Получите обратный вызов пользовательского преобразователя.
     *
     * @return \Closure
     */
    public function getUserResolver()
    {
        return $this->userResolver ?: function () {
            //
        };
    }

    /**
     * Установите обратный вызов пользовательского преобразователя.
     *
     * @param  \Closure  $callback
     * @return $this
     */
    public function setUserResolver(\Closure $callback)
    {
        $this->userResolver = $callback;

        return $this;
    }

    /**
     * Получите обратный вызов route resolver.
     *
     * @return \Closure
     */
    public function getRouteResolver()
    {
        return $this->routeResolver ?: function () {
            //
        };
    }

    /**
     * Установить обратный вызов преобразователя маршрута.
     *
     * @param  \Closure  $callback
     * @return $this
     */
    public function setRouteResolver(\Closure $callback)
    {
        $this->routeResolver = $callback;

        return $this;
    }

    /**
     * Получите все входные данные и файлы для запроса.
     *
     * @return array
     */
    public function toArray(): array
    {
        return $this->all();
    }

    /**
     * Определите, существует ли данное смещение.
     *
     * @param  mixed $offset
     * @return bool
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return Arr::has(
            $this->all() + $this->route()->parameters(),
            $offset
        );
    }

    /**
     * Получите значение с заданным смещением.
     *
     * @param  mixed  $offset
     * @return mixed
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return $this->__get($offset);
    }

    /**
     * Установите значение с заданным смещением.
     *
     * @param  string  $offset
     * @param  mixed  $value
     * @return void
     */
    #[\ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        $this->getInputSource()->set($offset, $value);
    }

    /**
     * Удалите значение с заданным смещением.
     *
     * @param  string  $offset
     * @return void
     */
    #[\ReturnTypeWillChange]
    public function offsetUnset($offset)
    {
        $this->getInputSource()->remove($offset);
    }

    /**
     * Проверьте, установлен ли элемент ввода в запросе.
     *
     * @param  string  $key
     * @return bool
     */
    public function __isset($key)
    {
        return ! is_null($this->__get($key));
    }

    /**
     * Получите входной элемент из запроса.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return Arr::get($this->all(), $key, function () use ($key) {
            return $this->route($key);
        });
    }
}