<?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 '';
	}
}
