#!/usr/bin/env python
# import the necessary packages
#from asyncio.windows_events import NULL
import numpy as np
from simplejson import JSONEncoder
import rospy
import sys
import json
from enum import Enum, auto
from std_msgs.msg import String
from diagnostic_msgs.msg import KeyValue
import datetime
import copy

from tunstall_manager.srv import command_tunstall_manager, command_delete_file

import time
# from rospy.exceptions import ROSException
def to_seconds(date):
    return time.mktime(date.timetuple())

# TODO:
#

class TScenario (Enum):
	NONE = auto()
	CAFE = auto()
	CANSANCIO = auto()
	SEGURIDAD = auto()
	PUERTA_CERRADA = auto()

# preguntar por los argumentos de estas clases
class TSensorType (Enum):
	DOOR = auto()
	PIR = auto()
	CHAIR = auto()
	UNKNOWN = auto()

class TSensor:
	sensor_id : 0
	sensor_position = (0.0,0.0)
	sensor_room = "Unknown"
	sensor_type = "Unknown"
	sensor_status = (False,0)

''' MAIN CLASS'''
class tunstall_manager_node:
	def __init__(self) -> None:
		self.pub_alarm = rospy.Publisher('/tunstall/result', String, queue_size=10)
		self.pub_task = rospy.Publisher('/mqtt2ros', KeyValue, queue_size=10)
		self.sub_trigger = rospy.Subscriber('/tunstall/trigger', String, self.tunstall_trigger_callback)
		self.sub_recognizer = rospy.Subscriber('/face_recognizer/name',String, self.face_recognized_callback)
		self.sub_bt_manager = rospy.Subscriber('/bt_manager/task_manager_result',String, self.bt_manager_callback)
		self.rate = rospy.Rate(1)
		self.sensor_database = None
		self.db_counter = 0

		self.sensor_file = rospy.get_param('~sensorFile')
		self.move_command_file = rospy.get_param('~move_command_file')
		self.speak_command_file = rospy.get_param('~speak_command_file')
		self.face_detect_command_file = rospy.get_param('~face_detect_command_file')
		self.wait_command_file = rospy.get_param('~wait_command_file')

		self.verbose = rospy.get_param('~verbose')
		self.active = True
		self.scenario = TScenario.NONE
		self.time_threshold = 3 # 60 # minutes

		self.check_door_counter = 0

		# detect face timeout
		self.face_detection_received_msg = False
		self.activate_timer = False
		self.waiting_for_task = ""

		# Variable para indicar si se ha enviado el comando de detección de caras
		# self.face_detect_command_sent = False

		# activate timer for each 5 minutes
		rospy.Timer(rospy.Duration(3*60), self.cansancio_timer_callback)

		# services for handling commands
		self.srv_cmd = rospy.Service('~command_tunstall_manager', command_tunstall_manager, self.handle_command_tunstall_manager)
		self.srv_del = rospy.Service('~command_delete_file', command_delete_file, self.handle_command_delete_file)

		# load info from files
		self.load_sensors_from_file()
		self.load_speak_command_from_file()
		self.load_move_command_from_file()
		self.load_face_detect_command_from_file()
		self.load_wait_command_from_file()

        # load sensor database from json file
        # sensors = {}
        # id ==> {place (dupla), type (enum), status (bool)}
		# example:
        # 	{
		# 		id : 1,
		# 		position : (7.1, 3.4),
		# 		room : "2.3.6i",
		# 		name : "Paco"
		# 		type : TSensorType.CHAIR,
		# 		status : [[False, 0]]
		# 	}

		while not rospy.is_shutdown():
			self.rate.sleep()

	''' CALLBACK METHODS '''
	# callback for the node services:
	# 	on/off
	# 	save history
	# 	delete all sensors history
	def handle_command_tunstall_manager(self,req):

        # start node processing
		if req.task_command == "on":
			self.active = True
			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Received command: ON")

			return True

		# save the sensor information to file
		elif req.task_command == "save":

			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Received command: SAVE")

			# Saving the data on a Json file
			self.save_db()

			return True

		# delete all sensors history
		elif req.task_command == "restart":

			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Received command: RESTART")

			for sensor in self.sensor_db_dict['sensor_db']:
				sensor ["status"] = sensor["status"][-1]
				if self.verbose:
					print(sensor)

			return True

		# pause the node processing
		elif req.task_command == "off":
			self.active = False
			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Received command: OFF")

			return True

		else:
			return False

	# callback for service 'delete' history for specific sensor
	def handle_command_delete_file(self,req):

		# check if we want delete a single element
		if req.task_command == "delete":
			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Received command: DELETE")

				encontrado = False
				for sensor in self.sensor_db_dict['sensor_db']:

					if req.id == sensor['id']:
						sensor['status'] = sensor["status"][-1]
						encontrado = True
						if self.verbose:
							# for debugging
							print(sensor)
						break

				if not encontrado:
					rospy.loginfo("[tunstall_manager_node] ERROR: id not found")

			return True

		else:
			return False

	def bt_manager_callback(self, msg):
		task_result = json.loads(str(msg.data))
		task_name = task_result["name"]
		task_status = task_result["status"]

		if task_name == self.waiting_for_task and task_status == "success" and self.activate_timer:
			rospy.loginfo("[tunstall_manager_node] TIMER for face detection activated.")
			rospy.Timer(rospy.Duration(30), self.face_detection_timer_callback, oneshot = True)
			self.activate_timer = False

	# callback to check CANSANCIO
	def cansancio_timer_callback(self, event):

		# Do this only if nothing else is going on
		if self.scenario != TScenario.NONE:
			return

		rospy.loginfo("[tunstall_manager_node] Cansancio WATCHDOG triggered")

		# Check time for all CHAIR sensors and trigger alarm if needed
		for sensor in self.sensor_db_dict['sensor_db']:
			if sensor["type"] == TSensorType.CHAIR:
				first_room = sensor["room"]
				first_name = sensor["name"]
				is_active  = sensor["status"][-1][0]
				time_difference = (rospy.get_time()-sensor["status"][-1][1])/60

				print(f'[tunstall_manager_node] {first_name} [{sensor["id"]}] has been {is_active} for {time_difference} minutes')

				if is_active and (time_difference > self.time_threshold):
					# this has been sitting for more than one hour
					if self.verbose:
						rospy.loginfo("[tunstall_manager_node] Chair TIMER: more than " + str(self.time_threshold) + " minutes have passed")

					# Find the corresponding door
					for sensor in self.sensor_db_dict['sensor_db']:
						if sensor["type"] == TSensorType.DOOR and sensor["room"] == first_room:
							my_door = sensor
							is_open = not my_door["status"][-1][0]
							found = True
							break

					if found:
						self.scenario = TScenario.CANSANCIO

						if is_open:
							# puerta abierta
							rospy.loginfo("[tunstall_manager_node] Door is open")
							self.send_move_command(first_name)
							rospy.loginfo ("[tunstall_manager_node] Moving to location")
							self.send_face_detect_command("on")
							rospy.loginfo ("[tunstall_manager_node] Detectando cara")
							self.waiting_for_task = "GO_TO_" + first_name
							self.activate_timer = True

						else:
							# puerta cerrada
							rospy.loginfo("[tunstall_manager_node] Door closed, I need help to open it")
							self.send_speak_command("I need someone to open " + first_name + "'s door")

							# save data for later processing
							self.linked_door_id = my_door["id"]
							self.target = first_name

							# update scenario
							# self.scenario = TScenario.PUERTA_CERRADA

							# Launch a timer to check the door every 2 seconds (up to 30s)
							self.check_door_timer = rospy.Timer(rospy.Duration(2), self.check_door_timer_callback)

			##

	# callback for timer for checking if a door is open
	def check_door_timer_callback(self, event):
		rospy.loginfo("[tunstall_manager_node] Check door WATCHDOG triggered")

		# get last status of the door
		linked_door_sensor = self.find_sensor_by_id(self.linked_door_id)
		is_open = not linked_door_sensor["status"][-1][0]

		if is_open:
			# stop timer
			self.check_door_timer.shutdown()

			# Go to the position of the chair sensor and activate face_detection_node
			self.send_move_command(self.target)
			self.send_face_detect_command("on")
			self.waiting_for_task = "GO_TO_" + self.target
			self.activate_timer = True

			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Door open! Moving to " + self.target + "'s chair")
				rospy.loginfo("[tunstall_manager_node] Sending detect face command")

			return

		# check up to 15 times
		self.check_door_counter += 1
		if self.check_door_counter > 15:
			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] Check door timer TIMEOUT")

			self.check_door_counter = 0
			self.check_door_timer.shutdown()

			# cancel scenario
			self.scenario = TScenario.NONE

	def face_detection_timer_callback(self, event):
		if not self.face_detection_received_msg:

			self.face_detect_command_sent = False
			self.send_face_detect_command("off")
			self.send_move_command("docking_station")

			if self.verbose:
				rospy.loginfo('[tunstall_manager_node] No se ha detectado ninguna cara en los últimos 30 segundos.')
				rospy.loginfo('[tunstall_manager_node] Detect face off')
				rospy.loginfo('[tunstall_manager_node] Scenario cancelled, moving back to docking station')

			# cancel scenario
			self.scenario = TScenario.NONE

		else:
			# reset flag
			self.face_detection_received_msg = False

	# process face recognition result
	def face_recognized_callback(self,msg):

		# Face recognition (and hence detection) have been completed
		self.face_detection_received_msg = True

		recognized_name = str(msg.data)

		if self.verbose:
			rospy.loginfo("[tunstall_manager_node] Recognized name: " + recognized_name)

		text_to_say = ""
		if self.scenario == TScenario.CAFE:
			# callback from the topic "face_recognizer/nombre"
			if recognized_name == "desconocido":
				text_to_say = "This is not your seat, please, stand up"

			elif recognized_name == "Roberto":
				text_to_say = "Your coffee is in the yellow mug, sugar free, as you like"

			elif recognized_name == "Paco":
				text_to_say = "Your coffee is in the paper cup, with lactose free milk "

			else:
				text_to_say = "I am sorry, I do not have a coffee for you"

		elif self.scenario == TScenario.CANSANCIO:
			if recognized_name == "desconocido":
				text_to_say = "You should take a rest"
			else:
				text_to_say = "You should take a rest, " + recognized_name

		elif self.scenario == TScenario.SEGURIDAD:
			too_late = "22:00:00"
			too_early = "07:00:00"
			if self.verbose:
				rospy.loginfo("[tunstall_manager_node] too late is after " + too_late)
				rospy.loginfo("[tunstall_manager_node] too early is before " + too_early)

			if recognized_name == "Roberto":
				current_time =self.get_time_zone()
				if current_time> too_late or current_time < too_early:
					rospy.loginfo("[tunstall_manager_node] Current time zone is dangerous")
					text_to_say = recognized_name + ", you do not have permission to be here at this time" # + current_time

				else:
					for sensor in self.sensor_db_dict['sensor_db']:
						if sensor["type"] == TSensorType.CHAIR:
							if sensor["status"][-1][0]:
								if sensor["name"] == "Roberto":
									text_to_say = recognized_name + " there is someone in your chair"
								elif sensor["name"] == "Paco":
									text_to_say = recognized_name + " if you are loocking for Paco, he is in his chair"

			elif recognized_name == "Paco":
				current_time =self.get_time_zone()
				if current_time> too_late or current_time < too_early:
					rospy.loginfo("[tunstall_manager_node] Current time zone is dangerous")
					text_to_say = recognized_name + ", do you have permission to be here at this time?" # + current_time + "?"

				else:
					for sensor in self.sensor_db_dict['sensor_db']:
						if sensor["type"] == TSensorType.CHAIR:
							if sensor["status"][-1][0]:
								if sensor["name"] == "Paco":
									text_to_say = recognized_name + " There is someone in your chair"
								elif sensor["name"] == "Roberto":
									text_to_say = recognized_name + " if you are loocking for Roberto, he is in his chair"

			elif str(msg.data) == "Desconocido":
				current_time =self.get_time_zone()
				if current_time> too_late or current_time < too_early:
					rospy.loginfo("[tunstall_manager_node] Current time zone is dangerous")
					text_to_say = recognized_name + ", leave now or i will call security"

				else:

					for sensor in self.sensor_db_dict['sensor_db']:
						if sensor["type"] == TSensorType.CHAIR:
							if sensor["status"][-1][0]:
								if sensor["name"] == "Paco":
									text_to_say = "If you are looking for Paco, he is in his chair."
								elif sensor["name"] == "Roberto":
									text_to_say ="If you are looking for Roberto, he is in his chair."

			else:
				current_time =self.get_time_zone()
				if current_time> too_late or current_time < too_early:
					rospy.loginfo("[tunstall_manager_node] Current time zone is dangerous")
					text_to_say = recognized_name + ", leave now or i will call security"

				else:

					for sensor in self.sensor_db_dict['sensor_db']:
						if sensor["type"] == TSensorType.CHAIR:
							if sensor["status"][-1][0]:
								if sensor["name"] == "Paco":
									text_to_say = "If you are looking for Paco, he is in his chair."
								elif sensor["name"] == "Roberto":
									text_to_say ="If you are looking for Roberto, he is in his chair."

		# Now speak! and go back to the docking station
		if self.verbose:
			rospy.loginfo("[tunstall_manager_node] Saying" + text_to_say)

		self.send_speak_command(text_to_say)
		self.send_move_command("docking_station")

		# Reset state
		self.scenario = TScenario.NONE

	# a sensor has been triggered
	def tunstall_trigger_callback(self,msg):

		if not self.active:
			return

		split_data = msg.data.split("_")
		code_id = split_data[0]
		code = code_id[0:2]
		id = code_id[2:4]

		if self.verbose:
			rospy.loginfo("[tunstall_manager_node] Received tunstall trigger: " + str(code) + ", " + str(id))

		date_time_str = split_data[1]
		date_time_str = date_time_str[1:27]

		date_time_obj = datetime.datetime.strptime(date_time_str, "%Y-%m-%d %H:%M:%S.%f")
		timestamp = to_seconds(date_time_obj)

		# SCENARIOS:
		# -- [1] CAFE: A chair is activated, the robot goes there with two cups, recognizes the person and offers their coffee.
		# -- [2] CANSANCIO: Every 5 minutes, the node checks if any chair has been activated for more than one hour. If so, goes to ask him to get some exercise. (No face recognition) ALthough it could use face recognition to say de person's
		# -- [3] SECURITY: All chairs are activated, the main door is open and the PIR is activated. The robot goes to the PIR, recognizes the face and ask for security if needed.
		# -- Secondary: in all scenarios, if the robot needs to traverse a closed door, it asks for help, waits some time and if nobody opens it then cancels the mission.

		# a door has been opened
		if code == "AQ" :
			if self.check_type(id,TSensorType.DOOR):
				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] Door: " + str(id) + " has been opened")

				aux = False
				self.add_to_history(aux, timestamp, id)
			else:
				rospy.logwarn("[tunstall_manager_node] Warning: This kind of sensor does not use this code")

		# a door has been closed
		elif code == "AR" :
			if self.check_type(id,TSensorType.DOOR):
				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] Door: " + str(id) + " has been closed")

				aux = True
				self.add_to_history(aux, timestamp, id)
			else:
				rospy.logwarn("[tunstall_manager_node] Warning: This kind of sensor does not use this code")

		# someone has sit
		elif code == "BA" :
			if self.check_type(id,TSensorType.CHAIR):
				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] Chair: " + str(id) + " has been activated")

				aux = True
				self.add_to_history(aux, timestamp, id)
			else:
				rospy.logwarn("[tunstall_manager_node] Warning: This kind of sensor does not use this code")

		# someone has stood up
		elif code == "AZ" :
			if self.check_type(id,TSensorType.CHAIR):
				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] Chair: " + str(id) + " has been deactivated")

				aux = False
				self.add_to_history(aux, timestamp, id)
			else:
				rospy.logwarn("[tunstall_manager_node] Warning: This kind of sensor does not use this code")

		# someone has passed the way: PIR activated
		elif code == "BH" :
			if self.check_type(id,TSensorType.PIR):
				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] PIR: " + str(id) + " has been activated")

				aux = True
				self.add_to_history(aux, timestamp, id)
			else:
				rospy.logwarn("[tunstall_manager_node] Warning: This kind of sensor does not use this code")

		self.save_db()
		self.print_all_sensors()

		# STATE MACHINE
		if self.scenario == TScenario.NONE:

			if  code == "BA":
				# A chair has been occuppied.
				# CAFE scenario triggered
				self.scenario = TScenario.CAFE

				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] Pasando a escenario CAFE ...")

				my_sensor = self.find_sensor_by_id(id)
				first_room = my_sensor["room"]
				first_name = my_sensor["name"]

				# Find the corresponding door
				found = False
				for sensor in self.sensor_db_dict['sensor_db']:
					if sensor["type"] == TSensorType.DOOR and sensor["room"] == first_room:
						my_door = sensor
						is_open = not my_door["status"][-1][0]
						found = True
						break

				if found:
					if is_open:
						# puerta abierta
						rospy.loginfo("[tunstall_manager_node] Puerta abierta")
						self.send_move_command(first_name)
						self.send_face_detect_command("on")
						self.waiting_for_task = "GO_TO_" + first_name
						self.activate_timer = True

					else:
						# puerta cerrada
						rospy.loginfo("[tunstall_manager_node] Puerta cerrada, necesito que me abran la puerta")
						self.send_speak_command("I need someone to open " + first_name + "'s door")

						# save data for later processing
						self.linked_door_id = my_door["id"]
						self.target = first_name

						# update scenario
						# self.scenario = TScenario.PUERTA_CERRADA

						# Launch a timer to check the door every 2 seconds (up to 30s)
						self.check_door_timer = rospy.Timer(rospy.Duration(2), self.check_door_timer_callback)
				else:
					rospy.loginfo("[tunstall_manager_node] No hay puerta asociada al sensor")

			if code == 	"BH":
				# PIR sensor has been triggered.
				# SEGURIDAD scenario triggered
				self.scenario = TScenario.SEGURIDAD

				if self.check_type(id,TSensorType.PIR):
					if self.verbose:
						rospy.loginfo("[tunstall_manager_node] PIR: " + str(id) + " has been activated")

				self.send_move_command("PIR")
				self.send_face_detect_command("on")
				self.waiting_for_task = "GO_TO_PIR"
				self.activate_timer = True

				if self.verbose:
					rospy.loginfo("[tunstall_manager_node] Dirigiéndose al PIR... ")
					rospy.loginfo("[tunstall_manager_node] Detectando cara...")
					rospy.loginfo("[tunstall_manager_mode] Elaborando una respuesta... ")

	''' AUXILIARY METHODS '''
	# builds a JSON message to be sent to the robot for talking
	def send_speak_command(self, text):
		# complete SAY command
		self.speak_command["data"]["text"]["talkcode"] = text
		self.speak_command["time"]["t"] = rospy.get_time()

		# create message and publish it
		msg = KeyValue()
		msg.key = "interventions/famd/INFO"
		msg.value = json.dumps(self.speak_command)
		self.pub_task.publish(msg)

	# builds a JSON message to be sent to the robot for going somewhere
	def send_move_command(self, place):
		# complete GO command
		self.move_command["data"]["label"] = place
		self.move_command["time"]["t"] = rospy.get_time()

		# create message and publish it
		msg = KeyValue()
		msg.key = "interventions/famd/INFO"
		msg.value = json.dumps(self.move_command)
		self.pub_task.publish(msg)

	# builds a JSON message to start the face detection process
	def send_face_detect_command(self, command, timeout_timer = False):
		# complete FACE_DETECT command
		self.face_detect_command["time"]["t"] = rospy.get_time()

		if command == "on" :
			self.face_detect_command["data"]["task_command"] = command
			self.face_detect_command_sent = True

		elif command == "off":
			self.face_detect_command["data"]["task_command"] = command
			self.face_detect_command_sent = False

		else:
			rospy.loginfo("[tunstall_manager_node] Face detect command not valid")

		# create message and publish it
		msg = KeyValue()
		msg.key = "interventions/famd/INFO"
		msg.value = json.dumps(self.face_detect_command)
		self.pub_task.publish(msg)

		# launch a 30s timer to check if a face has been detected
		if timeout_timer:
			rospy.Timer(rospy.Duration(30), self.face_detection_timer_callback, oneshot = True)

	# builds a JSON message to be sent to the robot for waiting for some time
	def send_wait_command(self):
		# complete GO command
		self.wait_command["time"]["t"] = rospy.get_time()

		# create message and publish it
		msg = KeyValue()
		msg.key = "interventions/famd/INFO"
		msg.value = json.dumps(self.wait_command)
		self.pub_task.publish(msg)

	# check if the type of the sensor with the provided ID is correct
	def check_type(self, id, in_type):
		for sensor in self.sensor_db_dict['sensor_db']:
			if int(id) == sensor['id']:
				return sensor['type'] == in_type

	# add an event to the sensor history
	def add_to_history(self, aux, timestamp, id):

		encontrado = False
		for sensor in self.sensor_db_dict['sensor_db']:
			if int(id) == sensor['id']:
				sensor['status'].append([aux,timestamp])
				encontrado = True
				if self.verbose:
					# for debugging
					print(sensor)
				break

		if not encontrado:
			rospy.logwarn("[tunstall_manager_node] WARNING: id not found")

	# load 'go' command from file
	def load_move_command_from_file(self):
		json_file = open(self.move_command_file,"r")
		self.move_command = json.load(json_file)
		json_file.close()

	# load 'speak' command from file
	def load_speak_command_from_file(self):
		json_file = open(self.speak_command_file,"r")
		self.speak_command = json.load(json_file)
		json_file.close()

	def load_face_detect_command_from_file(self):
		json_file = open(self.face_detect_command_file,"r")
		self.face_detect_command = json.load(json_file)
		json_file.close()

	def load_wait_command_from_file(self):
		json_file = open(self.wait_command_file,"r")
		self.wait_command = json.load(json_file)
		json_file.close()

	# print all sensor information to console
	def print_all_sensors(self):
		for sensor in self.sensor_db_dict['sensor_db']:
			print(sensor)

	# load sensor information from JSON file (node parameter)
	def load_sensors_from_file(self):
		json_file = open(self.sensor_file,"r")
		self.sensor_db_dict = json.load(json_file)
		json_file.close()

		# convert every sensor type from string to TSensorType
		for sensor in self.sensor_db_dict['sensor_db']:
			self.string_to_other(sensor)

		# for debug: create a fake timestamp with the current one
		# chairs = lambda x : x["type"] == TSensorType.CHAIR
		# for sensor in filter(chairs, self.sensor_db_dict['sensor_db']):
		# 	sensor["status"][-1][1] = rospy.get_time()

		# and print them
		self.print_all_sensors()

	# change sensor type from STRING to ENUM
	def string_to_other(self,sensor):
		if sensor['type'] == 'CHAIR' :
			sensor['type'] = TSensorType.CHAIR
		elif sensor['type'] == 'DOOR':
			sensor['type'] = TSensorType.DOOR
		elif sensor['type'] == 'PIR':
			sensor['type'] = TSensorType.PIR
		else:
			sensor['type'] = TSensorType.UNKNOWN

	# change sensor type from ENUM to STRING
	def other_to_string(self, sensor):
		if sensor['type'] == TSensorType.CHAIR :
			sensor['type'] = 'CHAIR'
		elif sensor['type'] == TSensorType.DOOR:
			sensor['type'] = 'DOOR'
		elif sensor['type'] == TSensorType.PIR:
			sensor['type'] = 'PIR'
		elif sensor['type'] == TSensorType.UNKNOWN:
			sensor['type'] = 'UNKNOWN'

	# save complete DB to file
	def save_db(self):
		json_file = open(self.sensor_file,"w")
		#local_sensor_db_dict = dict(self.sensor_db_dict)
		copied_dict = copy.deepcopy(self.sensor_db_dict)
		for sensor in copied_dict['sensor_db']:
			# for debugging
			print(sensor)
			self.other_to_string(sensor)
			print(sensor) # for debug
		json.dump(copied_dict, json_file)
		json_file.close()

	def find_sensor_by_id(self, id):
		for sensor in self.sensor_db_dict['sensor_db']:
			if sensor["id"] == int(id):
				return sensor

		rospy.loginfo("[tunstall_manager_node] ERROR: id not found")
		return None

	def get_time_zone(self):

		t = time.localtime()
		current_time = time.strftime("%H:%M:%S", t)
		rospy.loginfo("[tunstall_manager_node] Current Time = " + current_time)

		return current_time

