import struct
import logging

logger = logging.getLogger("packetracer")

# Allows to activate/decative the auto-update of a header field.
# Note: _update_fields() has to be implemented
FIELD_FLAG_AUTOUPDATE	= 1
# Identifies the header field defining the next higher layer type
# Allows to auto-set the value on concatenation: "pkt = eth0 + ip0" sets type field in eth0
# Auto-sets FIELD_FLAG_AUTOUPDATE
# Note: _update_fields() has to be implemented and _update_higherlayer_id() has to be called in it.
FIELD_FLAG_IS_TYPEFIELD	= 2


def get_setter(varname, is_field_type_simple=True, is_field_static=True):
	"""
	varname -- name of the variable to set the property for
	is_field_type_simple -- get property for simple static or dynamic type if True, else TriggerList
	is_field_static -- if is_field_type_simple is True: get static type (int, fixed size bytes, ...),
		else dynamic (format "xs") which can change in format (eg DNS names)

	return -- set-property for simple types or triggerlist
	"""
	varname_shadowed = "_%s" % varname

	def setfield_simple(obj, value):
		"""
		Set value for a simple field

		value -- bytes, int or None
		"""
		if obj._unpacked is not None and not obj._unpacked:
			# obj._unpacked = None means: dissect not yet finished
			obj._unpack()
		if value is None and obj.__getattribute__(varname_shadowed + "_active"):
			# deactivate active field
			object.__setattr__(obj, varname_shadowed + "_active", False)
			obj._header_format_changed = True
			# logger.debug("deactivating field: %s" % varname_shadowed)
		elif value is not None and not obj.__getattribute__(varname_shadowed + "_active"):
			# activate inactive field
			object.__setattr__(obj, varname_shadowed + "_active", True)
			obj._header_format_changed = True
			# logger.debug("activating field: %s" % varname_shadowed)

		if value is not None and not is_field_static:
			# simple dynamic field: update format
			format_new = "%ds" % len(value)
			# logger.debug(">>> changing format for dynamic field: %r / %s / %s" %
			# (obj.__class__, varname_shadowed, format_new))
			object.__setattr__(obj, varname_shadowed + "_format", format_new)
			obj._header_format_changed = True

		#logger.debug("setting simple field: %r=%r" % (varname_shadowed, value))
		object.__setattr__(obj, varname_shadowed, value)
		obj._header_changed = True
		obj._notify_changelistener()

	def setfield_triggerlist(obj, value):
		"""
		Clear list and add value as only value.

		value -- Packet, bytes (single or as list)
		"""
		tl = obj.__getattribute__(varname_shadowed)

		if type(tl) is list:
			# We need to create the original TriggerList in order to unpack correctly
			# _triggerlistName = [b"bytes", callback] or
			# _triggerlistName = [b"", callback] (default initiation)
			# logger.debug(">>> initiating TriggerList")
			tl = obj._header_fields_dyn_dict[varname_shadowed](
				obj,
				dissect_callback=tl[1],
				buffer=tl[0],
				headerfield_name=varname_shadowed
			)
			object.__setattr__(obj, varname_shadowed, tl)
		# This will trigger unpacking

		del tl[:]

		# TriggerList: avoid overwriting dynamic fields eg when using keyword constructor Class(key=val)
		if type(value) is list:
			tl.extend(value)
		else:
			tl.append(value)
		obj._header_changed = True
		obj._notify_changelistener()

	if is_field_type_simple:
		return setfield_simple

	return setfield_triggerlist


def get_getter(varname, is_field_type_simple=True):
	"""
	varname -- name of the variable to set the property for
	is_field_type_simple -- get property for simple static or dynamic type if True, else TriggerList
	return -- get-property for simple type or triggerlist
	"""
	varname_shadowed = "_%s" % varname

	def getfield_simple(obj):
		"""
		Unpack field ondemand
		"""
		# logger.debug("getting value for simple field: %s" % varname_shadowed)
		if obj._unpacked is not None and not obj._unpacked:
			obj._unpack()
		# logger.debug("getting simple field: %r=%r" %
		# (varname_shadowed, obj.__getattribute__(varname_shadowed)))
		return obj.__getattribute__(varname_shadowed)

	def getfield_triggerlist(obj):
		tl = obj.__getattribute__(varname_shadowed)
		# logger.debug(">>> getting Triggerlist for %r: %r" % (obj.__class__, tl))

		if type(tl) is list:
			# _triggerlistName = [b"bytes", callback] or
			# _triggerlistName = [b"", callback] (default initiation)
			tl = obj._header_fields_dyn_dict[varname_shadowed](
				obj,
				dissect_callback=tl[1],
				buffer=tl[0],
				headerfield_name=varname_shadowed
			)
			object.__setattr__(obj, varname_shadowed, tl)

		return tl

	if is_field_type_simple:
		return getfield_simple

	return getfield_triggerlist


