<?php
declare(strict_types=1);

namespace Lib\Mail;

use Lib\Mail\Body;


/**
 * imap协议直连服务器无需 imap扩展
 * @time 2022/8/5 14:17
 * Class Imap
 * @package App\Mail\lib\socket
 */
class Imap {

    /**
     * @var resource
     */
    private $socket;

    /**
     * ssl://imap.qq.com:993
     * @var string
     */
    private $host;

    /** 邮箱
     * @var
     */
    private $username;

    /**
     * 邮箱密码,单独设置的imap密码
     * @var
     */
    private $password;

    /**
     * 请求时增加的tag
     * @var int
     */
    private $tagNum = 0;

    /**
     * 标签名称
     * @var string
     */
    private $tagName    =   'A';

    /**
     * imap服务器设置id 邮箱邮件服务器是必须的
     * @var string[]
     */
    private $imapId = [
        'name'  =>  'global so Client',
        'version'   =>  '1',
        'os'    =>  'global so',
        'os-version'    =>  '1.0'
    ];


    /**
     * 超时时间
     * @var int
     */
    private $timeout = 30;

    /**
     * 搜索字段,标题,邮件主题
     */
    const SEARCH_FILED_SUBJECT = 'SUBJECT';

    /**
     * 搜索字段,发件人
     */
    const SEARCH_FILED_FROM = 'FROM';

    /**
     * 搜索字段,收件人
     */
    const SEARCH_FILED_TO = 'TO';

//    const SEARCH_FILED_SUBJECT = 'RFC822.TEXT';


    /**
     * 邮件便签
     * @var string[]
     */
    public $flags = [
        'SEEN'      => '\\Seen', // 已读
        'DELETED'   => '\\Deleted', //删除
        'ANSWERED'  => '\\Answered',//已回复
        'DRAFT'     => '\\Draft',//草稿
        'FLAGGED'   => '\\Flagged',//星标
        'FORWARDED' => '$Forwarded',
        'MDNSENT'   => '$MDNSent',
        '*'         => '\\*',
    ];

    /**
     * 已读 标记
     */
    const FLAGS_SEEN = 'SEEN';

    /**
     * 删除 标记
     */
    const FLAGS_DELETED = 'DELETED';

    /**
     * 已回复 标记
     */
    const FLAGS_ANSWERED = 'ANSWERED';

    /**
     * 草稿 标记
     */
    const FLAGS_DRAFT = 'DRAFT';

    /**
     * 星标 标记
     */
    const FLAGS_FLAGGED = 'FLAGGED';

    /**
     * 添加邮件标签
     */
    const FLAGS_APPEND = '+';

    /**
     * 删除邮件标签
     */
    const FLAGS_REMOVE = '-';


    /**
     * 是否自动关闭
     * @var bool
     */
    private $auto_colse = true;

    /**
     * 调试模式
     * @var bool
     */
    private $debug = false;

    /**
     * 调试,记录日志的目录
     * @var string
     */
    private $debugWritePath = '';

    /**
     * 登录imap服务器
     * @param string $host ssl://imap.qq.com:993  imap.qq.com:143
     * @param string $username 邮箱地址 xxxxx@xx.com
     * @param string $password 密码,此密码非邮箱登录密码,是imap服务器单独设置的密码
     * @param bool $readOnly 是否以只读模式打开邮箱
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 11:06
     */
    public function login(string $host,string $username,string $password,bool $readOnly=false){
        $this->host =   $host;
        $this->username =   $username;
        $this->password =   $password;

        // 查看服务器所支持的功能列表
//        $this->request('CAPABILITY');

        // 设置id,必须在登录前,163是强制的
        $strId = '';
        foreach ($this->imapId as $k=>$str){
            if(!is_array($str)){
                $strId .= '"'.str_replace(['"',"'"],'',$k).'" "'.str_replace(['"',"'"],'',$str).'" ';
            }
        }
//        "name" "测试本地 Client" "version" "1" "os" "测试本地" "os-version" "1.0"
        $this->request('ID ('.trim($strId).')');// 这里就不处理命令返回的信箱了

        // 登录
        $result = $this->request("LOGIN {$username} {$password}");

        //解析登录数据每个服务商返回的登录结果不一样,很难兼容
        if($result[0] != 'ok'){
            throw new \Exception('IMAP Login Error:'.end($result[1]),403);
        }

        // 是否是只读模式
        // 只读模式不可操作邮箱任何内容,只可查看
        if($readOnly){
            $this->request('EXAMINE '.$username);
        }

        return true;
    }