# ENTRY POINT
def main(args):
  rospy.init_node('tunstall_manager_node', anonymous=True)
  node = tunstall_manager_node()
  try:
    rospy.spin()
  except KeyboardInterrupt:
    print("Shutting down")

if __name__ == '__main__':
    main(sys.argv)


import torch
import torch.nn as nn
import torch.nn.functional as F

''' ResNet Decoder Start '''
# form: https://github.com/julianstastny/VAE-ResNet18-PyTorch/blob/master/model.py
class ResizeConv2d(nn.Module):

    def __init__(self, in_channels, out_channels, kernel_size, scale_factor, mode='nearest'):
        super().__init__()
        self.scale_factor = scale_factor
        self.mode = mode
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=1)

    def forward(self, x):
        x = F.interpolate(x, scale_factor=self.scale_factor, mode=self.mode)
        x = self.conv(x)
        return x

class BasicBlockDec(nn.Module):

    def __init__(self, in_planes, stride=1):
        super().__init__()

        planes = int(in_planes/stride)

        self.conv2 = nn.Conv2d(in_planes, in_planes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(in_planes)
        # self.bn1 could have been placed here, but that messes up the order of the layers when printing the class

        if stride == 1:
            self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
            self.bn1 = nn.BatchNorm2d(planes)
            self.shortcut = nn.Sequential()
        else:
            self.conv1 = ResizeConv2d(in_planes, planes, kernel_size=3, scale_factor=stride)
            self.bn1 = nn.BatchNorm2d(planes)
            self.shortcut = nn.Sequential(
                ResizeConv2d(in_planes, planes, kernel_size=3, scale_factor=stride),
                nn.BatchNorm2d(planes)
            )

    def forward(self, x):
        out = torch.relu(self.bn2(self.conv2(x)))
        out = self.bn1(self.conv1(out))
        out += self.shortcut(x)
        out = torch.relu(out)
        return out

class ResNet18Dec(nn.Module):

    def __init__(self, num_Blocks=[2,2,2,2], z_dim=10, nc=3):
        super().__init__()
        self.in_planes = 512

        self.linear = nn.Linear(z_dim, 512)

        self.layer4 = self._make_layer(BasicBlockDec, 256, num_Blocks[3], stride=2)
        self.layer3 = self._make_layer(BasicBlockDec, 128, num_Blocks[2], stride=2)
        self.layer2 = self._make_layer(BasicBlockDec, 64, num_Blocks[1], stride=2)
        self.layer1 = self._make_layer(BasicBlockDec, 64, num_Blocks[0], stride=1)
        self.conv1 = ResizeConv2d(64, nc, kernel_size=3, scale_factor=2)

    def _make_layer(self, BasicBlockDec, planes, num_Blocks, stride):
        strides = [stride] + [1]*(num_Blocks-1)
        layers = []
        for stride in reversed(strides):
            layers += [BasicBlockDec(self.in_planes, stride)]
        self.in_planes = planes
        return nn.Sequential(*layers)

    def forward(self, z):
        x = self.linear(z)
        x = x.view(z.size(0), 512, 1, 1)
        x = F.interpolate(x, scale_factor=4)
        x = self.layer4(x)
        x = self.layer3(x)
        x = self.layer2(x)
        x = self.layer1(x)
        x = torch.sigmoid(self.conv1(x))
        x = x.view(x.size(0), 3, 64, 64)
        return x

class ResNet34Dec(nn.Module):

    def __init__(self, latent_size=32 + 64, num_Blocks=[3, 4, 6, 3], nc=3):
        super().__init__()
        self.in_planes = 512

        self.linear = nn.Linear(latent_size, 512)

        self.layer4 = self._make_layer(BasicBlockDec, 256, num_Blocks[3], stride=2)
        self.layer3 = self._make_layer(BasicBlockDec, 128, num_Blocks[2], stride=2)
        self.layer2 = self._make_layer(BasicBlockDec, 64, num_Blocks[1], stride=2)
        self.layer1 = self._make_layer(BasicBlockDec, 64, num_Blocks[0], stride=1)
        self.conv1 = ResizeConv2d(64, 64, kernel_size=3, scale_factor=2)
        self.conv2 = ResizeConv2d(64, 64, kernel_size=3, scale_factor=2)
        self.conv3 = ResizeConv2d(64, nc, kernel_size=3, scale_factor=2)

    def _make_layer(self, BasicBlockDec, planes, num_Blocks, stride):
        strides = [stride] + [1]*(num_Blocks-1)
        layers = []
        for stride in reversed(strides):
            layers += [BasicBlockDec(self.in_planes, stride)]
        self.in_planes = planes
        return nn.Sequential(*layers)

    def forward(self, z):
        x = self.linear(z)
        x = x.view(z.size(0), 512, 1, 1)
        x = F.interpolate(x, scale_factor=4)
        x = self.layer4(x)
        x = self.layer3(x)
        x = self.layer2(x)
        x = self.layer1(x)
        x = self.conv1(x)
        x = self.conv2(x)
        x = torch.sigmoid(self.conv3(x))
        # x = x.view(x.size(0), 3, 64, 64)
        return x
''' ResNet Decoder End '''


class Solution:
    from typing import List
    def numberOfSubarrays(self, nums: List[int], k: int) -> int:
        # subarray problem with exact k constrain
        def helper(maximum):
            left, right = 0, 0
            res = 0
            count = 0
            for right in range(len(nums)):
                if nums[right] %2 == 1:
                    count += 1
                while count > maximum and left <= right:
                    count -= nums[left]%2==1
                    left += 1
                res += right - left
            return res
        return helper(k) - helper(k-1)


"""
Modify the class Rational of Lab No2 to perform the following tasks:
- adding two Rational numbers. The result should be stored in reduced form;
- subtracting two Rational numbers. The result should be stored in reduced form;
- multiplying two Rational numbers. The result should be stored in reduced form;
- dividing two Rational numbers. The result should be stored in reduced form;
- comparison two Rational numbers.
"""
import math
class Rational:
    def __init__(self, numerator=1, denumerator=1):
        if not isinstance(numerator, int) and not isinstance(denumerator, int):
            raise TypeError("Numerator and denumerator are integer numbers")
        elif denumerator == 0:
            raise ArithmeticError("The denumerator is not zero")
        else:
            divider = math.gcd(numerator, denumerator)
            self.__numerator = numerator // divider
            self.__denumerator = denumerator // divider

    def decorator(func):
        def inner(self, other):
            obj = func(self, other)
            divider = math.gcd(self.numerator, obj.denumerator)
            obj.numerator //= divider
            obj.denumerator //= divider
            return obj
        return inner

    @decorator
    def __add__(self, other):
        lcm = self.denumerator * other.denumerator // math.gcd(self.denumerator, other.denumerator)
        self.numerator = self.numerator * lcm // self.denumerator + other.numerator * lcm // other.denumerator
        return self

    @decorator
    def __sub__(self, other):
        lcm = self.denumerator * other.denumerator // math.gcd(self.denumerator, other.denumerator)
        self.numerator = self.numerator * lcm // self.denumerator - other.numerator * lcm // other.denumerator
        return self

    @decorator
    def __mul__(self, other):
        self.numerator = self.numerator * other.numerator
        self.denumerator = self.denumerator * other.denumerator
        return self

    @decorator
    def __truediv__(self, other):
        if other.numerator == 0:
            raise ZeroDivisionError('The numerator of the fraction we divide by cannot be zero')
        self.numerator *= other.denumerator
        self.denumerator *= other.numerator
        return self

    def __eq__(self, other):
        if not isinstance(other, Rational):
            return NotImplemented
        return self.numerator / self.denumerator == other.numerator / other.denumerator

    def __ne__(self, other):
        if not isinstance(other, Rational):
            return NotImplemented
        return self.numerator / self.denumerator != other.numerator / other.denumerator

    def __gt__(self, other):
        if not isinstance(other, Rational):
            return NotImplemented
        return self.numerator / self.denumerator > other.numerator / other.denumerator

    def __ge__(self, other):
        if not isinstance(other, Rational):
            return NotImplemented
        return self.numerator / self.denumerator >= other.numerator / other.denumerator

    def __lt__(self, other):
        if not isinstance(other, Rational):
            return NotImplemented
        return self.numerator / self.denumerator < other.numerator / other.denumerator

    def __le__(self, other):
        if not isinstance(other, Rational):
            return NotImplemented
        return self.numerator / self.denominator <= other.numerator / other.denominator

    @property
    def printer(self):
        return str(self.__numerator) + '/' + str(self.__denumerator)

    @property
    def count(self):
        return self.__numerator / self.__denumerator

    @property
    def numerator(self):
        return self.__numerator

    @property
    def denumerator(self):
        return self.__denumerator

    @numerator.setter
    def numerator(self, numerator):
        if not isinstance(numerator, int):
            raise TypeError("Numerator is integer number")
        else:
            divider = math.gcd(numerator, self.__denumerator)
            self.__numerator = numerator // divider
            self.__denumerator //= divider

    @denumerator.setter
    def denumerator(self, denumerator):
        if not isinstance(denumerator, int):
            raise TypeError("Denumerator is integer number")
        else:
            divider = math.gcd(denumerator, self.__numerator)
            self.__numerator //= divider
            self.__denumerator = denumerator // divider

if __name__ == "__main__":
    number = Rational(5, 10)
    print('Rational number ', number.printer)
    print('Equals ', number.count)
    number1 = Rational(1, 2)
    print('Summa of: ', number.printer, ' and', number1.printer, ' = ', (number + number1).printer)
    print('Different of: ', number.printer, ' and', number1.printer, ' = ', (number - number1).printer)
    print('Multiply of: ', number.printer, ' and', number1.printer, ' = ', (number * number1).printer)
    print('Division of: ', number.printer, ' and', number1.printer, ' = ', (number / number1).printer)
    print(number > number1)


"""Stormpath Default relay state."""

from .base import (
    CollectionResource,
    FixedAttrsDict,
)

class DefaultRelayState(FixedAttrsDict):
    writable_attrs = (
        'callback_uri',
        'organization',
        'state',
    )

class DefaultRelayStateList(CollectionResource):
    """Stormpath DefaultRelayState list."""
    resource_class = DefaultRelayState

    def create(self, properties=None, expand=None, **params):
        if properties is None:
            properties = {}

        return super(DefaultRelayStateList, self).create(
            properties, expand, **params)

    @staticmethod
    def _sanitize_property(value):
        from stormpath.resources import Organization

        if isinstance(value, Organization):
            return {'nameKey': value.name_key}

        return CollectionResource._sanitize_property(value)


"""This function constructs a dictionary with abinitio_energies.

Input:
    fname (str) path/filename of ase.db file
    selection (list) ase.db selection
"""
import warnings
import numpy as np
from tqdm import tqdm
from ase.data import chemical_symbols
from ase.symbols import string2symbols, symbols2numbers
# get_distances requires ASE 3.16 or above.
from ase.geometry import get_layers, get_distances
from catlearn.api.ase_atoms_api import images_connectivity
from catlearn.featurize.periodic_table_data import get_radius

ads_syms = ['H', 'C', 'O', 'N', 'S', 'F', 'Cl', 'P', 'Na', 'K']

def catalysis_hub_to_info(images):
    raise NotImplementedError("Coming soon.")

def autogen_info(images):
    """Return a list of atoms objects with atomic group information
    attached to atoms.subsets.
    This information is  needed by some functions in the
    AdsorbateFingerprintGenerator.

    Parameters
    ----------
    images : list
        list of atoms objects representing adsorbates on slabs.
        No further information is required in atoms.subsets.
    """
    images = images_connectivity(images)
    for i, atoms in tqdm(enumerate(images)):
        # Pre-allocate subsets dictionary.
        if not hasattr(atoms, 'subsets'):
            atoms.subsets = {}

        # Identify adsorbate atoms.
        if 'ads_atoms' not in atoms.subsets:
            ads_atoms = detect_adsorbate(atoms)
            atoms.subsets['ads_atoms'] = ads_atoms
            if len(ads_atoms) == 0:
                continue

        # Identify slab atoms.
        if 'slab_atoms' not in atoms.subsets:
            atoms.subsets['slab_atoms'] = slab_index(atoms)
        if not (len(atoms.subsets['slab_atoms']) +
                len(atoms.subsets['ads_atoms']) ==
                len(atoms)):
            raise AssertionError("all atoms must belong to slab or adsorbate.")

        # Identify atoms at node distance 0 and 1 from the surface-ads bond.
        if ('chemisorbed_atoms' not in atoms.subsets or
            'site_atoms' not in atoms.subsets or
                'ligand_atoms' not in atoms.subsets):
            chemi, site, ligand = info2primary_index(atoms)
            atoms.subsets['chemisorbed_atoms'] = chemi
            atoms.subsets['site_atoms'] = site
            atoms.subsets['ligand_atoms'] = ligand

        # Identify surface layers.
        if ('bulk_atoms' not in atoms.subsets or
            'termination_atoms' not in atoms.subsets or
           'subsurf_atoms' not in atoms.subsets):
            bulk, term, subsurf = detect_termination(atoms)
            atoms.subsets['bulk_atoms'] = bulk
            atoms.subsets['termination_atoms'] = term
            atoms.subsets['subsurf_atoms'] = subsurf
    return images

def termination_info(images):
    """Return a list of atoms objects with attached information about
    the slab termination, the slab second outermost layer and the bulk slab
    compositions.

    Parameters
    ----------
    images : list
        list of atoms objects representing adsorbates on slabs.
        The atoms objects must have the following keys in atoms.subsets:

            - 'ads_atoms' : list
                indices of atoms belonging to the adsorbate
            - 'slab_atoms' : list
                indices of atoms belonging to the slab
    """
    for atoms in tqdm(images):
        bulk, subsurf, term = detect_termination(atoms)
        atoms.subsets['bulk_atoms'] = bulk
        atoms.subsets['termination_atoms'] = term
        atoms.subsets['subsurf_atoms'] = subsurf
    return images

def detect_adsorbate(atoms):
    """Return a list of indices of atoms belonging to an adsorbate.

    Parameters
    ----------
    atoms : object
        An ase atoms object.
    """
    if 'ads_atoms' in atoms.subsets:
        return atoms.subsets['ads_atoms']
    if 'slab_atoms' in atoms.subsets:
        return ads_index(atoms)
    if (len(np.unique(atoms.get_tags())) >= 4 and
        min(atoms.get_tags()) <= -1 and
       max(atoms.get_tags()) > 2):
        return tags2ads_index(atoms)
    # Use species chemical formula if known.
    try:
        species = atoms.info['key_value_pairs']['species']
    except KeyError:
        warnings.warn("'species' key or .info['key_value_pairs'] missing.")
        return sym2ads_index(atoms, ads_syms=ads_syms)
    if species == '':
        return []
    elif '-' in species or '+' in species or ',' in species:
        raise NotImplementedError('Co-adsorption.')
    try:
        return formula2ads_index(atoms, species)
    except AssertionError:
        try:
            return connectivity2ads_index(atoms, species)
        except AssertionError:
            return last2ads_index(atoms, species)

def detect_termination(atoms):
    """ Returns three lists, the first containing indices of bulk atoms and
    the second containing indices of atoms in the second outermost layer, and
    the last denotes atoms in the outermost layer or termination or the slab.

    Parameters
    ----------
    atoms : object.
        The atoms object must have the following keys in atoms.subsets:

        'slab_atoms' : list
            indices of atoms belonging to the slab
    """
    if (len(np.unique(atoms.get_tags())) >= 4 and
        min(atoms.get_tags()) <= 0 and
       max(atoms.get_tags()) > 2):
        bulk, term, subsurf = tags_termination(atoms)
    elif len(atoms.constraints) == 1:
        bulk, term, subsurf = constraints_termination(atoms)
    elif 'key_value_pairs' not in atoms.info:
        bulk, term, subsurf = connectivity_termination(atoms)
    elif 'layers' in atoms.info['key_value_pairs']:
        bulk, term, subsurf = layers_termination(atoms)
    else:
        bulk, term, subsurf = connectivity_termination(atoms)
    try:
        sx, sy = atoms.info['key_value_pairs']['supercell'].split('x')
        sx = int(''.join(i for i in sx if i.isdigit()))
        sy = int(''.join(i for i in sy if i.isdigit()))
        if int(sx) * int(sy) != len(term):
            msg = str(len(term)) + ' termination atoms identified.' + \
                ' ' + atoms.info['key_value_pairs']['facet'] + \
                ' ' + atoms.info['key_value_pairs']['supercell'] + \
                '. id=' + str(atoms.info['id'])
            warnings.warn(msg)
    except KeyError:
        pass
    if 'site_atoms' in atoms.subsets:
        for a_s in atoms.subsets['site_atoms']:
            if a_s not in term:
                msg = 'site atom not in term.'
                try:
                    msg += ' ' + str(atoms.info['key_value_pairs']['phase'])
                except KeyError:
                    pass
                try:
                    msg += ' ' + str(atoms.info['key_value_pairs']['facet'])
                    msg += ' ' + str(
                            atoms.info['key_value_pairs']['supercell'])
                except KeyError:
                    pass
                if 'id' in atoms.info:
                    msg += ' ' + str(atoms.info['id'])
                warnings.warn(msg)
                break
    return bulk, term, subsurf

def check_reconstructions(image_pairs):
    """Return a list of database ids, for adsorbate/slab structures, which
    has a reconstructed slab with respect to the reference slab.

    Parameters
    ----------
    image_pairs : list
        List of tuples containing pairs of ASE atoms objects.
        The first element in each tuple must represent an adsorbate*slab
        structure and the second element must represent a slab.
    """
    reconstructed = []
    for i, row in enumerate(image_pairs):
        identical = compare_slab_connectivity(row[0], row[1])
        if not identical:
            reconstructed.append(i)
    return reconstructed

def compare_slab_connectivity(atoms, reference_atoms):
    """Return a boolean for whether an adsorbate has caused a slab to
    reconstruct and change it's connectivity.

    Parameters
    ----------
    atoms : object
        ASE atoms object with connectivity and 'slab_atoms' subsets attached.
        This represents an adsorbate*slab structure.
    reference_atoms : object
        ASE atoms object with connectivity and 'slab_atoms' subsets attached.
        This represents a slab structure.

    Returns
    ----------
    identical : boolean
        Are the connectivities within the slabs identical or not.
    """
    slab_atoms = atoms.subsets['slab_atoms']
    slab_cm = atoms.connectivity[:, slab_atoms][slab_atoms, :]
    reference_cm = reference_atoms.connectivity
    return np.allclose(slab_cm, reference_cm)

def slab_index(atoms):
    """ Returns a list of indices of atoms belonging to the slab.
    These are defined as atoms that are not belonging to the adsorbate.

    Parameters
    ----------
    atoms : ase atoms object
        The atoms object must have the key 'ads_atoms' in atoms.subsets:

            - 'ads_atoms' : list
                indices of atoms belonging to the adsorbate
    """
    chemi = [a.index for a in atoms if a.index not in
             atoms.subsets['ads_atoms']]
    return chemi

def ads_index(atoms):
    """ Returns a list of indices of atoms belonging to the adsorbate.
    These are defined as atoms that are not belonging to the slab.

    Parameters
    ----------
    atoms : ase atoms object
        The atoms object must have the key 'ads_atoms' in atoms.subsets:

            - 'slab_atoms' : list
                indices of atoms belonging to the adsorbate
    """
    ads = [a.index for a in atoms if a.index not in
           atoms.subsets['slab_atoms']]
    return ads

def sym2ads_index(atoms, ads_syms):
    """Return the indexes of atoms from the global list of adsorbate symbols.

    Parameters
    ----------
    atoms : object
        An ase atoms object.
    """
    ads_atoms = [a.index for a in atoms if a.symbol in ads_syms]

    return ads_atoms

def connectivity2ads_index(atoms, species):
    """Return the indexes of atoms from the global list of adsorbate symbols.

    Parameters
    ----------
    atoms : object
        ASE atoms object with connectivity attached.
        This represents an adsorbate*slab structure.
    species : str
        chemical formula of the adsorbate.
    """
    composition = string2symbols(species)
    ads_atoms = []
    for symbol in composition:
        if (composition.count(symbol) ==
           atoms.get_chemical_symbols().count(symbol)):
            ads_atoms += [atom.index for atom in atoms if
                          atom.symbol == symbol]

    if len(ads_atoms) == len(composition):
        return ads_atoms
    elif len(ads_atoms) == 0:
        raise AssertionError("Formula adsorbate identification failed.")

    # If an atom in species also occurs in the slab, infer by connectivity.
    connected_atoms = []
    for atom in ads_atoms:
        edges = atoms.connectivity[atom, :]
        connected_atoms += [i for i, bonds in enumerate(edges) if
                            bonds > 0 and
                            atoms[i].symbol in composition and
                            atoms[i].symbol != atoms[atom].symbol]
    ads_atoms += connected_atoms

    # Final check.
    ua_ads, uc_ads = np.unique(
        np.array(atoms.get_chemical_symbols())[ads_atoms].sort(),
        return_counts=True)
    ua_comp, uc_comp = np.unique(composition.sort(), return_counts=True)
    if ua_ads != ua_comp:
        msg = str(ua_ads) + " != " + str(ua_comp)
        raise AssertionError(msg)
    elif uc_ads != uc_comp:
        msg = str(uc_ads) + " != " + str(uc_comp)
        raise AssertionError(msg)

    return list(np.unique(ads_atoms))

def slab_positions2ads_index(atoms, slab, species):
    """Return the indexes of adsorbate atoms identified by comparing positions
    to a reference slab structure.

    Parameters
    ----------
    atoms : object
    """
    composition = string2symbols(species)
    ads_atoms = []
    for symbol in composition:
        if (composition.count(symbol) ==
           atoms.get_chemical_symbols().count(symbol)):
            ads_atoms += [atom.index for atom in atoms if
                          atom.symbol == symbol]

    ua_ads, uc_ads = np.unique(ads_atoms, return_counts=True)
    ua_comp, uc_comp = np.unique(composition, return_counts=True)
    if ua_ads == ua_comp and uc_ads == uc_comp:
        return ads_atoms

    p_a = atoms.get_positions()
    p_r = slab.get_positions()

    for s in composition:
        if s in np.array(atoms.get_chemical_symbols())[ads_atoms]:
            continue
        symbol_count = composition.count(s)
        index_a = np.where(np.array(atoms.get_chemical_symbols()) == s)[0]
        index_r = np.where(np.array(slab.get_chemical_symbols()) == s)[0]
        _, dist = get_distances(p_a[index_a, :], p2=p_r[index_r, :],
                                cell=atoms.cell, pbc=True)
        # Assume all slab atoms are closest to their reference counterpart.
        deviations = np.min(dist, axis=1)
        # Sort deviations.
        ascending = np.argsort(deviations)
        # The highest deviations are assumed to be new atoms.
        ads_atoms += list(index_a[ascending[-symbol_count:]])

    # Final check.
    ua_ads, uc_ads = np.unique(
        np.array(atoms.get_chemical_symbols())[ads_atoms].sort(),
        return_counts=True)
    ua_comp, uc_comp = np.unique(composition.sort(), return_counts=True)
    if ua_ads != ua_comp:
        msg = str(ua_ads) + " != " + str(ua_comp)
        raise AssertionError(msg)
    elif uc_ads != uc_comp:
        msg = str(uc_ads) + " != " + str(uc_comp)
        raise AssertionError(msg)

    return ads_atoms

def tags2ads_index(atoms):
    """Return the indexes of atoms from the global list of adsorbate symbols.

    Parameters
    ----------
    atoms : object
        An ase atoms object. `atoms.tags` must label adsorbate atoms with 0 or
        negative numbers.
    """
    ads_atoms = [a.index for a in atoms if a.tag < 0]

    if 'key_value_pairs' in atoms.info:
        if 'species' in atoms.info['key_value_pairs']:
            species = atoms.info['key_value_pairs']['species']
            numbers = sorted(symbols2numbers(species))
            if numbers != sorted(atoms[ads_atoms].numbers):
                msg = 'Incorrect tags!'
                raise AssertionError(msg)
    return ads_atoms

def last2ads_index(atoms, species):
    """Return the indexes of the last n atoms in the atoms object, where n is
    the length of the composition of the adsorbate species. This function will
    work on atoms objects, where the slab was set up first,
    and the adsorbate was added after.

    Parameters
    ----------
    atoms : ase atoms object.
        atoms.info must be a dictionary containing the key 'key_value_pairs',
        which is expected to contain CATMAP standard adsorbate structure
        key value pairs. See the ase db to catmap module in catmap.
        the key value pair 'species' must be the
        chemical formula of the adsorbate.
    species : str
        chemical formula of the adsorbate.
    """
    n_ads = len(string2symbols(species))
    natoms = len(atoms)
    ads_atoms = list(range(natoms - n_ads, natoms))
    if sorted(symbols2numbers(species)) != sorted(atoms[ads_atoms].numbers):
        raise AssertionError("last index adsorbate identification failed.")
    warnings.warn("Adsorbate identified by last index.")
    return ads_atoms

def formula2ads_index(atoms, species):
    """Return the indexes of atoms, which have symbols matching the chemical
    formula of the adsorbate. This function will not work for adsorbates
    containing the same elements as the slab.

    Parameters
    ----------
    atoms : ase atoms object.
        atoms.info must be a dictionary containing the key 'key_value_pairs',
        which is expected to contain CatMAP standard adsorbate structure
        key value pairs. See the ase db to catmap module in catmap.
        the key value pair 'species' must be the
        chemical formula of the adsorbate.
    species : str
        chemical formula of the adsorbate.
    """
    try:
        composition = string2symbols(species)
    except ValueError:
        print(species)
        raise
    ads_atoms = [a.index for a in atoms if a.symbol in composition]
    if sorted(symbols2numbers(species)) != sorted(atoms[ads_atoms].numbers):
        raise AssertionError("ads atoms identification by formula failed.")
    return ads_atoms

def layers2ads_index(atoms, species):
    """ Returns the indexes of atoms in layers exceeding the number of layers
    stored in the key value pair 'layers'.

    Parameters
    ----------
    atoms : ase atoms object.
        atoms.info must be a dictionary containing the key 'key_value_pairs',
        which is expected to contain CatMAP standard adsorbate structure
        key value pairs. See the ase db to catmap module in catmap.
        the key value pair 'species' must be the
        chemical formula of the adsorbate and 'layers' must be an integer.
    species : str
        chemical formula of the adsorbate.
    """
    li, lz = auto_layers(atoms)
    layers = int(atoms.info['key_value_pairs']['layers'])
    ads_atoms = [a.index for a in atoms if li[a.index] > layers - 1]
    if sorted(symbols2numbers(species)) != sorted(atoms[ads_atoms].numbers):
        raise AssertionError("ads atoms identification by layers failed.")
    return ads_atoms

def z2ads_index(atoms, species):
    """ Returns the indexes of the n atoms with the highest position
    in the z direction,
    where n is the number of atoms in the chemical formula from the species
    key value pair.

    Parameters
    ----------
    atoms : ase atoms object.
        atoms.info must be a dictionary containing the key 'key_value_pairs',
        which is expected to contain CatMAP standard adsorbate structure
        key value pairs. See the ase db to catmap module in catmap.
        the key value pair 'species'.
    species : str
        chemical formula of the adsorbate.
    """
    composition = string2symbols(species)
    n_ads = len(composition)
    z = atoms.get_positions()[:, 2]
    ads_atoms = np.argsort(z)[-n_ads:]
    if sorted(symbols2numbers(species)) != sorted(atoms[ads_atoms].numbers):
        raise AssertionError("adsorbate identification by z coord failed. " +
                             str(species))
    return ads_atoms

def info2primary_index(atoms):
    """ Returns lists identifying the nearest neighbors of the adsorbate atoms.

    Parameters
    ----------
    atoms : ase atoms object.
        The atoms object must have the following keys in atoms.subsets:

        'ads_atoms' : list
            indices of atoms belonging to the adsorbate
        'slab_atoms' : list
            indices of atoms belonging to the slab
    """
    slab_atoms = atoms.subsets['slab_atoms']
    ads_atoms = atoms.subsets['ads_atoms']

    cm = atoms.connectivity
    chemi = []
    site = []
    ligand = []
    for a_a in ads_atoms:
        for a_s in slab_atoms:
            if cm[a_a, a_s] > 0:
                site.append(a_s)
                chemi.append(a_a)
    chemi = list(np.unique(chemi))
    site = list(np.unique(site))
    for j in site:
        for a_s in slab_atoms:
            if cm[a_s, j] > 0:
                ligand.append(a_s)
    ligand = [lig for lig in list(np.unique(ligand)) if lig not in site]
    if len(chemi) is 0 or len(site) is 0:
        print(chemi, site, ligand)
        msg = 'No adsorption site detected.'
        if 'id' in atoms.info:
            msg += ' id=' + str(atoms.info['id'])
        warnings.warn(msg)
    elif len(ligand) < 2:
        msg = 'Site has less than 2 nearest neighbors.'
        if 'id' in atoms.info:
            msg += ' id=' + str(atoms.info['id'])
        warnings.warn(msg)
    return chemi, site, ligand

def tags_termination(atoms):
    """Return lists bulk, term and subsurf containing atom indices belonging to
    those subsets of a surface atoms object. CatKit and ase.build contain
    functions that by default store this information in tags.

    Parameters
    ----------
    atoms : object
        the termination atoms should have tag=1 and subsequent layers should be
        tagged in increasing order.
    """
    term = [a.index for a in atoms if a.tag == 1]
    subsurf = [a.index for a in atoms if a.tag == 2]
    bulk = [a.index for a in atoms if a.tag > 1]
    return bulk, term, subsurf

def layers_termination(atoms, miller=(0, 0, 1)):
    """Return lists bulk, term and subsurf containing atom indices belonging to
    those subsets of a surface atoms object.
    This function relies on ase.atoms.get_layers, default atomic radii, and a
    slab oriented in the xy plane, where the termination in the z+ direction
    is the surface.

    Parameters
    ----------
    atoms : object
        The atoms object must have the following keys in atoms.subsets:

        'slab_atoms' : list
            indices of atoms belonging to the slab.
    """
    il, zl = auto_layers(atoms, miller=miller)
    il_slab = list(il)
    for index in sorted(atoms.subsets['ads_atoms'], reverse=True):
        del il_slab[index]
    nlayers = len(np.unique(il_slab))
    try:
        db_layers = atoms.info['key_value_pairs']['layers']
        if db_layers != nlayers:
            msg = str(db_layers) + ' != ' + str(nlayers)
            if 'id' in atoms.info:
                msg += ' id=' + str(atoms.info['id'])
            warnings.warn(msg)
    except(KeyError):
        if nlayers < 3:
            msg = 'nlayers = ' + str(nlayers)
            if 'id' in atoms.info:
                msg += ' id=' + str(atoms.info['id'])
            warnings.warn(msg)
    # bulk atoms can include subsurface atoms.
    term = [a.index for a in atoms if il[a.index] >= nlayers - 1 and
            a.index not in atoms.subsets['ads_atoms']]
    bulk = [a.index for a in atoms if il[a.index] <= nlayers - 2]
    subsurf = [a.index for a in atoms if il[a.index] == nlayers - 2]
    return bulk, term, subsurf

def constraints_termination(atoms):
    """Return lists bulk, term and subsurf containing atom indices belonging to
    those subsets of a surface atoms object.
    This function relies on the connectivity of the atoms and it assumes that
    bulk atoms are those that have are constrained in the first constraint.

    Parameters
    ----------
    atoms : object
        atoms.connectivity should be a connectivity matrix.
        The atoms object must have the following keys in atoms.subsets:

        'slab_atoms' : list
            indices of atoms belonging to the slab.
    """
    cm = atoms.connectivity.copy()
    np.fill_diagonal(cm, 0)
    # List coordination numbers of slab atoms.
    slab_atoms = atoms.subsets['slab_atoms']
    coord = np.count_nonzero(cm[slab_atoms, :][:, slab_atoms], axis=1)
    bulk = []
    term = []
    max_coord = np.max(coord)
    for j, c in enumerate(coord):
        a_s = atoms.subsets['slab_atoms'][j]
        if a_s in atoms.constraints[0].get_indices():
            bulk.append(a_s)
        elif c >= max_coord - 2:
            bulk.append(a_s)
        else:
            term.append(a_s)
    subsurf = []
    for a_b in bulk:
        for a_t in term:
            if cm[a_t, a_b] > 0:
                subsurf.append(a_b)
    subsurf = list(np.unique(subsurf))
    return bulk, term, subsurf

def connectivity_termination(atoms):
    """Return lists bulk, term and subsurf containing atom indices belonging to
    those subsets of a surface atoms object.
    This function relies on the connectivity of the atoms.

    Parameters
    ----------
    atoms : object
        atoms.connectivity should be a connectivity matrix.
        The atoms object must have the following keys in atoms.subsets:

        'slab_atoms' : list
            indices of atoms belonging to the slab
    """
    raise NotImplementedError("Todo: identify and remove backside termination")
    cm = atoms.connectivity.copy()
    np.fill_diagonal(cm, 0)
    # List coordination numbers of slab atoms.
    slab_atoms = atoms.subsets['slab_atoms']
    coord = np.count_nonzero(cm[slab_atoms, :][:, slab_atoms], axis=1)
    bulk = []
    term = []
    max_coord = np.max(coord)
    bulk = atoms.subsets['slab_atoms']
    for j, c in enumerate(coord):
        a_s = atoms.subsets['slab_atoms'][j]
        if c >= max_coord - 2:
            bulk.append(a_s)
        else:
            term.append(a_s)
    subsurf = []
    for a_b in bulk:
        for a_t in term:
            if cm[a_t, a_b] > 0:
                subsurf.append(a_b)
    subsurf = list(np.unique(subsurf))
    return bulk, term, subsurf

def auto_layers(atoms, miller=(0, 0, 1)):
    """Returns two arrays describing which layer each atom belongs
    to and the distance between the layers and origo.
    Assumes the tolerance corresponds to the average atomic radii of the slab.

    Parameters
    ----------
    atoms : object
        The atoms object must have the following keys in atoms.subsets:

        'slab_atoms' : list
            indices of atoms belonging to the slab
    """
    radii = [get_radius(z) for z in atoms.numbers[atoms.subsets['slab_atoms']]]
    radius = np.average(radii) / 2.
    li, lz = get_layers(atoms, miller=miller, tolerance=radius)
    return li, lz

def attach_cations(atoms, anion_number=8):
    """Attaches list of cation and anion atomic indices.

    Parameters
    ----------
    atoms : object
        ase.Atoms object.
    anion_number : int
        Atomic number of the anion of this chalcogenide.
    """
    if anion_number not in atoms.numbers:
        raise ValueError('Anion ' + chemical_symbols[anion_number] +
                         'not in atoms')
    atoms.subsets['cation_atoms'] = [a.index for a in atoms if
                                     a.number != anion_number]
    atoms.subsets['anion_atoms'] = [a.index for a in atoms if
                                    a.number == anion_number]


import linecache
import sys
import os
import hou

#print '======= in convert to acescg =========='

tp = hou.pwd()
mp = tp.parm('master_path')
if mp.rawValue() != '':
    par = tp.parent()
    parpar = par.parent()

    tta = parpar.createNode('tex_to_aces', node_name="tex_to_aces")
    tta.setPosition((par.position().x(), par.position().y() - 1))
    #tta.moveToGoodPosition()
    tta.parm('tp_path').set(tp.path())
else:
    hou.ui.displayMessage('Master path should not be empty')


from django import template
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404
from django.template import loader
from django.urls import reverse
from django.views import View

from contracts.common.utils.process import html_to_pdf
from contracts.filters import ContractFilter
from contracts.forms import ContractForm, DocumentForm, ReviewForm
from contracts.models import Contracts, Attachments, Review, Document
from django.core.paginator import (
    Paginator,
    EmptyPage,
    PageNotAnInteger,
)

@login_required(login_url="/login/")
def index(request):
    # Get page number from request,
    # default to first page
    default_page = 1
    page = request.GET.get('page', default_page)

    home_index_contracts = Contracts.objects.order_by('-created_date')
    all_contracts = Contracts.objects.order_by('-created_date')
    items = Contracts.objects.all()

    # Paginate items
    items_per_page = 10
    paginator = Paginator(items, items_per_page)

    try:
        items_page = paginator.page(page)
    except PageNotAnInteger:
        items_page = paginator.page(default_page)
    except EmptyPage:
        items_page = paginator.page(paginator.num_pages)

    # Provide filtered, paginated library items
    context = {"items_page": items_page}

    context = {'segment': 'index'}
    context = {"all_contracts": all_contracts, "my_index_contracts": home_index_contracts, "items_page": items_page}
    context.update(dict(Contracts.total_info()))
    context['best_month'] = Contracts.best_month()
    context['contracts_month_report'], context['contracts_month_report_labels'] = Contracts.contracts_month_report()
    html_template = loader.get_template('home/index.html')
    return HttpResponse(html_template.render(context, request))

def add_contract(request):
    if request.method != 'POST':
        # No data submitted, create a blank form.
        form = ContractForm()
    else:
        # POST data submitted, process data.
        form = ContractForm(request.POST, request.FILES)
        if form.is_valid():
            # Create a new entry and assign to contract.
            add_new_contract = form.save(commit=False)
            # Set the new contract attribute to the current user.
            add_new_contract.user = request.user
            # Save to database now the object has all the required data.
            add_new_contract.save()
            return redirect('all_contracts')

    context = {"form": form}
    # return render(request, 'contracts/add_contract.html', context)
    html_template = loader.get_template('home/add_contract.html')
    return HttpResponse(html_template.render(context, request))

def update_contract(request, edit_id):
    contract = Contracts.objects.get(id=edit_id)

    if request.method != 'POST':
        form = ContractForm(instance=contract)
    else:
        form = ContractForm(request.POST, request.FILES, instance=contract)

        if form.is_valid():
            form.save()
            return redirect('all_contracts')

    context = {"contract": contract, 'form': form}
    # return render(request, 'contracts/update_contract.html', context)
    html_template = loader.get_template('home/update_contract.html')
    return HttpResponse(html_template.render(context, request))

def detail_contracts(request, detail_id):
    detail_contract = Contracts.objects.get(id=detail_id)

    attach = Attachments.objects.filter(contracts_id=detail_contract)

    reviews = Review.objects.filter(contract=detail_contract)

    documents = Document.objects.filter(contract=detail_contract)

    context = {'detail_contract': detail_contract, 'attachments': attach, 'reviews': reviews, 'documents': documents}

    # return render(request, 'contracts/detail.html', context)
    html_template = loader.get_template('home/detail_contract.html')
    return HttpResponse(html_template.render(context, request))

def delete_contract(request, delete_id):
    contract = Contracts.objects.get(id=delete_id)

    if request.method == 'POST':
        contract.delete()
        return redirect('all_contracts')

    context = {"contract": contract}
    # return render(request, 'contracts/delete_contract.html', context)
    html_template = loader.get_template('home/delete_contract.html')
    return HttpResponse(html_template.render(context, request))

def add_review(request, contract_id):
    contract = get_object_or_404(Contracts, pk=contract_id)

    if request.method != 'POST':
        # No data submitted, create a blank form.
        form = ReviewForm()
    else:
        # POST data submitted, process data.
        form = ReviewForm(request.POST, request.FILES)
        if form.is_valid():
            # Create a new entry and assign to contract.
            newReview = form.save(commit=False)
            # Set the new contract attribute to the current user.
            newReview.user = request.user
            # Save to database now the object has all the required data.
            newReview.contract = contract

            newReview.save()

            return redirect('detail_contracts', newReview.contract.id)

    context = {"form": form, 'contract': contract}
    # return render(request, 'contracts/new_review.html', context)
    html_template = loader.get_template('home/add_review.html')
    return HttpResponse(html_template.render(context, request))

def update_review(request, review_id):
    # review = Review.objects.get(id=review_id)
    review = get_object_or_404(Review, pk=review_id, user=request.user)

    if request.method != 'POST':
        form = ReviewForm(instance=review)
    else:
        form = ReviewForm(request.POST, request.FILES, instance=review)
        if form.is_valid():
            form.save()
            return redirect('detail_contracts', review.contract.id)

    context = {"review": review, 'form': form}
    # return render(request, 'contracts/update_review.html', context)
    html_template = loader.get_template('home/update_review.html')
    return HttpResponse(html_template.render(context, request))

def delete_review(request, review_id):
    review = get_object_or_404(Review, pk=review_id, user=request.user)

    review.delete()

    return redirect('detail_contracts', review.contract.id)

def add_document(request, contract_id):
    contract = get_object_or_404(Contracts, pk=contract_id)

    if request.method != 'POST':
        # No data submitted, create a blank form.
        form = DocumentForm()
    else:
        # POST data submitted, process data.
        form = DocumentForm(request.POST, request.FILES)
        if form.is_valid():
            # Create a new entry and assign to contract.
            newDocument = form.save(commit=False)
            # Set the new contract attribute to the current user.
            newDocument.user = request.user
            # Save to database now the object has all the required data.
            newDocument.contract = contract

            newDocument.save()

            return redirect('detail_contracts', newDocument.contract.id)

    context = {"form": form, 'contract': contract}
    # return render(request, 'contracts/new_document.html', context)
    html_template = loader.get_template('home/add_document.html')
    return HttpResponse(html_template.render(context, request))

def update_document(request, document_id):
    # review = Review.objects.get(id=review_id)
    document = get_object_or_404(Document, pk=document_id, user=request.user)

    if request.method != 'POST':
        form = DocumentForm(instance=document)
    else:
        form = DocumentForm(request.POST, request.FILES, instance=document)
        if form.is_valid():
            form.save()
            return redirect('detail_contracts', document.contract.id)

    context = {"document": document, 'form': form}
    # return render(request, 'contracts/update_document.html', context)
    html_template = loader.get_template('home/update_document.html')
    return HttpResponse(html_template.render(context, request))

def delete_document(request, document_id):
    document = get_object_or_404(Review, pk=document_id, user=request.user)

    document.delete()

    return redirect('detail_contracts', document.contract.id)

def all_contracts(request):
    # Get page number from request,
    # default to first page
    default_page = 1
    page = request.GET.get('page', default_page)

    all_contracts = Contracts.objects.order_by('-created_date')
    items = Contracts.objects.all()

    my_Filter = ContractFilter(request.GET, queryset=all_contracts)
    all_contracts = my_Filter.qs

    # Paginate items
    items_per_page = 10
    paginator = Paginator(items, items_per_page)

    try:
        items_page = paginator.page(page)
    except PageNotAnInteger:
        items_page = paginator.page(default_page)
    except EmptyPage:
        items_page = paginator.page(paginator.num_pages)

    # Provide filtered, paginated library items
    context = {"items_page": items_page}
    context = {"all_contracts": all_contracts, "my_Filter": my_Filter, "items_page": items_page}
    # context = {"all_contracts": all_contracts, "items_page": items_page}
    # return render(request, 'contracts/all_contracts.html', context)

    html_template = loader.get_template('home/all_contracts.html')
    return HttpResponse(html_template.render(context, request))

class GeneratePdf(View):
    def get(self, request, detail_id, *args, **kwargs):
        # report_data = Contracts.objects.all()
        report_data = Contracts.objects.get(id=detail_id)

        context = {"report_data": report_data}
        # invoice
        pdf = html_to_pdf('home/pdf/results.html', context)
        # getting the template
        # pdf = html_to_pdf('pdf/results.html', context)
        # All Reports
        # pdf = html_to_pdf('pdf/results-all.html', context)

        # rendering the template
        return HttpResponse(pdf, content_type='application/pdf')

@login_required(login_url="/login/")
def pages(request):
    context = {}
    # All resource paths end in .html.
    # Pick out the html file name from the url. And load that template.
    try:

        load_template = request.path.split('/')[-1]

        if load_template == 'admin':
            return HttpResponseRedirect(reverse('admin:index'))
        context['segment'] = load_template

        html_template = loader.get_template('home/' + load_template)
        return HttpResponse(html_template.render(context, request))

    except template.TemplateDoesNotExist:

        html_template = loader.get_template('home/page-404.html')
        return HttpResponse(html_template.render(context, request))

    except:
        html_template = loader.get_template('home/page-500.html')
        return HttpResponse(html_template.render(context, request))


from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker

from app.config import settings

engine = create_async_engine(
    settings.DB_DSN,
    echo=settings.DB_ECHO,
    future=True,
)

async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession, future=True, autoflush=False)

