# -*- coding: utf-8 -*-
# Copyright (C) 2013, the Pyzo development team
#
# Yoton is distributed under the terms of the 2-Clause BSD License.
# The full license can be found in 'license.txt'.

import sys
import time
import struct
import socket

# import select  # to determine wheter a socket can receive data
if sys.platform.startswith("java"):  # Jython
    from select import cpython_compatible_select as select, error as SelectErr
else:
    from select import select, error as SelectErr

from yoton.misc import bytes


## Constants

# Error code for interruptions
try:
    from errno import EINTR
except ImportError:
    EINTR = None

# Queue size
BUF_MAX_LEN = 10000

# Define buffer size. For recv 4096 or 8192 chunk size is recommended.
# For sending, in principle as large as possible, but prevent too much
# message copying.
BUFFER_SIZE_IN = 2**13
BUFFER_SIZE_OUT = 1 * 2**20

# Reserved slots (slot 0 does not exist, so we can test "if slot: ")
# The actual slots start counting from 8.
# SLOT_TEST = 3 -> Deprecated
SLOT_CONTEXT = 2
SLOT_DUMMY = 1

# Struct header packing
HEADER_FORMAT = "<7Q"
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)

# Constant for control bytes
CONTROL_BYTES, = struct.unpack("<Q", b"YOTON".ljust(8))


## Helper functions


def can_send(s, timeout=0.0):
    """can_send(bsd_socket, timeout=0.0)

    Given a socket, uses select() to determine whether it can be used
    for sending. This function can deal with system interrupts.

    """
    while True:
        try:
            can_recv, can_send, tmp = select([], [s], [], timeout)
            break
        except SelectErr:
            # Skip errors caused by interruptions.
            type, value, tb = sys.exc_info()
            del tb
            if value.args[0] != EINTR:
                raise
    return bool(can_send)


def can_recv(s, timeout=0.0):
    """can_recv(bsd_socket, timeout=0.0)

    Given a socket, uses select() to determine whether it can be used
    for receiving. This function can deal with system interrupts.

    """
    while True:
        try:
            can_recv, can_send, tmp = select([s], [], [], timeout)
            break
        except SelectErr:
            # Skip errors caused by interruptions.
            type, value, tb = sys.exc_info()
            del tb
            if value.args[0] != EINTR:
                raise
    return bool(can_recv)


def send_all(s, text, stutdown_after_sending=True):
    """send_all(socket, text, stutdown_after_sending=True)

    Send all text to the socket. Used during handshaking and in
    the clientserver module.

    If stutdown_after_sending, the socket is shut down. Some protocols
    rely on this.

    It is made sure that the text ends with a CRLF double-newline code.

    """

    # Ensure closing chars
    if not text.endswith("\r\n"):
        text += "\r\n"

    # Make bytes
    bb = text.encode("utf-8")

    # Send all bytes
    try:
        s.sendall(bb)  # -> n
    except socket.error:
        return -1  # Socket closed down badly

    # Shutdown connection nicely from here
    if stutdown_after_sending:
        try:
            s.shutdown(socket.SHUT_WR)
        except socket.error:
            pass


def recv_all(s, timeout=-1, end_at_crlf=True):
    """recv_all(socket, timeout=-1, end_at_crlf=True)

    Receive text from the socket (until socket receiving is shut down).
    Used during handshaking and in the clientserver module.

    If end_at_crlf, a message is also ended at a CRLF double-newline code,
    and a shutdown is not necessary. This takes a tiny bit longer.

    """

    # Init parts (start with one byte, such that len(parts) is always >= 2
    parts = [b" "]

    # Determine number of bytes to get per recv
    nbytesToGet = BUFFER_SIZE_IN
    if end_at_crlf:
        nbytesToGet = 1

    # Set end bytes
    end_bytes = b"\r\n"

    # Set max time
    if timeout <= 0:
        timeout = 2**32
    maxtime = time.time() + timeout

    # Receive data
    while True:
        # Receive if we can
        if can_recv(s):
            # Get part
            try:
                part = s.recv(nbytesToGet)
                parts.append(part)
            except socket.error:
                return None  # Socket closed down badly

            # Detect end by shutdown (EOF)
            if not part:
                break

            # Detect end by \r\n
            if end_at_crlf and (parts[-2] + parts[-1]).endswith(end_bytes):
                break

        else:
            # Sleep
            time.sleep(0.01)

            # Check time
            if time.time() > maxtime:
                bb = b"".join(parts[1:])
                return bb.decode("utf-8", "ignore")

    # Combine parts (discarded first (dummy) part)
    bb = b"".join(parts[1:])

    # Try returning as Unicode
    try:
        return bb.decode("utf-8", "ignore")
    except UnicodeError:
        return "<UnicodeError>"