    /**
     * 退出当前操作的邮箱
     * 一般情况不需要
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 10:46
     */
    public function loginOut():bool {
        return $this->request('LOGOUT')[0] == 'ok';
    }

    /**
     * @param resource $socket
     */
    public function setSocket($socket,$auto_colse=true): void
    {
        $this->socket = $socket;
        // 是否自动关闭
        $this->auto_colse = $auto_colse;
    }


    /**
     * 删除带有delete标签的邮件,删除的邮件不可恢复。
     * 类似清空
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 10:55
     */
    public function clearDelete():bool {
        return $this->request('EXPUNGE')[0] == 'ok';
    }

    /**
     * [
    [EXISTS] => 67 总邮件数
    [RECENT] => 1 最新的邮件
    [UIDVALIDITY] => 1
    [FLAGS] => Array 此文件夹可以操控的标签
    (
    [0] => \Answered 回复
    [1] => \Seen 已读
    [2] => \Deleted 删除
    [3] => \Draft 草稿
    [4] => \Flagged 星标
    )

    [PERMANENTFLAGS] => Array
    (
    [0] => \Answered
    [1] => \Seen
    [2] => \Deleted
    [3] => \Draft
    [4] => \Flagged
    )
    ]
     * 选择文件夹
     * @param string $folder
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2022/11/22 16:28
     */
    public function selectFolder($folder='INBOX'){

        // 选择文件夹
        $result = $this->request("SELECT \"{$folder}\"");
        if($result[0] != 'ok'){
            throw new \Exception('select folder error:'.end($result[1]));
        }

        $list = [];
        foreach ($result[1] as $item){
            $item = trim($item);
            // 总数量
            if(preg_match("/^\* (\d+) EXISTS$/i",$item,$m)){
                $list['EXISTS'] = $m[1];
            }
            // 最近的
            elseif (preg_match("/^\* (\d+) RECENT$/i",$item,$m)){
                $list['RECENT'] = $m[1];
            }
            // 未读
            elseif (preg_match("/^\*.*\[UNSEEN (\d+)\]/i",$item,$m)){
                $list['UNSEEN'] = $m[1];
            }
            // tag
            elseif (preg_match("/^\* FLAGS \((.*)\)$/i",$item,$m)){
                $list['FLAGS'] = explode(' ',$m[1]);
            }
            // 其他
            elseif (preg_match("/^\*.*\[(.*)\]/i",$item,$m)){
//               是否有(),有小扩号就是数组
//               * OK [PERMANENTFLAGS (\* \Answered \Flagged \Deleted \Draft \Seen)] Permanent flags
                if(preg_match("/\((.*)\)/i",$item,$m2)){
                    $em = explode(' ',$m[1]);
                    $list[$em[0]] = explode(' ',$m2[1]);
                }else{
                    $em = explode(' ',$m[1]);
                    $list[$em[0]] = $em[1];
                }
            }
        }

        return $list;
    }

    /**
     * 关闭当前打开的文件夹,
     * 使用selectFolder后才可以使用此函数
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 10:40
     */
    public function closeFolder():bool{
        return $this->request('CLOSE')['0'] == 'ok';
    }

