##
# .protocol.pbuffer
##
"""
Pure Python message buffer implementation.

Given data read from the wire, buffer the data until a complete message has been
received.
"""
__all__ = ['pq_message_stream']

from io import BytesIO
import struct
from .message_types import message_types

xl_unpack = struct.Struct('!xL').unpack_from

class pq_message_stream(object):
	"""
	Provide a message stream from a data stream.
	"""

	_block = 512
	_limit = _block * 4
	def __init__(self):
		self._strio = BytesIO()
		self._start = 0

	def truncate(self):
		"""
		Remove all data in the buffer.
		"""

		self._strio.truncate(0)
		self._start = 0

	def _rtruncate(self, amt = None):
		"""
		[internal] remove the given amount of data.
		"""

		strio = self._strio
		if amt is None:
			amt = self._strio.tell()
		strio.seek(0, 2)
		size = strio.tell()
		# if the total size is equal to the amt,
		# then the whole thing is going to be truncated.
		if size == amt:
			strio.truncate(0)
			return

		copyto_pos = 0
		copyfrom_pos = amt
		while True:
			strio.seek(copyfrom_pos)
			data = strio.read(self._block)
			# Next copyfrom
			copyfrom_pos = strio.tell()
			strio.seek(copyto_pos)
			strio.write(data)
			if len(data) != self._block:
				break
			# Next copyto
			copyto_pos = strio.tell()

		strio.truncate(size - amt)

	def has_message(self, xl_unpack = xl_unpack, len = len):
		"""
		Whether the buffer has a message available.
		"""

		strio = self._strio
		strio.seek(self._start)
		header = strio.read(5)
		if len(header) < 5:
			return False
		length, = xl_unpack(header)
		if length < 4:
			raise ValueError("invalid message size '%d'" %(length,))
		strio.seek(0, 2)
		return (strio.tell() - self._start) >= length + 1

	def __len__(self, xl_unpack = xl_unpack, len = len):
		"""
		Number of messages in buffer.
		"""

		count = 0
		rpos = self._start
		strio = self._strio
		strio.seek(self._start)
		while True:
			# get the message metadata
			header = strio.read(5)
			rpos += 5
			if len(header) < 5:
				# not enough data for another message
				break
			# unpack the length from the header
			length, = xl_unpack(header)
			rpos += length - 4

			if length < 4:
				raise ValueError("invalid message size '%d'" %(length,))
			strio.seek(length - 4 - 1, 1)

			if len(strio.read(1)) != 1:
				break
			count += 1
		return count

	def _get_message(self,
		mtypes = message_types,
		len = len,
		xl_unpack = xl_unpack,
	):
		strio = self._strio
		header = strio.read(5)
		if len(header) < 5:
			return
		length, = xl_unpack(header)
		typ = mtypes[header[0]]

		if length < 4:
			raise ValueError("invalid message size '%d'" %(length,))
		length -= 4
		body = strio.read(length)
		if len(body) < length:
			# Not enough data for message.
			return
		return (typ, body)

	def next_message(self):
		if self._start > self._limit:
			self._rtruncate(self._start)
			self._start = 0

		self._strio.seek(self._start)
		msg = self._get_message()
		if msg is not None:
			self._start = self._strio.tell()
		return msg

	def __next__(self):
		if self._start > self._limit:
			self._rtruncate(self._start)
			self._start = 0

		self._strio.seek(self._start)
		msg = self._get_message()
		if msg is None:
			raise StopIteration
		self._start = self._strio.tell()
		return msg

	def read(self, num = 0xFFFFFFFF, len = len):
		if self._start > self._limit:
			self._rtruncate(self._start)
			self._start = 0

		new_start = self._start
		self._strio.seek(new_start)
		l = []
		while len(l) < num:
			msg = self._get_message()
			if msg is None:
				break
			l.append(msg)
			new_start += (5 + len(msg[1]))
		self._start = new_start
		return l

	def write(self, data):
		# Always append data; it's a stream, damnit..
		self._strio.seek(0, 2)
		self._strio.write(data)

	def getvalue(self):
		self._strio.seek(self._start)
		return self._strio.read()
