# -*- coding: utf-8 -*-
"""
hyper/http20/response
~~~~~~~~~~~~~~~~~~~~~

Contains the HTTP/2 equivalent of the HTTPResponse object defined in
httplib/http.client.
"""
import logging
import zlib

from ..common.decoder import DeflateDecoder
from ..common.headers import HTTPHeaderMap

log = logging.getLogger(__name__)


def strip_headers(headers):
    """
    Strips the headers attached to the instance of any header beginning
    with a colon that ``hyper`` doesn't understand. This method logs at
    warning level about the deleted headers, for discoverability.
    """
    # Convert to list to ensure that we don't mutate the headers while
    # we iterate over them.
    for name in list(headers.keys()):
        if name.startswith(b':'):
            del headers[name]


class HTTP20Response(object):
    """
    An ``HTTP20Response`` wraps the HTTP/2 response from the server. It
    provides access to the response headers and the entity body. The response
    is an iterable object and can be used in a with statement (though due to
    the persistent connections used in HTTP/2 this has no effect, and is done
    soley for compatibility).
    """
    def __init__(self, headers, stream):
        #: The reason phrase returned by the server. This is not used in
        #: HTTP/2, and so is always the empty string.
        self.reason = ''

        status = headers[b':status'][0]
        strip_headers(headers)

        #: The status code returned by the server.
        self.status = int(status)

        #: The response headers. These are determined upon creation, assigned
        #: once, and never assigned again.
        self.headers = headers

        # The response trailers. These are always intially ``None``.
        self._trailers = None

        # The stream this response is being sent over.
        self._stream = stream

        # We always read in one-data-frame increments from the stream, so we
        # may need to buffer some for incomplete reads.
        self._data_buffer = b''

        # This object is used for decompressing gzipped request bodies. Right
        # now we only support gzip because that's all the RFC mandates of us.
        # Later we'll add support for more encodings.
        # This 16 + MAX_WBITS nonsense is to force gzip. See this
        # Stack Overflow answer for more:
        # http://stackoverflow.com/a/2695466/1401686
        if b'gzip' in self.headers.get(b'content-encoding', []):
            self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS)
        elif b'deflate' in self.headers.get(b'content-encoding', []):
            self._decompressobj = DeflateDecoder()
        else:
            self._decompressobj = None

    @property
    def trailers(self):
        """
        Trailers on the HTTP message, if any.

        .. warning:: Note that this property requires that the stream is
                     totally exhausted. This means that, if you have not
                     completely read from the stream, all stream data will be
                     read into memory.
        """
        if self._trailers is None:
            self._trailers = self._stream.gettrailers() or HTTPHeaderMap()
            strip_headers(self._trailers)

        return self._trailers

    def read(self, amt=None, decode_content=True):
        """
        Reads the response body, or up to the next ``amt`` bytes.

        :param amt: (optional) The amount of data to read. If not provided, all
            the data will be read from the response.
        :param decode_content: (optional) If ``True``, will transparently
            decode the response data.
        :returns: The read data. Note that if ``decode_content`` is set to
            ``True``, the actual amount of data returned may be different to
            the amount requested.
        """
        if amt is not None and amt <= len(self._data_buffer):
            data = self._data_buffer[:amt]
            self._data_buffer = self._data_buffer[amt:]
            response_complete = False
        elif amt is not None:
            read_amt = amt - len(self._data_buffer)
            self._data_buffer += self._stream._read(read_amt)
            data = self._data_buffer[:amt]
            self._data_buffer = self._data_buffer[amt:]
            response_complete = len(data) < amt
        else:
            data = b''.join([self._data_buffer, self._stream._read()])
            response_complete = True

        # We may need to decode the body.
        if decode_content and self._decompressobj and data:
            data = self._decompressobj.decompress(data)

        # If we're at the end of the request, we have some cleaning up to do.
        # Close the stream, and if necessary flush the buffer.
        if response_complete:
            if decode_content and self._decompressobj:
                data += self._decompressobj.flush()

            if self._stream.response_headers:
                self.headers.merge(self._stream.response_headers)

        # We're at the end, close the connection.
        if response_complete:
            self.close()

        return data

    def read_chunked(self, decode_content=True):
        """
        Reads chunked transfer encoded bodies. This method returns a generator:
        each iteration of which yields one data frame *unless* the frames
        contain compressed data and ``decode_content`` is ``True``, in which
        case it yields whatever the decompressor provides for each chunk.

        .. warning:: This may yield the empty string, without that being the
                     end of the body!
        """
        while True:
            data = self._stream._read_one_frame()

            if data is None:
                break

            if decode_content and self._decompressobj:
                data = self._decompressobj.decompress(data)

            yield data

        if decode_content and self._decompressobj:
            yield self._decompressobj.flush()

        self.close()

        return

    def fileno(self):
        """
        Return the ``fileno`` of the underlying socket. This function is
        currently not implemented.
        """
        raise NotImplementedError("Not currently implemented.")

    def close(self):
        """
        Close the response. In effect this closes the backing HTTP/2 stream.

        :returns: Nothing.
        """
        self._stream.close()

    # The following methods implement the context manager protocol.
    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()
        return False  # Never swallow exceptions.


class HTTP20Push(object):
    """
    Represents a request-response pair sent by the server through the server
    push mechanism.
    """
    def __init__(self, request_headers, stream):
        #: The scheme of the simulated request
        self.scheme = request_headers[b':scheme'][0]
        #: The method of the simulated request (must be safe and cacheable,
        #: e.g. GET)
        self.method = request_headers[b':method'][0]
        #: The authority of the simulated request (usually host:port)
        self.authority = request_headers[b':authority'][0]
        #: The path of the simulated request
        self.path = request_headers[b':path'][0]

        strip_headers(request_headers)

        #: The headers the server attached to the simulated request.
        self.request_headers = request_headers

        self._stream = stream

    def get_response(self):
        """
        Get the pushed response provided by the server.

        :returns: A :class:`HTTP20Response <hyper.HTTP20Response>` object
            representing the pushed response.
        """
        return HTTP20Response(self._stream.getheaders(), self._stream)

    def cancel(self):
        """
        Cancel the pushed response and close the stream.

        :returns: Nothing.
        """
        self._stream.close(8)  # CANCEL