    /**
     * 搜索邮件,如果要获得列表,需要配合fetch使用
     * @param array $criteria 搜索[key=>value,...]
     * @param bool $return_uid 是否返回uid,默认返回邮件编号
     * @return array 返回的是搜索出来的uid,每个文件夹下面uid是唯一的
     * @throws \Exception
     * @author:dc
     * @time 2022/11/24 15:46
     */
    public function search(array $criteria,$return_uid = false):array{
        // 命令,是否返回uid
        $cmd = ($return_uid ? 'UID ' : '').'SEARCH';

        foreach ($criteria as $k=>$v){
            $v  =   addslashes($v);
            $cmd .= " {$k} \"{$v}\"";
        }

        // 获取搜索到的 uid
        $result = $this->request($cmd);

        if($result[0] != 'ok'){
            throw new \Exception('search error:'.end($result[1]));
        }

        $uids = [];
        foreach ($result[1] as $item){
            // 匹配uid
            if(preg_match("/^\* SEARCH ([0-9\s]{1,})$/i",$item,$m)){
                $uids = array_merge($uids,explode(' ',$m[1]));
            }
        }

        return $uids;
    }


    /**
     * 设置标签,删除标签
     * @param string|int|array $data 邮件的编号或者uid
     * @param array $flag 标记属性 [Imap::FLAGS_SEEN,..]
     * @param string $mod 删除或者添加 Imap::FLAGS_APPEND|Imap::FLAGS_REMOVE
     * @param bool $is_uid 是否使用uid进行标记
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/24 16:58
     */
    public function flags($data,array $flag,string $mod = self::FLAGS_APPEND,bool $is_uid = false):bool {

        if(is_array($data)){
            $data = implode(',',$data);
        }

        // flags 标记
        foreach ($flag as $k=>$item){
            if(!isset($this->flags[$item])){
                unset($flag[$k]);
            }else{
                $flag[$k] = $this->flags[$item];
            }
        }

        // 是否是规定中的值
        if(!in_array($mod,[self::FLAGS_APPEND,self::FLAGS_REMOVE])){
            throw new \Exception('set flags error: mod');
        }

        // 请求标记
        $result = $this->request(($is_uid?'UID ':'')."STORE {$data} {$mod}FLAGS.SILENT (".implode(' ',$flag).")");

        return $result[0] == 'ok';
    }

    /**
     * 获取邮箱所有文件夹
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2022/11/24 17:20
     */
    public function getFolder():array {
        // 获取数据
        $result = $this->request('LIST "" *');

        $folder = [];
        foreach ($result[1] as $item){
            // 解析源数据
            if(preg_match('/^\* LIST \(([\\a-z\s]{0,})\) "(.*)" "(.*)"/Ui',$item,$m)){
                $folder[] = [
                    'parent'    =>  $m[2],
                    // 源文件夹名称,在进行 select的时候必须用未解析的文件夹名称
                    'folder'    =>  $m[3],
                    // 解析过的文件夹名称
                    'parseFolder'    =>  mb_convert_encoding($m[3], 'UTF-8', 'UTF7-IMAP'),
                    // 是否可选择
                    'isSelect'  =>  strpos($m[1],'NoSelect')===false,
                ];
            }
        }

        return $folder;
    }

    /**
     * 创建文件夹
     * $folder 目录 创建二级目录用/  a/b
     * @param string $folder
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/24 23:35
     */
    public function folderCreate(string $folder):bool {
        // 需要转码
        $folder = mb_convert_encoding($folder,'UTF7-IMAP','UTF-8');
//        A003 CREATE owatagusiam 顶级
//        A003 CREATE owatagusiam/owatagusiam2 有上下级关系的文件夹
        $res = $this->request('CREATE '.$folder);

        if ($res[0] == 'ok'){
            return true;
        }

        throw new \Exception('create folder error:'.end($res[1]));

    }