async def get_db():
    session = async_session()
    try:
        yield session
        await session.commit()
    finally:
        await session.close()


import os
import unittest
from collections import OrderedDict
from unittest import mock

import opentelemetry.exporter.jaeger.proto.grpc.translate as pb_translator
from opentelemetry import trace as trace_api
from opentelemetry.exporter.jaeger.proto.grpc import JaegerExporter

# pylint:disable=no-name-in-module
# pylint:disable=import-error
from opentelemetry.exporter.jaeger.proto.grpc.gen import model_pb2
from opentelemetry.exporter.jaeger.proto.grpc.translate import (
    _SCOPE_NAME_KEY,
    _SCOPE_VERSION_KEY,
    NAME_KEY,
    VERSION_KEY,
    Translate,
)
from opentelemetry.sdk import trace
from opentelemetry.sdk.environment_variables import (
    OTEL_EXPORTER_JAEGER_CERTIFICATE,
    OTEL_EXPORTER_JAEGER_ENDPOINT,
    OTEL_EXPORTER_JAEGER_GRPC_INSECURE,
    OTEL_EXPORTER_JAEGER_TIMEOUT,
    OTEL_RESOURCE_ATTRIBUTES,
)
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SpanExportResult
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.test.spantestutil import (
    get_span_with_dropped_attributes_events_links,
)
from opentelemetry.trace.status import Status, StatusCode

