"""
Example definition of a new protocol called "NewProtocol" (RFC -1).
New modules are placed into to appropriate layerXYZ-directory.
Last but not least: every protocol needs a testcase in tests/test_pypacker.py
"""
import logging

from pypacker import pypacker, triggerlist
from pypacker.pypacker_meta import FIELD_FLAG_AUTOUPDATE, FIELD_FLAG_IS_TYPEFIELD
from pypacker.layer3 import ip
from pypacker.layer4 import tcp
from pypacker.structcbs import unpack_H

logger = logging.getLogger("pypacker")

TYPE_VALUE_IP = 0x66


class Option(pypacker.Packet):
	"""Packet used for options field. See NewProtocol below."""
	__hdr__ = (
		("some_value", "B", 0x00),
	)


class NewProtocol(pypacker.Packet):
	"""
	New protocols are subclassing Packet class and represent a layer in a multi-layer
	network Packet like 'NewProtocol | IP | TCP ...'.
	The whole structure is oriented	at the ISO/OSI protocol layers where
	every layer contains a reference to the next upper layer. As an example this layer
	'NewProtocol', when parsing from raw bytes, will have a reference to the next upper
	layer 'IP' which can be access via pkt[UpperLayerClass]"""

	# The protocol header is basically defined by the static field
	# "__hdr__" (see layer12/ethernet.py). See code documentation
	# for classes "MetaPacket" and "Packet" in pypacker/pypacker.py for
	# deeper information.
	__hdr__ = (
		# Simple constant fields: fixed format, not changing length
		# marked as type field: defines type of next upper layer, here: IP. See __handler__
		("type", "B", TYPE_VALUE_IP, FIELD_FLAG_IS_TYPEFIELD),
		("src", "4s", b"\xff" * 4),
		("dst", "4s", b"\xff" * 4),
		# Simple constant field, marked for auto update, see _update_fields(...).
		# Stores the full header length inclusive options.
		("hlen", "H", 14, FIELD_FLAG_AUTOUPDATE),
		# Simple constant field, deactivated (see layer567/http.py -> startline)
		# Switching between active/inactive should be avoided because of performance penalty :/
		("idk", "H", None),
		# Again a simple constant field
		("flags", "B", 0),
		# Dynamic field: bytestring format, *can* change in length, see layer567/dns.py
		# Field type should be avoided because of performance penalty :/
		("yolo", None, b"1234"),
		# TriggerList field: allows multiple (variable length) values in any order
		# Can contain: raw bytes, key/value-tuples (see layer567/httpy.py) and Packets (see layer3/ip.py)
		# For NewProtocol this TriggerList usually will contain key/value Tuples like (b"\x00", b"1")
		("options", None, triggerlist.TriggerList)
	)

	# Convenient access should be enabled using properties eg using pypacker.get_property_xxx(...)
	src_s = pypacker.get_property_ip4("src")
	dst_s = pypacker.get_property_ip4("dst")
	# xxx_s = pypacker.get_property_mac("xxx")
	# xxx_s = pypacker.get_property_dnsname("xxx")

	# Setting/getting values smaller then 1 Byte should be enabled using properties (see layer3/ip.py -> v, hl)
	def __get_flag_fluxcapacitor(self):
		return (self.flags & 0x80) >> 15

	def __set_flag_fluxcapacitor(self, value):
		value_shift = (value & 1) << 15
		self.flags = (self.flags & ~0x80) | value_shift

	flag_fluxcapacitor = property(__get_flag_fluxcapacitor, __set_flag_fluxcapacitor)

	@staticmethod
	def _parse_options(buf):
		"""
		Callback to parse contents for TriggerList-field options,
		see _dissec(...) -> self.options(...).
		return -- [Option(), ...]
		"""
		ret = []
		off = 0

		while off < len(buf):
			ret.append(Option(buf[off: off + 2]))
			off += 2
		return ret

	def _dissect(self, buf):
		"""
		_dissect(...) must be overwritten if the header format can change
		from its original format. This is generally the case when
		- using TriggerLists (see layer3/ip.py)
		- a simple field could get deactivated (see layer12/ethernet.py -> vlan)
		- using dynamic fields (see layer567/dns.py -> Query -> name)

		In NewProtocol idk can get deactivated, options is a TriggerList
		and yolo is a dynamic field so _dissect(...) needs to be defined.
		"""
		# Header fields are not yet accessible in _dissect(...) so basic information
		# (type info, header length, bytes of dynamic content etc) has to be parsed manually.
		# Extract type information of next layer, here it can only be 0x66 but we extract it anyway.
		higher_layer_type = buf[0]
		# logger.debug("Found type: 0x%X" % higher_layer_type)
		total_header_length = unpack_H(buf[9: 11])[0]
		yolo_len = 4 if higher_layer_type == TYPE_VALUE_IP else 5  # length of yolo is derived from type
		# logger.debug("Found length: %d, yolo=%d" % (total_header_length, yolo_len))
		tl_bts = buf[12 + yolo_len: total_header_length]  # options are the the end of the header
		# logger.debug("Bytes for TriggerList: %r" % tl_bts)
		# self.name_of_triggerlist(buf, lambda tlbuf: [...]) should be called to initiate TriggerLists,
		# otherwise the list will be empty. _parse_options(...) is a callback returning a list
		# of [raw bytes | key/value tuples | Packets] parsed from tl_bts.
		self.options(tl_bts, NewProtocol._parse_options)

		# hlen, handler_id must be returned to initiate the handler of the next
		# upper layer and makes it accessible (eg via ethernet.higher_layer).
		# Which handler to be initialized generally depends on the type information (here higher_layer_type)
		# found in the current layer (see layer12/ethernet.py -> type).
		# Here higher_layer_type can become the value 0x66 (defined by __handler__ field) and
		# as a result ip.IP will be created as upper layer using the bytes given by "buf[total_header_length:]".
		return total_header_length, higher_layer_type

	# Handler can be registered by defining the static dictionary
	# __handler__ where the key is extracted from raw bytes in _dissect(...).
	# The value is the Packet class used to create the next upper layer (here ip.IP).
	# Add the "FIELD_FLAG_IS_TYPEFIELD" to the corresponding type field in __hdr__.
	__handler__ = {TYPE_VALUE_IP: ip.IP}  # just 1 possible upper layer

	def _update_fields(self):
		"""
		_update_fields(...) should be overwritten to update fields which depend on the state
		of the packet like lengths, checksums etc (see layer3/ip.py -> len, sum)
		aka auto-update fields.	The variable XXX_au_active indicates
		if the field XXX should be updated (True) or not
		(see layer3/ip.py -> bin() -> len_au_active). XXX_au_active is
		available if the field has the flag "FIELD_FLAG_AUTOUPDATE" in __hdr__,
		default value is True. _update_fields(...) is implicitly called by bin(...).
		"""
		if self._changed() and self.hlen_au_active:
			self.hlen = self.header_len

	def bin(self, update_auto_fields=True):
		"""
		bin(...)  should only be overwritten to allow more complex assemblation eg adding padding
		at the end of all layers instead of the current layer (see layer12/ethernet.py -> padding).
		The variable update_auto_fields indicates if fields should be updated in general.
		"""
		return pypacker.Packet.bin(self, update_auto_fields=update_auto_fields) + b"somepadding"

	def direction(self, other):
		"""
		direction(...) should be overwritten to be able to check directions to an other packet
		(see layer12/ethernet.py)
		"""
		direction = 0

		if self.src == other.src and self.dst == other.dst:
			direction |= pypacker.Packet.DIR_SAME
		if self.src == other.dst and self.dst == other.src:
			direction |= pypacker.Packet.DIR_REV

		if direction == 0:
			direction = pypacker.Packet.DIR_UNKNOWN
		return direction

	def reverse_address(self):
		"""
		reverse_address(...) should be overwritten to be able to reverse
		source/destination addresses (see layer12/ethernet.py)
		"""
		self.src, self.dst = self.dst, self.src