    /**
     * 修改文件夹名称
     * @param string $oldFolder 久文件夹名称
     * @param string $newFolder 新文件夹名称
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 10:24
     */
    public function folderRename(string $oldFolder, string $newFolder){
        // 需要转码
        $newFolder  =   mb_convert_encoding($newFolder,'UTF7-IMAP','UTF-8');
//        RENAME oldfolder newfolder
        $res = $this->request("RENAME {$oldFolder} {$newFolder}");

        if ($res[0] == 'ok'){
            return true;
        }

        throw new \Exception('rename folder error:'.end($res[1]));
    }

    /**
     * 删除文件夹
     * @param string $folder
     * @return bool
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 10:25
     */
    public function folderDelete(string $folder):bool{

//        A683 DELETE blurdybloop/test
        $res = $this->request('DELETE '.$folder);

        if ($res[0] == 'ok'){
            return true;
        }

        throw new \Exception('rename folder error:'.end($res[1]));

    }


    /**
     * 获取邮件头,并解析
     * @param $data
     * @param false $is_uid
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2022/11/23 14:25
     */
    public function fetchHeader($data,$is_uid = false){
        $result = $this->fetch($data,'header',$is_uid);
        // 解析header字段
        foreach ($result as $key=>$item){
            $result[$key]['HEADER.FIELDS'] = $this->parseHeader($item['HEADER.FIELDS']);
        }

        return $result;

    }

    /**
     * 解析header字段
     * @param $header
     * @return array
     * @author:dc
     * @time 2022/11/23 14:54
     */
    public function parseHeader($header){
        /****** 把数据中的 : 转换一下 *******/
        $header = explode("\n",$header);
        foreach ($header as $k=>$str){
            $first_str = '';
            if(substr_count($str,':') > 1){
                $str = explode(':',$str);
                $first_str = $str[0];
                if($first_str){
                    unset($str[0]);
                }
                $str = implode('_(_(_(_0_)_)_)_',$str);
                if($first_str) $str = $first_str.':'.$str;
            }
            $header[$k] = empty($first_str) ? $str : ltrim($str);
        }
        $header = implode("\n",$header);
        /******end*******/

        preg_match_all(
            "/^([^\r\n:]+)\s*[:]\s*([^\r\n:]+(?:[\r]?[\n][ \t][^\r\n]+)*)/m",
            $header,
            $matches,
            PREG_SET_ORDER
        );
        $headers = [];
        foreach($matches as $match){
            $match[2] = str_replace(['_(_(_(_0_)_)_)_'],[':'],$match[2]);
            if(strpos($match[2],'=?')!==false){
                $match2=iconv_mime_decode($match[2],ICONV_MIME_DECODE_CONTINUE_ON_ERROR,'utf-8');
                $match[2]   =   $match2;
            }

            if(isset($headers[$match[1]]) && is_array($headers[$match[1]])){
                $headers[$match[1]][]=$match[2];
            }elseif(isset($headers[$match[1]])){
                $headers[$match[1]]=array($headers[$match[1]],$match[2]);
            }else{
                $headers[$match[1]]=$match[2];
            }
        }
        return $headers;

    }


    /**
     * 获取邮件内容,包括附件
     * @param mixed $data 1,1:10,[1,2,3]
     * @param string $saveFilePath 保存附件的目录
     * @param bool $is_uid 是否是UID查询
     * @throws \Exception
     * @author:dc
     * @time 2022/11/23 17:01
     */
    public function fetchBody($data,$saveFilePath=__DIR__,$is_uid = false){
        // 取得body
        $result = $this->fetch($data,'body',$is_uid);
        foreach ($result as &$item){
            if (!empty($item['RFC822.TEXT'])){
                // 解析
                $item['RFC822.TEXT'] = (new Body($item['RFC822.TEXT'],$saveFilePath))->getItem();
            }
        }

        return $result;
    }