def _translate_spans_with_dropped_attributes():
    span = get_span_with_dropped_attributes_events_links()
    translate = Translate([span])

    # pylint: disable=protected-access
    return translate._translate(pb_translator.ProtobufTranslator("svc"))

# pylint:disable=no-member
class TestJaegerExporter(unittest.TestCase):
    def setUp(self):
        # create and save span to be used in tests
        self.context = trace_api.SpanContext(
            trace_id=0x000000000000000000000000DEADBEEF,
            span_id=0x00000000DEADBEF0,
            is_remote=False,
        )

        self._test_span = trace._Span("test_span", context=self.context)
        self._test_span.start()
        self._test_span.end()

        # pylint: disable=protected-access

    def test_constructor_by_environment_variables(self):
        """Test using Environment Variables."""
        # pylint: disable=protected-access
        service = "my-opentelemetry-jaeger"

        collector_endpoint = "localhost:14250"

        env_patch = mock.patch.dict(
            "os.environ",
            {
                OTEL_EXPORTER_JAEGER_ENDPOINT: collector_endpoint,
                OTEL_EXPORTER_JAEGER_CERTIFICATE: os.path.dirname(__file__)
                + "/certs/cred.cert",
                OTEL_RESOURCE_ATTRIBUTES: "service.name=my-opentelemetry-jaeger",
                OTEL_EXPORTER_JAEGER_TIMEOUT: "5",
                OTEL_EXPORTER_JAEGER_GRPC_INSECURE: "False",
            },
        )

        env_patch.start()
        provider = TracerProvider(resource=Resource.create({}))
        trace_api.set_tracer_provider(provider)
        exporter = JaegerExporter()
        self.assertEqual(exporter.service_name, service)
        self.assertIsNotNone(exporter._collector_grpc_client)
        self.assertEqual(exporter.collector_endpoint, collector_endpoint)
        self.assertEqual(exporter._timeout, 5)
        self.assertIsNotNone(exporter.credentials)
        self.assertEqual(exporter.insecure, False)
        env_patch.stop()

    # pylint: disable=too-many-locals,too-many-statements
    def test_translate_to_jaeger(self):

        span_names = ("test1", "test2", "test3")
        trace_id = 0x6E0C63257DE34C926F9EFCD03927272E
        span_id = 0x34BF92DEEFC58C92
        parent_id = 0x1111111111111111
        other_id = 0x2222222222222222

        base_time = 683647322 * 10**9  # in ns
        start_times = (
            base_time,
            base_time + 150 * 10**6,
            base_time + 300 * 10**6,
        )
        durations = (50 * 10**6, 100 * 10**6, 200 * 10**6)
        end_times = (
            start_times[0] + durations[0],
            start_times[1] + durations[1],
            start_times[2] + durations[2],
        )

        span_context = trace_api.SpanContext(
            trace_id, span_id, is_remote=False
        )
        parent_span_context = trace_api.SpanContext(
            trace_id, parent_id, is_remote=False
        )
        other_context = trace_api.SpanContext(
            trace_id, other_id, is_remote=False
        )

        event_attributes = OrderedDict(
            [
                ("annotation_bool", True),
                ("annotation_string", "annotation_test"),
                ("key_float", 0.3),
            ]
        )

        event_timestamp = base_time + 50 * 10**6
        # pylint:disable=protected-access
        event_timestamp_proto = (
            pb_translator._proto_timestamp_from_epoch_nanos(event_timestamp)
        )

        event = trace.Event(
            name="event0",
            timestamp=event_timestamp,
            attributes=event_attributes,
        )

        link_attributes = {"key_bool": True}

        link = trace_api.Link(
            context=other_context, attributes=link_attributes
        )

        default_tags = [
            model_pb2.KeyValue(
                key="span.kind",
                v_type=model_pb2.ValueType.STRING,
                v_str="internal",
            ),
        ]

        otel_spans = [
            trace._Span(
                name=span_names[0],
                context=span_context,
                parent=parent_span_context,
                events=(event,),
                links=(link,),
                kind=trace_api.SpanKind.CLIENT,
                resource=Resource(
                    attributes={"key_resource": "some_resource"}
                ),
            ),
            trace._Span(
                name=span_names[1],
                context=parent_span_context,
                parent=None,
                resource=Resource({}),
            ),
            trace._Span(
                name=span_names[2],
                context=other_context,
                parent=None,
                resource=Resource({}),
                instrumentation_scope=InstrumentationScope(
                    name="name", version="version"
                ),
            ),
        ]

        otel_spans[0].start(start_time=start_times[0])
        # added here to preserve order
        otel_spans[0].set_attribute("key_bool", False)
        otel_spans[0].set_attribute("key_string", "hello_world")
        otel_spans[0].set_attribute("key_float", 111.22)
        otel_spans[0].set_attribute("key_tuple", ("tuple_element",))
        otel_spans[0].set_status(
            Status(StatusCode.ERROR, "Example description")
        )
        otel_spans[0].end(end_time=end_times[0])

        otel_spans[1].start(start_time=start_times[1])
        otel_spans[1].end(end_time=end_times[1])

        otel_spans[2].start(start_time=start_times[2])
        otel_spans[2].set_status(Status(StatusCode.OK))
        otel_spans[2].end(end_time=end_times[2])

        translate = Translate(otel_spans)
        # pylint: disable=protected-access
        spans = translate._translate(pb_translator.ProtobufTranslator("svc"))

        span1_start_time = pb_translator._proto_timestamp_from_epoch_nanos(
            start_times[0]
        )
        span2_start_time = pb_translator._proto_timestamp_from_epoch_nanos(
            start_times[1]
        )
        span3_start_time = pb_translator._proto_timestamp_from_epoch_nanos(
            start_times[2]
        )

        span1_end_time = pb_translator._proto_timestamp_from_epoch_nanos(
            end_times[0]
        )
        span2_end_time = pb_translator._proto_timestamp_from_epoch_nanos(
            end_times[1]
        )
        span3_end_time = pb_translator._proto_timestamp_from_epoch_nanos(
            end_times[2]
        )

        span1_duration = pb_translator._duration_from_two_time_stamps(
            span1_start_time, span1_end_time
        )
        span2_duration = pb_translator._duration_from_two_time_stamps(
            span2_start_time, span2_end_time
        )
        span3_duration = pb_translator._duration_from_two_time_stamps(
            span3_start_time, span3_end_time
        )

        expected_spans = [
            model_pb2.Span(
                operation_name=span_names[0],
                trace_id=pb_translator._trace_id_to_bytes(trace_id),
                span_id=pb_translator._span_id_to_bytes(span_id),
                start_time=span1_start_time,
                duration=span1_duration,
                flags=0,
                tags=[
                    model_pb2.KeyValue(
                        key="key_bool",
                        v_type=model_pb2.ValueType.BOOL,
                        v_bool=False,
                    ),
                    model_pb2.KeyValue(
                        key="key_string",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="hello_world",
                    ),
                    model_pb2.KeyValue(
                        key="key_float",
                        v_type=model_pb2.ValueType.FLOAT64,
                        v_float64=111.22,
                    ),
                    model_pb2.KeyValue(
                        key="key_tuple",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="('tuple_element',)",
                    ),
                    model_pb2.KeyValue(
                        key="key_resource",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="some_resource",
                    ),
                    model_pb2.KeyValue(
                        key="otel.status_code",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="ERROR",
                    ),
                    model_pb2.KeyValue(
                        key="otel.status_description",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="Example description",
                    ),
                    model_pb2.KeyValue(
                        key="span.kind",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="client",
                    ),
                    model_pb2.KeyValue(
                        key="error",
                        v_type=model_pb2.ValueType.BOOL,
                        v_bool=True,
                    ),
                ],
                references=[
                    model_pb2.SpanRef(
                        ref_type=model_pb2.SpanRefType.CHILD_OF,
                        trace_id=pb_translator._trace_id_to_bytes(trace_id),
                        span_id=pb_translator._span_id_to_bytes(parent_id),
                    ),
                    model_pb2.SpanRef(
                        ref_type=model_pb2.SpanRefType.FOLLOWS_FROM,
                        trace_id=pb_translator._trace_id_to_bytes(trace_id),
                        span_id=pb_translator._span_id_to_bytes(other_id),
                    ),
                ],
                logs=[
                    model_pb2.Log(
                        timestamp=event_timestamp_proto,
                        fields=[
                            model_pb2.KeyValue(
                                key="annotation_bool",
                                v_type=model_pb2.ValueType.BOOL,
                                v_bool=True,
                            ),
                            model_pb2.KeyValue(
                                key="annotation_string",
                                v_type=model_pb2.ValueType.STRING,
                                v_str="annotation_test",
                            ),
                            model_pb2.KeyValue(
                                key="key_float",
                                v_type=model_pb2.ValueType.FLOAT64,
                                v_float64=0.3,
                            ),
                            model_pb2.KeyValue(
                                key="message",
                                v_type=model_pb2.ValueType.STRING,
                                v_str="event0",
                            ),
                        ],
                    )
                ],
                process=model_pb2.Process(
                    service_name="svc",
                    tags=[
                        model_pb2.KeyValue(
                            key="key_resource",
                            v_str="some_resource",
                            v_type=model_pb2.ValueType.STRING,
                        )
                    ],
                ),
            ),
            model_pb2.Span(
                operation_name=span_names[1],
                trace_id=pb_translator._trace_id_to_bytes(trace_id),
                span_id=pb_translator._span_id_to_bytes(parent_id),
                start_time=span2_start_time,
                duration=span2_duration,
                flags=0,
                tags=default_tags,
                process=model_pb2.Process(
                    service_name="svc",
                ),
            ),
            model_pb2.Span(
                operation_name=span_names[2],
                trace_id=pb_translator._trace_id_to_bytes(trace_id),
                span_id=pb_translator._span_id_to_bytes(other_id),
                start_time=span3_start_time,
                duration=span3_duration,
                flags=0,
                tags=[
                    model_pb2.KeyValue(
                        key="otel.status_code",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="OK",
                    ),
                    model_pb2.KeyValue(
                        key="span.kind",
                        v_type=model_pb2.ValueType.STRING,
                        v_str="internal",
                    ),
                    model_pb2.KeyValue(
                        key=NAME_KEY,
                        v_type=model_pb2.ValueType.STRING,
                        v_str="name",
                    ),
                    model_pb2.KeyValue(
                        key=VERSION_KEY,
                        v_type=model_pb2.ValueType.STRING,
                        v_str="version",
                    ),
                    model_pb2.KeyValue(
                        key=_SCOPE_NAME_KEY,
                        v_type=model_pb2.ValueType.STRING,
                        v_str="name",
                    ),
                    model_pb2.KeyValue(
                        key=_SCOPE_VERSION_KEY,
                        v_type=model_pb2.ValueType.STRING,
                        v_str="version",
                    ),
                ],
                process=model_pb2.Process(
                    service_name="svc",
                ),
            ),
        ]

        # events are complicated to compare because order of fields
        # (attributes) in otel is not important but in jeager it is
        # pylint: disable=no-member
        self.assertCountEqual(
            spans[0].logs[0].fields,
            expected_spans[0].logs[0].fields,
        )

        self.assertEqual(spans, expected_spans)

    def test_max_tag_value_length(self):
        span = trace._Span(
            name="span",
            resource=Resource(
                attributes={
                    "key_resource": "some_resource some_resource some_more_resource"
                }
            ),
            context=trace_api.SpanContext(
                trace_id=0x000000000000000000000000DEADBEEF,
                span_id=0x00000000DEADBEF0,
                is_remote=False,
            ),
        )

        span.start()
        span.set_attribute("key_bool", False)
        span.set_attribute("key_string", "hello_world hello_world hello_world")
        span.set_attribute("key_float", 111.22)
        span.set_attribute("key_int", 1100)
        span.set_attribute("key_tuple", ("tuple_element", "tuple_element2"))
        span.end()

        translate = Translate([span])

        # does not truncate by default
        # pylint: disable=protected-access
        spans = translate._translate(pb_translator.ProtobufTranslator("svc"))
        tags_by_keys = {
            tag.key: tag.v_str
            for tag in spans[0].tags
            if tag.v_type == model_pb2.ValueType.STRING
        }
        self.assertEqual(
            "hello_world hello_world hello_world", tags_by_keys["key_string"]
        )
        self.assertEqual(
            "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"]
        )
        self.assertEqual(
            "some_resource some_resource some_more_resource",
            tags_by_keys["key_resource"],
        )

        # truncates when max_tag_value_length is passed
        # pylint: disable=protected-access
        spans = translate._translate(
            pb_translator.ProtobufTranslator("svc", max_tag_value_length=5)
        )
        tags_by_keys = {
            tag.key: tag.v_str
            for tag in spans[0].tags
            if tag.v_type == model_pb2.ValueType.STRING
        }
        self.assertEqual("hello", tags_by_keys["key_string"])
        self.assertEqual("('tup", tags_by_keys["key_tuple"])
        self.assertEqual("some_", tags_by_keys["key_resource"])

    def test_export(self):
        client_mock = mock.Mock()
        spans = []
        exporter = JaegerExporter()
        exporter._grpc_client = client_mock
        status = exporter.export(spans)
        self.assertEqual(SpanExportResult.SUCCESS, status)

    def test_export_span_service_name(self):
        resource = Resource.create({SERVICE_NAME: "test"})
        span = trace._Span(
            "test_span", context=self.context, resource=resource
        )
        span.start()
        span.end()
        client_mock = mock.Mock()
        exporter = JaegerExporter()
        exporter._grpc_client = client_mock
        exporter.export([span])
        self.assertEqual(exporter.service_name, "test")

    def test_dropped_span_attributes(self):
        spans = _translate_spans_with_dropped_attributes()
        tags_by_keys = {
            tag.key: tag.v_str or tag.v_int64 for tag in spans[0].tags
        }
        self.assertEqual(1, tags_by_keys["otel.dropped_links_count"])
        self.assertEqual(2, tags_by_keys["otel.dropped_attributes_count"])
        self.assertEqual(3, tags_by_keys["otel.dropped_events_count"])

    def test_dropped_event_attributes(self):
        spans = _translate_spans_with_dropped_attributes()
        fields_by_keys = {
            tag.key: tag.v_str or tag.v_int64
            for tag in spans[0].logs[0].fields
        }
        # get events
        self.assertEqual(
            2,
            fields_by_keys["otel.dropped_attributes_count"],
        )


