import os
import logging
import collections
import threading
import subprocess
import time

from subprocess import PIPE, TimeoutExpired

logger = logging.getLogger("pra_framework")

class ZmapWrapper(object):
	def __init__(self, **kwargs):
		# there ary many arguments so we take a more efficient approach
		timestamp = time.strftime("%Y_%M_%d__%H_%M_%S")

		# Default parameters to be set. This is more efficient flexible
		# than explicitly setting them in __init__() via self._xyz...
		prop_defaults = {
			"port_target": 1234,
			"filename_output_csv" : None,
			"filename_blacklist_target_ip": None,
			# TODO: just for online scans
			"filename_whitelist_target_ip": None,
			"dirname_logs": None,
			# scan options
			"rate_kbit_per_s": 1000,
			"probes_per_target": 1,
			"timeout_wait": 0,
			# network options
			"interface_name": "eth0",
			"mac_source": None,
			"mac_gw": None,
			# probe module, we only use the pra module
			"probe_module": "tcp_synscan_pra",
			#"probe_module": "tcp_synscan",
			# additional options
			"marker_encoding": 0,
			"markervalue": None,
			"markerbits_value": 32,
			"markerbits_checksum": 0,
			"markerbits_dst_small" : 0,
			# disable status outputs, directly exit scanner after finishing (for small address ranges to speed up)
			"disable_monitor": 0,
			"filename_config": None,
			# target adresses as string: "1.2.3.4 1.2.4.0/24 ..."
			"target_addresses": None,
			"verbosity": 3,
			#
			"ready_callback": None,
			"name_timestamp": timestamp,
			# directory which contains "src/zmap wihtout trailing slash"
			"base_dir_zmap": "../zmap",
			# avoid establishing process communication and don't wait for process to finish
			"fast_mode": False,
			"scan_timeout": -1
		}

		for (prop, default) in prop_defaults.items():
			setattr(self, prop, kwargs.get(prop, default))

		if self.target_addresses is None:
			raise Exception("Not address given to scan")

		self.filename_zmap_bin = os.path.join(self.base_dir_zmap, "src/zmap")
		# use absolute path
		#self.filename_zmap_bin = os.path.abspath(self.filename_zmap_bin)
		# logger.debug("zmap binary: %s" % self.filename_zmap_bin)

		"""
		if self.filename_blacklist_target_ip is None:
			# TODO: change to full blacklist file
			self.filename_zmap_blacklist = os.path.join(self.base_dir_zmap, "blacklist.conf")
			#self.filename_blacklist_target_ip = os.path.join(self.base_dir_zmap, "blacklist.conf_minimal")
		"""
		self._scan_process = None
		self._is_running = False

	def start(self):
		"""
		Start a port scan. This call is blocking until the
		process is finished. Call stop() to terminate process earlier.
		"""
		if self._is_running:
			logger.warning("scanner is allready running")
			return
		# aggregate scan parameters
		cmd = [self.filename_zmap_bin]

		# basic arguments
		# this is generally not needed when using destination port marker but we set is per default
		cmd.extend(["-p", "%d" % self.port_target])
		if self.filename_blacklist_target_ip is not None:
			cmd.extend(["-b", self.filename_blacklist_target_ip])
		"""
		if self.filename_whitelist_target_ip is not None:
			cmd.extend(["-w", self.filename_whitelist_target_ip])
		"""
		if self.filename_output_csv is not None:
			cmd.extend(["-o", self.filename_output_csv])
			# data output
			cmd.extend(["-O", "csv"])
			cmd.extend(["-f", "classification,saddr,daddr,daddr_inner_icmp,sport,dport,success"])
		# scan options
		cmd.extend(["-B", "%dK" % self.rate_kbit_per_s])
		cmd.extend(["-P", "%d" % self.probes_per_target])
		if self.filename_output_csv is None:
			cmd.extend(["-c", "%d" % self.timeout_wait])
		else:
			# wait at minimum 2 seconds at end of scan to receive feedback
			cmd.extend(["-c", "%d" % 2])
		#cmd.extend(["--hello-timeout", self._timeout_syn])
		# network options
		if self.mac_gw is not None:
			cmd.extend(["-G", self.mac_gw])
		if self.mac_source is not None:
			cmd.extend(["--source-mac", self.mac_source])
		cmd.extend(["-i", self.interface_name])
		# probe modules
		cmd.extend(["-M", self.probe_module])
		# additional options
		cmd.extend(["--marker_encoding", "%d" % self.marker_encoding])
		if self.markervalue is not None:
			cmd.extend(["--use_markervalue", "1"])
			cmd.extend(["--markervalue", "%d" % self.markervalue])
		cmd.extend(["--markerbits_value", "%d" % self.markerbits_value])
		cmd.extend(["--markerbits_checksum", "%d" % self.markerbits_checksum])
		cmd.extend(["--disable_monitor", "%d" % self.disable_monitor])

		if self.filename_config is not None:
			cmd.extend(["-C", self.filename_config])
		cmd.extend(["-v", "%d" % self.verbosity])
		# TODO: activate for zmap internal Logging
		if self.dirname_logs is not None:
			cmd.extend(["-L", self.dirname_logs])
		# limit scan time
		if self.scan_timeout > 0:
			cmd.extend(["-t", "%d" % self.scan_timeout])
		# two cores are enough to achieve a maximum speed
		#cmd.extend(["--cores", "0"])
		#cmd.extend(["--cores", "0,1"])
		cmd.extend(["--cores", "0,1,2"])
		# addresses as list
		cmd.extend(self.target_addresses)

		logger.debug("starting scanner: " + " ".join(cmd))

		self._is_running = True

		#return

		# start the process
		try:
			#self._scan_process = subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
			if not self.fast_mode:
				self._scan_process = subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
			else:
				self._scan_process = subprocess.Popen(cmd)
		except Exception:
			logger.warning("could not start process, killing it")
			self._scan_process.kill()

		#if self.fast_mode:
		#	return
		# if self._scan_process.returncode != 0:
		#	logger.warning("something went wrong while scanning, check output")
		# wait until scan has finished
		try:
			self._scan_process.wait()
			self._check_process_output()
		except TimeoutExpired:
			# this will update date for status retrieval (time left)
			self._check_process_output()

		if self._is_running:
			self.stop()

		if self.ready_callback is not None:
			# self._extract_ips()
			self.ready_callback(self)

	def _check_process_output(self):
		try:
			outs, errs = self._scan_process.communicate(timeout=1)
			#if self.disable_monitor == 0:
			#	logger.debug("standard output\n:" + outs.decode("ASCII"))
			#	logger.debug("error output:\n" + errs.decode("ASCII"))
		except:
			pass

	def stop(self):
		if not self._is_running:
			logger.warning("scanner was not running (anymore?)")
			return
		self._is_running = False
		# avoid ResourceWarning: unclosed file <_io.BufferedReader name=7>
		try:
			self._scan_process.stdout.close()
			self._scan_process.stdin.close()
			self._scan_process.stderr.close()
		except:
			pass

		try:
			# self._scan_process.terminate()
			# do it the hard way, we don't need any resume storing
			self._scan_process.kill()
		except ProcessLookupError as e:
			# logger.warning("could not terminate process")
			# logger.warning(e)
			# assume process already finished
			pass
