<?php

namespace Yurun\Util;

use Yurun\Util\YurunHttp\Attributes;
use Yurun\Util\YurunHttp\Http\Psr7\Consts\MediaType;
use Yurun\Util\YurunHttp\Http\Psr7\UploadedFile;
use Yurun\Util\YurunHttp\Http\Request;

class HttpRequest
{
    /**
     * 处理器.
     *
     * @var \Yurun\Util\YurunHttp\Handler\IHandler|null
     */
    private $handler;

    /**
     * 需要请求的Url地址
     *
     * @var string
     */
    public $url;

    /**
     * 发送内容，可以是字符串、数组（支持键值、Yurun\Util\YurunHttp\Http\Psr7\UploadedFile，其中键值会作为html编码，文件则是上传）.
     *
     * @var string|object|array
     */
    public $content;

    /**
     * `curl_setopt_array()`所需要的第二个参数.
     *
     * @var array
     */
    public $options = [];

    /**
     * 请求头.
     *
     * @var array
     */
    public $headers = [];

    /**
     * Cookies.
     *
     * @var array
     */
    public $cookies = [];

    /**
     * 失败重试次数，默认为0.
     *
     * @var int
     */
    public $retry = 0;

    /**
     * 重试回调.
     *
     * @var callable|null
     */
    public $retryCallback;

    /**
     * 是否使用代理，默认false.
     *
     * @var bool
     */
    public $useProxy = false;

    /**
     * 代理设置.
     *
     * @var array
     */
    public $proxy = [];

    /**
     * 是否验证证书.
     *
     * @var bool
     */
    public $isVerifyCA = false;

    /**
     * CA根证书路径.
     *
     * @var string|null
     */
    public $caCert;

    /**
     * 连接超时时间，单位：毫秒.
     *
     * @var int
     */
    public $connectTimeout = 30000;

    /**
     * 总超时时间，单位：毫秒.
     *
     * @var int
     */
    public $timeout = 30000;

    /**
     * 下载限速，为0则不限制，单位：字节
     *
     * @var int|null
     */
    public $downloadSpeed;

    /**
     * 上传限速，为0则不限制，单位：字节
     *
     * @var int|null
     */
    public $uploadSpeed;

    /**
     * 用于连接中需要的用户名.
     *
     * @var string|null
     */
    public $username;

    /**
     * 用于连接中需要的密码
     *
     * @var string|null
     */
    public $password;

    /**
     * 请求结果保存至文件的配置.
     *
     * @var mixed
     */
    public $saveFileOption = [];

    /**
     * 是否启用重定向.
     *
     * @var bool
     */
    public $followLocation = true;

    /**
     * 最大重定向次数.
     *
     * @var int
     */
    public $maxRedirects = 10;

    /**
     * 证书类型
     * 支持的格式有"PEM" (默认值), "DER"和"ENG".
     *
     * @var string
     */
    public $certType = 'pem';

    /**
     * 一个包含 PEM 格式证书的文件名.
     *
     * @var string
     */
    public $certPath = '';
    /**
     * 使用证书需要的密码
     *
     * @var string
     */
    public $certPassword;

    /**
     * certType规定的私钥的加密类型，支持的密钥类型为"PEM"(默认值)、"DER"和"ENG".
     *
     * @var string
     */
    public $keyType = 'pem';

    /**
     * 包含 SSL 私钥的文件名.
     *
     * @var string
     */
    public $keyPath = '';

    /**
     * SSL私钥的密码
     *
     * @var string
     */
    public $keyPassword;

    /**
     * 请求方法.
     *
     * @var string
     */
    public $method = 'GET';

    /**
     * Http 协议版本.
     *
     * @var string
     */
    public $protocolVersion = '1.1';

    /**
     * 是否启用连接池，默认为 null 时取全局设置.
     *
     * @var bool|null
     */
    public $connectionPool;

    /**
     * 是否启用 WebSocket 压缩.
     *
     * @var bool
     */
    public $websocketCompression = false;

    /**
     * 代理认证方式.
     *
     * @var array
     */
    public static $proxyAuths = [];

    /**
     * 代理类型.
     *
     * @var array
     */
    public static $proxyType = [];

