#! /usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2005 (ita)

import os, popen2, sys, time, random
import Params, Task, pproc
from Params import debug, error, trace, fatal

# output a stat file (data for gnuplot) when running tasks in parallel
dostat=0

def process_cmd_output(cmd_stdout, cmd_stderr):
	stdout_eof = stderr_eof = 0
	while not (stdout_eof and stderr_eof):
		if not stdout_eof:
			str = cmd_stdout.read()
			if not str: stdout_eof = 1
			else: sys.stdout.write(str)
		if not stderr_eof:
			str = cmd_stderr.read()
			if not str: stderr_eof = 1
			else: sys.stderr.write(str)
		#time.sleep(0.1)

# run commands in a portable way
# the subprocess module backported from python 2.4 and should work on python >= 2.2
def exec_command(str):
	# for now
	trace("system command -> "+ str)
	if Params.g_verbose==1: print str
	proc = pproc.Popen(str, shell=1, stdout=pproc.PIPE, stderr=pproc.PIPE)
	process_cmd_output(proc.stdout, proc.stderr)
	stat = proc.wait()
	if stat & 0xff: return stat | 0x80
	return stat >> 8

# kind of iterator - the data structure is a bit complicated (price to pay for flexibility)
class JobGenerator:
	def __init__(self, tree):
		self.m_tree = tree

		# we will use self.m_current_group = self.m_tasks.pop()
		self.m_tasks = Task.g_tasks

		# current group
		self.m_current_group    = {}

		# this is also a list that we pop to get the next task list
		self.m_task_prio_lst    = []

		# this is the list of the tasks
		self.m_current_task_lst = {}

		# progress bar
		self.m_total     = 0
		self.m_processed = 0

		self.compute_total()

		self.m_switchflag=1 # postpone

	def compute_total(self):
		self.m_total=0
		for htbl in self.m_tasks:
			for tasks in htbl.values():
				self.m_total += len(tasks)

	# warning, this one is recursive ..
	def get_next(self):
		try:
			t = self.m_current_task_lst.pop(0)
			self.m_processed += 1
			return t
		except:
			try:
				self.m_current_task_lst = self.m_current_group[ self.m_task_prio_lst.pop(0) ]
			except:
				try:
					self.m_current_group = self.m_tasks.pop(0)
					self.m_task_prio_lst = self.m_current_group.keys()
					self.m_task_prio_lst.sort()
				except:
					error("no more task to give")
					return None
			return self.get_next()

	def progress(self):
		return (self.m_processed, self.m_total)

	def postpone(self, task):
		self.m_processed -= 1
		# shuffle the list - some fanciness of mine (ita)
		self.m_switchflag=-self.m_switchflag
		if self.m_switchflag>0: self.m_current_task_lst = [task]+self.m_current_task_lst
		else:                   self.m_current_task_lst.append(task)
		#self.m_current_task_lst = [task]+self.m_current_task_lst

	def debug(self):
		error("debugging a task: something went wrong:")
		#trace("tasks to run in order")
		#Task.g_tasks.reverse()
		s=""
		for t in Task.g_tasks:
			s += str(t.m_idx)+" "
		trace(s)
		#Task.g_tasks.reverse()

