<?php
namespace lib;
/**
* 导入类
*
* @version 1.0.0
*/
class import
{
    public $cfg;       //配置
    public $src;       //源文件
    public $srcType;   //类类型 array|file
    public $filename;  //全路径文件名
    public $dirname;   //目录路径
    public $basename;  //文件名
    public $extension; //文件后缀名
    public $noextname; //不包含后缀的文件名
    public $outfilename='';  //输出的文件名
    public $codepage='';  //文件编码
    public $lastError='';  //最后出错信息
    public $lineNum;   //当前行
    public $rules;     //规则
    public $header = []; //表头
    public $keys = []; //键集
    public $excelData = []; //excel 数据
    public $srcData = null; //excel 数据
    public $data = []; //主数据
    public $cacheCell = []; //全局单元格值查询缓存

	public function __construct()
	{
       ;
    }

    /**
     * xls|xlsv转csv
     * @param string $src  源文件名
     * @param string $dist 目录文件名
     * https://xlswriter-docs.viest.me/zh-cn/kuai-su-shang-shou/chuang-jian-wen-jian
     * @return bool
     */
	public function xlsToCsv($src,$dist)
	{
	    $piSrc = pathinfo($src);
	    if(strtolower($src)==strtolower($dist)){
	        $this->lastError = '目标文件不能与源文件同名 ['.$src.']';
	        return false;
	    }
	    if(strtolower($piSrc['extension'])=='csv'){
	        if(false == copy($src,$dist)){
    	        $this->lastError = '生成目标文件出错 ['.$dist.']';
	        }
	        return true;
	    }
	    
       try{
    	    $config   = ['path' => $piSrc['dirname']];
    	    $excel  = new \Vtiful\Kernel\Excel($config);
            $fp = fopen($dist, 'w');
            /*
            $res = $excel->openFile($piSrc['basename'])->openSheet()->setType(
                            [
                                \Vtiful\Kernel\Excel::TYPE_STRING,
                                \Vtiful\Kernel\Excel::TYPE_INT,
                                \Vtiful\Kernel\Excel::TYPE_DOUBLE,
                                \Vtiful\Kernel\Excel::TYPE_TIMESTAMP
                            ])->putCSV($fp);
            */
            $errorType = error_reporting();
            error_reporting(0); //先屏蔽错误
            $res = $excel->openFile($piSrc['basename'])->openSheet()->putCSV($fp); //DLL 调用
            error_reporting($errorType); //恢复之前错误提示级别
            if( $res == false ){
                $this->lastError = '打开EXCEL文件出错 ['.$dist.']';
                return false;
            }
            fclose($fp);
        }catch(Exception $e){
            fclose($fp);
        }
        return true;
	}
    /**
     * 找开待导入的文件
     * @param string $src  源文件名
     * @return bool
     */
	public function open($src)
	{
        $piSrc = pathinfo($src);
        $this->filename = str_replace('\\','/',$src);
        $this->src = $this->filename;
        $this->dirname = str_replace('\\','/',$piSrc['dirname']);
        $this->basename = $piSrc['basename'];
        $this->extension = strtolower($piSrc['extension']);
        $this->noextname = $piSrc['filename'];
	    if(!in_array($this->extension,['csv','xls','xlsx'])){
	        $this->lastError = '['.$src.'] 不支持的文件类型,仅支持 .csv .xls .xlsx';
	        return false;
	    }
        if($this->extension !='csv' ){
            $this->filename = sprintf('%s/%s.csv', $this->dirname, $this->noextname);
            if($this->xlsToCsv($src, $this->filename)){
                 $this->extension = 'csv';
                 $this->basename = $this->noextname.'.'.$this->extension;
                 $this->codepage = 'UTF-8';
            }else{
                return false;
            }
        }
        return true;
	}
	

