<?php

namespace app\common\arw\adjfut\src;

use \CurlHandle;
use think\Exception;

class Curl
{
	/**
	 * 请求前缀
	 *
	 * @var string
	 */
	private $baseUrl = '';
	/**
	 * 返回数据类型
	 *
	 * @var string
	 */
	private $dataType = 'json';
	/**
	 * 事件
	 *
	 * @var array
	 */
	private $event = [];
	/**
	 * 绑定
	 *
	 * @var array
	 */
	private $bind = [];
	/**
	 * 配置项
	 *
	 * @var array
	 */
	private $config = [
		/**
		 * http header
		 */
		CURLOPT_HTTPHEADER => [],
		/**
		 * http 超时
		 */
		CURLOPT_TIMEOUT => 0,
		/**
		 * http 代理
		 */
		CURLOPT_PROXY => '',
		/**
		 * http 代理端口
		 */
		CURLOPT_PROXYPORT => '',
		/**
		 * 来源页面
		 */
		CURLOPT_REFERER => '',
		/**
		 * 用户代理
		 */
		CURLOPT_USERAGENT => '',
		/**
		 * 响应中是否显示header
		 */
		CURLOPT_HEADER => 0
	];
	/**
	 * Get请求参数
	 *
	 * @var null
	 */
	private $httpParams = null;
	/**
	 * Post请求参数
	 *
	 * @var null
	 */
	private $httpData = null;
	/**
	 * 异常信息
	 *
	 * @var string
	 */
	private $error = '';
	/**
	 * url是否编码
	 *
	 * @var boolean
	 */
	private $urlEncode = false;
	/**
	 * 验证http请求状态码
	 *
	 * @var boolean
	 */
	private $httpCode = true;
	/**
	 * 请求路径
	 *
	 * @var string
	 */
	private $path = '';
	/**
	 * 请求url
	 *
	 * @var string
	 */
	private $url = '';
	/**
	 * 请求类型
	 *
	 * @var string
	 */
	private $method = 'get';
	/**
	 * 返回内容
	 *
	 * @var string
	 */
	private $content = '';
	/**
	 * 请求信息
	 *
	 * @var array
	 */
	private $status = [];
	/**
	 * 调试模式
	 *
	 * @var boolean
	 */
	private $debug = false;
	/**
	 * 是否上传文件
	 *
	 * @var boolean
	 */
	private $isUploadFile = false;

	/**
	 * 初始化
	 *
	 * @date 2022-03-24
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function __construct()
	{
	}

	/**
	 * 初始化请求
	 *
	 * @return $this
	 */
	public static function instance()
	{
		return new self;
	}

	/**
	 * 获取请求url
	 */
	public function getUrl(): string
	{
		return $this->url;
	}

	/**
	 * 获取请求路径
	 */
	public function getPath(): string
	{
		return $this->path;
	}

	/**
	 * 获取返回内容
	 *
	 * @return string
	 */
	public function getContent(): string
	{
		return $this->content;
	}

	/**
	 * 格式化返回
	 *
	 * @param string $type 返回类型（默认使用 $this->dataType）
	 * @return mixed
	 * @date 2022-03-24
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function formatContent(string $type = '')
	{
		$content = $this->content;
		if (!$type) {
			$type = $this->dataType;
		}
		switch ($type) {
			case 'json':
			case 'object':
				return  json_decode($content, $type === 'json');
				break;
			default:
				return $content;
				break;
		}
	}

	/**
	 * 获取请求信息
	 */
	public function getStatus(): array
	{
		return $this->status;
	}

	/**
	 * 设置是否上传文件
	 */
	public function setIsUploadFile(bool $isUploadFile): self
	{
		$this->isUploadFile = $isUploadFile;
		return $this;
	}

	/**
	 * 设置请求路径
	 */
	public function setPath(string $path): self
	{
		if (count(explode('?', $path)) != 1) {
			throw new Exception("请求路径不能携带参数 请使用setParams设置请求参数", 1);
		}
		$this->path = $path;
		return $this;
	}

	/**
	 * 设置调试模式
	 */
	public function setDeBug(bool $debug): self
	{
		$this->debug = $debug;
		return $this;
	}

	/**
	 * 验证http状态码
	 * 
	 * @param boolean|int $code
	 */
	public function setHttpCode($code): self
	{
		$this->httpCode = $code;
		return $this;
	}

	/**
	 * 设置url是否编码
	 */
	public function setUrlEncode(bool $urlEncode): self
	{
		$this->urlEncode = $urlEncode;
		return $this;
	}

	/**
	 * 设置请求前缀
	 */
	public function setBaseUrl(string $baseUrl): self
	{
		if (count(explode('?', $baseUrl)) != 1) {
			throw new Exception("请求前缀不能携带参数 请使用setParams设置请求参数", 1);
		}
		$this->baseUrl = $baseUrl;
		return $this;
	}

	/**
	 * 设置返回数据类型
	 */
	public function setDataType(string $dataType): self
	{
		$this->dataType = $dataType;
		return $this;
	}