    /**
     * @param $data
     * @param false $is_uid
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2022/11/23 10:46
     */
    public function fetch($data,$header2Body='header',$is_uid = false):array{
        // string 必须是 0:99  开始:结束
        if(is_numeric($data)){
            $data = intval($data);
        }
        elseif(is_string($data)){
            if(!preg_match("/^\d+:\d+$/",$data)){
                $data = '';
            }
        }elseif (is_array($data)){
            foreach ($data as $k=>$v){
                if(!is_numeric($v)){
                    unset($data[$k]);
                }
            }
            $data = array_unique(array_values($data));
            $data = implode(',',$data);
        }

        if(empty($data)){
            throw new \Exception('fetch data error');
        }

        // (UID RFC822.SIZE RFC822.HEADER FLAGS INTERNALDATE BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY CONTENT-TRANSFER-ENCODING BCC IN-REPLY-TO MAIL-FOLLOWUP-TO MAIL-REPLY-TO MESSAGE-ID REFERENCES RESENT-BCC RETURN-PATH SENDER X-DRAFT-INFO)])
        // 指定字段 BODY.PEEK[HEADER.FIELDS (DATE FROM TO ...)]
        // 所有header字段 RFC822.HEADER
        // RFC822.TEXT 内容包括附件
        // body字段必须放最后
        if($header2Body=='header'){
//            BODY.peek必须放最后
            $filed = 'UID FLAGS INTERNALDATE RFC822.SIZE BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY CONTENT-TRANSFER-ENCODING BCC IN-REPLY-TO MAIL-FOLLOWUP-TO MAIL-REPLY-TO MESSAGE-ID REFERENCES RESENT-BCC RETURN-PATH SENDER X-DRAFT-INFO)]';
        }elseif($header2Body=='body'){
            $filed = 'RFC822.TEXT';
        }else{
            $filed = 'UID FLAGS INTERNALDATE';
        }

        // 读取数据
        $result = $this->request(($is_uid?'UID ':'')."FETCH {$data} ($filed)");

        if($result[0] != 'ok'){
            throw new \Exception('fetch error:'.end($result[1]));
        }

        // 开始解析数据
        $list = [];
        foreach ($result[1] as $item){
            $item = trim($item);
            // 匹配出id和数据
            if(preg_match("/\* (\d+) FETCH \(([\w\s\d\r\n\W]{1,})\)$/i",$item,$line)){
                $list[$line[1]] = $this->parseFetch($line[2]);
            }
        }

        // 返回结果
        return $list;

    }

