<?php

/**
 * Copyright 2019 Huawei Technologies Co.,Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License.  You may obtain a copy of the
 * License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 */

namespace Obs\Internal;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\RequestException;
use Obs\ObsException;
use Obs\Internal\Common\Model;
use Obs\Internal\Resource\Constants;
use Obs\Log\ObsLog;
use Psr\Http\Message\StreamInterface;
use Obs\Internal\Common\CheckoutStream;

trait GetResponseTrait
{

	protected $exceptionResponseMode = true;
	
	protected $chunkSize = 65536;
	
	protected function isClientError(Response $response)
	{
		return $response -> getStatusCode() >= 400 && $response -> getStatusCode() < 500;
	}
	
	protected function parseXmlByType($searchPath, $key, &$value, $xml, $prefix)
	{	
		$type = 'string';
		
		if(isset($value['sentAs'])){
			$key = $value['sentAs'];
		}
		
		if($searchPath === null){
			$searchPath = '//'.$prefix.$key;
		}
		
		if(isset($value['type'])){
			$type = $value['type'];
			if($type === 'array'){
				$items = $value['items'];
				if(isset($value['wrapper'])){
				    $paths = explode('/', $searchPath);
				    $size = count($paths);
				    if ($size > 1) {
				        $end = $paths[$size - 1];
				        $paths[$size - 1] = $value['wrapper'];
				        $paths[] = $end;
				        $searchPath = implode('/', $paths) .'/' . $prefix;
				    }
				}
				
				$array = [];
				if(!isset($value['data']['xmlFlattened'])){
					$pkey = isset($items['sentAs']) ? $items['sentAs'] : $items['name'];
					$_searchPath = $searchPath .'/' . $prefix .$pkey;
				}else{
					$pkey = $key;
					$_searchPath = $searchPath;
				}
				if($result = $xml -> xpath($_searchPath)){
					if(is_array($result)){
						foreach ($result as $subXml){
							$subXml = simplexml_load_string($subXml -> asXML());
							$subPrefix = $this->getXpathPrefix($subXml);
							$array[] = $this->parseXmlByType('//'.$subPrefix. $pkey, $pkey, $items, $subXml, $subPrefix);
						}
					}
				}
				return $array;
			}else if($type === 'object'){
				$properties = $value['properties'];
				$array = [];
				foreach ($properties as $pkey => $pvalue){
					$name = isset($pvalue['sentAs']) ? $pvalue['sentAs'] : $pkey;
					$array[$pkey] = $this->parseXmlByType($searchPath.'/' . $prefix .$name, $name, $pvalue, $xml, $prefix);
				}
				return $array;
			}
		}
		
		if($result = $xml -> xpath($searchPath)){
			if($type === 'boolean'){
				return strval($result[0]) !== 'false';
			}else if($type === 'numeric' || $type === 'float'){
				return floatval($result[0]);
			}else if($type === 'int' || $type === 'integer'){
				return intval($result[0]);
			}else{
				return strval($result[0]);
			}
		}else{
			if($type === 'boolean'){
				return false;
			}else if($type === 'numeric' || $type === 'float' || $type === 'int' || $type === 'integer'){
				return null;
			}else{
				return '';
			}
		}
	}

	private function isJsonResponse($response) {
	    return $response -> getHeaderLine('content-type') === 'application/json';
    }

	private function parseCommonHeaders($model, $response){
	    $constants = Constants::selectConstants($this -> signature);
	    foreach($constants::COMMON_HEADERS as $key => $value){
			$model[$value] = $response -> getHeaderLine($key);
		}
	}
	
	protected function parseItems($responseParameters, $model, $response, $body)
	{
		$prefix = '';
		
		$this->parseCommonHeaders($model, $response);
		
		$closeBody = false;
		try{
			foreach ($responseParameters as $key => $value){
				if(isset($value['location'])){
					$location = $value['location'];
					if($location === 'header'){
						$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
						$isSet = false;
						if(isset($value['type'])){
							$type = $value['type'];
							if($type === 'object'){
								$headers = $response -> getHeaders();
								$temp = [];
								foreach ($headers as $headerName => $headerValue){
									if(stripos($headerName, $name) === 0){
									    $metaKey = rawurldecode(substr($headerName, strlen($name)));
									    $temp[$metaKey] = rawurldecode($response -> getHeaderLine($headerName));
									}
								}
								$model[$key] = $temp;
								$isSet = true;
							}else{
								if($response -> hasHeader($name)){
									if($type === 'boolean'){
										$model[$key] = ($response -> getHeaderLine($name)) !== 'false';
										$isSet = true;
									}else if($type === 'numeric' || $type === 'float'){
										$model[$key] = floatval($response -> getHeaderLine($name));
										$isSet = true;
									}else if($type === 'int' || $type === 'integer'){
										$model[$key] = intval($response -> getHeaderLine($name));
										$isSet = true;
									}
								}
							}
						}
						if(!$isSet){
							$model[$key] = rawurldecode($response -> getHeaderLine($name));
						}
					}else if($location === 'xml' && $body !== null){
					    if(!isset($xml) && ($xml = simplexml_load_string($body -> getContents()))){
							$prefix = $this ->getXpathPrefix($xml);
						}
						$closeBody = true;
						$model[$key] = $this -> parseXmlByType(null, $key,$value, $xml, $prefix);
					}else if($location === 'body' && $body !== null){
						if(isset($value['type']) && $value['type'] === 'stream'){
							$model[$key] = $body;
						}else if (isset($value['type']) && $value['type'] === 'json') {
                            $jsonBody = trim($body -> getContents());
                            if ($jsonBody && ($data = json_decode($jsonBody, true))) {
                                if (is_array($data)) {
                                    $model[$key] = $data;
                                } elseif (strlen($data)) {
                                    ObsLog::commonLog(ERROR, "response body %s, and jsonToArray data is %s",$body, $data);
                                }
                            }
                            $closeBody = true;
                        } else {
							$model[$key] = $body -> getContents();
							$closeBody = true;
						}
					}
				}
			}
		}finally {
			if($closeBody && $body !== null){
				$body -> close();
			}
		}
	}
	