class Serial:
	def __init__(self, generator):
		self.m_generator = generator
	def start(self):
		debug("Serial start called")
		#self.m_generator.debug()
		while 1:
			# get next Task
			proc = self.m_generator.get_next()
			if proc is None:
				break

			trace("retrieving task "+str(proc.m_idx))

			# # =======================
			if proc.m_hasrun:
				error("task has already run! "+str(proc.m_idx))

			if not proc.may_start():
				trace("delaying task no "+str(proc.m_idx))
				self.m_generator.postpone(proc)
				self.m_generator.debug()
				proc = None
				continue
			# # =======================

			proc.prepare()
			#proc.debug()

			#trace("m_sig is "+str(proc.m_sig))
			#trace("obj output m_sig is "+str(proc.m_outputs[0].get_sig()))

			#continue
			if not proc.must_run():
				proc.m_hasrun=2
				debug("task is up-to_date "+str(proc.m_idx))
				continue

			trace("executing task "+str(proc.m_idx))

			# display the command that we are about to run
			if not Params.g_commands['configure']:
				(s, t) = self.m_generator.progress()
				col1=''
				col2=''
				try:
					col1=Params.g_colors[proc.m_action.m_name]
					col2=Params.g_colors['NORMAL']
				except: pass
				print '[%d/%d] %s%s%s' % (s, t, col1, proc.m_str, col2)

			# run the command, it might be a function attached to an action
			# usually we will only popen a string
			if proc.m_action.m_function_to_run:
				ret = proc.m_action.m_function_to_run(proc)
			else:
				ret = exec_command(proc.m_cmd)

			# non-zero means something went wrong
			if ret:
				error("task failed! (return code %s and task id %s)"%(str(ret), str(proc.m_idx)))
				proc.debug(1)
				return ret

			try: proc.update_stat()
			except: error('the nodes have not been produced !')
			proc.m_hasrun=1

			# register the task to the ones that have run - useful for debugging purposes
			#Tasks.g_tasks_done.append(proc)	

			"""try:
				proc.apply()
			except KeyboardInterrupt:
				raise
			except:
				print "hum hum .. task failed!"
			"""

		debug("Serial end")
		if not Params.g_commands['configure']:
			print "Build finished successfully"
		return 0

import threading
import Queue


lock = None
condition = None
count = 0
stop = 0
running = 0


"""
#! /usr/bin/gnuplot -persist
set terminal png
set output "output.png"
set yrange [-1:6]
plot 'test.dat' using 1:3 with linespoints
"""
stat = []
class TaskPrinter(threading.Thread):
	def __init__(self, id, master):
		threading.Thread.__init__(self)
		self.setDaemon(1)
		self.m_master = master
		self.start()

	def run(self):
		global count, lock, running, stat
		while 1:
			lock.acquire()
			stat.append( (time.time(), count, running) )
			lock.release()
			time.sleep(0.1)

class TaskConsumer(threading.Thread):
	def __init__(self, id, master):
		threading.Thread.__init__(self)
		self.setDaemon(1)
		self.m_master = master
		self.m_id     = id
		self.start()

		self.m_count = 0
		self.m_stop  = 0

	def notify(self):
		global condition
		condition.acquire()
		condition.notify()
		condition.release()

	def do_stat(self, num):
		global running
		lock.acquire()
		running += num
		lock.release()

	def run(self):
		global lock, count, stop, running
		while 1:
			lock.acquire()
			self.m_stop  = stop
			lock.release()

			if self.m_stop:
				time.sleep(1)
				continue

			# take the next task
			proc = self.m_master.m_ready.get(block=1)

			self.do_stat(1)

			# display the label for the command executed
			print proc.m_str

			# run the command, it might be a function attached to an action
			# usually we will only popen a string
			if proc.m_action.m_function_to_run:
				ret = proc.m_action.m_function_to_run(proc)
			else:
				ret = exec_command(proc.m_cmd)

			self.do_stat(-1)

			if ret:
				lock.acquire()
				error("task failed! (return code %s and task id %s)"%(str(ret), str(proc.m_idx)))
				proc.debug(1)
				count -= 1
				stop   = 1
				self.notify()
				lock.release()
				continue

			try:
				proc.update_stat()
			except:
				lock.acquire()
				error('the nodes have not been produced !')
				count -= 1
				stop = 1
				self.notify()
				lock.release()

			proc.m_hasrun = 1

			lock.acquire()
			count -= 1
			lock.release()
			self.notify()

