"""Packet read and write routines using network sockets."""

from pypacker import pypacker
from pypacker.layer12 import ethernet

import socket
import logging

logger = logging.getLogger("pypacker")


class SocketHndl(object):
	"""
	Simple socket handler for layer 2 and 3 reading/writing.
	"""

	ETH_P_ALL		= 0x0003
	MODE_LAYER_2		= 0
	MODE_LAYER_3		= 1

	def __init__(self, iface_name="lo", mode=MODE_LAYER_2, timeout=3, buffersize_recv=2 ** 26, buffersize_send=2 ** 26):
		"""
		iface_name -- bind to the given interface, mainly for MODE_LAYER_2
		mode -- set socket-mode for sending data (used by send() and sr()). The following modes are supported:
			MODE_LAYER_2: layer 2 packets have to be provided (Ethernet etc)
			MODE_LAYER_3: layer 3 packets have to be provided (IP, ARP etc), mac is auto-resolved
		timeout -- read timeout in seconds
		bufferspace -- amount of buffer used for receiving and sending
		"""

		self.iface_name = iface_name
		self._socket_send = None
		self._socket_recv = None
		self.__mode = mode

		logger.info("creating socket on interface: %s" % iface_name)
		# use raw socket for receiving in all modes
		self._socket_recv = socket.socket(socket.AF_PACKET,
							socket.SOCK_RAW,
							socket.htons(SocketHndl.ETH_P_ALL))
		self._socket_recv.settimeout(timeout)

		if iface_name is not None:
			self._socket_recv.bind((iface_name, SocketHndl.ETH_P_ALL))

		# same socket for sending
		if mode == SocketHndl.MODE_LAYER_2:
			self._socket_send = self._socket_recv
		# different socket for sending
		elif mode == SocketHndl.MODE_LAYER_3:
			self._socket_send = socket.socket(socket.AF_INET,
				socket.SOCK_RAW,
				socket.IPPROTO_RAW)
			self._socket_send.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1)

		self._socket_recv.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, buffersize_recv)
		self._socket_send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, buffersize_send)

	def send(self, bts, dst=None):
		"""
		Send the given bytes to network.

		bts -- the bytes to be sent
		dst -- destination for Layer 3 if mode is MODE_LAYER_3
		"""

		if self.__mode == SocketHndl.MODE_LAYER_2:
			self._socket_send.send(bts)
		elif self.__mode == SocketHndl.MODE_LAYER_3:
			self._socket_send.sendto(bts, (dst, 0))

	def recv(self):
		"""
		return -- bytes received from network
		"""
		return self._socket_recv.recv(65536)

	def __iter__(self):
		"""
		Call recv() until socket.timeout
		"""
		try:
			while True:
				yield self.recv()
		except socket.timeout:
			raise StopIteration

	def recvp(self, filter_match_recv=None, lowest_layer=ethernet.Ethernet, max_amount=1):
		"""
		Receive packets from network. This does the same as calling recv() but using a receive
		filter and received bytes will be converted to packets using class given by lowest_layer.
		Raises socket.timeout on timeout

		filter_match_recv -- filter as callback function to match packets to be retrieved.
			Callback-structure: fct(packet), Return True to accept a specific packet.
			Raise StopIteration to stop receiving packets, max_amount will match after all.
		lowest_layer -- packet class to be used to create new packets
		max_amount -- maximum amount of packets to be fetched
		return -- packets received from network as list
		"""

		received = []
		# logger.debug("listening for packets")

		while len(received) < max_amount:
			bts = self.recv()
			packet_recv = lowest_layer(bts)
			# logger.debug("got packet: %s" % packet_recv)
			try:
				if filter_match_recv(packet_recv):
					received.append(packet_recv)
			except TypeError:
				# no filter set
				received.append(packet_recv)
			except StopIteration:
				break

		return received

	def sr(self, packet_send, max_packets_recv=1, filter=None, lowest_layer=ethernet.Ethernet):
		"""
		Send a packet and receive answer packets. This will use information retrieved
		from direction() to retrieve answer packets. Raises socket.timeout on timeout

		packet_send -- pypacker packet to be sent
		max_packets_recv -- max packets to be received
		filter -- filter as lambda function to match packets to be retrieved,
			return True to accept a specific packet.
			Set to None to accept everything.
		lowest_layer -- packet class to be used to create new packets

		return -- packets receives
		"""

		received = []
		packet_send_clz = packet_send.__class__

		if self.__mode == SocketHndl.MODE_LAYER_2:
			self.send(packet_send.bin())
		elif self.__mode == SocketHndl.MODE_LAYER_3:
			# logger.debug("sr with layer 3: %s" % packet_send.dst_s)
			self.send(packet_send.bin(), dst=packet_send.dst_s)

		while len(received) < max_packets_recv:
			bts = self.recv()
			packet_recv = lowest_layer(bts)
			# logger.debug("got packet: %s" % packet_recv)
			try:
				if not filter(packet_recv):
					# filter didn't match
					continue
			except TypeError:
				# no filter set
				pass

			# packet_send_clz can be IP on MODE_LAYER_3, start to compare on corresponding receive-layer
			if packet_send.is_direction(packet_recv[packet_send_clz], pypacker.Packet.DIR_REV):
				# logger.debug("direction matched: %s" % packet_recv)
				received.append(packet_recv)

		return received

	def close(self):
		try:
			self._socket_send.close()
		except:
			pass
		try:
			self._socket_recv.close()
		except:
			pass