	private function writeFile($filePath, StreamInterface &$body, $contentLength)
	{
		$filePath = iconv('UTF-8', 'GBK', $filePath);
		if(is_string($filePath) && $filePath !== '')
		{
			$fp = null;
			$dir = dirname($filePath);
			try{
				if(!is_dir($dir))
				{
					mkdir($dir,0755,true);
				}
				
				if(($fp = fopen($filePath, 'w')))
				{
					while(!$body->eof())
					{
						$str = $body->read($this->chunkSize);
						fwrite($fp, $str);
					}
					fflush($fp);
					ObsLog::commonLog(DEBUG, "write file %s ok",$filePath);
				}
				else{
					ObsLog::commonLog(ERROR, "open file error,file path:%s",$filePath);
				}
			}finally{
				if($fp){
					fclose($fp);
				}
				$body->close();
				$body = null;
			}
		}
	}
	
	private function parseXmlToException($body, $obsException){
		try{
			$xmlErrorBody = trim($body -> getContents());
			if($xmlErrorBody && ($xml = simplexml_load_string($xmlErrorBody))){
				$prefix = $this->getXpathPrefix($xml);
				if ($tempXml = $xml->xpath('//'.$prefix . 'Code')) {
					$obsException-> setExceptionCode(strval($tempXml[0]));
				}
				if ($tempXml = $xml->xpath('//'.$prefix . 'RequestId')) {
					$obsException-> setRequestId(strval($tempXml[0]));
				}
				if ($tempXml = $xml->xpath('//'.$prefix . 'Message')) {
					$obsException-> setExceptionMessage(strval($tempXml[0]));
				}
				if ($tempXml = $xml->xpath('//'.$prefix . 'HostId')) {
					$obsException -> setHostId(strval($tempXml[0]));
				}
			}
		}finally{
			$body -> close();
		}
	}

	private function parseJsonToException($body, $obsException) {
        try {
            $jsonErrorBody = trim($body -> getContents());
            if ($jsonErrorBody && ($data = json_decode($jsonErrorBody, true))) {
                if (is_array($data)) {
                    if ($data['request_id']) {
                        $obsException -> setRequestId(strval($data['request_id']));
                    }
                    if ($data['code']) {
                        $obsException -> setExceptionCode(strval($data['code']));
                    }
                    if ($data['message']) {
                        $obsException -> setExceptionMessage(strval($data['message']));
                    }
                } elseif (strlen($data)) {
                    ObsLog::commonLog(ERROR, "response body %s, and jsonToArray data is %s",$body, $data);
                    $obsException-> setExceptionMessage("Invalid response data，since it is not json data");
                }
            }
        } finally {
            $body -> close();
        }
    }

    private function parseJsonToModel($body, $model) {
	    try{
	        $jsonErrorBody = trim($body -> getContents());
	        if ($jsonErrorBody && ($jsonArray = json_decode($jsonErrorBody, true))) {
	            if (is_array($jsonArray)) {
                    if ($jsonArray['request_id']) {
                        $model['RequestId'] = strval($jsonArray['request_id']);
                    }
                    if ($jsonArray['code']) {
                        $model['Code'] = strval($jsonArray['code']);
                    }
                    if ($jsonArray['message']) {
                        $model['Message']  = strval($jsonArray['message']);
                    }
                } elseif (strlen($jsonArray)) {
                    ObsLog::commonLog(ERROR, "response body %s, and jsonToArray data is %s",$body, $jsonArray);
                    $model['Message'] = "Invalid response data，since it is not json data";
                }
            }
        } finally {
            $body -> close();
        }
    }
	