# The following is a small scheduler, using an agressive scheme
# for making as many tasks available to the consumer threads
#
# Someone may come with a much better scheme, as i do not have too much
# time to spend on this (ita)
class Parallel:
	def __init__(self, tree, numjobs):
		# the tree we are working on
		self.m_tree = tree

		# maximum amount of consumers
		self.m_numjobs   = numjobs

		# the container of all tasks: a list of hashtables containing lists of tasks
		self.m_tasks = Task.g_tasks

		# progress bar
		self.m_total        = 0
		self.m_processed    = 1

		# tasks waiting to be processed
		self.m_outstanding  = []
		# tasks waiting to be run by the consumers
		self.m_ready        = Queue.Queue(150)
		# tasks that are awaiting for another task to complete
		self.m_frozen       = []

		# lock for self.m_count - count the amount of tasks active
		self.m_count        = 0
		self.m_stop         = 0

		# update the variables for the progress bar
		self.compute_total()

		global condition
		condition = threading.Condition()

		global lock
		lock = threading.Lock()

	def compute_total(self):
		self.m_total=0
		for htbl in self.m_tasks:
			for tasks in htbl.values():
				self.m_total += len(tasks)
	
	def read_values(self):
		#print "read values acquire lock"
		global lock, stop, count
		lock.acquire()
		self.m_stop  = stop
		self.m_count = count
		lock.release()
		#print "read values release lock"

	def wait_all_finished(self):
		global condition
		condition.acquire()
		while self.m_count>0:
			condition.wait()
			self.read_values()
		condition.release()

	def start(self):
		global count, lock, stop, condition, dostat

		# unleash the consumers
		for i in range(self.m_numjobs): TaskConsumer(i, self)

		if dostat: TaskPrinter(-1, self)

		# the current group
		group = None

		# current priority
		currentprio = 0

		loop=0

		# add the tasks to the queue
		while 1:
			self.read_values()
			if self.m_stop:
				self.wait_all_finished()
				break

			# if there are no tasks to run, wait for the consumers to eat all of them
			# and then skip to the next priority group
			if (not self.m_frozen) and (not self.m_outstanding):
				self.wait_all_finished()
				if not group:
					try:
						lst = self.m_tasks.pop(0)
						group = []
						keys = lst.keys()
						keys.sort()
						for key in keys:
							group.append( (key, lst[key]) )
					except:
						self.wait_all_finished()
						break

				(currentprio, self.m_outstanding) = group.pop(0)

			# for tasks that must run sequentially
			# (linking object files uses a lot of memory for example)
			if (currentprio%2)==1:
				# make sure there is no more than one task in the queue
				condition.acquire()
				while self.m_count>0:
					condition.wait()
					self.read_values()
				condition.release()
			else:
				# wait a little bit if there are enough jobs for the consumer threads
				condition.acquire()
				while self.m_count>self.m_numjobs+10:
					condition.wait()
					self.read_values()
				condition.release()

			loop += 1

			if not self.m_outstanding:
				self.m_outstanding = self.m_frozen
				self.m_frozen = []

			# now we are certain that there are outstanding or frozen threads
			if self.m_outstanding:
				proc = self.m_outstanding.pop(0)
				if not proc.may_start():
					# shuffle
					#print "shuf0"
					#self.m_frozen.append(proc)
					#self.m_frozen = [proc]+self.m_frozen
					if random.randint(0,1):
						#print "shuf1"
						self.m_frozen.append(proc)
					else:
						#print "shuf2"
						self.m_frozen = [proc]+self.m_frozen
					
					if not self.m_outstanding:
						condition.acquire()
						condition.wait()
						condition.release()

				else:
					proc.prepare()
					if not proc.must_run():
						proc.m_hasrun=2
						continue

					# display the command that we are about to run
					col1=''
					col2=''
					try:
						col1=Params.g_colors[proc.m_action.m_name]
						col2=Params.g_colors['NORMAL']
					except: pass
					proc.m_str = '[%d/%d] %s%s%s' % (self.m_processed, self.m_total, col1, proc.m_str, col2)

					lock.acquire()
					count += 1
					self.m_processed += 1
					lock.release()

					self.m_ready.put(proc, block=1)

		trace("amount of loops "+str(loop))
		global stat
		if dostat and stat:
			file = open('test.dat', 'w')
			(t1, queue, run) = stat[0]
			for (time, queue, run) in stat:
				file.write("%f %f %f\n" % (time-t1, queue, run))
			file.close()

