#! /usr/bin/python3

import sys
import getopt
import time
import json
import logging
import logging.handlers
import traceback
from gi.repository import GLib

import dbus
import dbus.mainloop.glib
from xorcom.twinstar.corosync import TwinstarPolicy
from xorcom.twinstar.corosync import ExceptionQuitMainloop

EV_TIMER_INIT = "EV_TIMER_INIT"
EV_TIMER_INIT_WAITING = "EV_TIMER_INIT_WAITING"
EV_TIMER_T1 = "EV_TIMER_T1"
EV_TIMER_REQ = "EV_TIMER_REQ"
EV_TIMER_KEEP_ALIVE = "EV_TIMER_KEEP_ALIVE"
EV_AB_ADDED = "EV_AB_ADDED"
EV_AB_REMOVED = "EV_AB_REMOVED"
EV_MSG_RECIVED = "EV_MSG_RECIVED"

MSG_AB_COUNTER_REQ = "MSG_AB_COUNTER_REQ"
MSG_AB_COUNTER_RES = "MSG_AB_COUNTER_RES"

TIMER_INIT = 1000
TIMER_INIT_WAITING = 10000
TIMER_T1 = 2000
TIMER_REQ = 2000
TIMER_KEEP_ALIVE = 10000

TW_LOGGER_NAME = 'twinstar-manager'

