<?php
/**
 * 透過Socket與Soft通訊
 *
 * auth: izumi.wang
 * date: 20181128
*/
namespace SunwareCore;
use Exception;

trait OPC_Soft {
	private $Port;
	private $Host;
	private $isLog;
	private $Path  = './log/';
	Private $Fname = 'OPC_Soft.log';
	## 建構子
	public function __construct ($host_name='localhost', $tcp_port='9001', $is_Log=true) {
        $this->SetCFG($host_name, $tcp_port, $is_Log);
	}
	## 設定參數
	public function SetCFG ($host, $port, $bool) {
		$this->Port = (int)$port;
		$this->Host = gethostbyname($host);
		$this->isLog = (bool)$bool;
		if (!is_dir($this->Path) && $this->isLog) 
			@mkdir($this->Path);
	}
	## 取得參數
	public function GetCFG () {
		return array(
			'host'  => $this->Host,
			'port'  => $this->Port,
			'isLog' => $this->isLog,
		);
	}
	## Soft Commend
	public function Commend ($cmd) {
		try {
			if (!$chk=$this->Parser($cmd))
				throw new Exception('Commend Unrecognized');
			if (!$chk['Points_Size'])
				throw new Exception('Point Size Is Zero');
			/*
			## 寫入Soft add by retry
			for ($rti = 0; $rti < 3; $rti++) {
				if (!$rtn=$this->sendBySocket($cmd)) {
					break;
				}
				usleep(200);
			}
			return $rtn;
			*/
			return $this->sendBySocket($cmd);
		} catch (Exception $e) {
			if ($this->isLog) {
				file_put_contents($this->Path.$this->Fname, sprintf('[%s] Host = %s , Port = %d , Cmd = %s , Error = %s , Line = %d', 
					date('Y-m-d H:i:s'), $this->Host, $this->Port, $cmd, $e->getMessage(), $e->getLine())."\r\n", FILE_APPEND);
			}
			
			return '';
		}
	}
	## 數量過多拆解
	public function Slice ($points, $len=999) {
		$len = min($len, 999);
		if (!$len) {
			return array();
		}
		$rtn = array();
		$total = count($points);
		$end = ceil($total / $len);
		$pots_key = array_keys($points);
		for($_idx = 0; $_idx < $end; $_idx++) {
			$tmp = array();
			foreach(array_slice($pots_key, ($_idx * $len), $len) as $_k) {
				$tmp[$_k] = $points[$_k];
			}
			$rtn[] = $tmp;
		}
		return $rtn;
	}
	## Soft Commend 解析
	public function Parser ($cmd, $only_tag=false) {
		$rtn = array();
		if (strlen($cmd) && in_array(substr($cmd, 0, 1), array('R', 'W'))) {
			$rtn['Action_Type'] = substr($cmd, 0, 1);
			$amt = min((int)substr($cmd, 1, 3), 999);
			$tag = explode(';', substr($cmd, 4, -1));
			for ($i=0; $i<$amt; $i++) {
				if (!isset($tag[$i]) || !$tag[$i]) {
					continue;
				}
				$softTag = substr($tag[$i], 0, 6);
				$softval = substr($tag[$i], 10);
				switch (substr($softTag, 0, 1)) {
					case 'R':
						$softval = $softval=='on' ? true : false;
					case 'S':
					case 'D':
						if (!$only_tag) 
							$rtn[substr($softTag, 0, 1).'_Points'][] = $softTag;
						
						$rtn[$softTag] = array(
							'decimal' => (int)substr($tag[$i], 6, 2),
							'value'   => trim($softval),
						);
						break;					
				}
			}
			
			if (!$only_tag) {
				$rtn['Points_Size'] = 0;
				foreach (array('D', 'R', 'S') as $t)
					if (isset($rtn[$t.'_Points']) && is_array($rtn[$t.'_Points']))
						$rtn['Points_Size'] += count($rtn[$t.'_Points']);
			}
		}
		
		return $rtn;
	}
	## 輸出 Commend
	public function Stringify ($points=array(), $isWrite=false) {
		try {
			$softCmd = '';
			if ($points && is_array($points)) {
				$tmp = array();
				foreach ($points as $tag=>$arr) {
					if ((strlen($tag) == 6 || strlen($tag) == 5) && 
						in_array(substr($tag, 0, 1), array('D', 'R', 'S')) && 
						is_numeric(substr($tag, 1))
					) {
						$decimal = 0;
						$value   = $arr;
						## 輸入陣列
						if (is_array($arr)) {
							$decimal = isset($arr['decimal']) ? (int)$arr['decimal'] : 0;
							$value   = isset($arr['value']) ? (string)$arr['value'] : '';
						}
						## R點 true值 轉小寫
						if (substr($tag, 0, 1) == 'R') 
							$value = in_array(strtolower($value), array(1, '1', true, 'true', 't', 'on'), true) ? 'on' : 'off';
						
						if (!$isWrite) 
							$value = '';
						
						## 組合完成
						$tmp[] = sprintf('%s%05d%02d', substr($tag, 0, 1), (int)substr($tag, 1), $decimal).'  '.sprintf('%s;', $value);
					}
					
					if (count($tmp) >= 999)
						break;
				}
				/*switch ($points['Action_Type']) {
					case 'R':
					case 'W':					
						foreach ($points as $tag=>$arr) {
							if ((strlen($tag) == 6 || strlen($tag) == 5) && 
								in_array(substr($tag, 0, 1), array('D', 'R', 'S')) && 
								is_numeric(substr($tag, 1))
							) {
								$decimal = 0;
								$value   = $arr;
								## 輸入陣列
								if (is_array($arr)) {
									$decimal = isset($arr['decimal']) ? (int)$arr['decimal'] : 0;
									$value   = isset($arr['value']) ? (string)$arr['value'] : '';
								}
								## R點 true值 轉小寫
								if (substr($tag, 0, 1) == 'R') 
									$value = in_array(strtolower($value), array(1, '1', true, 'true', 't', 'on'), true) ? 'on' : 'off';
								
								if ($points['Action_Type'] == 'R') 
									$value = '';
								
								## 組合完成
								$tmp[] = sprintf('%s%05d%02d', substr($tag, 0, 1), (int)substr($tag, 1), $decimal).'  '.sprintf('%s;', $value);
							}
							
							if (count($tmp) >= 999)
								break 2;
						}
						break;
				}
				*/
				if (count($tmp) > 0)
					$softCmd = ($isWrite ? "W" : "R").sprintf('%03d', count($tmp)).implode('', $tmp);
			
			}
			
			return $softCmd;
		} catch (Exception $e) {
			if ($this->isLog) {
				file_put_contents($this->Path.$this->Fname, sprintf('[%s] Host = %s , Port = %d , Points = %s , Error = %s , Line = %d', 
					date('Y-m-d H:i:s'), $this->Host, $this->Port, $cmd, $e->getMessage(), $e->getLine())."\r\n", FILE_APPEND);
			}
			
			return '';
		}
	}
	## Use PHP Socket
	private function sendBySocket ($input='') {
		## 建立Socket
		if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) 
			throw new Exception('socket_create() Error ['.socket_strerror(socket_last_error()).']');
		## 設置 Socket  Timeout
		## 發送
		socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 3, "usec" => 0));
		## 接收
		socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 5, "usec" => 0));
		## Socket 連線
		if (($result = socket_connect($socket, $this->Host, $this->Port)) === false) 
			throw new Exception('socket_connect() Error ['.socket_strerror(socket_last_error($socket)).']');
		## Socket 寫入失敗
		if(socket_write($socket, $input, strlen($input)) <= 0) {
			socket_close($socket);
			throw new Exception('socket_write() Error');	
		}
		## 資料量大時會有一次無法讀取完成的現象
		if (($bytes = socket_recv($socket, $buf, 8192, MSG_PEEK)) === false) {
			socket_close($socket);
			throw new Exception('socket_recv() Error ['.socket_strerror(socket_last_error($socket)).']');
		}	
		## 檢查第一次進來的 bytes
		$last_bytes = $bytes;
		## 執行次數
		$recv_cnt = 0;
		while(1) {
			if (($bytes = socket_recv($socket, $buf, 8192, MSG_PEEK)) === false) {
				socket_close($socket);
				throw new Exception('socket_recv() Error ['.socket_strerror(socket_last_error($socket)).']');
			}
			
			$recv_cnt++;
			if ($last_bytes == $bytes) {
				socket_close($socket);
				return $buf;
			}
			
			$last_bytes= $bytes;
			if ($recv_cnt > 5) {
				socket_close($socket);
				throw new Exception($buf.' [bytes='.$bytes.']');
			}
		}
		
		return '';
	}
}
?>