# encoding: utf-8
from __future__ import unicode_literals

from .util import PY_3_OR_HIGHER

try:
    import ssl
except ImportError:
    _HAVE_SSL = False
else:
    _HAVE_SSL = True

if PY_3_OR_HIGHER:
    import urllib.request as urllib_request
    import urllib.error as urllib_error
else:
    import urllib2 as urllib_request
    import urllib2 as urllib_error

import certifi

import json
from ssl import SSLError
import socket
import codecs
import select, time

from .api import TwitterCall, wrap_response, TwitterHTTPError

CRLF = b'\r\n'
MIN_SOCK_TIMEOUT = 0.0  # Apparenty select with zero wait is okay!
MAX_SOCK_TIMEOUT = 10.0
HEARTBEAT_TIMEOUT = 90.0

Timeout = {'timeout': True}
Hangup = {'hangup': True}
DecodeError = {'hangup': True, 'decode_error': True}
HeartbeatTimeout = {'hangup': True, 'heartbeat_timeout': True}


class HttpChunkDecoder(object):

    def __init__(self):
        self.buf = bytearray()
        self.munch_crlf = False

    def decode(self, data):  # -> (bytearray, end_of_stream, decode_error)
        chunks = []
        buf = self.buf
        munch_crlf = self.munch_crlf
        end_of_stream = False
        decode_error = False
        buf.extend(data)
        while True:
            if munch_crlf:
                # Dang, Twitter, you crazy. Twitter only sends a terminating
                # CRLF at the beginning of the *next* message.
                if len(buf) >= 2:
                    buf = buf[2:]
                    munch_crlf = False
                else:
                    break

            header_end_pos = buf.find(CRLF)
            if header_end_pos == -1:
                break

            header = buf[:header_end_pos]
            data_start_pos = header_end_pos + 2
            try:
                chunk_len = int(header.decode('ascii'), 16)
            except ValueError:
                decode_error = True
                break

            if chunk_len == 0:
                end_of_stream = True
                break

            data_end_pos = data_start_pos + chunk_len

            if len(buf) >= data_end_pos:
                chunks.append(buf[data_start_pos:data_end_pos])
                buf = buf[data_end_pos:]
                munch_crlf = True
            else:
                break
        self.buf = buf
        self.munch_crlf = munch_crlf
        return bytearray().join(chunks), end_of_stream, decode_error


class JsonDecoder(object):

    def __init__(self):
        self.buf = ""
        self.raw_decode = json.JSONDecoder().raw_decode

    def decode(self, data):
        chunks = []
        buf = self.buf + data
        while True:
            try:
                buf = buf.lstrip()
                res, ptr = self.raw_decode(buf)
                buf = buf[ptr:]
                chunks.append(res)
            except ValueError:
                break
        self.buf = buf
        return chunks


class Timer(object):

    def __init__(self, timeout):
        # If timeout is None, we never expire.
        self.timeout = timeout
        self.reset()

    def reset(self):
        self.time = time.time()

    def expired(self):
        """
        If expired, reset the timer and return True.
        """
        if self.timeout is None:
            return False
        elif time.time() - self.time > self.timeout:
            self.reset()
            return True
        return False


class SockReader(object):
    def __init__(self, sock, sock_timeout):
        self.sock = sock
        self.sock_timeout = sock_timeout

    def read(self):
        try:
            ready_to_read = select.select([self.sock], [], [], self.sock_timeout)[0]
            if ready_to_read:
                return self.sock.read()
        except SSLError as e:
            # Code 2 is error from a non-blocking read of an empty buffer.
            if e.errno != 2:
                raise
        return bytearray()