    /**
     * 自动扩展名标志.
     */
    const AUTO_EXT_FLAG = '.*';

    /**
     * 自动扩展名用的临时文件名.
     */
    const AUTO_EXT_TEMP_EXT = '.tmp';

    /**
     * 构造方法.
     *
     * @param array $options
     *
     * @return mixed
     */
    public function __construct($options = [])
    {
        $this->open($options);
    }

    /**
     * 析构方法.
     */
    public function __destruct()
    {
        $this->close();
    }

    /**
     * 打开一个新连接，初始化所有参数。一般不需要手动调用。
     *
     * @param array $options
     *
     * @return void
     */
    public function open($options = [])
    {
        $this->handler = YurunHttp::getHandler($options);
        $this->retry = 0;
        $this->retryCallback = null;
        $this->headers = $this->options = [];
        $this->url = $this->content = '';
        $this->useProxy = false;
        $this->proxy = [
            'auth'    => 'basic',
            'type'    => 'http',
        ];
        $this->isVerifyCA = false;
        $this->caCert = null;
        $this->connectTimeout = 30000;
        $this->timeout = 30000;
        $this->downloadSpeed = null;
        $this->uploadSpeed = null;
        $this->username = null;
        $this->password = null;
        $this->saveFileOption = [];
    }

    /**
     * 关闭连接。一般不需要手动调用。
     *
     * @return void
     */
    public function close()
    {
        if ($this->handler)
        {
            $handler = $this->handler;
            $this->handler = null;
            $handler->close();
        }
    }

    /**
     * 创建一个新会话，等同于new.
     *
     * @return static
     */
    public static function newSession()
    {
        return new static();
    }

    /**
     * 获取处理器.
     *
     * @return \Yurun\Util\YurunHttp\Handler\IHandler|null
     */
    public function getHandler()
    {
        return $this->handler;
    }

    /**
     * 设置请求地址
     *
     * @param string $url 请求地址
     *
     * @return static
     */
    public function url($url)
    {
        $this->url = $url;

        return $this;
    }

    /**
     * 设置发送内容，requestBody的别名.
     *
     * @param string|object|array $content 发送内容，可以是字符串、数组
     *
     * @return static
     */
    public function content($content)
    {
        return $this->requestBody($content);
    }

    /**
     * 设置参数，requestBody的别名.
     *
     * @param mixed $params 发送内容，可以是字符串、数组
     *
     * @return static
     */
    public function params($params)
    {
        return $this->requestBody($params);
    }

    /**
     * 设置请求主体.
     *
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组
     *
     * @return static
     */
    public function requestBody($requestBody)
    {
        $this->content = $requestBody;

        return $this;
    }

    /**
     * 批量设置CURL的Option.
     *
     * @param array $options curl_setopt_array()所需要的第二个参数
     *
     * @return static
     */
    public function options($options)
    {
        $thisOptions = &$this->options;
        foreach ($options as $key => $value)
        {
            $thisOptions[$key] = $value;
        }

        return $this;
    }

    /**
     * 设置CURL的Option.
     *
     * @param int   $option 需要设置的CURLOPT_XXX选项
     * @param mixed $value  值
     *
     * @return static
     */
    public function option($option, $value)
    {
        $this->options[$option] = $value;

        return $this;
    }

    /**
     * 批量设置请求头.
     *
     * @param array $headers 键值数组
     *
     * @return static
     */
    public function headers($headers)
    {
        $thisHeaders = &$this->headers;
        $thisHeaders = array_merge($thisHeaders, $headers);

        return $this;
    }

    /**
     * 设置请求头.
     *
     * @param string $header 请求头名称
     * @param string $value  值
     *
     * @return static
     */
    public function header($header, $value)
    {
        $this->headers[$header] = $value;

        return $this;
    }

    /**
     * 批量设置请求头，.
     *
     * @param array $headers 纯文本 header 数组
     *
     * @return static
     */
    public function rawHeaders($headers)
    {
        $thisHeaders = &$this->headers;
        foreach ($headers as $header)
        {
            $list = explode(':', $header, 2);
            $thisHeaders[trim($list[0])] = trim($list[1]);
        }

        return $this;
    }