if __name__ == '__main__':
	'''
	Command line parameters:
		-h - show the help and exit
		-c - send log to stdout
		-s - syslog facility name
		
	The nodes exchange with messages that are formatted as JSON objects:
	{'type':'<msg_type>','from_node'=<node_id>,..}
	'''
	log_facility_name = "local5"
	log_console = False
	opts, args = getopt.getopt(sys.argv[1:],"hcs:")
	for opt, arg in opts:
		if opt == '-h':
			print("Usage:\n\ttwinstar-manager [-h] | [-c] [-s <syslog facility name>]")
			print("where,\n\t-h - help\n\t-c - send messages to stdout\n\t-s <name> - syslog facility name")
			sys.exit()
		elif opt == '-s':
			log_facility_name = arg
		elif opt == '-c':
			log_console = True
		else:
			pass
	
	logger = logging.getLogger(TW_LOGGER_NAME)
	logger.setLevel(logging.INFO)
	console_formatter = logging.Formatter('%(asctime)s-TWM-%(levelname)s\t:%(message)s')
	syslog_formatter = logging.Formatter('twinstar-manager[%(process)d]: %(levelname)s: %(message)s')
	
	if log_console:
		console_log = logging.StreamHandler()
		#console_log.setLevel(logging.DEBUG)
		console_log.setFormatter(console_formatter)
		logger.addHandler(console_log)
	
	file_log = logging.handlers.SysLogHandler(address = '/dev/log', facility=log_facility_name)
	file_log.setFormatter(syslog_formatter)
	logger.addHandler(file_log)
	
	dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

	# Very simple policy that just print notifications and send keep-alive
	# messages every "interval"
	class AstribanksPolicy(TwinstarPolicy):
		def __init__(self):
				self.initialized = False
				try:
					super(AstribanksPolicy, self).__init__()
				except Exception:
					pass
				else:
					self.initialized = True
					self.twcs = self.corosync()
					self.nodeid = self.twcs.local_nodeid()
					# Give some verbose info
					me_str = ''
					if self.twcs.is_master():
						me_str = ' [me]'
					logger.info("nodeid: %u", self.nodeid)
					logger.info("master: %u%s" ,self.twcs.master_nodeid(), me_str)
					# --- Timers configuration:
					self.timers = {}
					# - EV_TIMER_INIT: waiting before entering INIT state after the program launching. 
					self.timers[EV_TIMER_INIT] = Timer(EV_TIMER_INIT, TIMER_INIT, self.handle_timer)
					# - EV_TIMER_INIT_WAITING - if we didn't receive any response from the remote server then try again when
					#   the timer rounds out.
					self.timers[EV_TIMER_INIT_WAITING] = Timer(EV_TIMER_INIT_WAITING, TIMER_INIT_WAITING, self.handle_timer)
					# - EV_TIMER_T1 - how long to wait for the system stabilization.
					self.timers[EV_TIMER_T1] = Timer(EV_TIMER_T1, TIMER_T1, self.handle_timer)
					# - EV_TIMER_REQ - how log to wait for a response from the remote server.
					self.timers[EV_TIMER_REQ] = Timer(EV_TIMER_REQ, TIMER_REQ, self.handle_timer)
					# - EV_TIMER_KEEP_ALIVE - keep alive timer
					self.timers[EV_TIMER_KEEP_ALIVE] = Timer(EV_TIMER_KEEP_ALIVE, TIMER_KEEP_ALIVE, self.handle_timer)
				
					self.set_state('INIT', EV_TIMER_INIT)

		def set_state(self, st_name, timer_name=None):
			self.state = st_name
			if timer_name is not None:
				self.timers[timer_name].start_timer()
			
		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_new_master(self, nodeid):
			me_str = ''
			if self.twcs.is_master():
				me_str = ' [me]'
			logger.info("new master: %d%s",nodeid, me_str)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_joined(self, nodeid, node_count):
			logger.info("joined: %d (node_count=%d)", nodeid, node_count)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_left(self, nodeid, node_count):
			logger.info("left: %d (node_count=%d)", nodeid, node_count)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_got_message(self, msg_s=None):
			#print "I: got_message: %s" % msg_s
			try:
				msg = json.loads(msg_s)
				#print "   From Node=%d, AstribanksCount=%d" % (msg['from_node'], msg['AstribanksCount'],)
			except ValueError:
				logger.error("handle_got_message(): wrong message format.Msg=%s", msg_s)
			else:
				if msg['from_node'] != self.nodeid:
					logger.info("got_message: %s", msg_s)
					#If it is a request from the second server then route the message to responder.
					if msg['type'] == MSG_AB_COUNTER_REQ:
						self.responder(msg)
					else:
						# This is a response from the second server. Route it to the current state
						# handler.
						ev = dict()
						ev['type'] = EV_MSG_RECIVED
						ev['rcvd_msg'] = msg
						self.states_map[self.state](self, ev)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_astribank_hotplug(self, nodeid, count, added, hardware_id, location, devpath):
			if added:
				added_str = 'ADDED'
				logger.info("astribank_hotplug: node %d total %d -- %s %s -- [%s] @%s", nodeid, count, added_str, devpath, hardware_id, location)
				ev_type = EV_AB_ADDED
			else:
				added_str = 'REMOVED'
				logger.info("astribank_hotplug: node %d total %d -- %s %s", nodeid, count, added_str, devpath)
				ev_type = EV_AB_REMOVED
			if nodeid == self.nodeid:
				ev = dict()
				ev['type'] = ev_type
				self.states_map[self.state](self, ev)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_runstep_begin(self, step_name, step_action):
			logger.info("run_step_begin: %-15s %s", step_name, step_action)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_runstep_end(self, step_name, step_action, step_status):
			logger.info("run_step_end: %-15s %s [%d]", step_name, step_action, step_status)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_runsteps_done(self, errors):
			logger.info("run_steps_done: Total errors = %d", errors)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_keepalive(self, nodeid):
			logger.debug("keepalive from: %d", nodeid)

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def handle_timer(self, timer_obj):
			timer_name = timer_obj.get_timer_name()
			timer_obj.clean()
			logger.debug("Timer event %s received", timer_name)
			ev = dict()
			ev['type'] = timer_name
			self.states_map[self.state](self, ev)
			return False

		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def responder(self, msg):
			if msg['type'] == MSG_AB_COUNTER_REQ:
				answer={}
				answer['type'] = MSG_AB_COUNTER_RES
				answer['AstribanksCount'] = int(self.twcs.astribanks_count())
				self.send_msg(answer, None)
			else:
				pass
			
		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def st_init(self, ev):
			logger.info("st_init: Event=%s", ev['type'])
			ev_type = ev['type']
			if ev_type == EV_TIMER_INIT or ev_type == EV_TIMER_INIT_WAITING:
				# Send request for number of astribanks connected to the second server
				msg={}
				msg['type'] = MSG_AB_COUNTER_REQ
				self.send_msg(msg, self.timers[EV_TIMER_REQ])
			elif ev_type == EV_MSG_RECIVED:
				rcvd_msg = ev.get('rcvd_msg')
				if rcvd_msg is not None and rcvd_msg['type'] == MSG_AB_COUNTER_RES:
					self.timers[EV_TIMER_INIT_WAITING].stop_timer()
					self.timers[EV_TIMER_REQ].stop_timer()
					astribanks_local = self.twcs.astribanks_count()
					if  astribanks_local + rcvd_msg['AstribanksCount'] == 0:
						logger.info("st_init: No Astribanks connected to any server. New state is IDLE")
						self.set_state('IDLE', EV_TIMER_KEEP_ALIVE)
					elif astribanks_local > 0:
						# Probably we are in the middle of Astribanks transition.
						# Wait until the status stabilization. 
						logger.info("st_init: There are %d Astribanks connected to this server. New state is WAIT_T1.",
								astribanks_local)
						self.set_state('WAIT_T1', EV_TIMER_T1)
					else: 
						logger.info("st_init: All Astribanks are connected to the second server. That server should decide what to do. New state is IDLE.")
						self.set_state('IDLE', EV_TIMER_KEEP_ALIVE)
			elif ev_type == EV_TIMER_REQ:
				# No response from the second server. Restart INIT
				logger.info("st_init: No response from the second server. Try again in %d seconds.", TIMER_INIT_WAITING/1000)
				self.timers[EV_TIMER_INIT_WAITING].start_timer()
				
			else:
				pass
		
		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def st_idle(self, ev):
			ev_type = ev['type']
			if ev_type == EV_AB_ADDED or ev_type == EV_AB_REMOVED:
				logger.info("st_idle: An Astribank was connected or disconnected to this server. New state is WAIT_T1.") 
				self.set_state('WAIT_T1', EV_TIMER_T1)
			elif ev_type == EV_TIMER_KEEP_ALIVE:
				logger.debug("st_idle: Event=%s", ev['type'])
				self.twcs.send_keep_alive()
				self.timers[EV_TIMER_KEEP_ALIVE].start_timer()
			else:
				logger.info("st_idle: Unhandled event received. Event=%s", ev['type'])
		
		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def st_wait_t1(self, ev):
			logger.info("st_wait_t1: Event=%s",ev['type'])
			ev_type = ev['type']
			if ev_type == EV_AB_ADDED or ev_type == EV_AB_REMOVED:
				logger.info("st_wait_t1: An Astribank was connected or disconnected to this server. Restart EV_TIMER_T1.") 
				self.timers[EV_TIMER_T1].stop_timer()
				self.timers[EV_TIMER_T1].start_timer()
			elif ev_type == EV_TIMER_T1:
				# There were no changes in the system during T1 timeout. The status is stable and we can get decision.
				self.set_state('IDLE', EV_TIMER_KEEP_ALIVE)
				if not self.twcs.is_master() and  self.twcs.astribanks_count() > 0:
					logger.info("st_wait_t1: This server is slave. Switch the Astribanks to the second server. New state is IDLE.")
					GLib.spawn_async(['twinstar', 'jump'], flags=GLib.SPAWN_SEARCH_PATH)
				else:
					logger.info("st_wait_t1: Nothing to do. New state is IDLE.")
			else:
				pass
		
		@ExceptionQuitMainloop(TW_LOGGER_NAME)
		def send_msg(self, msg, timer):
			msg['from_node'] = int(self.nodeid)
			msg_s = json.dumps(msg)
			logger.info("Sending message: %s",msg)
			self.twcs.send_message(msg_s)
			if timer is not None:
				timer.start_timer()
			
		
		states_map = {'INIT': st_init,
					  'IDLE': st_idle,
					  'WAIT_T1': st_wait_t1
					 }
		
	class Timer(object):
		def __init__(self, timer_name, timer_val, cb_func):
			self.timer_name = timer_name
			self.timer_val = timer_val
			self.cb_func = cb_func
			self.timer_id = None
			
		def start_timer(self):
			if self.timer_id is None:
				self.timer_id = GLib.timeout_add(self.timer_val, self.cb_func, self)
			
		def stop_timer(self):
			if self.timer_id is not None:
				GLib.source_remove(self.timer_id)
				self.timer_id = None
				
		def clean(self):
			self.timer_id = None
				
		def get_timer_name(self):
			return self.timer_name


	# By this moment the twinstar-corosync-bridge may not initialize dBUS services.
	# Therefore, we should wait for a reasonable time.
	for i in range(1,4):
		policy = AstribanksPolicy()
		if policy.initialized:
			break
		else:
			policy = None
			time.sleep(i)
	if policy is None:
		logger.error("Failed to connect to the twinstar-corosync-bridge via dBUS. Aborting.")
	else:
		#print('I: Begin main loop')
		logger.info("Start.")
		loop = GLib.MainLoop()
		loop.run()