	/**
	 * 设置http header
	 */
	public function setHeader(array $header): self
	{
		$_header = [];
		foreach ($header as $key => $value) {
			if (is_numeric($key)) {
				$_header[] = $value;
			} else {
				$_header[] = "$key: $value";
			}
		}
		$this->config[CURLOPT_HTTPHEADER] = array_merge($this->config[CURLOPT_HTTPHEADER], $_header);
		return $this;
	}

	/**
	 * 设置http 超时
	 */
	public function setTimeout(int $time): self
	{
		$this->config[CURLOPT_TIMEOUT] = $time <= 0 ? 5 : $time;
		return $this;
	}

	/**
	 * 设置http 代理
	 */
	public function setProxy(string $proxy): self
	{
		$this->config[CURLOPT_PROXY] = $proxy;
		return $this;
	}

	/**
	 * 设置http 代理端口
	 */
	public function setProxyPort(int $port): self
	{
		$this->config[CURLOPT_PROXYPORT] = $port;
		return $this;
	}

	/**
	 * 设置来源页面
	 */
	public function setReferer(string $referer = ""): self
	{
		$this->config[CURLOPT_REFERER] = $referer;
		return $this;
	}

	/**
	 * 设置用户代理
	 */
	public function setUserAgent(string $agent = ""): self
	{
		$this->config[CURLOPT_USERAGENT] = $agent;
		return $this;
	}

	/**
	 * http响应中是否显示header
	 */
	public function showResponseHeader(bool $show): self
	{
		$this->config[CURLOPT_HEADER] = $show ? 1 : 0;
		return $this;
	}

	/**
	 * 获取get请求参数
	 */
	public function getParams()
	{
		return $this->httpParams;
	}

	/**
	 * 设置get请求的参数
	 */
	public function setParams($params): self
	{
		$_params = $this->trigger('beforeSetParams', [$params]);
		$this->httpParams = $_params ? $_params : $params;
		return $this;
	}

	/**
	 * 获取post请求参数
	 */
	public function getData()
	{
		return $this->httpData;
	}

	/**
	 * 设置post请求的参数
	 */
	public function setData($data): self
	{
		$_data = $this->trigger('beforeSetData', [$data]);
		$this->httpData = $_data ? $_data : $data;
		return $this;
	}

	/**
	 * 设置http请求的cookie信息
	 */
	public function setCookie(array $cookie): self
	{
		$_cookie = [];
		foreach ($cookie as $key => $value) {
			$_cookie[] = "$key=$value";
		}
		$this->config[CURLOPT_COOKIE] = join(';', $_cookie);
		return $this;
	}

	/**
	 * 设置证书路径
	 */
	public function setCainfo(string $file): self
	{
		$this->config[CURLOPT_CAINFO] = $file;
		return $this;
	}

	/**
	 * 获取请求异常
	 */
	public function getError(): string
	{
		return $this->error;
	}

	/**
	 * 绑定
	 *
	 * @param string $name
	 * @param callable $cb
	 * @date 2022-03-26
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function bind(string $name, callable $cb): self
	{
		$this->bind[$name] = $cb;
		return $this;
	}

	/**
	 * GET请求
	 * @param string $path
	 * @return mixed
	 */
	public function get(string $path = '')
	{
		if ($path) {
			$this->setPath($path);
		}
		$this->method = 'get';
		$respone = $this->fetch();
		if (!$path) {
			return $this;
		}
		return $respone;
	}

	/**
	 * POST请求
	 *
	 * @param string $path
	 * @return mixed
	 */
	public function post(string $path = '')
	{
		if ($path) {
			$this->setPath($path);
		}
		$this->method = 'post';
		$respone = $this->fetch();
		if (!$path) {
			return $this;
		}
		return $respone;
	}

	/**
	 * PUT请求
	 *
	 * @param string $path
	 * @return mixed
	 */
	public function put(string $path = '')
	{
		if ($path) {
			$this->setPath($path);
		}
		$this->method = 'put';
		$respone = $this->fetch();
		if (!$path) {
			return $this;
		}
		return $respone;
	}

	/**
	 * DELETE请求
	 *
	 * @param string $path
	 * @return mixed
	 */
	public function delete(string $path = '')
	{
		if ($path) {
			$this->setPath($path);
		}
		$this->method = 'delete';
		$respone = $this->fetch();
		if (!$path) {
			return $this;
		}
		return $respone;
	}

	/**
	 * PATCH请求
	 *
	 * @param string $path
	 * @return mixed
	 */
	public function patch(string $path = '')
	{
		if ($path) {
			$this->setPath($path);
		}
		$this->method = 'patch';
		$respone = $this->fetch();
		if (!$path) {
			return $this;
		}
		return $respone;
	}