    /**
     * 设置请求头.
     *
     * @param string $header 纯文本 header
     *
     * @return static
     */
    public function rawHeader($header)
    {
        $list = explode(':', $header, 2);
        $this->headers[trim($list[0])] = trim($list[1]);

        return $this;
    }

    /**
     * 设置Accept.
     *
     * @param string $accept
     *
     * @return static
     */
    public function accept($accept)
    {
        $this->headers['Accept'] = $accept;

        return $this;
    }

    /**
     * 设置Accept-Language.
     *
     * @param string $acceptLanguage
     *
     * @return static
     */
    public function acceptLanguage($acceptLanguage)
    {
        $this->headers['Accept-Language'] = $acceptLanguage;

        return $this;
    }

    /**
     * 设置Accept-Encoding.
     *
     * @param string $acceptEncoding
     *
     * @return static
     */
    public function acceptEncoding($acceptEncoding)
    {
        $this->headers['Accept-Encoding'] = $acceptEncoding;

        return $this;
    }

    /**
     * 设置Accept-Ranges.
     *
     * @param string $acceptRanges
     *
     * @return static
     */
    public function acceptRanges($acceptRanges)
    {
        $this->headers['Accept-Ranges'] = $acceptRanges;

        return $this;
    }

    /**
     * 设置Cache-Control.
     *
     * @param string $cacheControl
     *
     * @return static
     */
    public function cacheControl($cacheControl)
    {
        $this->headers['Cache-Control'] = $cacheControl;

        return $this;
    }

    /**
     * 批量设置Cookies.
     *
     * @param array $cookies 键值对应数组
     *
     * @return static
     */
    public function cookies($cookies)
    {
        $this->cookies = array_merge($this->cookies, $cookies);

        return $this;
    }

    /**
     * 设置Cookie.
     *
     * @param string $name  名称
     * @param string $value 值
     *
     * @return static
     */
    public function cookie($name, $value)
    {
        $this->cookies[$name] = $value;

        return $this;
    }

    /**
     * 设置Content-Type.
     *
     * @param string $contentType
     *
     * @return static
     */
    public function contentType($contentType)
    {
        $this->headers['Content-Type'] = $contentType;

        return $this;
    }

    /**
     * 设置Range.
     *
     * @param string $range
     *
     * @return static
     */
    public function range($range)
    {
        $this->headers['Range'] = $range;

        return $this;
    }

    /**
     * 设置Referer.
     *
     * @param string $referer
     *
     * @return static
     */
    public function referer($referer)
    {
        $this->headers['Referer'] = $referer;

        return $this;
    }

    /**
     * 设置User-Agent.
     *
     * @param string $userAgent
     *
     * @return static
     */
    public function userAgent($userAgent)
    {
        $this->headers['User-Agent'] = $userAgent;

        return $this;
    }

    /**
     * 设置User-Agent，userAgent的别名.
     *
     * @param string $userAgent
     *
     * @return static
     */
    public function ua($userAgent)
    {
        return $this->userAgent($userAgent);
    }

    /**
     * 设置失败重试次数，状态码为5XX或者0才需要重试.
     *
     * @param int           $retry
     * @param callable|null $callback
     *
     * @return static
     */
    public function retry($retry, $callback = null)
    {
        $this->retry = $retry < 0 ? 0 : $retry;   // 至少请求1次，即重试0次
        $this->retryCallback = $callback;

        return $this;
    }

    /**
     * 代理.
     *
     * @param string $server 代理服务器地址
     * @param int    $port   代理服务器端口
     * @param string $type   代理类型，支持：http、socks4、socks4a、socks5
     * @param string $auth   代理认证方式，支持：basic、ntlm。一般默认basic
     *
     * @return static
     */
    public function proxy($server, $port, $type = 'http', $auth = 'basic')
    {
        $this->useProxy = true;
        $this->proxy = [
            'server'    => $server,
            'port'      => $port,
            'type'      => $type,
            'auth'      => $auth,
        ];

        return $this;
    }

    /**
     * 代理认证
     *
     * @param string $username
     * @param string $password
     *
     * @return static
     */
    public function proxyAuth($username, $password)
    {
        $this->proxy['username'] = $username;
        $this->proxy['password'] = $password;

        return $this;
    }

