<?php
/**
 * core.framework
 *
 * @category  Core
 * @package   Core_Response
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */

/**
 * Response. In charge of setting headers and keeping track of response data.
 *
 * @category  Core
 * @package   Core_Response
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_Response
{
    const CONTENT_CHARSET              = 'charset';

    const TYPE_JSON                    = 'application/json';
    const TYPE_XML                     = 'text/xml';
    const TYPE_URLENCODED              = 'application/x-www-form-urlencoded';

    const FORMAT_RAW                   = 'raw';
    const FORMAT_JSON                  = 'json';
    const FORMAT_XML                   = 'xml';
    const FORMAT_URLENCODED            = 'urlencoded';

    const STATUS_OK                    = 200;
    const STATUS_CREATED               = 201;
    const STATUS_MOVED_PERMANENTLY     = 301;
    const STATUS_MOVED_TEMPORARILY     = 302;
    const STATUS_NOT_MODIFIED          = 304;
    const STATUS_TEMPORARY_REDIRECT    = 307;
    const STATUS_PERMANENT_REDIRECT    = 308;
    const STATUS_BAD_REQUEST           = 400;
    const STATUS_UNAUTHORIZED          = 401;
    const STATUS_FORBIDDEN             = 403;
    const STATUS_NOT_FOUND             = 404;
    const STATUS_METHOD_NOT_ALLOWED    = 405;
    const STATUS_CONFLICT              = 409;
    const STATUS_PRECONDITION_FAILED   = 412;
    const STATUS_UNSUPPORTED_MEDIA     = 415;
    const STATUS_UNPROCESSABLE_ENTITY  = 422;
    const STATUS_INTERNAL_SERVER_ERROR = 500;
    const STATUS_SERVICE_NOT_AVAILABLE = 503;
    const STATUS_GATEWAY_TIMEOUT       = 504;

    /**
     * @var array
     */
    static protected $_knownTypes = array(
        self::FORMAT_JSON       => self::TYPE_JSON,
        self::FORMAT_XML        => self::TYPE_XML,
        self::FORMAT_URLENCODED => self::TYPE_URLENCODED,
    );

    /**
     * @var array
     */
    static protected $_messages = array(
        self::STATUS_OK                    => 'OK',
        self::STATUS_CREATED               => 'Created',
        self::STATUS_MOVED_PERMANENTLY     => 'Moved Permanently (request with GET)',
        self::STATUS_MOVED_TEMPORARILY     => 'Moved Temporarily (request with GET)',
        self::STATUS_NOT_MODIFIED          => 'Not modified',
        self::STATUS_TEMPORARY_REDIRECT    => 'Moved Temporarily (request with same method)',
        self::STATUS_PERMANENT_REDIRECT    => 'Moved Permanently (request with same method)',
        self::STATUS_BAD_REQUEST           => 'Bad request',
        self::STATUS_UNAUTHORIZED          => 'Unauthorized',
        self::STATUS_FORBIDDEN             => 'Forbidden',
        self::STATUS_NOT_FOUND             => 'Not found',
        self::STATUS_METHOD_NOT_ALLOWED    => 'Method not allowed',
        self::STATUS_CONFLICT              => 'Conflict',
        self::STATUS_PRECONDITION_FAILED   => 'Precondition failed',
        self::STATUS_UNSUPPORTED_MEDIA     => 'Unsupported media',
        self::STATUS_UNPROCESSABLE_ENTITY  => 'Unprocessable Entity',
        self::STATUS_INTERNAL_SERVER_ERROR => 'Internal server error',
        self::STATUS_SERVICE_NOT_AVAILABLE => 'Service not available',
        self::STATUS_GATEWAY_TIMEOUT       => 'Gateway timeout',
    );

    /**
     * @var mixed Response body
     */
    protected $_body;
    
    /**
     * @var string Response body
     */
    protected $_bodySerialized;
    
    /**
     * @var int Response code
     */
    protected $_code;

    /**
     * @var bool Suppress response code
     */
    protected $_suppressCode;

    /**
     * @var bool Inline headers inside the response body
     */
    protected $_inlineHeaders;

    /**
     * @var int Response encoding
     */
    protected $_encoding = 'UTF-8';

    /**
     * @var int Response type
     */
    protected $_type;

    /**
     * @var string Response format
     */
    protected $_format = 'raw';

    /**
     * @var array
     */
    protected $_supportedFormats = array(
        self::FORMAT_RAW,
        self::FORMAT_JSON,
        self::FORMAT_XML,
        self::FORMAT_URLENCODED,
    );

    /**
     * @var array Response headers
     */
    protected $_headers = array();

    /**
     * @var array Response params
     */
    protected $_params = array();

    /**
     * @var array Mandatory response headers
     */
    protected $_mandatoryHeaders = array(
        '',
    );

    /**
     * @var array Mandatory response headers if we have body set
     */
    protected $_mandatoryBodyHeaders = array(
        'Content-Type',
        'Content-MD5',
    );

    /**
     * @var Core_Request
     */
    protected $_request;

    /**
     * @var boolean
     */
    protected $_caching = true;

    /**
     * Class constructor
     *
     * @param string $body Response body
     * @param int $code
     */
    public function __construct($body = null, $code = self::STATUS_OK)
    {
        $this->setBody($body);
        $this->setCode($code);
    }

    /**
     * @param string $format
     *
     * @throws InvalidArgumentException
     * @return Core_Response
     */
    public function setFormat($format)
    {
        if (!in_array($format, $this->_supportedFormats)) {
            $message = sprintf(
                'Failed setting unsupported response format %s, supported are: %s',
                $format,
                implode(', ', $this->_supportedFormats)
            );
            throw new InvalidArgumentException($message);
        }
        
        $this->_format         = $format;
        $this->_bodySerialized = null;
        return $this;
    }

    /**
     * @return string Response format
     */
    public function getFormat()
    {
        return $this->_format;
    }

    /**
     * @param string $body
     *
     * @return Core_Response
     */
    public function setBody($body)
    {
        $this->_body           = $body;
        $this->_bodySerialized = null;
        return $this;
    }

    /**
     * @return string Response body.
     */
    public function getBody()
    {
        if (null !== $this->_body && null === $this->_bodySerialized) {
            switch ($this->getFormat()) {
                case self::FORMAT_JSON:
                    $this->_bodySerialized = json_encode($this->_body, JSON_PRETTY_PRINT);
                    break;
                case self::FORMAT_XML:
                    $this->_bodySerialized = $this->_arrayToXml($this->_body);
                    break;
                case self::FORMAT_URLENCODED:
                    $this->_bodySerialized = http_build_query($this->_body);
                    break;
                default:
                    $this->_bodySerialized = $this->_body;
            }
        }
        return $this->_bodySerialized;
    }

    /**
     * @param int $code
     *
     * @return \Core_Response
     */
    public function setCode($code)
    {
        $this->_code = $code;
        return $this;
    }

    /**
     * @return int
     */
    public function getCode()
    {
        return $this->_code;
    }

    /**
     * @param array $params
     *
     * @return \Core_Response
     */
    public function setParams(array $params)
    {
        foreach ($params as $name => $value) {
            $this->setParam($name, $value);
        }
        return $this;
    }

    /**
     * @return array
     */
    public function getParams()
    {
        return $this->_params;
    }

    /**
     * @param string $name  Param name
     * @param mixed  $value Param value
     *
     * @return \Core_Response
     */
    public function setParam($name, $value)
    {
        $this->_params[$name] = $value;
        return $this;
    }

    /**
     * @param string $name    Param name
     * @param mixed  $default Default value
     *
     * @return mixed
     */
    public function getParam($name, $default = null)
    {
        return (array_key_exists($name, $this->_params) ? $this->_params[$name] : $default);
    }

    /**
     * @param string $charset
     *
     * @return \Core_Response
     */
    public function setCharset($charset)
    {
        $this->setParam(self::CONTENT_CHARSET, $charset);
        return $this;
    }

    /**
     * @return string
     */
    public function getCharset()
    {
        return $this->getParam(self::CONTENT_CHARSET, 'UTF-8');
    }

    /**
     * @param string $type
     *
     * @return \Core_Response
     */
    public function setType($type)
    {
        $this->_type = $type;
        return $this;
    }

    /**
     * @return string
     */
    public function getType()
    {
        if (null === $this->_type) {
            if (null !== ($format = $this->getFormat())
                && array_key_exists($format, self::$_knownTypes)) {
                $this->_type = self::$_knownTypes[$format];
            } else {
                $this->_type = 'text/html';
            }
        }
        return $this->_type;
    }

    /**
     * @param bool $suppressCode
     *
     * @return \Core_Response
     */
    public function setSuppressCode($suppressCode)
    {
        $this->_suppressCode = (bool) $suppressCode;
        return $this;
    }

    /**
     * @return bool
     */
    public function isSuppressCode()
    {
        if (null === $this->_suppressCode) {
            $request = $this->getRequest();
            $this->setSuppressCode($request->getHeader('X-HTTP-Response-Suppress-Status-Code', false));
        }
        return $this->_suppressCode;
    }

    /**
     * @param bool $inlineHeaders
     *
     * @return \Core_Response
     */
    public function setInlineHeaders($inlineHeaders)
    {
        $this->_inlineHeaders = (bool) $inlineHeaders;
        return $this;
    }

    /**
     * @return bool
     */
    public function isInlineHeaders()
    {
        if (null === $this->_inlineHeaders) {
            $request = $this->getRequest();
            $this->setInlineHeaders($request->getHeader('X-HTTP-Response-Inline-Headers', false));
        }
        return $this->_inlineHeaders;
    }

    /**
     * @param Core_Request $request
     *
     * @return Core_Response
     */
    public function setRequest(Core_Request $request)
    {
        $this->_request = $request;
        return $this;
    }

    /**
     * If not set, fetch from Core_Application
     *
     * @return Core_Request
     */
    public function getRequest()
    {
        if (null === $this->_request) {
            /* @var $request Core_Request */
            $request = Core_Application::get('Request');
            $this->setRequest($request);
        }
        return $this->_request;
    }

    /**
     * @param $caching
     *
     * @return Core_Response
     */
    public function setCaching($caching)
    {
        $this->_caching = (bool) $caching;
        return $this;
    }

    /**
     * @return bool
     */
    public function isCaching()
    {
        return $this->_caching;
    }

    /**
     * @param string $name  Header name
     * @param mixed  $value Header value
     *
     * @return \Core_Response
     */
    public function setHeader($name, $value)
    {
        $name = $this->_normalize($name);
        $this->_headers[$name] = $value;
        return $this;
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    public function getHeader($name)
    {
        $name = $this->_normalize($name);
        switch($name) {
            case '':
                if ($this->isSuppressCode()) {
                    $code = self::STATUS_OK;
                } else {
                    $code = $this->getCode();
                }
                $message  = '';
                if (array_key_exists($code, self::$_messages)) {
                    $message = ' '. self::$_messages[$code];
                }
                $status = array(sprintf('HTTP/1.1 %d%s', $code, $message));
                if ($this->isSuppressCode()) {
                    // add custom headers which show the proper, not-suppressed code
                    $realCode = $this->getCode();
                    $status['X-HTTP-Suppressed-Status-Code'] = $realCode;
                    if (array_key_exists($realCode, self::$_messages)) {
                        $status['X-HTTP-Suppressed-Status-Message'] = self::$_messages[$realCode];
                    }
                }
                return $status;
            break;

            case 'Etag':
                return sprintf('"%s"', sha1($this->getBody()));
                break;

            case 'Cache-control':
                return 'Private';
                break;

            case 'Content-md5':
                return base64_encode(md5($this->getBody()));
                break;

            case 'Content-type':
                $type   = $this->getType();
                $format = $this->getFormat();
                if (array_key_exists($format, self::$_knownTypes) && $type !== self::$_knownTypes[$format]) {
                    $type .= '+'. $format;
                }
                $params = array_merge(array(
                    // ensure you always include these params
                    self::CONTENT_CHARSET => $this->getCharset(),
                ), $this->getParams());

                return sprintf('%s; %s', $type, http_build_query($params, null, '; '));
            break;
        }
        return array_key_exists($name, $this->_headers) ? $this->_headers[$name] : null;
    }

    /**
     * @param array $headers
     *
     * @return \Core_Response
     */
    public function setHeaders(array $headers)
    {
        foreach ($headers as $name => $value) {
            $this->setHeader($name, $value);
        }
        return $this;
    }

    /**
     * @return array
     */
    public function getHeaders()
    {
        $headers = $this->_mandatoryHeaders;
        if (null !== $this->getBody()) {
            $headers = array_merge($headers, $this->_mandatoryBodyHeaders);
        }

        if ($this->isCaching()) {
            $headers = array_merge(array('Etag', 'Cache-Control'), $headers);
        }

        $mandatory = array();
        foreach ($headers as $name) {
            $name                 = $this->_normalize($name);
            $header               = $this->getHeader($name);
            if (is_array($header)) {
                $mandatory       += $header;
            } else {
                $mandatory[$name] = $header;
            }
        }

        return array_merge($mandatory, $this->_headers);
    }

    /**
     * Will output set headers before outputting the response body.
     *
     * @return string
     */
    public function render()
    {
        $headers  = array();

        if ($this->isSuppressCode()) {
            $code = self::STATUS_OK;
        } else {
            $code = $this->getCode();
        }
        if ($this->isCaching()) {
            $request = $this->getRequest();
            if (null !== ($reqEtag = $request->getHeader('If-None-Match'))) {
                // request Etag provided, verify that it matches our response
                if ($reqEtag === $this->getHeader('Etag') && self::STATUS_OK === $this->getCode()) {
                    $code = self::STATUS_NOT_MODIFIED;
                    $this
                        ->setCode($code)
                        ->setHeader('Etag', $reqEtag)
                        ->setBody(null);
                }
            }
        }
        foreach ($this->getHeaders() as $name => $value) {
            if (!is_string($name)) {
                $header = $value;
            } else {
                $header = sprintf('%s: %s', $name, $value);
            }
            $headers[] = $header;
            header($header, true, $code);
        }
        $body = (string) $this->getBody();
        if ($this->isInlineHeaders()) {
            $body = implode("\r\n", $headers) ."\r\n\r\n". $body;
        }
        return $body;
    }

    /**
     * @return string Render object if echoed.
     */
    public function __toString()
    {
        try {
            $string = $this->render();
        } catch (Exception $e) {
            $message = "Exception caught by response: " . $e->getMessage()
                     . "\nStack Trace:\n" . $e->getTraceAsString();
            error_log($message, E_USER_ERROR);
            $string = '';
        }
        return $string;
    }

    /**
     * @param string $name
     *
     * @return string
     */
    protected function _normalize($name)
    {
        return ucfirst(strtolower($name));
    }

    /**
     *
     * @param array            $array Array to generate
     * @param SimpleXMLElement $xml   XML instance
     *
     * @return array
     */
    protected function _arrayToXml(array $array, SimpleXMLElement $xml = null)
    {
        if (null === $xml) {
            $template = sprintf('<?xml version="1.0" encoding="%s" ?><root />', $this->getCharset());
            $xml      = simplexml_load_string($template);
        }
        foreach($array as $name => $value) {
            if (is_array($value)) {
                $this->_arrayToXml($value, $xml->addChild($name));
            } else {
                $xml->addChild($name, $value);
            }
        }
        return $xml->asXml();
    }
}