## Package class


class Package(object):
    """Package(data, slot, source_id, source_seq, dest_id, dest_seq, recv_seq)

    Represents a package of bytes to be sent from one Context instance
    to another. A package consists of a header and the encoded message.

    To make this class as fast as reasonably possible, its interface
    is rather minimalistic and few convenience stuff is implemented.

    Parameters
    ----------
    data : bytes
        The message itself.
    slot : long
        The slot at which the package is directed. The integer is a hash of
        a string slot name.
    source_id : long
        The id of the context that sent this package.
    source_seq : long
        The sequence number of this package, counted at the sending context.
        Together with source_id, this fully identifies a package.
    dest_id : long (default 0)
        The id of the package that this package replies to.
    dest_seq : long (default 0)
        The sequence number of the package that this package replies to.
    recv_seq : long (default 0)
        The sequence number of this package counted at the receiving context.
        This is used to synchronize channels.

    When send, the header is composed of: four control bytes, the slot,
    the source_id, source_seq, dest_id and dest_seq.

    Notes
    -----
    A package should always have content. Packages without content are only
    used for low-level communication between two ContextConnection instances.
    The source_seq is then used as the signal. All other package attributes
    are ignored.

    """

    # The __slots__ makes instances of this class consume < 20% of memory
    # Note that this only works for new style classes.
    # This is important because many packages can exist at the same time
    # if a receiver can't keep up with a sender. Further, although Python's
    # garbage collector collects the objects after they're "consumed",
    # it does not release the memory, because it hopes to reuse it in
    # an efficient way later.
    __slots__ = [
        "_data",
        "_slot",
        "_source_id",
        "_source_seq",
        "_dest_id",
        "_dest_seq",
        "_recv_seq",
    ]

    def __init__(self, data, slot, source_id, source_seq, dest_id, dest_seq, recv_seq):
        self._data = data
        self._slot = slot
        #
        self._source_id = source_id
        self._source_seq = source_seq
        self._dest_id = dest_id
        self._dest_seq = dest_seq
        self._recv_seq = recv_seq

    def parts(self):
        """parts()

        Get list of bytes that represents this package.
        By not concatenating the header and content parts,
        we prevent unnecesary copying of data.

        """

        # Obtain header
        header = struct.pack(
            HEADER_FORMAT,
            CONTROL_BYTES,
            self._slot,
            self._source_id,
            self._source_seq,
            self._dest_id,
            self._dest_seq,
            len(self._data),
        )

        # Return header and message
        return header, self._data

    def __str__(self):
        """Representation of the package. Mainly for debugging."""
        return "At slot %i: %s" % (self._slot, repr(self._data))

    @classmethod
    def from_header(cls, header):
        """from_header(header)

        Create a package (without data) from the header of a message.
        Returns (package, data_length). If the header is invalid (checked
        using the four control bytes) this method returns (None, None).

        """
        # Unpack
        tmp = struct.unpack(HEADER_FORMAT, header)
        CTRL, slot, source_id, source_seq, dest_id, dest_seq, L = tmp
        # Create package
        p = Package(None, slot, source_id, source_seq, dest_id, dest_seq, 0)
        # Return
        if CTRL == CONTROL_BYTES:
            return p, L
        else:
            return None, None


# Constant Package instances (source_seq represents the signal)
PACKAGE_HEARTBEAT = Package(bytes(), SLOT_DUMMY, 0, 0, 0, 0, 0)
PACKAGE_CLOSE = Package(bytes(), SLOT_DUMMY, 0, 1, 0, 0, 0)