import socket
import time
import os
from _thread import *
from TraderManager import TraderManager
from multiprocessing.connection import Listener
import zmq
import UdpComms as U
import time
import json
import enum
import Traders.CommunicationClasses as comms

# ============ INCOMING ==============

def HandleIncomingCommand(msg_dict):
    commandType = msg_dict['commandType']
    if(commandType == comms.CommandType.LaunchTrader):
        traderManager.LaunchTrader(json.loads(msg_dict['data']))
        print(traderManager.ListTraderInterfaces())

def HandleIncomingData(msg_dict):
    dataType = msg_dict['dataType']
    target_pid = str(msg_dict['target_pid'])
    if(dataType == comms.DataType.LimitOrderBook):
        # send LOB to target_pid
        forward_lob = target_pid + '@' + json.dumps(msg_dict)
        server_socket_pub.send_string(forward_lob)
        print("LOB incoming")

def HandleIncomingAcknowledgement(msg_dict):
    # route acknowledgement to correct trader
    target_pid = str(msg_dict['target_pid'])
    acknowledgement_message = target_pid + "@" + json.dumps(msg_dict)
    server_socket_pub.send_string(acknowledgement_message)

# ============ OUTGOING ==============

def HandleIncomingMessage(message):

    msg_dict = json.loads(message)
    if(msg_dict['messageType'] == comms.MessageType.Command):
        HandleIncomingCommand(msg_dict)
    elif(msg_dict['messageType'] == comms.MessageType.Data):
        HandleIncomingData(msg_dict)
    elif(msg_dict['messageType'] == comms.MessageType.Acknowledgement):
        print("acknowledgement recieved   " + message)
        HandleIncomingAcknowledgement(msg_dict)