	private function parseXmlToModel($body, $model){
		try{
			$xmlErrorBody = trim($body -> getContents());
			if($xmlErrorBody && ($xml = simplexml_load_string($xmlErrorBody))){
				$prefix = $this->getXpathPrefix($xml);
				if ($tempXml = $xml->xpath('//'.$prefix . 'Code')) {
					$model['Code'] = strval($tempXml[0]);
				}
				if ($tempXml = $xml->xpath('//'.$prefix . 'RequestId')) {
					$model['RequestId'] = strval($tempXml[0]);
				}
				
				if ($tempXml = $xml->xpath('//'.$prefix . 'HostId')) {
					$model['HostId'] = strval($tempXml[0]);
				}
				if ($tempXml = $xml->xpath('//'.$prefix . 'Resource')) {
					$model['Resource'] = strval($tempXml[0]);
				}
				
				if ($tempXml = $xml->xpath('//'.$prefix . 'Message')) {
					$model['Message']  = strval($tempXml[0]);
				}
			}
		}finally {
			$body -> close();
		}
	}
	
	protected function parseResponse(Model $model, Request $request, Response $response, array $requestConfig)
	{
		$statusCode = $response -> getStatusCode();
		$expectedLength = $response -> getHeaderLine('content-length');
        $responseContentType = $response -> getHeaderLine('content-type');

		$expectedLength = is_numeric($expectedLength) ? floatval($expectedLength) : null;
		
		$body = new CheckoutStream($response->getBody(), $expectedLength);
		
		if($statusCode >= 300){
			if($this-> exceptionResponseMode){
				$obsException= new ObsException();
				$obsException-> setRequest($request);
				$obsException-> setResponse($response);
				$obsException-> setExceptionType($this->isClientError($response) ? 'client' : 'server');
				if ($responseContentType === 'application/json') {
				    $this->parseJsonToException($body, $obsException);
                } else {
                    $this->parseXmlToException($body, $obsException);
                }
				throw $obsException;
			}else{
				$this->parseCommonHeaders($model, $response);
                if ($responseContentType === 'application/json') {
                    $this->parseJsonToModel($body, $model);
                } else {
                    $this->parseXmlToModel($body, $model);
                }
			}
			
		}else{
			if(!empty($model)){
				foreach ($model as $key => $value){
					if($key === 'method'){
						continue;
					}
					if(isset($value['type']) && $value['type'] === 'file'){
					    $this->writeFile($value['value'], $body, $expectedLength);
					}
					$model[$key] = $value['value'];
				}
			}
			
			if(isset($requestConfig['responseParameters'])){
				$responseParameters = $requestConfig['responseParameters'];
				if(isset($responseParameters['type']) && $responseParameters['type'] === 'object'){
					$responseParameters = $responseParameters['properties'];
				}
				$this->parseItems($responseParameters, $model, $response, $body);
			}
		}
		
		$model['HttpStatusCode'] = $statusCode;
		$model['Reason'] = $response -> getReasonPhrase();
	}
	
	protected function getXpathPrefix($xml)
	{
		$namespaces = $xml -> getDocNamespaces();
		if (isset($namespaces[''])) {
			$xml->registerXPathNamespace('ns', $namespaces['']);
			$prefix = 'ns:';
		} else {
			$prefix = '';
		}
		return $prefix;
	}
	
	protected function buildException(Request $request, RequestException $exception, $message)
	{
		$response = $exception-> hasResponse() ? $exception-> getResponse() : null;
		$obsException= new ObsException($message ? $message : $exception-> getMessage());
		$obsException-> setExceptionType('client');
		$obsException-> setRequest($request);
		if($response){
			$obsException-> setResponse($response);
			$obsException-> setExceptionType($this->isClientError($response) ? 'client' : 'server');
			if ($this->isJsonResponse($response)) {
                $this->parseJsonToException($response -> getBody(), $obsException);
            } else {
                $this->parseXmlToException($response -> getBody(), $obsException);
            }
    		if ($obsException->getRequestId() === null) {
    		    $prefix = strcasecmp($this->signature, 'obs' ) === 0 ? 'x-obs-' : 'x-amz-';
    		    $requestId = $response->getHeaderLine($prefix . 'request-id');
    		    $obsException->setRequestId($requestId); 
    		}
		}
		return $obsException;
	}
	
	protected function parseExceptionAsync(Request $request, RequestException $exception, $message=null)
	{
		return $this->buildException($request, $exception, $message);
	}
	
	protected function parseException(Model $model, Request $request, RequestException $exception, $message=null)
	{
		$response = $exception-> hasResponse() ? $exception-> getResponse() : null;
		if($this-> exceptionResponseMode){
			throw $this->buildException($request, $exception, $message);
		}else{
			if($response){
				$model['HttpStatusCode'] = $response -> getStatusCode();
				$model['Reason'] = $response -> getReasonPhrase();
				$this->parseXmlToModel($response -> getBody(), $model);
			}else{
				$model['HttpStatusCode'] = -1;
				$model['Message'] = $exception -> getMessage();
			}
		}
	}
}