class TwitterJSONIter(object):

    def __init__(self, handle, uri, arg_data, block, timeout, heartbeat_timeout):
        self.handle = handle
        self.uri = uri
        self.arg_data = arg_data
        self.timeout_token = Timeout
        self.timeout = None
        self.heartbeat_timeout = HEARTBEAT_TIMEOUT
        if timeout and timeout > 0:
            self.timeout = float(timeout)
        elif not (block or timeout):
            self.timeout_token = None
            self.timeout = MIN_SOCK_TIMEOUT
        if heartbeat_timeout and heartbeat_timeout > 0:
            self.heartbeat_timeout = float(heartbeat_timeout)

    def __iter__(self):
        timeouts = [t for t in (self.timeout, self.heartbeat_timeout, MAX_SOCK_TIMEOUT)
                    if t is not None]
        sock_timeout = min(*timeouts)
        sock = self.handle.fp.raw._sock if PY_3_OR_HIGHER else self.handle.fp._sock.fp._sock
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        headers = self.handle.headers
        sock_reader = SockReader(sock, sock_timeout)
        chunk_decoder = HttpChunkDecoder()
        utf8_decoder = codecs.getincrementaldecoder("utf-8")()
        json_decoder = JsonDecoder()
        timer = Timer(self.timeout)
        heartbeat_timer = Timer(self.heartbeat_timeout)

        while True:
            # Decode all the things:
            try:
                data = sock_reader.read()
            except SSLError:
                yield Hangup
                break
            dechunked_data, end_of_stream, decode_error = chunk_decoder.decode(data)
            unicode_data = utf8_decoder.decode(dechunked_data)
            json_data = json_decoder.decode(unicode_data)

            # Yield data-like things:
            for json_obj in json_data:
                yield wrap_response(json_obj, headers)

            # Reset timers:
            if dechunked_data:
                heartbeat_timer.reset()
            if json_data:
                timer.reset()

            # Yield timeouts and special things:
            if end_of_stream:
                yield Hangup
                break
            if decode_error:
                yield DecodeError
                break
            if heartbeat_timer.expired():
                yield HeartbeatTimeout
                break
            if timer.expired():
                yield self.timeout_token


def handle_stream_response(req, uri, arg_data, block, timeout, heartbeat_timeout, verify_context=True):
    try:
        context = None
        if _HAVE_SSL:
            if not verify_context:
                context = ssl._create_unverified_context()
            else:
                context = ssl.create_default_context()
                context.load_verify_locations(cafile=certifi.where())
        handle = urllib_request.urlopen(req, context=context)
    except urllib_error.HTTPError as e:
        raise TwitterHTTPError(e, uri, 'json', arg_data)
    return iter(TwitterJSONIter(handle, uri, arg_data, block, timeout, heartbeat_timeout))

class TwitterStream(TwitterCall):
    """
    The TwitterStream object is an interface to the Twitter Stream
    API. This can be used pretty much the same as the Twitter class
    except the result of calling a method will be an iterator that
    yields objects decoded from the stream. For example::

        twitter_stream = TwitterStream(auth=OAuth(...))
        iterator = twitter_stream.statuses.sample()

        for tweet in iterator:
            # ...do something with this tweet...

    Per default the ``TwitterStream`` object uses
    [public streams](https://dev.twitter.com/docs/streaming-apis/streams/public).
    If you want to use one of the other
    [streaming APIs](https://dev.twitter.com/docs/streaming-apis), specify the URL
    manually.

    The iterator will yield until the TCP connection breaks. When the
    connection breaks, the iterator yields `{'hangup': True}`, and
    raises `StopIteration` if iterated again.

    Similarly, if the stream does not produce heartbeats for more than
    90 seconds, the iterator yields `{'hangup': True,
    'heartbeat_timeout': True}`, and raises `StopIteration` if
    iterated again.

    The `timeout` parameter controls the maximum time between
    yields. If it is nonzero, then the iterator will yield either
    stream data or `{'timeout': True}` within the timeout period. This
    is useful if you want your program to do other stuff in between
    waiting for tweets.

    The `block` parameter sets the stream to be fully non-blocking. In
    this mode, the iterator always yields immediately. It returns
    stream data, or `None`. Note that `timeout` supercedes this
    argument, so it should also be set `None` to use this mode.
    """
    def __init__(self, domain="stream.twitter.com", secure=True, auth=None,
                 api_version='1.1', block=True, timeout=None,
                 heartbeat_timeout=90.0, verify_context=True):
        uriparts = (str(api_version),)

        class TwitterStreamCall(TwitterCall):
            def _handle_response(self, req, uri, arg_data, _timeout=None):
                return handle_stream_response(
                    req, uri, arg_data, block,
                    _timeout or timeout, heartbeat_timeout, verify_context)

        TwitterCall.__init__(
            self, auth=auth, format="json", domain=domain,
            callable_cls=TwitterStreamCall,
            secure=secure, uriparts=uriparts, timeout=timeout, gzip=False,
            retry=False, verify_context=verify_context)
