<?php
namespace Anowave\Ec\vendor\Google\IO;

use Anowave\Ec\vendor\Google\Client as Google_Client;
use Anowave\Ec\vendor\Google\Http\Request as Google_Http_Request;
use Anowave\Ec\vendor\Google\Http\CacheParser as Google_Http_CacheParser;

abstract class AbstractIO
{
    const UNKNOWN_CODE = 0;
    const FORM_URLENCODED = 'application/x-www-form-urlencoded';
    private static $CONNECTION_ESTABLISHED_HEADERS = array(
        "HTTP/1.0 200 Connection established\r\n\r\n",
        "HTTP/1.1 200 Connection established\r\n\r\n",
    );
    private static $ENTITY_HTTP_METHODS = array(
        "POST" => null,
        "PUT" => null
    );
    private static $HOP_BY_HOP = array(
        'connection' => true,
        'keep-alive' => true,
        'proxy-authenticate' => true,
        'proxy-authorization' => true,
        'te' => true,
        'trailers' => true,
        'transfer-encoding' => true,
        'upgrade' => true
    );
    
    /** @var Google_Client */
    protected $client;
    
    public function __construct(Google_Client $client)
    {
        $this->client = $client;
        $timeout = $client->getClassConfig('Google_IO_Abstract', 'request_timeout_seconds');
        if ($timeout > 0)
        {
            $this->setTimeout($timeout);
        }
    }
    
    /**
     * Executes a Google_Http_Request
     * @param Google_Http_Request $request the http request to be executed
     * @return array containing response headers, body, and http code
     * @throws Google_IO_Exception on curl or IO error
     */
    abstract public function executeRequest(Google_Http_Request $request);
    
    /**
     * Set options that update the transport implementation's behavior.
     * @param $options
     */
    abstract public function setOptions($options);
    
    /**
     * Set the maximum request time in seconds.
     * @param $timeout in seconds
     */
    abstract public function setTimeout($timeout);
    
    /**
     * Get the maximum request time in seconds.
     * @return timeout in seconds
     */
    abstract public function getTimeout();
    
    /**
     * Test for the presence of a cURL header processing bug
     *
     * The cURL bug was present in versions prior to 7.30.0 and caused the header
     * length to be miscalculated when a "Connection established" header added by
     * some proxies was present.
     *
     * @return boolean
     */
    abstract protected function needsQuirk();
    
    /**
     * @visible for testing.
     * Cache the response to an HTTP request if it is cacheable.
     * @param Google_Http_Request $request
     * @return bool Returns true if the insertion was successful.
     * Otherwise, return false.
     */
    public function setCachedRequest(Google_Http_Request $request)
    {
        // Determine if the request is cacheable.
        if (Google_Http_CacheParser::isResponseCacheable($request))
        {
            $this
            ->client
            ->getCache()
            ->set($request->getCacheKey() , $request);
            return true;
        }
        
        return false;
    }
    
    /**
     * Execute an HTTP Request
     *
     * @param Google_Http_Request $request the http request to be executed
     * @return Google_Http_Request http request with the response http code,
     * response headers and response body filled in
     * @throws Google_IO_Exception on curl or IO error
     */
    public function makeRequest(Google_Http_Request $request)
    {
        // First, check to see if we have a valid cached version.
        $cached = $this->getCachedRequest($request);
        if ($cached !== false && $cached instanceof Google_Http_Request)
        {
            if (!$this->checkMustRevalidateCachedRequest($cached, $request))
            {
                return $cached;
            }
        }
        
        if (array_key_exists($request->getRequestMethod() , self::$ENTITY_HTTP_METHODS))
        {
            $request = $this->processEntityRequest($request);
        }
        
        list($responseData, $responseHeaders, $respHttpCode) = $this->executeRequest($request);
        
        if ($respHttpCode == 304 && $cached)
        {
            // If the server responded NOT_MODIFIED, return the cached request.
            $this->updateCachedRequest($cached, $responseHeaders);
            return $cached;
        }
        
        if (!isset($responseHeaders['Date']) && !isset($responseHeaders['date']))
        {
            $responseHeaders['date'] = date("r");
        }
        
        $request->setResponseHttpCode($respHttpCode);
        $request->setResponseHeaders($responseHeaders);
        $request->setResponseBody($responseData);
        // Store the request in cache (the function checks to see if the request
        // can actually be cached)
        $this->setCachedRequest($request);
        return $request;
    }
    
    /**
     * @visible for testing.
     * @param Google_Http_Request $request
     * @return Google_Http_Request|bool Returns the cached object or
     * false if the operation was unsuccessful.
     */
    public function getCachedRequest(Google_Http_Request $request)
    {
        if (false === Google_Http_CacheParser::isRequestCacheable($request))
        {
            return false;
        }
        
        return $this
        ->client
        ->getCache()
        ->get($request->getCacheKey());
    }
    