    /**
     * 设置超时时间.
     *
     * @param int $timeout        总超时时间，单位：毫秒
     * @param int $connectTimeout 连接超时时间，单位：毫秒
     *
     * @return static
     */
    public function timeout($timeout = null, $connectTimeout = null)
    {
        if (null !== $timeout)
        {
            $this->timeout = $timeout;
        }
        if (null !== $connectTimeout)
        {
            $this->connectTimeout = $connectTimeout;
        }

        return $this;
    }

    /**
     * 限速
     *
     * @param int $download 下载速度，为0则不限制，单位：字节
     * @param int $upload   上传速度，为0则不限制，单位：字节
     *
     * @return static
     */
    public function limitRate($download = 0, $upload = 0)
    {
        $this->downloadSpeed = $download;
        $this->uploadSpeed = $upload;

        return $this;
    }

    /**
     * 设置用于连接中需要的用户名和密码
     *
     * @param string $username 用户名
     * @param string $password 密码
     *
     * @return static
     */
    public function userPwd($username, $password)
    {
        $this->username = $username;
        $this->password = $password;

        return $this;
    }

    /**
     * 保存至文件的设置.
     *
     * @param string $filePath 文件路径
     * @param string $fileMode 文件打开方式，默认w+
     *
     * @return static
     */
    public function saveFile($filePath, $fileMode = 'w+')
    {
        $this->saveFileOption['filePath'] = $filePath;
        $this->saveFileOption['fileMode'] = $fileMode;

        return $this;
    }

    /**
     * 获取文件保存路径.
     *
     * @return string|null
     */
    public function getSavePath()
    {
        $saveFileOption = $this->saveFileOption;

        return isset($saveFileOption['filePath']) ? $saveFileOption['filePath'] : null;
    }

    /**
     * 设置SSL证书.
     *
     * @param string $path     一个包含 PEM 格式证书的文件名
     * @param string $type     证书类型，支持的格式有”PEM”(默认值),“DER”和”ENG”
     * @param string $password 使用证书需要的密码
     *
     * @return static
     */
    public function sslCert($path, $type = null, $password = null)
    {
        $this->certPath = $path;
        if (null !== $type)
        {
            $this->certType = $type;
        }
        if (null !== $password)
        {
            $this->certPassword = $password;
        }

        return $this;
    }

    /**
     * 设置SSL私钥.
     *
     * @param string $path     包含 SSL 私钥的文件名
     * @param string $type     certType规定的私钥的加密类型，支持的密钥类型为”PEM”(默认值)、”DER”和”ENG”
     * @param string $password SSL私钥的密码
     *
     * @return static
     */
    public function sslKey($path, $type = null, $password = null)
    {
        $this->keyPath = $path;
        if (null !== $type)
        {
            $this->keyType = $type;
        }
        if (null !== $password)
        {
            $this->keyPassword = $password;
        }

        return $this;
    }

    /**
     * 设置请求方法.
     *
     * @param string $method
     *
     * @return static
     */
    public function method($method)
    {
        $this->method = $method;

        return $this;
    }

    /**
     * 设置是否启用 WebSocket 压缩.
     *
     * @return static
     */
    public function websocketCompression(bool $websocketCompression): self
    {
        $this->websocketCompression = $websocketCompression;

        return $this;
    }

    /**
     * 设置是否启用连接池.
     *
     * @param bool $connectionPool
     *
     * @return static
     */
    public function connectionPool($connectionPool)
    {
        $this->connectionPool = $connectionPool;

        return $this;
    }

    /**
     * 处理请求主体.
     *
     * @param string|object|array $requestBody
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return array
     */
    protected function parseRequestBody($requestBody, $contentType)
    {
        $body = $files = [];
        if (\is_string($requestBody))
        {
            $body = $requestBody;
        }
        elseif (\is_array($requestBody) || \is_object($requestBody))
        {
            switch ($contentType)
            {
                case 'json':
                    $body = json_encode($requestBody);
                    $this->header('Content-Type', MediaType::APPLICATION_JSON);
                    break;
                default:
                    foreach ($requestBody as $k => $v)
                    {
                        if ($v instanceof UploadedFile)
                        {
                            $files[$k] = $v;
                        }
                        else
                        {
                            $body[$k] = $v;
                        }
                    }
                    $body = http_build_query($body, '', '&');
            }
        }
        else
        {
            throw new \InvalidArgumentException('$requestBody only can be string or array');
        }

        return [$body, $files];
    }