    /**
     * 解析头部信息,只能解析固定字段的属性
     * @param string $line
     * @return array
     * @author:dc
     * @time 2022/11/22 18:06
     */
    public function parseFetch($line):array{
        $result = [];
        $line = ltrim($line);
        $header = explode(' ',$line);
        do{

            switch ($header[0]){
                case 'UID':{
                    $result['UID'] = $header[1];
                    array_splice($header,0,2);
                    break;
                }
                case 'FLAGS':{
                    // 标记,标签
                    $result['FLAGS'] = $this->pregFiled(
                        $header,
                        "/^\(([\\a-z\s]{0,})\)/Ui"
                    );
                    $result['FLAGS'] = $result['FLAGS'] ? explode(' ',$result['FLAGS']) : [];
                    break;
                }
                case 'INTERNALDATE':{
                    // 内部时间
                    $result['INTERNALDATE'] = $this->pregFiled(
                        $header,
                        "/^\"([\\a-z\d\s]{0,})\"/Ui"
                    );
                    break;
                }
                case 'RFC822.SIZE':{
                    $result['RFC822.SIZE'] = $header[1];
                    array_splice($header,0,2);
                    break;
                }
                case 'BODYSTRUCTURE':{
                    array_shift($header);
                    $result['BODYSTRUCTURE']    =   '';
                    $leftNum = $RightNum = 0;
                    foreach ($header as $key=>$str){
                        // 还有括号不对等的情况,无语了
                        if($str == 'BODY[HEADER.FIELDS'){
                            break;
                        }
                        // 找出现的次数
                        $leftNum += substr_count($str,'(');
                        $RightNum += substr_count($str,')');
                        // 拼接起来
                        $result['BODYSTRUCTURE'] .= ' '.$str;
                        unset($header[$key]);
                        // 说明找到结束了
                        if($leftNum===$RightNum){
                            break;
                        }
                    }
                    $header = array_values($header);
                    break;
                }
                // RFC822.TEXT不能和 BODY[HEADER.FIELDS 同时存在,否则解析失败
                case 'RFC822.TEXT':{
                    array_shift($header);
                    $result['RFC822.TEXT'] = implode(' ',$header);
                    $header = [];
                    break;
                }
                case 'RFC822.HEADER':{
                    array_shift($header);
                    $result['RFC822.HEADER'] = implode(' ',$header);
                    $header = [];
                    break;
                }
                case 'BODY[HEADER.FIELDS':{
                    // header
                    $header = implode(' ',$header);
                    preg_match("/^BODY\[HEADER.FIELDS.*\]/Ui",$header,$m);
                    // 结果,提出掉字段字符
                    $result['HEADER.FIELDS'] = ltrim(mb_substr($header,mb_strlen($m[0]),mb_strlen($header)));
                    $header = [];//表示结束了
                    break;
                }
                default:{
                    array_shift($header);
                    break;
                }
            }
        }while($header);

        return $result;
    }

    /**
     * 匹配字段中的值
     * @param $header
     * @param $preg
     * @return mixed|string
     * @author:dc
     * @time 2022/11/23 14:38
     */
    private function pregFiled(&$header,$preg){
        array_shift($header);
        // 组成字符串
        $header = implode(' ',$header);
        // 找flags,非贪婪模式
        if(preg_match($preg,$header,$m)){
            $header = mb_substr($header,mb_strlen($m[0]),mb_strlen($header));
        }
        // 打散成数组
        $header =   explode(' ',$header);

        return $m[1]??'';
    }

    /**
     * imap服务器设置的id
     * @param array|string $id
     * @author:dc
     * @time 2022/11/22 11:53
     */
    public function setImapId($id){
        if(is_array($id)){
            $this->imapId = array_merge($this->imapId,$id);
        }else{
            $this->imapId['name'] = $id;
        }
    }


    /**
     * 打开socket链接
     * @param int $timeout
     * @throws \Exception
     * @author:dc
     * @time 2022/11/22 14:13
     */
    private function socketOpen(int $timeout = 30){
        if(!is_resource($this->socket)){
            $this->timeout = $timeout ? : 30;
            // 链接服务器
            $this->socket = stream_socket_client($this->host, $errno, $error, $this->timeout);

            if($error){
                throw new \Exception("socket error: {$error}");
            }

            if (!$this->socket) {
                throw new \Exception("{$this->host} connection fail.");
            }

            stream_set_timeout($this->socket, $this->timeout);
        }else{
            // 是否超时,超时关闭
            $meta = stream_get_meta_data($this->socket);
            if (isset($meta['timed_out']) && $meta['timed_out'] !== false) {
                $this->socketClose();
                // 重新链接
                $this->socketOpen();
            }
        }
    }


    /**
     * 最后一次执行时候的tag
     * @return string
     * @author:dc
     * @time 2022/11/22 13:54
     */
    protected function getLastTag(){
        return sprintf('%s%d', $this->tagName, $this->tagNum);
    }

    /**
     * 获取下一次执行的tag
     * @return string
     * @author:dc
     * @time 2022/11/22 13:52
     */
    protected function getNextTag(){
        $this->tagNum   +=  1;
        return sprintf('%s%d', $this->tagName, $this->tagNum);
    }