    /**
     * @visible for testing
     * Process an http request that contains an enclosed entity.
     * @param Google_Http_Request $request
     * @return Google_Http_Request Processed request with the enclosed entity.
     */
    public function processEntityRequest(Google_Http_Request $request)
    {
        $postBody = $request->getPostBody();
        $contentType = $request->getRequestHeader("content-type");
        
        // Set the default content-type as application/x-www-form-urlencoded.
        if (false == $contentType)
        {
            $contentType = self::FORM_URLENCODED;
            $request->setRequestHeaders(array(
                'content-type' => $contentType
            ));
        }
        
        // Force the payload to match the content-type asserted in the header.
        if ($contentType == self::FORM_URLENCODED && is_array($postBody))
        {
            $postBody = http_build_query($postBody, '', '&');
            $request->setPostBody($postBody);
        }
        
        // Make sure the content-length header is set.
        if (!$postBody || is_string($postBody))
        {
            $postsLength = strlen($postBody);
            $request->setRequestHeaders(array(
                'content-length' => $postsLength
            ));
        }
        
        return $request;
    }
    
    /**
     * Check if an already cached request must be revalidated, and if so update
     * the request with the correct ETag headers.
     * @param Google_Http_Request $cached A previously cached response.
     * @param Google_Http_Request $request The outbound request.
     * return bool If the cached object needs to be revalidated, false if it is
     * still current and can be re-used.
     */
    protected function checkMustRevalidateCachedRequest($cached, $request)
    {
        if (Google_Http_CacheParser::mustRevalidate($cached))
        {
            $addHeaders = array();
            if ($cached->getResponseHeader('etag'))
            {
                // [13.3.4] If an entity tag has been provided by the origin server,
                // we must use that entity tag in any cache-conditional request.
                $addHeaders['If-None-Match'] = $cached->getResponseHeader('etag');
            }
            elseif ($cached->getResponseHeader('date'))
            {
                $addHeaders['If-Modified-Since'] = $cached->getResponseHeader('date');
            }
            
            $request->setRequestHeaders($addHeaders);
            return true;
        }
        else
        {
            return false;
        }
    }
    
    /**
     * Update a cached request, using the headers from the last response.
     * @param Google_Http_Request $cached A previously cached response.
     * @param mixed Associative array of response headers from the last request.
     */
    protected function updateCachedRequest($cached, $responseHeaders)
    {
        $hopByHop = self::$HOP_BY_HOP;
        if (!empty($responseHeaders['connection']))
        {
            $connectionHeaders = array_map('strtolower', array_filter(array_map('trim', explode(',', $responseHeaders['connection']))));
            $hopByHop += array_fill_keys($connectionHeaders, true);
        }
        
        $endToEnd = array_diff_key($responseHeaders, $hopByHop);
        $cached->setResponseHeaders($endToEnd);
    }
    
    /**
     * Used by the IO lib and also the batch processing.
     *
     * @param $respData
     * @param $headerSize
     * @return array
     */
    public function parseHttpResponse($respData, $headerSize)
    {
        // check proxy header
        foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header)
        {
            if (stripos($respData, $established_header) !== false)
            {
                // existed, remove it
                $respData = str_ireplace($established_header, '', $respData);
                // Subtract the proxy header size unless the cURL bug prior to 7.30.0
                // is present which prevented the proxy header size from being taken into
                // account.
                if (!$this->needsQuirk())
                {
                    $headerSize -= strlen($established_header);
                }
                break;
            }
        }
        
        if ($headerSize)
        {
            $responseBody = substr($respData, $headerSize);
            $responseHeaders = substr($respData, 0, $headerSize);
        }
        else
        {
            $responseSegments = explode("\r\n\r\n", $respData, 2);
            $responseHeaders = $responseSegments[0];
            $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : null;
        }
        
        $responseHeaders = $this->getHttpResponseHeaders($responseHeaders);
        return array(
            $responseHeaders,
            $responseBody
        );
    }
    
    /**
     * Parse out headers from raw headers
     * @param rawHeaders array or string
     * @return array
     */
    public function getHttpResponseHeaders($rawHeaders)
    {
        if (is_array($rawHeaders))
        {
            return $this->parseArrayHeaders($rawHeaders);
        }
        else
        {
            return $this->parseStringHeaders($rawHeaders);
        }
    }
    
    private function parseStringHeaders($rawHeaders)
    {
        $headers = array();
        $responseHeaderLines = explode("\r\n", $rawHeaders);
        foreach ($responseHeaderLines as $headerLine)
        {
            if ($headerLine && strpos($headerLine, ':') !== false)
            {
                list($header, $value) = explode(': ', $headerLine, 2);
                $header = strtolower($header);
                if (isset($headers[$header]))
                {
                    $headers[$header] .= "\n" . $value;
                }
                else
                {
                    $headers[$header] = $value;
                }
            }
        }
        return $headers;
    }
    
    private function parseArrayHeaders($rawHeaders)
    {
        $header_count = count($rawHeaders);
        $headers = array();
        
        for ($i = 0;$i < $header_count;$i++)
        {
            $header = $rawHeaders[$i];
            // Times will have colons in - so we just want the first match.
            $header_parts = explode(': ', $header, 2);
            if (count($header_parts) == 2)
            {
                $headers[strtolower($header_parts[0]) ] = $header_parts[1];
            }
        }
        
        return $headers;
    }
}