    /**
     * 构建请求类.
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string|null         $method      请求方法，GET、POST等
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Request
     */
    public function buildRequest($url = null, $requestBody = null, $method = null, $contentType = null)
    {
        if (null === $url)
        {
            $url = $this->url;
        }
        if (null === $method)
        {
            $method = $this->method;
        }
        list($body, $files) = $this->parseRequestBody(null === $requestBody ? $this->content : $requestBody, $contentType);
        $request = new Request($url, $this->headers, $body, $method);
        $saveFileOption = $this->saveFileOption;
        $request = $request->withUploadedFiles($files)
                            ->withCookieParams($this->cookies)
                            ->withAttribute(Attributes::MAX_REDIRECTS, $this->maxRedirects)
                            ->withAttribute(Attributes::IS_VERIFY_CA, $this->isVerifyCA)
                            ->withAttribute(Attributes::CA_CERT, $this->caCert)
                            ->withAttribute(Attributes::CERT_PATH, $this->certPath)
                            ->withAttribute(Attributes::CERT_PASSWORD, $this->certPassword)
                            ->withAttribute(Attributes::CERT_TYPE, $this->certType)
                            ->withAttribute(Attributes::KEY_PATH, $this->keyPath)
                            ->withAttribute(Attributes::KEY_PASSWORD, $this->keyPassword)
                            ->withAttribute(Attributes::KEY_TYPE, $this->keyType)
                            ->withAttribute(Attributes::OPTIONS, $this->options)
                            ->withAttribute(Attributes::SAVE_FILE_PATH, isset($saveFileOption['filePath']) ? $saveFileOption['filePath'] : null)
                            ->withAttribute(Attributes::USE_PROXY, $this->useProxy)
                            ->withAttribute(Attributes::USERNAME, $this->username)
                            ->withAttribute(Attributes::PASSWORD, $this->password)
                            ->withAttribute(Attributes::CONNECT_TIMEOUT, $this->connectTimeout)
                            ->withAttribute(Attributes::TIMEOUT, $this->timeout)
                            ->withAttribute(Attributes::DOWNLOAD_SPEED, $this->downloadSpeed)
                            ->withAttribute(Attributes::UPLOAD_SPEED, $this->uploadSpeed)
                            ->withAttribute(Attributes::FOLLOW_LOCATION, $this->followLocation)
                            ->withAttribute(Attributes::CONNECTION_POOL, $this->connectionPool)
                            ->withAttribute(Attributes::RETRY, $this->retry)
                            ->withAttribute(Attributes::RETRY_CALLBACK, $this->retryCallback)
                            ->withAttribute(Attributes::WEBSOCKET_COMPRESSION, $this->websocketCompression)
                            ->withProtocolVersion($this->protocolVersion)
        ;
        foreach ($this->proxy as $name => $value)
        {
            $request = $request->withAttribute('proxy.' . $name, $value);
        }

        return $request;
    }