	/**
	 * 设置get传参前
	 *
	 * @param callable $cb($params)
	 * @param mixed $params 参数
	 * @date 2022-03-26
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function onBeforeSetParams(callable $cb): self
	{
		return $this->event('beforeSetParams', $cb);
	}

	/**
	 * 设置post传参前
	 *
	 * @param callable $cb($data)
	 * @param mixed $data 参数
	 * @date 2022-03-26
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function onBeforeSetData(callable $cb): self
	{
		return $this->event('beforeSetData', $cb);
	}

	/**
	 * 请求前
	 *
	 * @param callable $cb($ch)
	 * @param self $ch
	 * @date 2022-03-26
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function onBeforeFetch(callable $cb): self
	{
		return $this->event('beforeFetch', $cb);
	}

	/**
	 * 请求后
	 *
	 * @param callable $cb($ch,$info)
	 * @param self $ch
	 * @param array $info
	 * @param string $info[url] 请求完整url
	 * @param string $info[content] 返回信息
	 * @param array $info[status] 请求信息
	 * @date 2022-03-26
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function onAfterFetch(callable $cb)
	{
		return $this->event('afterFetch', $cb);
	}

	/**
	 * 请求异常(返回true阻止抛出异常)
	 *
	 * @param callable $cb($ch,$th)
	 * @param self $ch
	 * @param \Throwable $th
	 * @date 2022-03-26
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function onFetchError(callable $cb)
	{
		return $this->event('fetchError', $cb);
	}

	/**
	 * 发起请求
	 *
	 * @return mixed
	 * @date 2022-03-25
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	public function fetch()
	{
		$this->url = $url = $this->baseUrl . $this->path;

		$this->trigger('beforeFetch', [$this]);

		try {
			$httpParams = $this->httpParams;
			if (!empty($httpParams) && is_array($httpParams)) {
				$url .= (strpos($url, '?') === false ? '?' : '') . http_build_query($httpParams);
			}

			if ($this->urlEncode === false) {
				$url = urldecode($url);
			}

			$ch = $this->curlInit($url);
			// 设为TRUE把curl_exec()结果转化为字串，而不是直接输出 
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

			$this->content = $content = curl_exec($ch);
			$this->status = $status = curl_getinfo($ch);

			if ($content === false) {
				$this->error = curl_error($ch);
			}

			if ($this->debug) {
				dump([
					'ch' => $ch,
					'url' => $url,
					'httpParams' => $this->httpParams,
					'httpData' => $this->httpData,
					'contentArray' => json_decode($content, true),
					'content' => $content,
					'status' => $status
				]);
			}
			curl_close($ch);

			$httpCode = $this->httpCode;

			if ($httpCode === true) {
				$httpCode = 200;
			}
			if (is_numeric($httpCode)) {
				if (!isset($status['http_code'])) {
					throw new Exception("服务器未返回状态码", 1);
				}
				if ($status['http_code'] !== $httpCode) {
					throw new Exception(sprintf("服务器返回状态码异常[ %s ]", $status['http_code']), 1);
				}
			}

			$this->trigger('afterFetch', [$this, [
				'url' => $url,
				'content' => $content,
				'status' => $status
			]]);

			return $this->formatContent();
		} catch (\Throwable $th) {
			if (!$this->trigger('fetchError', [$this, $th])) {
				throw $th;
			}
		}
	}

	public function __call($name, $arguments)
	{
		array_unshift($arguments, $this);
		call_user_func_array($this->bind[$name], $arguments);
	}

	/**
	 * 事件
	 *
	 * @param string $type beforeSetParams | beforeSetData | beforeFetch | AfterFetch
	 * @param callable $cb
	 */
	private function event(string $type, callable $cb): self
	{
		$this->event[strtoupper($type)] = $cb;
		return $this;
	}

	/**
	 * 事件触发器
	 *
	 * @param string $type
	 * @return mixed
	 * @date 2022-03-25
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	private function trigger(string $type, array $args = [])
	{
		$type = strtoupper($type);
		if (isset($this->event[$type])) {
			return call_user_func_array($this->event[$type], $args);
		}
	}

	/**
	 * 初始化请求
	 *
	 * @param string $url
	 * @return resource|CurlHandle
	 * @date 2022-04-24
	 * @example
	 * @author arw
	 * @since 1.0.0
	 */
	private function curlInit(string $url)
	{
		$ch = curl_init($url);
		if (stripos($url, 'https://') !== false) {
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
			curl_setopt($ch, CURLOPT_SSLVERSION, 1);
		}

		foreach ($this->config as $option  => $value) {
			if (!$value && $option != CURLOPT_HEADER) {
				continue;
			}
			curl_setopt($ch, $option, $value);
		}

		switch ($this->method) {
			case 'get':
				break;
			case 'post':
				curl_setopt($ch, CURLOPT_POST, true);
				break;
			default:
				// delete put patch
				curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($this->method));
				break;
		}
		$httpData = $this->httpData;
		// 设置post body
		if ($httpData) {
			$data = null;
			if (is_array($httpData)) {
				if ($this->isUploadFile) {
					$data = $httpData;
				} else {
					$data = http_build_query($httpData);
				}
			} else if (is_string($httpData)) {
				$data = $httpData;
			}
			curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
		}
		return $ch;
	}
}