    /**
     * 自定义 csv 读取
     * @return var
     */
    public function _fgetcsv(&$fp, $length = null, $d = ',', $e = '"') {
         $d = preg_quote($d);
         $e = preg_quote($e);
         $_line = "";
         $eof=false;
         while ($eof != true) {
             $_line .= (empty ($length) ? fgets($fp) : fgets($fp, $length));
             $itemcnt = preg_match_all('/' . $e . '/', $_line, $dummy);
             if ($itemcnt % 2 == 0)
                 $eof = true;
         }
         $_csv_line = preg_replace('/(?: |[ ])?$/', $d, trim($_line));
         $_csv_pattern = '/(' . $e . '[^' . $e . ']*(?:' . $e . $e . '[^' . $e . ']*)*' . $e . '|[^' . $d . ']*)' . $d . '/';
         preg_match_all($_csv_pattern, $_csv_line, $_csv_matches);
         $_csv_data = $_csv_matches[1];
         for ($_csv_i = 0; $_csv_i < count($_csv_data); $_csv_i ) {
             $_csv_data[$_csv_i] = preg_replace('/^' . $e . '(.*)' . $e . '$/s', '$1' , $_csv_data[$_csv_i]);
             $_csv_data[$_csv_i] = str_replace($e . $e, $e, $_csv_data[$_csv_i]);
         }
         return empty ($_line) ? false : $_csv_data;
    }

    /**
     * 分析键名
     * @param array $arr  
     * @param array $rules  
     * @return bool
     */
	public function createKeys($arr)
	{
	    $this->header = $arr;
	    foreach($arr as $k=>$v){
	        $key = array_search($v, array_column($this->rules,'caption','key'));
	        if($key!==false){
	            $this->keys[$k] = $key;
	        }
	    }
	    foreach ($this->rules as $k=>$v){
	        if($v['must'] && !in_array($v['key'],$this->keys)){
    	        $this->lastError = '没在发现必填列 ['.$v['caption'].']';
    	        return false;
	        }
	        if($v['mustCols']){
	            foreach ($v['mustCols'] as $mc){
        	        if(!in_array($mc,$this->keys)){
            	        $this->lastError = '当 ['.$v['caption'].'] 存在时,需要 ['.$this->rules[$mc]['caption'].'] 也存在';
            	        return false;
        	        }
	            }
	        }
	    }
	    if(empty($this->keys)){
	        $this->lastError = '失败,没有任何配置或内容';
	        return false;
	    }
	    return true;
	}