    /**
     * 发送请求，所有请求的老祖宗.
     *
     * @param string|null         $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string              $method      请求方法，GET、POST等
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function send($url = null, $requestBody = null, $method = null, $contentType = null)
    {
        $request = $this->buildRequest($url, $requestBody, $method, $contentType);

        return YurunHttp::send($request, $this->handler);
    }

    /**
     * 发送 Http2 请求不调用 recv().
     *
     * @param string|null         $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string              $method      请求方法，GET、POST等
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function sendHttp2WithoutRecv($url = null, $requestBody = null, $method = 'GET', $contentType = null)
    {
        $request = $this->buildRequest($url, $requestBody, $method, $contentType)
                        ->withProtocolVersion('2.0')
                        ->withAttribute(Attributes::HTTP2_NOT_RECV, true);

        return YurunHttp::send($request, $this->handler);
    }

    /**
     * GET请求
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function get($url = null, $requestBody = null)
    {
        if (!empty($requestBody))
        {
            if (strpos($url, '?'))
            {
                $url .= '&';
            }
            else
            {
                $url .= '?';
            }
            $url .= http_build_query($requestBody, '', '&');
        }

        return $this->send($url, [], 'GET');
    }

    /**
     * POST请求
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function post($url = null, $requestBody = null, $contentType = null)
    {
        return $this->send($url, $requestBody, 'POST', $contentType);
    }

    /**
     * HEAD请求
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function head($url = null, $requestBody = null)
    {
        return $this->send($url, $requestBody, 'HEAD');
    }

    /**
     * PUT请求
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function put($url = null, $requestBody = null, $contentType = null)
    {
        return $this->send($url, $requestBody, 'PUT', $contentType);
    }

    /**
     * PATCH请求
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function patch($url = null, $requestBody = null, $contentType = null)
    {
        return $this->send($url, $requestBody, 'PATCH', $contentType);
    }

    /**
     * DELETE请求
     *
     * @param string              $url         请求地址，如果为null则取url属性值
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string|null         $contentType 内容类型，支持null/json，为null时不处理
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function delete($url = null, $requestBody = null, $contentType = null)
    {
        return $this->send($url, $requestBody, 'DELETE', $contentType);
    }

    /**
     * 直接下载文件.
     *
     * @param string              $fileName    保存路径，如果以 .* 结尾，则根据 Content-Type 自动决定扩展名
     * @param string              $url         下载文件地址
     * @param string|object|array $requestBody 发送内容，可以是字符串、数组，如果为空则取content属性值
     * @param string              $method      请求方法，GET、POST等，一般用GET
     *
     * @return \Yurun\Util\YurunHttp\Http\Response|null
     */
    public function download($fileName, $url = null, $requestBody = null, $method = 'GET')
    {
        $isAutoExt = self::checkDownloadIsAutoExt($fileName, $fileName);
        $result = $this->saveFile($fileName)->send($url, $requestBody, $method);
        if ($isAutoExt)
        {
            self::parseDownloadAutoExt($result, $fileName);
        }
        $this->saveFileOption = [];

        return $result;
    }

    /**
     * WebSocket.
     *
     * @param string $url
     *
     * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient
     */
    public function websocket($url = null)
    {
        $request = $this->buildRequest($url);

        return YurunHttp::websocket($request, $this->handler);
    }

    /**
     * 检查下载文件名是否要自动扩展名.
     *
     * @param string $fileName
     * @param string $tempFileName
     *
     * @return bool
     */
    public static function checkDownloadIsAutoExt($fileName, &$tempFileName)
    {
        $flagLength = \strlen(self::AUTO_EXT_FLAG);
        if (self::AUTO_EXT_FLAG !== substr($fileName, -$flagLength))
        {
            return false;
        }
        $tempFileName = substr($fileName, 0, -$flagLength) . self::AUTO_EXT_TEMP_EXT;

        return true;
    }

    /**
     * 处理下载的自动扩展名.
     *
     * @param \Yurun\Util\YurunHttp\Http\Response $response
     * @param string                              $tempFileName
     *
     * @return void
     */
    public static function parseDownloadAutoExt(&$response, $tempFileName)
    {
        $ext = MediaType::getExt($response->getHeaderLine('Content-Type'));
        if (null === $ext)
        {
            $ext = 'file';
        }
        $savedFileName = substr($tempFileName, 0, -\strlen(self::AUTO_EXT_TEMP_EXT)) . '.' . $ext;
        rename($tempFileName, $savedFileName);
        $response = $response->withSavedFileName($savedFileName);
    }
}

if (\extension_loaded('curl'))
{
    // 代理认证方式
    HttpRequest::$proxyAuths = [
        'basic' => \CURLAUTH_BASIC,
        'ntlm'  => \CURLAUTH_NTLM,
    ];

    // 代理类型
    HttpRequest::$proxyType = [
        'http'      => \CURLPROXY_HTTP,
        'socks4'    => \CURLPROXY_SOCKS4,
        'socks4a'   => 6,    // CURLPROXY_SOCKS4A
        'socks5'    => \CURLPROXY_SOCKS5,
    ];
}