def configure_packet_header(t, hdrs, header_fmt):
	if hdrs is None:
		return

	# Create a property for every field: property a -> get/set access to _a_shadowed.
	# Using properties will slow down access to header fields but it's needed:
	# This way we get informed about get-access (needed to check for unpack)
	# more efficiently than using __getattribute__ (slow access for header
	# fields vs. slow access for ALL class members).
	# Every header field will get two additional values set:
	# var_active = indicates if header is active
	# var_format = indicates the header format
	for hdr in hdrs:
		# Sanity checks
		# Max packet fields: [name, format, value, flags, {value:key})
		if len(hdr) > 4:
			logger.warning("Amount of field definitions > 4: %r", hdr)

		if hdr[1] is not None:
			try:
				struct.Struct(hdr[1])
			except struct.error:
				raise Exception("Invalid format specified in class %s for header '%s': '%s'" %
					(t.__module__ + "." + t.__name__, hdr[0], hdr[1]))

			if hdr[2] is not None:
				try:
					struct.Struct(hdr[1]).pack(hdr[2])
				except struct.error:
					raise Exception("Invalid value specified in class %s for header '%s' with format '%s': '%s'" %
						(t.__module__ + "." + t.__name__, hdr[0], hdr[1], hdr[2]))

		shadowed_name = "_%s" % hdr[0]
		t._header_field_names.append(shadowed_name)
		setattr(t, shadowed_name + "_active", True)

		# remember header format
		# t._header_field_infos[shadowed_name] = [True, hdr[1]]
		is_field_type_simple = False
		is_field_static = True

		if hdr[1] is not None or (hdr[2] is None or type(hdr[2]) == bytes):
			# simple static or simple dynamic type
			# we got one of: ("name", format, ???) = static or
			# ("name", None, [None, b"xxx"]) = dynamic
			# -> Format given = static, Format None = dynamic
			is_field_type_simple = True

			if hdr[1] is None:
				# assume simple dynamic field
				is_field_static = False

		setattr(t, shadowed_name + "_format", hdr[1])

		if is_field_type_simple:
			# assume simple static or simple dynamic type
			fmt = hdr[1]

			if hdr[2] is not None:
				# value given: field is active
				if fmt is None:
					# dynamic field
					fmt = "%ds" % len(hdr[2])
					setattr(t, shadowed_name + "_format", fmt)
				header_fmt.append(fmt)
				t._header_cached.append(hdr[2])
				# logger.debug("--------> field is active: %r" % hdr[0])
			#else:
			#	setattr(t, shadowed_name + "_active", False)

			# only simple fields can get deactivated
			setattr(t, shadowed_name + "_active", True if hdr[2] is not None else False)

			# check for auto-update
			if len(hdr) >= 4:
				field_flags = hdr[3]

				if field_flags & FIELD_FLAG_IS_TYPEFIELD != 0:
					#logger.debug("setting _id_fieldname: %r" % (hdr[0]))
					setattr(t, "_id_fieldname", hdr[0])
					# xxx__au_active must be set: read by _update_higherlayer_id
					field_flags |= FIELD_FLAG_AUTOUPDATE

				if field_flags & FIELD_FLAG_AUTOUPDATE != 0:
					#logger.debug("marking %s as auto-update" % hdr[0])
					# remember which fields are auto-update ones, default is active
					setattr(t, hdr[0] + "_au_active", True)

			# Setting/getting value is done via properties.
			# Set initial value via shadowed variable:
			# _varname <- varname [optional in subclass: <- varname_s]
			# logger.debug("init simple type: %s=%r" % (shadowed_name, hdr[2]))
			setattr(t, shadowed_name, hdr[2])
			setattr(t, hdr[0], property(
				get_getter(hdr[0], is_field_type_simple=True),
				get_setter(hdr[0], is_field_type_simple=True, is_field_static=is_field_static)
			))
		else:
			# assume TriggerList
			# Triggerlists don't have initial default values (and can't get deactivated)
			t._header_fields_dyn_dict[shadowed_name] = hdr[2]
			# initial value of TiggerLists is: values to init empty list
			setattr(t, shadowed_name, [b"", None])
			setattr(t, hdr[0], property(
				get_getter(hdr[0], is_field_type_simple=False),
				get_setter(hdr[0], is_field_type_simple=False, is_field_static=is_field_static)
			))
			# format and value needed for correct length in _unpack()
			# default is empty TriggerList -> 0 bytes
			header_fmt.append("0s")
			t._header_cached.append(b"")


def configure_packet_header_sub(t, hdrs_sub):
	if hdrs_sub is None:
		return

	for name_cbget_cbset in hdrs_sub:
		if len(name_cbget_cbset) < 2:
			logger.warning("subheader length < 2: %d", len(name_cbget_cbset))
			continue
		# logger.debug("setting subheader: %s", name_cbget_cbset[0])

		# (name, cb_get, cb_set)
		if len(name_cbget_cbset) == 3:
			setattr(t, name_cbget_cbset[0], property(name_cbget_cbset[1], name_cbget_cbset[2]))
		# (name, cb_get)
		else:
			setattr(t, name_cbget_cbset[0], property(name_cbget_cbset[1]))