def GetSourceFromMessage(message):
    msg_dict = json.loads(message)
    source = msg_dict['source_pid']
    return int(source)

#create the trading manager
traderManager = TraderManager()

#""" Socket for communicating with Unity """
unity_socket = U.UdpComms(udpIP="127.0.0.1", portTX=8000, portRX=8001, enableRX=True, suppressWarnings=True)
print("1")

#""" Trader bot socket """
server_context = zmq.Context()
print("2")
server_socket_pub = server_context.socket(zmq.PUB)
print("3")
server_socket_pub.bind("tcp://*:5555")
print("4")
server_socket_sub = server_context.socket(zmq.SUB)
print("5")
server_socket_sub.bind("tcp://*:5556")
print("6")
server_socket_sub.subscribe('0')
#server_socket_sub.subscribe("")

poller = zmq.Poller()
print("7")
poller.register(server_socket_sub, zmq.POLLIN)
print("8")
i = 0
while True:

    # ========= TRADERS =============
    #  Wait for next request from client
    socks = dict(poller.poll(timeout=0.01))
    if(server_socket_sub in socks and socks[server_socket_sub] == zmq.POLLIN):
        message_whole = server_socket_sub.recv_string()
        topic, message = message_whole.split("@")
        print(f"Trader socket rec request: {message_whole}")

        #  Send reply back to client
        #server_socket_pub.send_string(str (topic) + "@" + "ACKNOWLEDGE: " + message)

        unity_socket.SendData(message) # Send this string to other application
    else:
        pass

    # ========= UNITY =============
    message = unity_socket.ReadReceivedData() # read data

    if message != None: # if NEW data has been received since last ReadReceivedData function call

        HandleIncomingMessage(message)

    #data = connection.recv(4096)
    #if data:
    #    print("Received:", data.decode('utf-8'))
    #    response = "Message received by server"
    #    connection.sendall(response.encode('utf-8'))

while True:
    data = connection.recv(4096)
    if data:
        print("Received:", data.decode('utf-8'))
        response = "Message received by server"
        connection.sendall(response.encode('utf-8'))
    else:
        break