    /**
     * 检测数据
     * @param array $arr  
     * @param array $rules  
     * @return bool
     */
	public function checkItem(&$arr)
	{
	   $result = true;
	   $msg = '';
       foreach($arr as $k=>&$v){
            $v = trim(html_entity_decode($v));
            $old = $v;
            if($v!='' && $v!='!'  && $v!='！' && isset($this->rules[$k]['formatCell']) && !is_null($this->rules[$k]['formatCell'])){
                $c = $this->rules[$k]['formatCell'];
                if(!is_array($c)) $c = $this->cfg['formatCell'][$c];
                $params = [];
                foreach($c[3] as $p){
                    if($p=='@') array_push($params,$v);
                    else if(substr($p,0,1)=='@') array_push($params,$arr[substr($p,1)]);
                    else array_push($params,$p);
                }
                $cacheKey = md5(serialize($c).serialize($params));
                if(isset($this->cacheCell[$cacheKey])){
                    $v = $this->cacheCell[$cacheKey];
                }else{
                    $makeObject = makeOneArr($c[0],$c[1]);
                    $res = call_user_func_array ( array ( $makeObject , $c[2] ), $params );
                    if(!is_null($c[4]))
                        $v = $res[$c[4]];
                    else
                        $v = $res;
                    if(is_null($v)) $v = '';
                    $this->cacheCell[$cacheKey] = $v;
                }
            }
            if( isset($this->rules[$k]['must']) && $this->rules[$k]['must'] && ($v=='' || $v=='!' || $v=='！') ){
                if($old==''){
                    $msg = '['.$this->rules[$k]['caption'].'] 值不能为空';
                }else
                    $msg = '['.$this->rules[$k]['caption'].'] 系统未找到匹配项[ '.$old.' ]';
                $result = false;
            }else if( $old != '' && $old != '!' && $v==='' ){
                $msg = '['.$this->rules[$k]['caption'].'] 系统未找到匹配项[ '.$old.' ] codepage:'.$this->codepage; 
                $result = false;
            }else if($v!=='' && isset($this->rules[$k]['minlen']) && $this->rules[$k]['minlen']>0 && (strlen($v) < $this->rules[$k]['minlen']) ){
                $msg = '['.$this->rules[$k]['caption'].'] 最小长度为 ['.$this->rules[$k]['minlen'].'], 当前值为[ '.$v.' ]';
                $result = false;
            }else if($v!=='' && isset($this->rules[$k]['maxlen']) && $this->rules[$k]['maxlen']>0 && (strlen($v) > $this->rules[$k]['maxlen']) ){
                $msg = '['.$this->rules[$k]['caption'].'] 最大长度为 ['.$this->rules[$k]['maxlen'].'], 当前值为[ '.$v.' ]';
                $result = false;
            }else if($v!=='' && isset($this->rules[$k]['type']) && $this->rules[$k]['type']=='number' && !is_numeric($v) ){
                $msg = '['.$this->rules[$k]['caption'].'] 必须是一个数字, 当前值为[ '.$v.' ]';
                $result = false;
            }else if( isset($this->rules[$k]['type']) && $this->rules[$k]['type']=='date' && $v!=''){
                if(is_numeric($v)){
                    $ut = strtotime('1900-1-1') + ($v - 1) * 24 * 3600;
                }else{
                    $ut = strtotime($v);
                }
                if(!empty($ut)){
        		    $v = date(\mvc::$cfg['dateformat'], $ut);
                }else{
                    $msg = '['.$this->rules[$k]['caption'].'] 日期格式不正确, 当前值为[ '.$v.' ]';
                    $result = false;
                }
            }else if( isset($this->rules[$k]['type']) && $this->rules[$k]['type']=='time' && $v!=''){
                if(is_numeric($v)){
                    $ut = strtotime('1900-1-1') + ($v - 1) * 24 * 3600;
                }else{
                    $ut = strtotime($v);
                }
                if(!empty($ut)){
        		    $v = date(\mvc::$cfg['dateformat'], $ut);
                }else{
                    $msg = '['.$this->rules[$k]['caption'].'] 时间格式不正确, 当前值为[ '.$v.' ]';
                    $result = false;
                }
            }else if( isset($this->rules[$k]['type']) && $this->rules[$k]['type']=='datetime' && $v!=''){
                if(is_numeric($v)){
                    $ut = strtotime('1900-1-1') + ($v - 1) * 24 * 3600;
                }else{
                    $ut = strtotime($v);
                }
                if(!empty($ut)){
        		    $v = date(\mvc::$cfg['datetimeformat'], $ut);
                }else{
                    $msg = '['.$this->rules[$k]['caption'].'] 日期时间格式不正确, 当前值为[ '.$v.' ]';
                    $result = false;
                }
            }else if( isset($this->rules[$k]['checkfunction']) && !is_null($this->rules[$k]['checkfunction']) ){
                $result = $this->rules[$k]['checkfunction']($arr, $k, $v, $this);
            }
        }
        if($msg!='') $this->lastError = $msg;
        $arr['excelInfo']['line'] = $this->lineNum;
        $arr['excelInfo']['error'] = $msg;
	    return $result;
	}
	
	
    /**
     * 检测编码
     * @param string $str
     * @return string
     */
	public function getCodepageByString($str)
	{
	    $clist = ['GB18030','GB2312','ASCII','UTF-8','CP1251','CP1252'];
	    foreach($clist as $codpage){
	        $tmp = mb_convert_encoding($str, $codpage, $codpage);
	        if(md5($tmp) === md5($str) ){
	            return $codpage;
	        }
	    }
        return '';
	}
	/**
	 * 重复项检测
     * @param array|string $keys  例:  'code', ['code','name'], [['code','name'],['seq']]
     * @param bool $ignoreDup 是否忽略重复
	 * @return bool 返回true则有重复
	 */
	public function checkRepeat($keys,$ignoreDup=false){
		if($this->srcType=='') {
			throw new \Exception ( '须先执行 execute' );
		}
		if(!is_array($keys)) $keys = [$keys];
		if(!is_array($keys[0])) $keys = [$keys];
		foreach($keys as $group){
    		$arr = [];
    		$hasCol = false; //是否存在规则中的列
		    foreach($this->data as $i=>$item){
		        $md5 = '';
		        foreach ($item as $field => $value){
    		        if($value!='' && in_array($field,$group)){
    		            if(in_array($field,['s_apply_number','s_arri_number'])){
    		                $value = formatNum($value);
    		            }
    		            $md5 .= $value;
    		            $hasCol = true;
    		        }
		        }
		        if($md5!='' && $hasCol){
    		        $md5 = md5($md5);
    		        $arr[$i] = $md5;
		        }
		    }
		    $uniqueArr = array_unique($arr);
		    if($hasCol && count($uniqueArr) != count($arr)){
		        if($ignoreDup==false){
    		        $repeatArr = array_diff_assoc($arr,$uniqueArr);
    		        foreach(array_keys($repeatArr) as $i){
    		            $keyCaptions = [];
                        foreach($group as $v){
                            $keyCaptions[] = $this->rules[$v]['caption'];
                        }
                        $this->data[$i]['excelInfo']['error'] = '重复提交的数据,[ '.implode(',',$keyCaptions).' ]';
    		        }
    		        $this->lastError = $this->data[$i]['excelInfo']['error'];
    		        return true;
		        }else{
		            unset($this->data[$i]);
		        }
		    }
		}
        return false;
	}
	