class MetaPacket(type):
	"""
	This Metaclass is a more efficient way of setting attributes than using __init__.
	This is done by reading name, format and default value out of a mendatory __hdr__
	tuple in every subclass. This configuration is set one time when loading the module
	(not at instantiation). Attributes can be normally accessed using "obj.field" notation.
	General note: Callflaw is: __new__ (loading module) -> __init__ (initiate class)

	Header defintition example:
	__hdr__ = (
		("header1", "H", 123), # simple static field
		("header2", "H", None), # simple static field, inactive
		("header3", None, b"xxx"), # simple dynamic field
		("header4", None, None), # simple dynamic field, inactive
		("header5", None, Triggerlist) # TriggerList field
	)

	For values <1 byte a subheader definition eases up setting/getting those values:

	__hdr_sub__ = (
		("header1_sub",
			lambda val: val & 1							# callback to retrieve value
			lambda obj, val: obj.__setattr__(val & 1)	# callback to set value
		),
		...
	)

	CAUTION:
	- List et al are _SHARED_ among all instantiated classes! A copy is needed on
	changes to them without side effects
	- New protocols: header field names must be unique among other variable and method names
	"""
	def __new__(cls, clsname, clsbases, clsdict):
		# Slots (dct["__slots__"] = ...) can't be used because:
		# Setting default values (eg for _header_fields_dyn_dict) must
		# be done in __init__ which increases delay (init for every instantiation...)
		# Sidenote: Setting default values here creates readonly exception later:
		# __slots__ = ("var", ...) -> t.var = None -> p = Clz() -> p.var = 123 won't work (var is readonly)
		# See: https://stackoverflow.com/questions/820671/python-slots-and-attribute-is-read-only
		t = type.__new__(cls, clsname, clsbases, clsdict)
		# dictionary of TriggerLists: name -> TriggerListClass
		t._header_fields_dyn_dict = {}
		# cache header for performance reasons, will be set to bytes later on
		t._header_cached = []
		# all header names
		t._header_field_names = []
		# all header formats including byte order
		header_fmt = [">"]

		# varname holding the fieldname containing the id associated with body handler
		# eg Ethernet -> "type" or IP -> "p"
		t._id_fieldname = None

		# get header-infos: [("name", "format", value), ...]
		hdrs = getattr(t, "__hdr__", None)
		configure_packet_header(t, hdrs, header_fmt)

		# get sub-header-infos: [("name", cb_get, cb_set), ...]
		hdrs_sub = getattr(t, "__hdr_sub__", None)
		configure_packet_header_sub(t, hdrs_sub)

		# get handler classes, assume Packet class has no member "__handler__"
		handler = getattr(t, "__handler__", None)

		if handler is not None and len(handler) > 0:
			if handler.__class__ is not dict:
				print("Invalid format of __handler__: not a dictionary! %r", handler)
			else:
				t.load_handler(t, handler)

		# Set of higher_layer classes which force dissecting (update dependends):
		# IP->TCP => IP changed, TCP needs checksum update and
		# needs to be dissected for this => TCP is ud in ip
		update_deps = getattr(t, "UPDATE_DEPENDANTS", set())
		t._update_dependants = set([ud.__class__ for ud in update_deps])

		# logger.debug(">>> translated header names: %s/%r" % (clsname, t._header_name_translate))
		# current format as string
		t._header_format = struct.Struct("".join(header_fmt))
		# header size can be assigened by __init__() directly or given by _header_format.size
		t._header_len = t._header_format.size
		# track changes to header format (changes to simple dynamic fields or TriggerList)
		t._header_format_changed = False
		# cached header, return this if nothing changed
		t._header_cached = t._header_format.pack(*t._header_cached)
		# logger.debug("formatstring is: %s" % header_fmt)
		# body as raw byte string (None if handler is present)
		t._body_bytes = b""
		# next lower layer: a = b + c -> b will be lower layer for c
		t._lower_layer = None
		t._higher_layer = None
		# track changes to header values: This is needed for layers like TCP for
		# checksum-recalculation. Set to "True" on changes to header/body values, set to False on "bin()"
		# track changes to header values
		t._header_changed = False
		# track changes to body value like [None | bytes | body-handler] -> [None | bytes | body-handler]
		t._body_changed = False
		# objects which get notified on changes on header or body (shared)
		# needs to be None do identify none-initialized variable
		t._changelistener = None
		# parent of the packet which is contained in a triggerlist
		# parent_packet.triggerlist[sub] -> sub._triggelistpacket_parent == parent_packet
		t._triggelistpacket_parent = None
		# lazy handler data: [name, class, bytes]
		t._lazy_handler_data = None
		# Indicates the most top layer until which should be unpacked
		# (vs. lazy dissecting = just next higher layer)
		# Setting this to an unknown class will keep the next-layer-parsing going on
		t._final_unpack_clz = None
		# indicates if static header values got already unpacked
		# [True|False] = Status after dissect, None = pre-dissect (not unpacked)
		t._unpacked = None
		# indicates if this packet contains fragmented data saved as body bytes
		t._fragmented = False
		# concatination of errors, see packetracer.py -> ERROR_...
		t._errors = 0
		return t