# Parse from raw bytes
# First layer (Layer 2)
newproto_bytes = b"\x66" + b"AAAA" + b"BBBB" + b"\x00\x16" + b"\x00" + b"1234" + b"\x00A\x01B\x02C"
# Next upper layer (Layer 3)
ip_bytes = ip.IP().bin()
# Layer above upper layer (Layer 4)
tcp_bytes = tcp.TCP().bin()
newproto_pkt = NewProtocol(newproto_bytes + ip_bytes + tcp_bytes)

print()
print(">>> Layers of packet:")
print("Output all layers: %s" % newproto_pkt)
print("Access some fields: 0x%X %s %s" % (newproto_pkt.type, newproto_pkt.src, newproto_pkt.dst))
print("Access next upper layer (IP): %s" % newproto_pkt[ip.IP])
print("A layer above IP (TCP): %s" % newproto_pkt[tcp.TCP])


# Create new Packet by defining every single header and adding higher layers
newproto_pkt = NewProtocol(
	type=0x66, src=b"AAAA", dst=b"BBBB", hlen=0x11, yolo=b"1234", options=[b"\x00A\x01B\x02C"]) +\
	ip.IP() +\
	tcp.TCP()

print()
print(">>> Layers of packet:")
print("Output all layers: %s" % newproto_pkt)
print("Access some fields: 0x%X %s %s" % (newproto_pkt.type, newproto_pkt.src, newproto_pkt.dst))
print("Access next upper layer (IP): %s" % newproto_pkt[ip.IP])
print("A layer above IP (TCP): %s" % newproto_pkt[tcp.TCP])
