#!/usr/bin/env python

# This file is part of Window-Switch.
# Copyright (c) 2009-2013 Antoine Martin <antoine@nagafix.co.uk>
# Window-Switch is released under the terms of the GNU GPL v3

import time
import threading

from winswitch.objects.common import ModifiedCallbackObject

class StatefulObject(ModifiedCallbackObject):
	"""
	Utility superclass for objects which have a state ("status") and
	want to be able to register callback for state transitions.
	Those callbacks can be time limited and may return False when they no longer want to fire.
	"""

	def __init__(self, all_valid, final):
		ModifiedCallbackObject.__init__(self)
		self.status = ""
		#transients:
		self.all_valid_statuses = all_valid
		self.final_statuses = final					#states that are final and we will not change from
		self.statuses = []							#historical list of all states
		self.status_update_count = 0				#this is incremented everytime the status is modified
		self.status_update_callbacks = []
		self.threading_lock = threading.RLock()

	def is_status_final(self):
		return	self.status in self.final_statuses

	def add_status_update_callback(self, from_status, to_status, callback, clear_it=True, timeout=30):
		from_status_list = None
		if from_status:
			from_status_list = [from_status]
		to_status_list = None
		if to_status:
			to_status_list = [to_status]
		self.do_add_status_update_callback(from_status_list, to_status_list, callback, clear_it, timeout)

	def do_add_status_update_callback(self, from_status_list, to_status_list, callback, clear_it, timeout):
		"""
		Adds a callback that fires on a specific status change.
		It can be used once (set clear_it=True) or a number of times: just return False when done to clear it.
		The timeout in seconds can be used to ensure that the callback only gets called within a fixed window of time.
		(simply ignored and discarded if the status change comes after that)
		"""
		if self.debug_callbacks:
			self.sdebug("self=%s" % self, from_status_list, to_status_list, callback, clear_it, timeout)
		assert callback
		until = None
		if timeout:
			until = time.time()+timeout
		try:
			self.threading_lock.acquire()
			self.status_update_callbacks.append((from_status_list, to_status_list, callback, clear_it, until))
		finally:
			self.threading_lock.release()
		if self.debug_callbacks:
			self.sdebug("current callbacks=%s" % str(self.status_update_callbacks), from_status_list, to_status_list, callback, clear_it, timeout)

	def set_status(self, new_status, touch=True):
		"""
		Changes the status of the session and fires the status update callbacks (if the status is changed),
		also fires the modified callbacks from touch() if touch=True.
		Returns True if the session status was modified.
		"""
		try:
			if self.status == new_status:
				return	False
			if new_status not in self.all_valid_statuses:
				self.serror("invalid status", new_status, touch)
				return	False
			if self.status in self.final_statuses:		#ie: cannot change after SESSION_CLOSED...
				self.serror("already at final state %s" % self.status, new_status, touch)
				return	False
			self.do_set_status(new_status, touch)
		except Exception, e:
			self.serr(None, e, new_status, touch)
			return	False

	def do_set_status(self, new_status, touch=True):
		prev_status = self.status
		self.status_update_count += 1
		self.statuses.append(new_status)
		self.status = new_status
		if touch:
			self.touch()
		self.fire_status_callbacks(prev_status, new_status)

	def fire_status_callbacks(self, prev_status, new_status):
		if self.debug_callbacks:
			self.sdebug("self=%s, callbacks=%s" % (self, self.status_update_callbacks), prev_status, new_status)
		try:
			self.threading_lock.acquire()
			copy = self.status_update_callbacks[:]
			dead_callbacks = []
			for item in copy:
				(from_status_list, to_status_list, callback, clear_it, until) = item
				if self.debug_callbacks:
					self.sdebug("testing %s" % str(item), new_status)
				if from_status_list and (prev_status not in from_status_list):
					if self.debug_callbacks:
						self.sdebug("callback %s: from status %s not in %s" % (str(item), prev_status, str(from_status_list)), new_status)
					continue
				if to_status_list and (new_status not in to_status_list):
					if self.debug_callbacks:
						self.sdebug("callback %s: target status %s not in %s" % (str(item), new_status, str(to_status_list)), new_status)
					continue
				if until is not None and until<time.time():
					if self.debug_callbacks:
						self.sdebug("callback %s has timed out" % str(item), new_status)
					dead_callbacks.append(item)
					continue
				if clear_it and item not in dead_callbacks:
					dead_callbacks.append(item)
				try:
					ret = callback()
				except Exception, e:
					self.serr("error on callback=%s" % callback, e, new_status)
					ret = None
				self.sdebug("%s: %s()=%s" % (item, callback, ret), new_status)
				if not ret and item not in dead_callbacks:
					dead_callbacks.append(item)
			new_callbacks = [x for x in self.status_update_callbacks if x not in dead_callbacks]
			self.status_update_callbacks = new_callbacks
			return	True
		finally:
			self.threading_lock.release()