    /**
     * 导入Csv文件
     * @param string $src  源文件名
     * @return bool
     */
	public function loadCsvFile($src)
	{
	    $result = true;
        if($this->open($src)){
            try{
                $fp = fopen($this->filename, 'r');
                if($fp){
                    $first = null;
                    $this->lineNum = 0;
                    while (($lineStr= stream_get_line($fp, 4096,"\r\n")) !== false) {
                        if($this->codepage==''){
                            $this->codepage = $this->getCodepageByString($lineStr);
                        }
                        if($this->codepage!='UTF-8'){
                            $lineStr = iconv($this->codepage, 'UTF-8//IGNORE', $lineStr);
                            //$lineStr = mb_convert_encoding($lineStr, 'UTF-8', $this->codepage);
                        }
                        $arr = str_getcsv($lineStr, ',','"','\\');
                        $this->lineNum++;
                        if(is_null($first)){
                            $first = $arr;
                            if(false == $this->createKeys($first)){
                                fclose($fp);
                                return false;
                            }
                            continue;
                        }
                        $item = [];
                        foreach($this->keys as $i=>$key){
                            $item[$key] = str_replace('"','\"',trim($arr[$i]));
                        }
                        if(!$this->checkItem($item)){
                            $result = false;
                        }
                        array_push($this->excelData,$arr);
                        array_push($this->data,$item);
                    }
                    if (!feof($fp)) {
            	        $this->lastError = '['.$src.'] 读取文件数据失败,未读取完整内容';
            	        $result = false;
                    }
                    fclose($fp);
                }
            }catch(Exception $e){
     	        $this->lastError = '['.$src.'] 读取文件出现未知错误';
                fclose($fp);
                return false;
            }
        }else{
            return false;
        }
        return $result;
	}
	