    /**
     * 向服务器发送一个noop消息,表示什么也不做,
     * 只是用来保持通信,避免长久没有操作,丢失掉连接
     * @throws \Exception
     * @author:dc
     * @time 2022/11/25 11:15
     */
    public function noop():bool {
        $status = $this->request('NOOP');
        return $status[0] == 'ok';
    }

    /**
     * debug模式
     * @param bool $debug
     * @param string $logPath
     * @author:dc
     * @time 2022/12/6 11:10
     */
    public function debug($debug=true,$logPath=''){
        $this->debug = $debug;
        $this->debugWritePath = $logPath ? $logPath : storage_path('logs');
    }

    /**
     * 记录日志
     * @param string $message
     * @author:dc
     * @time 2022/12/6 11:13
     */
    private function log(string $message){
        if($this->debug){
            if(!is_dir($this->debugWritePath)){
                @mkdir($this->debugWritePath,0775,true);
            }
            @file_put_contents($this->debugWritePath.'/imap.log',date('Y-m-d H:i:s  ').$message.PHP_EOL,FILE_APPEND);
        }
    }

    /**
     * 获取数据集
     * @param $cmd
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2022/11/22 11:41
     */
    public function request($cmd){
        // 链接
        $this->socketOpen();

        $tag = $this->getNextTag();

        // 命令行必须要结束换行
        $cmd = trim($tag.' '.$cmd);

        // 记录日志
        $this->log($cmd);

        // 写入数据
        $r = fwrite($this->socket, $cmd."\r\n");
        if (!$r) {
            $this->socketClose();
            throw new \Exception("request error write");
        }

        // 读取数据
        $result =   []; // 返回数据
        $status = 'no'; // 状态
        $i = 1;
        while ($i) {
            $append = false;// 是否追加到上一个元素值里面
            // 这里不指定长度,读取一行
            $line = @fgets($this->socket);
            if ($line === false || !mb_strlen($line)) {
                $end = true;
            }else{


                // 是否有其他的固定串
                preg_match("/\{(\d+)\}\r\n$/",$line,$number);
                if($number[1]??0){
                    // 获取到固定字符串
                    $appendLine = fread($this->socket, intval($number[1]));
                    // 替换 {123}
                    $line   =   str_replace($number[0],$appendLine,$line);
                }

                // 记录日志
                $this->log($line);

                // 是否结束了,到了最后一行
                if(strpos($line,$tag.' ') === 0){
                    $end = true;
                    $status = strtolower(explode(' ',$line)[1]??'');
                    // 脚本异常
                    if($status == 'bad'){
                        throw new \Exception('request bad:'.$line);
                    }
                }else{
                    if(strpos($line,'* ') !==0 ){
                        $append = true;
                    }
                }

                // 结果数组
                if($append){
                    $result[$i-1] .= $line;
                }else{
                    $result[$i] = $line;
                    $i++;
                }

            }
            // 结束了
            if(!empty($end)){
                $i = false;
            }
        }

//        print_r($result);

        return [$status,$result];

    }

    /**
     * 是否读取结束
     * @return bool
     * @author:dc
     * @time 2022/11/22 14:14
     */
    protected function socketEof()
    {
        if (!is_resource($this->socket)) {
            return true;
        }

        // If a connection opened by fsockopen() wasn't closed
        // by the server, feof() will hang.
        $start = microtime(true);

        if (feof($this->socket) ||
            ($this->timeout && (microtime(true) - $start > $this->timeout))
        ) {
            $this->socketClose();
            return true;
        }

        return false;
    }

    /**
     * 关闭socket链接
     * @author:dc
     * @time 2022/11/22 10:45
     */
    protected function socketClose(){
        if(is_resource($this->socket)){
            fclose($this->socket);
        }
        $this->socket   =   false;
    }




    public function __construct()
    {
    }

    public function __destruct()
    {
        if($this->auto_colse){
            //        echo '关闭';
            // TODO: Implement __destruct() method.
            $this->socketClose();
        }

    }



}