
"""A Yarn is just a non-preemptive thread.

	It is recommended that you spin .py files from .pyy files
	using the 'spin' compiler (which is really just a short shell
	script), which also requires that you "import Yarn".

	Assuming you have done those things, this module provides the following:

	Starting and stopping Yarns:

		spawn func(params)  # Call func(params) asynchronously now.
		spawn func(p) at t  # Call func(p) at sim-time t.
		waitfor func(p)     # Call func(p) synchronously (wait for it)

		Note that in all cases above, func() must be a Yarn func.
		Using waitfor in any function makes it a Yarn func.

	Pre-defined Yarns (must be waitfor'd or spawn'd):

		Yarn.sleep(s)    		# Sleep s seconds in sim-time.

	Other functions:

		Yarn.run()				# Start the simulation.  Returns when last Yarn done.
		Yarn.simtime()          # The current simulation time, starts at 0.

	[January 2007; Simon Funk]
"""

import Heap

class Yarn:
	"""A Yarn is a thread in suspension.

		generator.next() resumes the thread.
		resumeWhen is an object describing when to resume the thread.  None == Immediately.
		returnTo is another Yarn to resume when this one dies, if any.
	"""
	def __init__(self, generator, resumeWhen=None):
		self.generator  = generator
		self.resumeWhen = resumeWhen
		self.returnTo   = None

	def resume(self):
		return self.generator.next()

	def __cmp__(self, yarn):
		if self.resumeWhen is yarn.resumeWhen:
			return 0
		if self.resumeWhen is None:
			return -1
		if yarn.resumeWhen is None:
			return 1
		return cmp(self.resumeWhen,yarn.resumeWhen)


class YarnBall:
	"A YarnBall manages a bunch of Yarn."

	# This is the currently "active" YarnBall instance, which is
	#  the one used by spawn, etc., so that Yarn methods don't
	#  have to remember what YarnBall they're a part of.
	active  = None

	# The default YarnBall created just by importing this module...
	default = None

	def __init__(self):
		self.queue      = Heap.new()
		self.simtime    = 0.
		YarnBall.active = None

#
# A yarn must yield one of three things:
#
# (0, val)  - meaning it is complete and returning that value,
# (1, when) - meaning it wishes to be resumed at when, or
# (2, yarn) - meaning it wishes to call yarn.
#
# Throwing StopIteration is the same as returning (0, None)
#
# Any other exception will be passed up to the parent if any.

	def run(self):
		while True:

			try:
				yarn = self.queue.pop()
			except:
				#print "All done!", self.simtime
				return

			if yarn.resumeWhen:
				self.simtime = yarn.resumeWhen	# For now...

			YarnBall.active = self
			try:
				action, val = yarn.resume()
			except StopIteration:
				action = 0
				val    = None
				e      = None
			except Exception, e:
				action = 0
				val    = None
			else:
				e      = None

			YarnBall.active = None

			if action == 0:		# Return
				yarn2 = yarn.returnTo		# Note yarn is dead now as a thread.
				if yarn2:

					yarn.returnTo    = val	# Short-lived alternate use of returnTo as return value carrier
					yarn.resumeWhen  = e	# Short-lived alternate use of resumeWhen as exception carrier

					yarn2.resumeWhen = None
					self.queue.push(yarn2)

				elif e:
					print 'WARNING: Yarn died with unhandled exception (%s)'%e

			elif action == 1:	# Re-queue
				yarn.resumeWhen = val
				self.queue.push(yarn)

			elif action == 2:	# Call
				val.returnTo = yarn
				self.queue.push(val)

			else:
				assert False

	def spawn(self, generator):
		self.queue.push(Yarn(generator))

	def spawnAt(self, delay, generator):
		self.queue.push(Yarn(generator, self.simtime+delay))


# The default also becomes active, until and if
# you start run() or create another YarnBall().
# Once you've done that, you should be referencing
# your YarnBalls by name, as in sim1.spawn(), etc..
YarnBall.default = YarnBall()
YarnBall.active  = YarnBall.default

# Run the default YarnBall:

def run():
	YarnBall.default.run()

# Methods for starting new yarns.

def spawn(yarn):
	'Spawn an asynchronous method for immediate execution.'
	YarnBall.active.spawn(yarn)

def spawnAt(delay, yarn):
	'Spawn an asynchronous method for execution after some delay.'
	YarnBall.active.spawnAt(delay, yarn)

# Methods for calling within yarns:

# Turns out plain ol' "return" works fine after all (don't know why
# I thought it didn't):
#def done():
#	raise StopIteration

def sleep(time):
	yield (1, YarnBall.active.simtime + time)

def simtime():
	return YarnBall.active.simtime