    /**
     * 导入 Xlsx 文件
     * @param string $src  源文件名
     * @return bool
     */
	public function loadXlsxFile($src)
	{
	   $piSrc = pathinfo($src);
       try{
    	    $config   = ['path' => $piSrc['dirname']];
    	    $excel  = new \Vtiful\Kernel\Excel($config);
    	    $excel->openFile($piSrc['basename'])->openSheet();
    	    $header = $excel->nextRow();
    	    $cellTypes = [];
    	    foreach($header as $k=>$v){
    	        $cellTypes[] = \Vtiful\Kernel\Excel::TYPE_STRING;
    	    }
    	    $excel->setType($cellTypes);
            $res = $excel->getSheetData();
            array_unshift($res,$header);
            if( $res == false ){
                $this->lastError = '打开EXCEL文件出错 ['.$dist.']';
                return false;
            }
        }catch(Exception $e){
            $this->lastError = '打开EXCEL文件出错 ['.$dist.']';
            return false;
        }
        return $this->loadArray($res);
	}

   /**
     * 导入数组
     * @param array $src  数组数据
     * @return bool
     */
	public function loadArray($src)
	{
        $first = null;
        $this->lineNum = 0;
        $result = true;
		if($this->srcData) $this->srcData = $src;
        foreach ($src as $arr) {
            $this->lineNum++;
            $hasvalue = false;
            foreach($arr as $v){
                if($v!='') $hasvalue = true;
            }
            if(!$hasvalue){
                continue;
            }
            if(is_null($first)){
                $first = $arr;
                if(false == $this->createKeys($first)){
                    return false;
                }
                continue;
            }
            $item = [];
			$reArr = ['&nbsp;'=>''];
            foreach($this->keys as $i=>$key){
				$arr[$i] = strtr($arr[$i], $reArr);
                $item[$key] = trim($arr[$i]);
            }
            if(!$this->checkItem($item)){
                $result = false;
            }
            array_push($this->excelData,$arr);
            array_push($this->data,$item);
        }
        return $result;
	}

    /**
     * 导入 主入口
     * @param string|array $src  源文件名|数组值
     * @param array $rules  相关规则
     * @return bool
     */
	public function execute($src,$rules)
	{
	    \mvc::loadConfig('import');
	    $this->cfg = \mvc::$cfg['import'];
        foreach($rules as $k=>$v){
            $this->rules[$v['key']] = $v;
        }
        if(is_array($src)){
            $this->srcType = 'array';
            $result = $this->loadArray($src);
        }else{
            $this->srcType = 'file';
	        $this->src = $src;
	        $this->filename = $src;
            $extname = end(explode('.',strtolower($src)));
            if($extname=='xls' || $extname == 'xlsx'){
                $result = $this->loadXlsxFile($src);
            }else{
                $result = $this->loadCsvFile($src);
            }
        }
	    if($result && empty($this->data)){
	        $this->lastError = '没有数据';
	        $result = false;
	    }
        return $result;
	}
	

    /**
     * 写入内容
     * @return string filename
     */
	public function output()
	{
	    if($this->srcType == 'file'){
    	    if($this->lastError==='' && empty($this->data)){
    	        $this->lastError = '没有数据';
    	    }
    	    if($this->lastError!==''){
    	        $this->outfilename = $this->src . '.back.csv';
        	    //@unlink($this->src);
        	    $fp = fopen($this->outfilename, 'w');
        	    if(empty($this->data)){
        	        fputcsv($fp, [$this->lastError]);
        	    }else{
            	    $this->header[] = '错误信息';
                    fputcsv($fp, $this->header, ',','"');
            	    foreach($this->data as $i=>$v){
            	        foreach($this->excelData[$i] as $ei=>$ev){
            	            $this->excelData[$i][$ei] .=''; 
            	        }
            	        $this->excelData[$i][] = $v['excelInfo']['error'];
            	        fputcsv($fp, $this->excelData[$i], ',','"');
            	    }
        	    }
        	    fclose($fp);
        	    return $this->outfilename;
    	    }else{
        	    return '';
    	    }
	    }else if($this->srcType=='array'){
	        return $this->data;
	    }
	}
}