#!/usr/bin/python
# --------------------------------------------------------------------------- #
# $Id: pointless,v 1.354 2004/01/24 19:49:00 pandr Exp $
# --------------------------------------------------------------------------- #

import sys
import os
if os.path.exists("POINTLESS_HACK"):
	sys.path.append("POINTLESS_HACK")
try:
	import libpointless
except ImportError, e:
	sys.stderr.write("""
Pointless failed to import the libpointless module. This could mean a) that the
pointless package you have installed is broken or b) that you have just
compiled pointless, but have not sourced src/pypath.sh yet. If you do not
understand b), option a) applies :)

The error given by import was: %s

""" % e)
	sys.exit(0)
from libpointless import VERSION, LOCALEDIR
from random import random
from string import *
import getopt, string, re
import ConfigParser
import weakref
import copy
import md5
import glob
import shutil
from pllshared import *
from pllcontroller import *
import ploptions   # options and their default settings
import plsetup     # resource setup stuff
import pllog       # log module
import plfiles     # filehandler
try:
	# Use Lars Gustaebel's tarfile module, if available:
	# http://www.gustaebel.de/lars/tarfile/
	import tarfile
	tarfile.is_tarfile
	TARSUPPORT = 1
except (ImportError, AttributeError):
	tarfile = None
	TARSUPPORT = 0

# --------------------------------------------------------------------------- #
# Globals

__id__ = "$Id: pointless,v 1.354 2004/01/24 19:49:00 pandr Exp $"
renderer = None
pll = None
INFINITY = 1000
DEBUG = 0 ## flag used to turn on/off extra debugging

PROFILE = 0 ## flag used to turn on/off profiling
if PROFILE:
	import profile
	import pstats

# --------------------------------------------------------------------------- #
# HACK to work-around a problem in the tarfile module
# without this hack, one is not capable of first trying to read stdin
# as a tgz stream and then if this fails re-read it as ordinary stdin
# 

class Stdin:
	def __init__(self):
		self.buf = ""
		self.readfromstart = 1
	def read(self, size):
		if not self.buf:
			self.buf = sys.stdin.read(size)
			self.readfromstart = 0
			return self.buf
		else:
			if self.readfromstart:
				self.readfromstart = 0
				return self.buf
			else:
				return sys.stdin.read(size)
	def reset(self):
		self.readfromstart = 1

# --------------------------------------------------------------------------- #
# Node and group objects

class Node:
	""" 
		DESCRIPTION:
			The base class for everything that can be in the scene graph.
			It is essentially the python wrapping of the Node class in the
			CPP world of pointless.
	"""
	def __init__(self, this, context):
		assert(self.__class__ != Node)
		self.controllers = []
		self.this        = None
		self._size       = v3(size(0,0), size(0,0), size(0,0))
		self._hrefs      = [v3(0,0,0)]
		self._vrefs      = [v3(0,0,0)]
		self.context     = context
		self.debug_draw  = 0
		self.discardable = 0
		if this: self.set_this(this)
	def __repr__(self):
		return "<"+self.__class__.__name__+">"
	def set_this(self, this):
		self.this = this
		cppsize = libpointless.node_get_size(self.this)
		self._size.x.set(cppsize[0], cppsize[1])
		self._size.y.set(cppsize[2], cppsize[3])
		self._size.z.set(cppsize[4], cppsize[5])
	def name(self):
		if self.this: 
			return libpointless.node_name(self.this)
		else: 
			return "Node"
	def full_name(self):
		if self.this: 
			return libpointless.node_full_name(self.this)
		else: 
			return "Node"
	def get_pos(self):
		if self.this:
			pos = libpointless.node_get_pos(self.this)
			return v3(pos[0], pos[1], pos[2])
		else:
			return v3(0.0, 0.0, 0.0)
	def set_pos(self, pos):
		if self.this: 
			return libpointless.node_set_pos(self.this, int(pos.x), 
				int(pos.y), int(pos.z))
	def get_size(self): return self._size
	def set_visible(self, *args):
		if self.this: return libpointless.node_set_visible(self.this, *args)
	def set_alpha(self, *args):
		if self.this: return libpointless.node_set_alpha(self.this, *args)
	def _debug_draw(self): pass
	def init_debug_draw(self):
		if self.debug_draw: self._debug_draw()

class Leaf(Node):
	def __init__(self, this, context=None):
		Node.__init__(self, this, context)

class Stretchable(Node):
	""" 
		DESCRIPTION:
			The base class for all nodes that may change size by the process 
			of stretching (or shrinking) to fill available space. 
	"""
	def __init__(self, natural=v3(0,0,0), stretchability=0, shrinkability=0,
			context=None, stretch_dim=0):
		assert(self.__class__ != Stretchable) # must derive from this class
		Node.__init__(self, this=None, context=context)
		self.stretchability = stretchability
		self.shrinkability  = shrinkability
		self._natural = natural
		self._size = v3(size(0, natural.x), size(0, natural.y),
			size(0, natural.z))
		assert(stretch_dim >= 0 and stretch_dim <= 2)
		self.stretch_dim = stretch_dim
	def resize(self): pass
	def set_natural(self, natural):
		self._natural = natural
		self._size = v3(size(0,natural.x), size(0, natural.y), \
			size(0, natural.z))
		self.resize()
	def get_natural(self): return self._natural
	def set_stretch(self, stretch):
		if self.stretch_dim == 0:
			new_x = self._natural.x + self.stretchability*stretch
			self._size.x = size(0, new_x)
		elif self.stretch_dim == 1:
			new_y = self._natural.y + self.stretchability*stretch
			self._size.y = size(0, new_y)
		elif self.stretch_dim == 2:
			new_z = self._natural.z + self.stretchability*stretch
			self._size.z = size(0, new_z)
		self.resize()

class Rectangle(Stretchable):
	""" 
		DESCRIPTION:
			This is essentially the python wrapping of the rectangle
			method in the nodefactory class in the CPP world of pointless.
	"""
	def __init__(self, natural, context, ll_color, lr_color=None, ul_color=None, 
			ur_color=None):
		Stretchable.__init__(self, natural=natural, 
			stretchability=INFINITY*INFINITY, context=context)
		assert(isinstance(ll_color, Color)), ll_color
		self.ll_color = ll_color
		if not lr_color:
			self.lr_color = ll_color
		else:
			self.lr_color = lr_color
		if not ul_color:
			self.ul_color = ll_color
		else:
			self.ul_color = ul_color
		if not ur_color:
			self.ur_color = ll_color
		else:
			self.ur_color = ur_color
		x = libpointless.nodefactory_rectangle(natural.x, natural.y, 
			self.ll_color.rgba, self.lr_color.rgba, self.ul_color.rgba, self.ur_color.rgba)
		self.set_this(x)
	def resize(self):
		size = self.get_size()
		self.this = libpointless.nodefactory_rectangle(size.x.max, size.y.max, 
			self.ll_color.rgba, self.lr_color.rgba, self.ul_color.rgba, self.ur_color.rgba)

## FIXME Lout operates with 6 gap modes!!!
eGlueType = Enumeration("eGlueType",
	[ "EDGE_TO_EDGE",
	"REF_TO_REF",
	"REF_TO_REF_FORCE"
	]
)

class Penalty(Node):
	def __init__(self, penalty=0, context=None):
		Node.__init__(self, this=None, context=context)
		self.discardable = 1
		self.penalty = penalty

class Glue(Stretchable):
	""" 
		DESCRIPTION:
			This is the base class for all glue elements both
			horizontal, vertical and absolute.
	"""
	def __init__(self, context, natural=v3(0,0,0), stretchability=0, 
			shrinkability=0, type = eGlueType.EDGE_TO_EDGE, stretch_dim=0, 
			pos=v3(0,0,0), discardable=1):
		Stretchable.__init__(self, natural=natural, 
			stretchability=stretchability, shrinkability=shrinkability, 
			context=context, stretch_dim=stretch_dim)
		self.type = type
		self.pos = pos
		self.discardable = discardable
	def is_vertical(self):
		return self.stretch_dim == 1
	def is_horizontal(self):
		return self.stretch_dim == 0
	def is_absolute(self):
		return (self.pos.x != 0) or (self.pos.y != 0) or (self.pos.z != 0)
	def set_width(self, width):
		assert(not self.is_vertical())
		self._size.x.max = width
	def merge(self, glue):
		if glue.is_absolute():
			self.set_natural(glue.get_natural())
		else:
			assert(self.stretch_dim == glue.stretch_dim) 
			self.set_natural(self.get_natural() + glue.get_natural())
	def zero(self):
		self.set_natural(v3(0,0,0))

class Group(Node):
	""" 
		DESCRIPTION: 
			FIXME
		METHODS
			add_and_pos(self, node, pos):
				adds a node at a specific (local) position and expands 
				the _size accordingly. Furthermore new hrefs and/or 
				vrefs are added as needed.
			add_right(self, node, space):
				FIXME
			add_below(self, node, space, type):
				FIXME
			add_absolute(self, node, pos):
	"""
	def __init__(self, this):
		Node.__init__(self, this, context=None)
		self.children = []
	def __repr__(self):
		return "<C Group instance at %s>" % (self.this,)
	def add(self, arg):
		self.children.append(arg)
		if arg.this:
			return libpointless.group_add(self.this, arg.this)
		else: return 1
	def add_pos_by_ref(self, self_ref, node, node_ref, pos):
		# sadly, this:
		#   node_pos = self_ref + pos - node_ref
		# is slower than this:
		x = self_ref.x + pos.x - node_ref.x
		y = self_ref.y + pos.y - node_ref.y
		z = self_ref.z + pos.z - node_ref.z
		node_pos = v3(x,y,z)
		self.add_and_pos(node, node_pos)
	def add_and_pos(self, node, pos):
		# Expand size
		self._size.x.max = max(self._size.x.max, node._size.x.max + pos.x)
		self._size.x.min = min(self._size.x.min, node._size.x.min + pos.x)
		self._size.y.max = max(self._size.y.max, node._size.y.max + pos.y)
		self._size.y.min = min(self._size.y.min, node._size.y.min + pos.y)
		self._size.z.max = max(self._size.z.max, node._size.z.max + pos.z)
		self._size.z.max = min(self._size.z.min, node._size.z.min + pos.z)
		# Append / move refs
		if node._hrefs[-1].x + pos.x > self._hrefs[-1].x:
			self._hrefs.append(copy.copy(node._hrefs[-1]))
			self._hrefs[-1] += v3(pos.x, 0, 0)
		if node._hrefs[0].x + pos.x < self._hrefs[0].x:
			self._hrefs = [copy.copy(node._hrefs[0])] + self._hrefs
			self._hrefs[0] += v3(pos.x, 0, 0)
		if node._vrefs[-1].y + pos.y < self._vrefs[-1].y:
			self._vrefs.append(copy.copy(node._vrefs[-1]))
			self._vrefs[-1] += v3(0, pos.y, 0)
		if node._vrefs[0].y + pos.y > self._vrefs[0].y:
			self._vrefs = [copy.copy(node._vrefs[0])] + self._vrefs
			self._vrefs[0] += v3(0, pos.y, 0)
		# Place node
		node.set_pos(pos)
		self.add(node)
	def add_right(self, node, space):
		#if len(self.children) == 0:
			#space = 0
		pos = copy.copy(self._hrefs[-1])
		pos.x = self._size.x.max
		self.add_pos_by_ref(pos, node, node._hrefs[0], v3(space,0,0))
		#print "Node pos after: ", node.get_pos()
	def add_below(self, node, space, type):
		if type == eGlueType.EDGE_TO_EDGE:
			pos = copy.copy(self._vrefs[-1])
			pos.y = self._size.y.min - node._size.y.max - space
			self.add_and_pos(node, pos)
		elif type == eGlueType.REF_TO_REF:
			if len(self.children) == 0: space = 0
			space = max(space, self._vrefs[-1].y - self._size.y.min + 
				node._size.y.max - node._vrefs[0].y)
			self.add_pos_by_ref(self._vrefs[-1], node, node._vrefs[0], 
				v3(0,-space,0))
		elif type == eGlueType.REF_TO_REF_FORCE:
			self.add_pos_by_ref(self._vrefs[-1], node, node._vrefs[0], 
				v3(0,-space,0))
		else:
			assert(0) # invalid type
	def add_absolute(self, node, pos):
		node.set_pos(pos)
		self.add(node)
	def _debug_draw(self):
		size = self.get_size()
		r = random()*0.9 + 0.1
		g = random()*0.9 + 0.1
		b = random()*0.9 + 0.1
		a = 0.2
		print size, self.get_pos()
		x = libpointless.nodefactory_rectangle(
			size.x.size(), size.y.size(),
			(r, g, b, a),
			(r, g, b, a),
			(r, g, b, a),
			(r, g, b, a)
			)
		self.debug_gfx = Leaf(x)
		print self.debug_gfx.get_size()
		self.debug_gfx.set_pos(v3(size.x.min,size.y.min,0))
		#renderer.get_root().add(self.debug_gfx)
		self.add(self.debug_gfx)
	def init_debug_draw(self):
		for c in self.children: c.init_debug_draw()
		if self.debug_draw: self._debug_draw()

class Step:
	"""Token to signify a new slide_step"""
	def __init__(self, controller_token):
		assert(isinstance(controller_token, ControllerToken))
		self.controller_token = controller_token
	def __repr__(self): return "Step(%s)" % self.controller_token.__repr__()

class ControllerToken:
	"""Token to signify a controller"""
	def __init__(self, controller_class, parameters, slide_no, slide_step):
		self.controller_class = controller_class
		self.parameters       = parameters
		self.slide_no         = slide_no
		self.slide_step       = slide_step
	def __repr__(self): return "ControllerToken(%s)"%str(self.controller_class)
	def produce(self):
		c = self.controller_class(self.parameters)
		c.slide_no = self.slide_no
		c.slide_step = self.slide_step
		return c

# --------------------------------------------------------------------------- #
# paragraph breaking

class NodeList(list):
	""" 
		DESCRIPTION:
			The base class for lists of tokens (inner nodes). The elements in 
			the list are positioned next to each other according to the glue. 
			If the glue between two elements is missing then glue of size 0 is 
			assumed. 
		METHODS:
		set_step_span
			This is a util function helping to determine how far down the 
			tree the controllers needs to be attached. We want the controllers 
			to be as high as possible (i.e. only _one_ controller fading in 
			the slide, rather than n controllers on each leaf fading in the 
			slide). Look for the mark_slidesteps() and attach_controllers() 
			functions elsewhere. In effect each inner node can cover a 'span' 
			of slide-steps. If an inner-node covers a span of the type [n,m] 
			where m>n, we need to look further down. If an inner node covers 
			a span of the type [n,n] the controller for step n can be attached 
			to the node.
		arrange
			This base class will simply make a "pass" in the arrange method 
			but derived classes are supposed to set glue in one of the three 
			dimensions. 
		makenode
			A method that is supposed to return a Group node containing 
			the content of the NodeList, properly formatted. Note that
			the list of tokens contains quite different types of
			tokes, e.g. Controller, Leaf and Glue tokens. All
			Controller objects are attached to the the Group node that
			this method eventually returns.
	"""
	def __init__(self, nodes):
		self += nodes
		self.step_span = None
		self.group = None
	def __repr__(self, level=0):
		res = ''
		for x in self:
			indent = "  "*level
			if isinstance(x, list):
				res += indent + x.__class__.__name__+"\n"
				#res += indent + "(span: %s)\n"%self.step_span.__repr__()
				res += x.__repr__(level=level+1)
			else:
				res += indent + x.__repr__()+"\n"
		return res
	def set_step_span(self, span):
		self.step_span = span
	def arrange(self, available):
		pass
	def makenode(self, available):
		self.arrange(available)
		x = libpointless.nodefactory_group("Group")
		if not x: return None
		g = Group(x)
		controllers = []
		glue = None
		for n in self:
			if isinstance(n, ControllerToken):
				## simply collect all controllers in the list of tokens
				controllers.append(n)
				continue
			if isinstance(n, Glue):
				if not glue:
					glue = n
				elif (glue.is_vertical() and n.is_absolute()):
					## HACK: this is a meaningless case that we wont allow
					assert(0), "You can't have vglue before absolute glue"
				else:
					glue.merge(n)
				continue
			if isinstance(n, list):
				assert(isinstance(n, NodeList)),"Only NodeList (and derived) allowed here"
				w = g.get_size().x.size()
				if glue:
					if glue.is_vertical(): w = 0
					else: w += glue.get_size().x.size()
				n.makenode(available - v3(w,0,0))
				n = n.group
			assert(isinstance(n, Node)), \
				"How did this end up in the list of tokens: %s. Context: %s." \
				% (str(n), str(self))
			if glue == None:
				## no glue implies horizontal glue of size 0
				g.add_right(n, 0)
			elif glue.is_vertical():
				g.add_below(n, glue.get_size().y.size(), glue.type)
			elif glue.is_absolute():
				g.add_absolute(n, glue.pos)
			else:
				g.add_right(n, glue.get_size().x.size())
			glue = None
		for c in controllers:
			## attach controllers to the group node
			controller = c.produce()
			controller.set_node(g)
			pll.slideshow.add_observer(controller)
		###g.debug_draw = 1
		self.group = g
		#return g

def strip_discardable_front(l):
	while len(l) and isinstance(l[0], Node) and l[0].discardable:
		del(l[0])
def strip_discardable_back(l):
	while len(l) and isinstance(l[-1], Node) and l[-1].discardable:
		l.pop()

class BorderInfo:
	def __init__(self, borders):
		self.top = 0
		self.bottom = 0
		self.left = 0
		self.right = 0
		if borders=='all': borders = '<>^_'
		if borders:
			self.top    = borders.find("^") >= 0
			self.bottom = borders.find("_") >= 0
			self.left   = borders.find("<") >= 0
			self.right  = borders.find(">") >= 0

class Cell(NodeList):
	def __init__(self, nodes, cellpadding=None, borderinfo=None, bordercolor=None):
		NodeList.__init__(self, nodes)
		assert(not borderinfo or isinstance(borderinfo, BorderInfo))
		self.borderinfo = borderinfo
		self.cellpadding = cellpadding
		assert(not bordercolor or isinstance(bordercolor, Color))
		self.bordercolor = bordercolor
		self.row = None
		self.table = None
	def get_cellpadding(self):
		if self.cellpadding != None:
			return self.cellpadding
		if self.row.cellpadding != None:
			return self.row.cellpadding
		return self.row.table.cellpadding
	def get_borderinfo(self):
		if self.borderinfo != None:
			return self.borderinfo
		if self.row.borderinfo != None:
			return self.row.borderinfo
		return self.row.table.borderinfo
	def get_bordercolor(self):
		if self.bordercolor != None:
			return self.bordercolor
		if self.row.bordercolor != None:
			return self.row.bordercolor
		return self.row.table.bordercolor

class Row(NodeList):
	"""
	"""
	def __init__(self, nodes, cellpadding, borderinfo, bordercolor):
		NodeList.__init__(self, nodes)
		assert(not borderinfo or isinstance(borderinfo, BorderInfo))
		self.borderinfo = borderinfo
		self.cellpadding = cellpadding
		assert(not bordercolor or isinstance(bordercolor, Color))
		self.bordercolor = bordercolor

class Table(NodeList):
	""" 
		DESCRIPTION: 
			A Table is a list of table rows. Thus, the Table will contain
			Row's, and each Row must contain only NodeLists
			(the cells of the row).
			The makenode() method is overridden in order to layout all the
			cells first, and then modify the width of the individual cells
			according to the widest cell in each column.
	"""
	def __init__(self, nodes, num_cells, cellpadding, borderinfo, bordercolor):
		NodeList.__init__(self, nodes)
		assert(isinstance(borderinfo, BorderInfo))
		self.borderinfo = borderinfo
		assert(not bordercolor or isinstance(bordercolor, Color)), bordercolor
		self.bordercolor = bordercolor
		assert(cellpadding != None)
		self.cellpadding = cellpadding
		for row in nodes:
			assert(type(row) == Row), "type: "+type(row).__str__(row)
			for e in row:
				assert(type(e) == Cell), ("Wrong type %s, expected Cell" % type(e))
		self.num_cells = num_cells
		self.set = 0
		self.cellpadding = cellpadding

	def makenode(self, available):

		x = libpointless.nodefactory_group("TableGroup")
		if not x: return None
		g = Group(x)
		
		for row in self:
			row.table = self
			for cell in row:
				cell.row = row
				cell.makenode(available)

		numcols    = max([len(r) for r in self])
		colwidths  = [0.0 for x in range(0,numcols)]
		rowheights = [size(0.0,0.0) for x in self]

		# Find colwidths and rowheights
		for row, row_num in zip(self, range(len(self))):
			for cell, col_num in zip(row, range(len(row))):

				cell_width = cell.group.get_size().x.size() + \
					cell.get_cellpadding()*2
				if cell_width > colwidths[col_num]:
					colwidths[col_num] = cell_width

				#cell_height = cell.group.get_size().y.size() + \
				#	cell.get_cellpadding()*2
				#if cell_height > rowheights[row_num]:
				#	rowheights[row_num] = cell_height
				h_max = cell.group.get_size().y.max + cell.get_cellpadding()
				if h_max > rowheights[row_num].max:
					rowheights[row_num].max = h_max
				h_min = cell.group.get_size().y.min - cell.get_cellpadding()
				if h_min < rowheights[row_num].min:
					rowheights[row_num].min = h_min
					
		# Place cells and borders
		pos = v3(0,0,0)
		rows = zip(self, rowheights, range(len(self)))
		rows.reverse()
		width = sum(colwidths)
		edges = {}
		for row, row_height, row_num in rows:
			pos.x = 0
			h = row_height
			for cell, col_num in zip(row, range(len(row))):
				cellpadding = cell.get_cellpadding()
				cellpos = pos + v3(cellpadding, -h.min, 0)
				g.add_and_pos(cell.group, cellpos)
				w = colwidths[col_num]
				color = cell.get_bordercolor()
				if cell.get_borderinfo().bottom:
					edge = "h,%i,%i"%(col_num,row_num)
					if not edges.has_key(edge):
						h_line = pll.rectangle(w, 1, color)
						g.add_absolute(h_line , pos)
						edges[edge] = 1
				if cell.get_borderinfo().top:
					edge = "h,%i,%i"%(col_num,row_num-1)
					if not edges.has_key(edge):
						h_line = pll.rectangle(w, 1, color)
						g.add_absolute(h_line , pos + v3(0,h.size(),0))
						edges[edge] = 1
				if cell.get_borderinfo().left:
					edge = "v,%i,%i"%(col_num,row_num)
					if not edges.has_key(edge):
						v_line = pll.rectangle(1, h.size(), color)
						g.add_absolute(v_line , pos)
						edges[edge] = 1
				if cell.get_borderinfo().right:
					edge = "v,%i,%i"%(col_num+1,row_num)
					if not edges.has_key(edge):
						v_line = pll.rectangle(1, h.size(), color)
						g.add_absolute(v_line , pos + v3(w,0,0))
						edges[edge] = 1
				pos.x += w
			pos.y += h.size()
		g.get_size().x.max += cellpadding
		g.get_size().y.max += cellpadding
		g._hrefs = [v3(g.get_size().x.min, g.get_size().y.min, 0)]
		g._vrefs = [v3(g.get_size().x.min, g.get_size().y.min, 0)]
		self.group = g
				
class StretchList(NodeList):
	""" 
		DESCRIPTION:
			This class overrides the arrange() method in order to set
			the glue. That is, we call the set_stretch() method on
			all the Glue tokens in the list with proper arguments.
		METHODS:
		set_glues
			Compute the width of the elements in the list of tokens
			and compare it with the desired width. Compute a stretch
			factor based on the difference between these two numbers
			and apply this on all the glue elements in the list. The
			list of tokens will then end up having the desired width.
	"""
	def __init__(self, nodes):
		NodeList.__init__(self, nodes)
		self.set = 0
	def arrange(self, available):
		assert(not self.set), "Oops. arrange called twice on StretchList"
		##### self.set_glues(available.x)
		self.set = 1
		wanted_width = available.x
		natural_width = sum(map(lambda x: x._size.x.min + x._size.x.max, self))
		if natural_width < wanted_width:
			stretch_wanted = wanted_width - natural_width
			glues = filter(lambda x: isinstance(x, Stretchable), self)
			stretch_available = sum(map(lambda x: x.stretchability, glues))
			pllog.debug(gt("Stretching (%f -> %f)"%(natural_width, wanted_width)))
			if stretch_available == 0: return
			## FIXME MAGIC - why 1.0 ???
			stretch_factor = 1.0*stretch_wanted / stretch_available 
			pllog.debug(gt("want %f, natural %f, avail %f" % 
				(wanted_width, natural_width, stretch_available)))
			for g in glues:
				g.set_stretch(stretch_factor)
		elif natural_width > wanted_width:
			shrink_wanted = natural_width - wanted_width
			glues = filter(lambda x: isinstance(x, Stretchable), self)
			shrink_available = sum(map(lambda x: x.shrinkability, glues))
			if shrink_available == 0: return
			## FIXME MAGIC - why 1.0 ???
			shrink_factor = 1.0*shrink_wanted / shrink_available 
			for g in glues:
				g.set_stretch(-shrink_factor)
	
class ParList(NodeList):
	""" 
		DESCRIPTION:
			This class overrides the arrange method in order to do line 
			breaking. The new self.list contains elements whose
			width doesn't exceed the available width.
		METHODS:
		arrange(self, available):
			The self.list is traversed and rebuild using two temporary lists 
			(lines and controllers), one consisting of all the controller 
			elements in the self.list and another consisting of line elements, 
			i.e. list of tokens constituting lines. 
	"""
	def __init__(self, nodes, adjustment = eAdjustmentType.ADJUSTED):
		NodeList.__init__(self, nodes)
		self.enumtype = adjustment
	def arrange(self, available):
		lines = []	
		controllers = []
		width = 0
		# pointer to the last processed Glue element and its width
		last_breakable = None
		## flag telling whether we should make a break 
		break_now = 0
		line = []
		for n in self:
			if isinstance(n, ControllerToken):
				controllers.append(n)
				continue
			if isinstance(n, NodeList):
				## group the elements into a single new element
				n.makenode(available - v3(width, 0, 0))
				n = n.group
				assert(n)
			if isinstance(n, Glue):
				if n.is_vertical():
					lines.append(self.make_line(line))
					lines.append(n)
					line  = []
					width = 0
					continue
				last_breakable = (n, width)
			if isinstance(n, Penalty) and n.penalty == -INFINITY:
				break_now = 1
			line.append(n)
			width += n.get_size().x.size()
			if width > available.x or break_now:
				if not last_breakable or break_now:
					if not break_now:
						pllog.warn(gt("Unable to break line"))
					if len(lines): lines.append(pll.vertical_bl_glue(
						n.context.font.baselineskip))
					lines.append(self.make_line(line, last_line = 1))
					width = 0
					line  = []
				else:
					where = line.index(last_breakable[0])
					baselineskip = last_breakable[0].context.font.baselineskip
					if len(lines):
						lines.append(pll.vertical_bl_glue(baselineskip))
					lines.append(self.make_line(line[:where]))
					width -= last_breakable[1]
					line  = line[where+1:]
				break_now = 0
				last_breakable = None
		if len(line):
			## proper handling of the last elements 
			if len(lines) and not isinstance(lines[-1], Glue):
				lines.append(pll.vertical_bl_glue(
					line[-1].context.font.baselineskip))
			lines.append(self.make_line(line, last_line=1))
		self[:] = controllers + lines
	def make_line(self, line, last_line=0):
		prefix = []
		suffix = []
		strip_discardable_front(line)
		strip_discardable_back(line)
		if self.enumtype == eAdjustmentType.CENTERED:
			prefix = [pll.hfill_glue()]
			suffix = [pll.hfill_glue()]
		elif last_line:
			suffix = [pll.hfill_glue()]
		return StretchList(prefix+line+suffix)

# --------------------------------------------------------------------------- #
# The Renderer

class Renderer:
	""" 
		DESCRIPTION: 
			This is essentaillty the python wrapping of the renderer
			which is implemented in the CPP world.
	"""
	def __init__(self):
		self.fullscreen = ploptions.fullscreen
		libpointless.renderer_init(
			self.fullscreen,
			plsetup.geometry.get_width(),
			plsetup.geometry.get_height(),
			plsetup.geometry.get_placement()
		)
		self.root = Group(libpointless.renderer_get_root())
		self.debug_osd = 0
		self.resolutions = str(libpointless.renderer_get_resolutions()).split(' ')
		if '' in self.resolutions: 
			self.resolutions.remove('') 
		## FIXME - this is not good
		try:
			self.resolutions.sort(lambda x,y: int.__cmp__(int(x.split('x')[0]),\
				int(y.split('x')[0])))
		except ValueError: pass
		self.resolution_index = 0
		try:
			myindex = libpointless.renderer_get_resolution()
			if myindex:
				self.resolution_index = \
					self.resolutions.index(libpointless.renderer_get_resolution())
		except ValueError: pass
	def __del__(self, avoidexception=libpointless.renderer_shutdown):
		avoidexception()
	def get_root(self):
		return self.root
	def get_width(self):
		return int(libpointless.renderer_get_width())
	def get_height(self):
		return int(libpointless.renderer_get_height())
	def get_fullscreen(self):
		return libpointless.renderer_get_fullscreen()
	def toggle_fullscreen(self):
		libpointless.renderer_toggle_fullscreen();
		if self.get_fullscreen():
			self.fullscreen = 1
		else:
			self.fullscreen = 0
	def set_resolution(self, res):
		if self.resolutions.__contains__(res):
			libpointless.renderer_set_resolution(res)
			return 1
		else:
			return 0
	def increase_resolution(self):
		self.resolution_index += 1
		if self.resolution_index >= len(self.resolutions):
			self.resolution_index=0
		self.set_resolution(self.resolutions[self.resolution_index])
	def decrease_resolution(self):
		self.resolution_index -= 1
		if self.resolution_index < 0:
			self.resolution_index = len(self.resolutions) - 1
		self.set_resolution(self.resolutions[self.resolution_index])
	def get_resolution(self):
		return str(libpointless.renderer_get_resolution())
	def get_resolutions(self):
		return self.resolutions
	def print_at(self,str,x=2,y= 1):
		## default debug location in the python part of pointless is (2,1)
		if self.debug_osd: libpointless.renderer_print_at(x, y, str)
	def getevent(self, block):
		return libpointless.renderer_getevent(block)
	def toggle_debug_osd(self):
		self.debug_osd = not self.debug_osd
		libpointless.renderer_set_debug_osd(self.debug_osd)
	def set_wireframe(self, wf):
		libpointless.renderer_set_wireframe(wf)
	def get_wireframe(self):
		return libpointless.renderer_get_wireframe()
	def frame(self):
		return libpointless.renderer_frame()
	def load_textures(self):
		libpointless.renderer_load_textures()
	def init_gl(self):
		libpointless.renderer_init_gl()
	def rootadd(self, arg):
		self.root.add(arg)
	def newroot(self):
		libpointless.renderer_newroot()
		self.root = Group(libpointless.renderer_get_root())
	def make_screenshot(self, filename=None, scale=1.0):
		if not filename:
			used_nums = ['000'] + [
				x[-7:-4] for x in os.listdir(".")
				if re.search("^plshot[0-9]{3}\.png$", x)
			]
			used_nums.sort()
			filename = "plshot%03i.png" % (int(used_nums[-1]) + 1)
		if not libpointless.renderer_make_screenshot(filename, scale):
			pllog.abort(gt("Writing screenshot to '%s' failed"%filename))
		else:
			pllog.info(gt("Wrote screenshot in '%s'"%filename))
		
# --------------------------------------------------------------------------- #
# Director of slideshow

class SlideInfo:
	def __init__(self, slide, steps):
		self.slide = slide
		self.steps = steps

class SlideShow:
	""" 
		DESCRIPTION: 
			This class is used to control the slideshow, the methods
			describes the actions to take when one switches from one
			state to another.
	"""
	def __init__(self):
		self.current_slide = None
		self.slide_no = 0
		self.slide_step = 0
		self.slides = {}
		self.observers = {}
		self.lo_fps = 0
	def set_lo_fps(self, lo_fps):
		self.lo_fps = lo_fps
	def add_slide(self, slide, slide_no, steps):
		self.slides[slide_no] = SlideInfo(slide, steps)
	def add_observer(self, observer):
		slide_no = observer.slide_no
		slide_step = observer.slide_step
		key = "%i:%i" % (slide_no, slide_step)
		if not self.observers.has_key(key):
			self.observers[key] = []
		self.observers[key].append(observer)
	def get_observers(self, slide_no, slide_step):
		key = "%i:%i" % (slide_no, slide_step)
		if self.observers.has_key(key): return self.observers[key]
		return []
	def cur_slide(self):
		return self.slides[self.slide_no]
	def get_slide(self, slide_no):
		if self.slides.has_key(slide_no): return self.slides[slide_no]
		else: return None
	def next_step(self):
		new_slide_step = self.slide_step + 1
		if new_slide_step > self.cur_slide().steps:
			self.next_slide()
		else:
			self.enter_step(new_slide_step)
			self.slide_step = new_slide_step
	def prev_step(self):
		new_slide_step = self.slide_step - 1
		if new_slide_step < 0:
			self.prev_slide(at_bottom=1)
		else:
			self.leave_step(self.slide_step)
			self.slide_step = new_slide_step
	def next_slide(self, at_bottom = 0, instant = 0):
		new_slide_no   = self.slide_no + 1
		new_slide_step = 0
		if not self.get_slide(new_slide_no):
			return
		if at_bottom: new_slide_step = self.get_slide(new_slide_no).steps
		self.move_to_slide(new_slide_no, instant=instant)
		self.move_to_step (new_slide_step, instant=1)
	def prev_slide(self, at_bottom = 0, instant = 0):
		new_slide_no  = self.slide_no - 1
		if not self.get_slide(new_slide_no):
			return
		if at_bottom: new_slide_step = self.get_slide(new_slide_no).steps
		else: new_slide_step = 0
		self.move_to_slide(new_slide_no, instant=instant)
		self.move_to_step (new_slide_step, instant=1)
	def move_to_slide(self, slide_no, instant=0):
		if self.slide_no == slide_no: return
		self.leave_slide(self.slide_no, instant)
		self.enter_slide(slide_no, instant)
		self.slide_no = slide_no
	def enter_step(self, step_no, instant=0):
		instant = instant or self.lo_fps
		for o in self.get_observers(self.slide_no, step_no):
			o.on_enter(instant=instant)
	def leave_step(self, step_no, instant=0):
		instant = instant or self.lo_fps
		for o in self.get_observers(self.slide_no, step_no):
			o.on_leave(instant=instant)
	def enter_slide(self, slide_no, instant=0):
		instant = instant or self.lo_fps
		for o in self.get_observers(slide_no, 0):
			o.on_enter(instant=instant)
	def leave_slide(self, slide_no, instant=0):
		instant = instant or self.lo_fps
		for o in self.get_observers(slide_no, 0):
			o.on_leave(instant=instant)
	def move_to_step(self, slide_step, instant=0):
		instant = instant or self.lo_fps
		for s in range(1, self.cur_slide().steps + 1):
			if s <= slide_step:
				self.enter_step(s, instant=instant)
			else:
				self.leave_step(s, instant=instant)
		self.slide_step = slide_step
	def first_slide(self):
		self.move_to_slide(1)
		self.move_to_step(0, instant=1)
	def last_slide(self):
		self.move_to_slide(max(self.slides.keys()))
		self.move_to_step(0, instant=1)
	def set_show_pos(self, slide_no, slide_step):
		self.move_to_slide(slide_no)
		self.move_to_step(slide_step, instant=1)
	def get_show_pos(self):
		return (self.slide_no, self.slide_step)
	def __arrange(self):
		assert(0)
		for slide_info in self.slides.values():
			slide = slide_info.slide
			slide.arrange()
	def status_str(self):
		return "Slide: %i:%i" % self.get_show_pos()
	def reset(self):
		for k in self.observers.keys():
			for o in self.observers[k]:
				o.init()
				o.on_leave(instant=1)

# --------------------------------------------------------------------------- #
		
class SlideStepping:
	def __init__(self, slide_no, slide_steps):
		self.slide_no = slide_no
		self.slide_steps = slide_steps

def parse_attributes(s):
	tokens = [""]
	in_quote = None
	escape = 0
	for i in range(len(s)):
		if in_quote:
			if escape:
				tokens[-1] += s[i]
				escape = 0
				continue
			if s[i] == '\\':
				escape = 1
				continue
			if s[i] == in_quote:
				in_quote = None
				continue
			tokens[-1] += s[i]
			continue
		if s[i] in "'\"":
			in_quote = s[i]
			if len(tokens[-1]): tokens.append("")
			continue
		if s[i] in ":=,":
			if len(tokens[-1]): tokens.append("")
			continue
		if s[i] in " \t": continue
		tokens[-1] += s[i]
	result = {}
	if (len(tokens) % 2):
		return None
	while len(tokens):
		k = tokens.pop(0)
		string_v = tokens.pop(0)
		try:
			v = string_v
			v = float(string_v)
			v = int(string_v)
		except ValueError:
			pass
		result[k] = v
	return result

class Pll:
	""" 
		DESCRIPTION: FIXME
	"""
	def __init__(self, slideshow, scale, slide_size):
		self.slideshow = slideshow
		self.commands = {}
		self.line_no = 0
		self.largefactor = 2
		self.hugefactor = 3
		self.smallfactor = 0.75
		self.tinyfactor = 0.5
		self.basesize = PlSize(str(ploptions.default_fontsize))
		self.fs = FontStateStack();
		self.fs.set_font(ploptions.default_fontname, 
			self.basesize.pixels(scale))
		self.fs.push()
		self.scale = scale
		self.slide_size = slide_size
		self.background = self.group("Background")
		renderer.rootadd(self.background)
		self.slide_no = 0
		self.slide_step = 0
		self.slide_steps = []
		self.no_slide_step = 0
		self.filename = None
		self.tempfileno = 0
		self.tempfiles = []
	def create_cache(self):
		assert(self.filename), "Unable to make cachefiles before setting filename"
		if not os.path.isdir(self.cachedir):
			try:
				os.mkdir(self.cachedir)
			except OSError:
				hashname = md5.new(self.cachedir).hexdigest()
				hashname = hashname[:16]
				self.cachedir = "/tmp/"+hashname ## FIXME hardcoding
				if not os.path.isdir(self.cachedir):
					try:
						os.mkdir(self.cachedir)
					except OSError:
						raise RuntimeError, gt("Unable to create cache")
	def get_tempfilename(self):
		self.create_cache()
		self.tempfileno += 1
		tempfilename = "%s/pl%03i-tmp" % (self.cachedir, self.tempfileno)
		self.tempfiles.append(tempfilename)
		return tempfilename
	def get_cachefilename(self, realfilename):
		self.create_cache()
		realfilename = os.path.abspath(realfilename)
		hashname = md5.new(realfilename).hexdigest()
		hashname = hashname[:16]
		return "%s/%s"%(self.cachedir, hashname)
	def set_lines(self, lines, filename):
		if filename:
			self.filename = filename
			self.cachedir = "%s.cache" % os.path.abspath(filename)
		self.lines = lines
		self.line_no = 1
	def new_slide(self):
		self.slide_no += 1
		self.slide_step = 0
		stepping = SlideStepping(self.slide_no, 0)
		self.slide_steps.append(stepping)
	def next_slide_step(self):
		self.slide_step += 1
		self.slide_steps[-1].slide_steps = self.slide_step
	def skip_next_step(self):
		self.no_slide_step = 1
	def get_slide_size(self):
		return self.slide_size
	def import_module(self, modulename):
		try:
			module = __import__(modulename)
		except ImportError, e:
			raise PllParseError(e)
		module.pll = self
		module.register()
		for cmd in vars(module).keys():
			if not isinstance(vars(module)[cmd], Command): continue
			self.commands[cmd] = vars(module)[cmd]
	def add_command(self, name, execute, slidestep = 0, transform = 0, 
			break_condition = re_newline, vars = 0, empty = 0):
		cmd = Command(name, execute, slidestep, transform, break_condition, vars, empty)
		self.commands[cmd.name] = cmd
		return cmd
	def include_file(self, filename):
		## FIXME what happens if f includes filename f
		filename = plfiles.find_file(filename)
		file  = plfiles.open_file(filename)
		lines = file.readlines()
		self.lines = lines + self.lines
	def eval(self, text):
		return self.parse([text], break_condition=re_eof, skip_white=1)
	def parse(self, lines, break_condition=re_newline, remove_break=0, skip_white=0):
		result = NodeList([])
		while len(lines):
			# Check for break-condition
			if break_condition:
				r = break_condition.search(lines[0])
				if r:
					if remove_break:
						lines[0] = lines[0][r.end(0):]
					break
			# End of line
			if lines[0] == "\n" or lines[0] == "":
				del(lines[0])
				if len(lines): lines[0] = " " + lines[0]
				self.line_no += 1
				continue
			# Check for comment
			r = re_comment.search(lines[0])
			if r:
				lines[0] = lines[0][r.end():]
				continue
			# Check for =command
			r = re_command.search(lines[0])
			if r:
				cmd_name = r.group(1)
				if cmd_name == 'slide' and len(result):
					break
				cmd_method = r.group(2)
				#cmd_args = r.group(4)
				cmd_break_condition = None
				cmd_remove_break = 0
				cmd_is_begin_type = 0
				if re_begin.search(cmd_name):
					cmd_name = cmd_name[6:] # chop off 'begin-'
					cmd_break_condition = re.compile('^=end-'+cmd_name)
					cmd_remove_break = 1
					cmd_is_begin_type = 1
				if self.commands.has_key(cmd_name):
					cmd = self.commands[cmd_name]
					if cmd_is_begin_type and cmd.empty:
						raise PllParseError("Command =%s does not have a =begin-%s form."%(cmd.name,cmd.name), self.line_no)
					lines[0] = lines[0][r.end(0):]
					cmd_args = None
					if r.group(3):
						in_quote = None
						for c, i in zip(lines[0], range(len(lines[0]))):
							if in_quote:
								if c == in_quote:
									in_quote = None
								continue
							if c in "'\"":
								in_quote = c
								continue
							if c == ')': break ## FIXME what if in a .set cmd
						cmd_args = lines[0][0:i]
						lines[0] = lines[0][i+1:]
					if not cmd_break_condition:
						cmd_break_condition = cmd.break_condition
					cmd_result = None
					cmd.push_attrs()
					if cmd_method == '.set' or cmd_method == '.with':
						h = parse_attributes(cmd_args)
						if h == None:
							raise PllParseError("Could not parse attributes '%s'" % cmd_args, self.line_no)
						if cmd_method == '.with':
							cmd.set_attrs(h)
						else: # .set
							cmd.pop_attrs()
							cmd.set_base_attrs(h)
							continue
					elif cmd_method:
						raise PllParseError("Unknown method "+cmd_method)
					if cmd_args:
						args = cmd.parse_args(cmd_args)
					else:
						args = []
					if cmd.transform:
						text = ""
						while len(lines):
							r = cmd_break_condition.search(lines[0])
							if not r:
								text += lines[0]
								del(lines[0])
								self.line_no += 1
							else:
								text += lines[0][:r.start(0)]
								if cmd_remove_break:
									lines[0] = lines[0][r.end(0):]
								else:
									lines[0] = lines[0][r.start(0):]
								break
						try:
							if not len(lines):
								# If loop ate all text, lines[0] fail unless:
								lines.append("")
							lines[0] = (cmd.execute(args, text, vars=cmd) +
								lines[0])
							#if cmd_method == '.with':
							cmd.pop_attrs()
						except PllCommandError, e:
							print gt("Line %i: Error executing =%s." %
								(self.line_no, cmd_name))
							for eline in str(e).split("\n"):
								print "**", eline
						continue
					if cmd.slidestep:
						if self.no_slide_step:
							self.no_slide_step = 0
						else:
							self.next_slide_step()
							result += [self.item_step()]
					parser = lambda : self.parse(lines, break_condition = 
						cmd_break_condition, remove_break = cmd_remove_break)
					try:
						cmd_result = cmd.execute(args = args, parser = parser,\
							vars = cmd)
						#if cmd_method == '.with':
						cmd.pop_attrs()
					except PllCommandError, e:
						print gt("Line %i: Error executing =%s." %
							(self.line_no, cmd_name))
						for eline in str(e).split("\n"):
							print "**", eline
						continue
					assert(type(cmd_result) == list), "Commands must return lists"
					result += cmd_result
					continue
			# Linewrap
			r = re_linewrap.search(lines[0])
			if r:
				del(lines[0])
				continue
			# Escapes
			r = re_escapes.search(lines[0])
			if r:
				lines[0] = lines[0][r.end():]
				if r.group(0) == ' ':
					result += [self.interword_glue()]
				else:
					result += [self.letter(ord(r.group(1)))]
				continue
			# Blocks
			r = re_blockstart.search(lines[0])
			if r:
				self.fs.push()
				#vs.push()
				lines[0] = lines[0][1:]
				continue
			r = re_blockend.search(lines[0])
			if r:
				self.fs.pop()
				#vs.pop()
				lines[0] = lines[0][1:]
				continue
			# Letters
			r = re_letters.search(lines[0])
			if r:
				lines[0] = lines[0][r.end():]
				result += [self.letter(ord(r.group(0)))]
				continue
			# Space
			r = re_space.search(lines[0])
			if r:
				lines[0] = lines[0][r.end():]
				if not skip_white and len(result) and \
						not isinstance(result[-1], Glue):
					result += [self.interword_glue()]
				continue
			badinput = lines[0][0:10]
			badinput = badinput.replace('\n','\\n')
			lines[:]=[]
			raise PllParseError("Parse error: Don't know how to handle: '%s'"%badinput, self.line_no)
		strip_discardable_front(result)
		strip_discardable_back(result)
		return result
	def letter(self, unicode):
		fs = self.fs.current()
		x = libpointless.nodefactory_letter(fs.font.this, unicode,
			fs.color.rgba, fs.dropshadow)
		if x: return Leaf(x, context=fs)
		else:
			pllog.warn(gt("No glyph for unicode %i in font '%s'" % \
				(unicode,fs.font.fontname)))
			pllog.warn(gt("inserting a '#' to substitute unicode %i" % unicode))
			x = libpointless.nodefactory_letter(fs.font.this, 0x0023, fs.color.rgba, fs.dropshadow)
			if x: return Leaf(x, context=fs)
			else: assert(0), "Unable to find char '#' in this font. Giving up"
	def linebreak(self):
		return Penalty(penalty=-INFINITY, context=self.fs.current())
	def interword_glue(self, scale=1.0):
		fs = self.fs.current()
		glue = Glue(
			## FIXME MAGIC 
			natural = v3(fs.font.size * 0.3333333 * scale, 0, 0), 
			stretchability = 5,
			context = self.fs.current()
		)
		return glue
	def hfill_glue(self, skip=0):
		glue = Glue(
			natural = v3(skip, 0, 0),
			stretchability=INFINITY,
			context = self.fs.current()
		)
		return glue
	def horizontal_glue(self, skip):
		glue = Glue(
			natural = v3(skip, 0, 0),
			stretchability = 0,
			context = self.fs.current(),
			discardable = 0
		)
		return glue
	def absolute_glue(self, abs_pos=v3(0,40,20)):
		glue = Glue(
			context = self.fs.current(), 
			pos = abs_pos 
		)
		return glue
	def vertical_glue(self, skip):
		glue = Glue(
			natural = v3(0,skip,0),
			type = eGlueType.EDGE_TO_EDGE,
			context = self.fs.current(),
			stretch_dim = 1
		)
		return glue
	def vertical_bl_glue(self, skip, force=0):
		if force:
			gluetype = eGlueType.REF_TO_REF_FORCE
		else:
			gluetype = eGlueType.REF_TO_REF
		glue = Glue(natural = v3(0,skip,0),
			type = gluetype,
			context = self.fs.current(),
			stretch_dim = 1)
		return glue
	def backgroundimage(self, stretch, filename):
		f = self.find_file(filename)
		if f:
			x = libpointless.nodefactory_backgroundimage(
				renderer.get_width(), renderer.get_height(), stretch, f)
			if x: return Leaf(x)
	def image(self, filename, center):
		gray_alpha = 0
		f = self.find_file(filename)
		if f:
			x = libpointless.nodefactory_image(f, center, gray_alpha, Color().rgba, 0.0)
			if x: return Leaf(x)
	def gaimage(self, filename, center):
		gray_alpha = 1
		f = self.find_file(filename)
		if f:
			x = libpointless.nodefactory_image(f, center, gray_alpha, self.fs.current().color.rgba, self.fs.current().dropshadow)
			if x: return Leaf(x)
	def rectangle(self, width, height, ll_color, lr_color=None, 
			ul_color=None, ur_color=None):
		glue = Rectangle(
			natural=v3(width, height, 0),
			context=self.fs.current(),
			ll_color=ll_color,
			lr_color=lr_color,
			ul_color=ul_color,
			ur_color=ur_color
			)
		return glue
	def group(self, name):
		x = libpointless.nodefactory_group(name)
		if x: return Group(x)
	def paragraphlist(self, nodes, adjustment = eAdjustmentType.ADJUSTED):
		return ParList(nodes, adjustment = adjustment)
	def nodelist(self, nodes):
		return NodeList(nodes)
	def row(self, nodes, cellpadding, borderspec, bordercolor):
		cells = []
		if len(nodes) and not isinstance(nodes[0], Cell):
			nodes = [Cell([])] + nodes
		for n in nodes:
			if isinstance(n, Cell):
				# each =cel cmd implies a glue element and a new Nodelist
				cells.append(n)
				continue
			cells[-1].append(n)
		for c in cells:
			strip_discardable_front(c)
			strip_discardable_back(c)
		if self.metarow:
			cells_new = []
			for m in self.metarow:
				if len(m):
					cells_new.append(copy.copy(m))
				else:
					if len(cells):
						cells_new.append(cells[0])
						del(cells[0])
			cells = cells_new
		if borderspec:
			borderinfo = BorderInfo(borderspec)
		else:
			borderinfo = None
		if bordercolor:
			c = Color()
			c.set_by_string(bordercolor)
		else:
			c = None
		return Row(cells, cellpadding, borderinfo, c)
	def setmetarow(self, celllist):
		self.metarow = celllist
	def table(self, nodes, cellpadding, borderspec, bordercolor):
		nodes = [ x for x in nodes if isinstance(x, Row) ]
		if bordercolor:
			c = Color()
			c.set_by_string(bordercolor)
		else:
			c = None
		return Table(nodes, len(nodes), cellpadding, BorderInfo(borderspec), c)
	def cellmark(self, cellpadding, borderspec, bordercolor):
		if borderspec:
			borderinfo = BorderInfo(borderspec)
		else:
			borderinfo = None
		if bordercolor:
			c = Color()
			if not c.set_by_string(bordercolor):
				print c.error_msg
			print bordercolor, c
		else:
			c = None
		return Cell([], cellpadding, borderinfo, c)
	def stretchlist(self, nodes):
		return StretchList(nodes)
	def slide_controller(self, parameters = 0):
		if not parameters:
			## proper default parameters:
			parameters = {'in_delay':0.8, 'move_in':(0.0, 0.0, 0.0), 
				'move_out':(0.0, 0.0,0.0)} 
		return ControllerToken(AlphaFader, parameters, self.slide_no, self.slide_step)
	def item_controller(self, slide_step=None):
		if not slide_step:
			slide_step = self.slide_step
		return ControllerToken(AlphaFader, {}, self.slide_no, slide_step)
	def item_step(self):
		return Step(self.item_controller())
	def find_file(self, filename):
		return plfiles.find_file(filename)

# --------------------------------------------------------------------------- #
# Font stuff

class Font:
	def __init__(self, fontname, size):
		f = plfiles.find_file(fontname)
		self.fontname = fontname
		self.size = size
		self.this = libpointless.get_font(f, int(size))
		if not self.this:
			raise RuntimeError, ("Could not load font '%s'" % fontname)
		metric = libpointless.get_font_metric(self.this)
		self.baselineskip = metric["baselineskip"]
		self.underline_position = metric["underline_position"]
		self.underline_thickness = metric["underline_thickness"]
		if abs(self.underline_position) > self.size:
			pllog.warn(gt("Possibly broken ttf metrics. Underline pos: %f" % 
				self.underline_position))
			self.underline_position = 0
		if abs(self.underline_thickness) > self.size:
			pllog.warn(gt("Possibly broken ttf metrics. Underline thickness: %f" %
				self.underline_thickness))
			self.underline_thickness = 0

class FontState:
	def __init__(self, font, color, dropshadow):
		assert(isinstance(font, Font))
		self.font  = font
		assert(isinstance(color, Color))
		self.color = color
		self.dropshadow = dropshadow
	def copy(self):
		return FontState(self.font, self.color, self.dropshadow)

class FontStateStack:
	def __init__(self):
		self.fontcache = {}
		self.stack = []
	def _get_font(self, fontname, size):
		fontkey = ("%s:%i" % (fontname, size))
		if self.fontcache.has_key(fontkey):
			return self.fontcache[fontkey]
		else:
			font = None
			try:
				font = Font(fontname, size)
			except (RuntimeError), e:
				pllog.warn(gt("%s. Substituting with default font" % e))
				try:
					font = Font(ploptions.default_fontname, size)
				except (RuntimeError), e:
					pllog.abort(gt("Could not load default font: %s") % e)
			self.fontcache[fontkey] = font
			return font
	def push(self):
		if len(self.stack) == 0:
			pllog.abort(gt("No default font set"));
		# FIXME: Cache the fontcontexts
		self.stack.append(self.stack[-1].copy())
	def pop(self):
		self.stack.pop()
	def current(self):
		return self.stack[-1]
	def set_size(self, size):
		if not len(self.stack):
			pllog.abort(gt("Set size but no font selected"))
		font = self._get_font(self.current().font.fontname, size)
		# FIXME: Cache the fontcontexts
		self.stack[-1] = self.stack[-1].copy()
		self.stack[-1].font = font
	def set_dropshadow(self, size):
		assert(len(self.stack))
		self.stack[-1] = self.stack[-1].copy()
		self.stack[-1].dropshadow = size
	def set_font(self, fontname, size=None):
		assert(fontname), "Must specify name of font"
		if not size:
			assert(len(self.stack))
			size=self.current().font.size
		font = self._get_font(fontname, size)
		if not len(self.stack):
			fontstate = FontState(font, Color(rgba=(1,1,1,1)), dropshadow=0)
			self.stack.append(fontstate)
		else:
			# FIXME: Cache the fontcontexts
			self.stack[-1] = self.stack[-1].copy()
			self.stack[-1].font = font

# --------------------------------------------------------------------------- #
# auxilary functions used by main 

def sum(list):
	return reduce(lambda x,y: x+y, list, 0)

def print_tree(root, indent = 0):
	pllog.warn("%s(%i)%s" % (" "*indent, sys.getrefcount(root), root.name()))
	if isinstance(root, Group):
		for c in root.children:
			print_tree(c, indent+1)

def mark_slidesteps(slide, step, collector):
	if isinstance(slide, list):
		assert(isinstance(slide, NodeList))
		start = step
		for x in slide:
			step = mark_slidesteps(x, step, collector)
			if isinstance(x, Step):
				slide.remove(x)
		slide.set_step_span([start, step])
		return step
	else:
		if isinstance(slide, Step):
			step += 1
			collector[step] = slide
		return step

def attach_controllers(slide, collector):
	if isinstance(slide, list):
		assert(isinstance(slide, NodeList))
		if slide.step_span[0] == slide.step_span[1]:
			step = slide.step_span[0]
			if step > 0:
				assert(collector.has_key(step))
				#print "Attaching ", collector[step].controller_token, "to ", slide
				slide.append(collector[step].controller_token)
		else:
			for x in slide:
				attach_controllers(x, collector)
	else:
		if isinstance(slide, Leaf):
			pllog.warn(gt("Internal error. Trying to attach a controller on a leaf node"))

def setup_controllers(slide):
	collector = {}
	mark_slidesteps(slide, 0, collector)
	attach_controllers(slide, collector)

goto_show_pos = None

def run_slideshow():
	global goto_show_pos
	slideshow   = SlideShow()
	if ploptions.powersave:
		slideshow.set_lo_fps(1)
	if ploptions.banner:
		filedata = [x+'\n' for x in ploptions.banner.split('\n')]
	elif ploptions.inputfile == "-":
		## handle stdin input
		if TARSUPPORT:
			stdin = Stdin() ## HACK
			try:
				tar = tarfile.open(mode="r|gz", fileobj=stdin)
				workdir = "%s.stdin.cache" % plfiles.get_home()
				for tarinfo in tar: 
					tar.extract(tarinfo,workdir)
				tar.close()
				f = glob.glob(workdir+'/*.pll')[0]
				file = plfiles.open_file(f)
				filedata = file.readlines()
			except tarfile.ReadError:
				stdin.reset()
				filedata = stdin.read("").splitlines(1)
		else:
			filedata = sys.stdin.readlines()
	else:
		## handle ordinary file input
		if TARSUPPORT:
			if tarfile.is_tarfile(ploptions.inputfile):
				pllog.info("Input file is in tar.gz/tgz format")
				tgzinput = ploptions.inputfile
				tmp = ploptions.inputfile.replace('.tar.gz','')
				ploptions.inputfile = tmp.replace('.tgz','')
				extrapath = "%s.cache" % ploptions.inputfile
				plfiles.add_to_path(extrapath)
				tar = tarfile.open(tgzinput, "r:gz")
				for tarinfo in tar:
					tar.extract(tarinfo,os.path.dirname(ploptions.inputfile))
					pllog.info("%s is %i bytes in size and is," % (tarinfo.name, tarinfo.size))
					if tarinfo.isreg(): pllog.info("a regular file.")
					elif tarinfo.isdir(): pllog.info("a directory.")
					else: pllog.info("something else.")
				tar.close()
			else:
				pllog.info("Input file does not appear to be a tar.gz/tgz file")
		f = plfiles.find_file(ploptions.inputfile)
		file = plfiles.open_file(f)
		filedata = file.readlines()
	## Calculate size of slide based on window size and aspect ratio.
	corrected_height = renderer.get_height()*ploptions.aspect
	corrected_width  = renderer.get_width()
	corrected_size   = min(corrected_height, corrected_width)
	real_height = corrected_size / ploptions.aspect
	real_width  = corrected_size
	left_offset = (renderer.get_width() - real_width) / 2.0 ## FIXME MAGIC
	left_margin = 0.03 * real_width ## FIXME MAGIC
	top_offset  = (renderer.get_height() - real_height) / 2.0 ## FIXME MAGIC
	top_margin  = 0.03 * real_height ## FIXME MAGIC
	slide_size = v3(
		size(left_offset, left_offset + real_width),
		size(top_offset,  top_offset  + real_height),
		size(0,0)
	)
	global pll
	# hardcoding 600.0 here is ok. It is just a matter of scaling our
	# concept of size. The 'definition' of size in pointless is thus:
	# "One size-unit is one pixel when running 800x600, aspect 4:3".
	scale = real_height / 600.0
	pll = Pll(slideshow, scale, slide_size)
	pll.import_module("pllbasics")
	pll.set_lines(filedata, ploptions.inputfile)
	slidelist = []
	if ploptions.packtgz:
		if not TARSUPPORT:
			pllog.abort("cannot export to tgz without the python tarfile module, cf. http://www.gustaebel.de/lars/tarfile/")
		image_file_list = []
	while len(pll.lines):
		if ploptions.packtgz:
			for l in map(None,pll.lines):
				r = re.search('=background_image|=image|=absolute_image', l)
				if r:
					## FIXME make this more safe
					t = re.sub('.*=(background_|absolute_)?image\("','',l)
					s =  re.sub('"(,.*)?\).*','',t)
					if image_file_list.count(s.strip()) == 0:
						image_file_list.append(s.strip())
		sys.stdout.write("[")
		sys.stdout.flush()
		slide = pll.parse(pll.lines, break_condition=None, skip_white=1)
		if slide: slidelist.append(slide)
		else: pllog.warn(gt("Internal problem: parser returned empty slide"))
		sys.stdout.write("%i] " % len(slidelist))
		sys.stdout.flush()
	print
	for slide in slidelist:
		setup_controllers(slide)
	no_of_slides = 0
	for slide, stepping in map(None, slidelist, pll.slide_steps):
		## FIXME MAGIC
		available = v3(real_width - 2.0*left_margin, INFINITY, INFINITY)
		if DEBUG:
			print "================================="
			print "Slide: ", stepping.slide_no
			print "Dump : ", slide
			print "================================="
		slide.makenode(available)
		g = slide.group
		g.set_pos(v3(left_margin + left_offset, top_offset + real_height - 
			(top_margin + g._size.y.max - g._vrefs[0].y), 0)) 
		g.init_debug_draw()
		renderer.rootadd(g)
		slideshow.add_slide(g, stepping.slide_no, stepping.slide_steps)
		no_of_slides += 1
	renderer.load_textures()
	renderer.init_gl()
	slideshow.reset()
	if ploptions.packtgz:
		i = 0
		while((i < no_of_slides)):
			## FIXME png files from PLLINPUT path must be included too
			i += 1
			slideshow.move_to_slide(i, instant=1)
			magic = 100000 # hack - something larger than the number of steps
			slideshow.move_to_step(magic, instant=1); 
			renderer.frame()
		cachedir = "%s.cache" % os.path.abspath(ploptions.inputfile)
		cachefiles = glob.glob(cachedir+'/*')
		## collect all the files in a single working directory 
		packtgzwork = ploptions.tmpwork+'/tgz'
		if not os.path.isdir(ploptions.tmpwork):
			os.mkdir(ploptions.tmpwork)
		if os.path.isdir(packtgzwork):
			shutil.rmtree(packtgzwork)
		os.mkdir(packtgzwork)
		basename = os.path.basename(ploptions.inputfile)
		workcachedir = "%s/%s.cache" % (packtgzwork, basename)
		os.mkdir(workcachedir)
		dest="%s/%s" % (packtgzwork, basename)
		shutil.copyfile(ploptions.inputfile,dest)
		for name in image_file_list:
			filename = plfiles.find_file(name)
			dest="%s/%s.cache/%s" % (packtgzwork, basename, os.path.basename(filename))
			shutil.copyfile(filename,dest)
		for name in cachefiles:
			dest="%s/%s.cache/%s" % (packtgzwork, basename, os.path.basename(name))
			shutil.copyfile(name,dest)
		tgzfiles = glob.glob(packtgzwork+'/*') ## could be constructed more safe - FIXME
		tgzout = "%s.tgz" % os.path.basename(glob.glob(packtgzwork+'/*.pll')[0])
		tar = tarfile.open(tgzout, "w:gz")
		for name in tgzfiles:
			myname = name.replace(packtgzwork,'')
			pllog.info('Adding file %s as %s to tgz ball' % (name, myname))
			tar.add(name,myname)
		tar.close()
		print (gt('%s packed into %s' % (ploptions.inputfile,tgzout)))
		## cleanup working directory
		if os.path.isdir(packtgzwork):
			shutil.rmtree(packtgzwork)
		return 0
	if ploptions.export and (ploptions.exportps or ploptions.exportpdf):
		pllog.abort('This export format is not supported yet')
		return 0
	if ploptions.export:
		## dump screenshots and export dumpinfo
		i = 0
		name = plsetup.merger.get_exportbasename()
		thumb_width = 200 ## FIXME MAGIC
		thumb_scale = thumb_width / float(renderer.get_width())
		thumb_height = renderer.get_height() * thumb_scale
		while((i < no_of_slides)):
			i += 1
			slideshow.move_to_slide(i, instant=1)
			# Hack - something larger than the number of steps
			slideshow.move_to_step(100000, instant=1);  ## FIXME MAGIC
			running_no = plsetup.merger.next_running_no()
			shot_name = name + running_no + ".png"
			thumb_shot_name = "small-"+shot_name 
			renderer.frame()
			renderer.make_screenshot(plsetup.merger.get_exportdir() + shot_name)
			renderer.make_screenshot(plsetup.merger.get_exportdir() + \
					thumb_shot_name, thumb_scale)
			plsetup.merger.add_merge_info([running_no, \
			                       shot_name, \
								   renderer.get_width(), \
								   renderer.get_height(), \
								   thumb_shot_name, \
								   thumb_width, \
								   thumb_height])
		if ploptions.exportraw:
			plsetup.merger.dump_raw()
		else:
			plsetup.merger.merge()
		return 0
	else:
		if PROFILE: return
		## Normal slideshow behavior 
		if goto_show_pos:
			slideshow.set_show_pos(goto_show_pos[0], goto_show_pos[1])
		else:
			slideshow.set_show_pos(1,0)
		## now, call frame and wait for key-events, the real main-loop
		while(1):
			controllers_run = renderer.frame()
			renderer.print_at(slideshow.status_str())
			if controllers_run > 0 or renderer.debug_osd:
				event = renderer.getevent(0) 
			else:
				event = renderer.getevent(1) 
				# do one more frame to make next frame
				# (where controllers will run again)
				# have a sane delta_time
				renderer.frame()
#			event = renderer.getevent(ploptions.powersave)  ## BUG, pointless-devel Wed, 12 Nov 2003 17:26:24
			if not event: continue
			(event_type, event_data) = event
			# Unknown event
			if event_type != libpointless.KEYDOWN and \
			   event_type != libpointless.MOUSEBUTTONDOWN and \
			   event_type != libpointless.VIDEORESIZE:
				pllog.warn(gt("Unknown event type"));
				continue
			# Resize event
			if event_type == libpointless.VIDEORESIZE:
				# Hack to get rid of extra resize event from wm's running
				# in opaque resize mode...
				for a in range(1,100):
					e = renderer.getevent(0)
				pllog.debug(gt("Resizing to %d x %d" % (event_data)))
				goto_show_pos = slideshow.get_show_pos()
				renderer.newroot()

				return 1
			# Mouse button pressed
			if event_type == libpointless.MOUSEBUTTONDOWN:
				but = event_data[0]
				if but in (libpointless.BUTTON_LEFT, 
						libpointless.BUTTON_WHEEL_DOWN):
					slideshow.next_step()
				elif but in (libpointless.BUTTON_RIGHT, 
						libpointless.BUTTON_WHEEL_UP):
					slideshow.prev_step()
				elif but == libpointless.BUTTON_MIDDLE:
					pass
				else:
					pllog.warn(gt("Unknown mouse button: %i"%but))
				continue
			# Key pressed
			(sym, mod, type) = event_data
			if sym == libpointless.K_q:
				return 0
			elif sym == libpointless.K_d:
				renderer.toggle_debug_osd()
			elif sym == libpointless.K_w:
				renderer.set_wireframe(not renderer.get_wireframe())
			elif (sym == libpointless.K_SPACE) or \
				(sym == libpointless.K_DOWN) or \
				(sym == libpointless.K_j):
				slideshow.next_step()
			elif (sym == libpointless.K_UP) or (sym == libpointless.K_k):
				slideshow.prev_step()
			elif (sym == libpointless.K_RIGHT) or (sym == libpointless.K_l):
				quick = mod == libpointless.KMOD_SHIFT
				slideshow.next_slide(at_bottom=quick, instant=quick)
			elif (sym == libpointless.K_LEFT) or (sym == libpointless.K_h):
				quick = mod == libpointless.KMOD_SHIFT
				slideshow.prev_slide(at_bottom=quick, instant=quick)
			elif (sym == libpointless.K_0):
				slideshow.first_slide()
			elif (sym == libpointless.K_g) and (mod == libpointless.KMOD_SHIFT):
				slideshow.last_slide()
			elif (sym == libpointless.K_s):
				renderer.make_screenshot(scale=0.5)
			elif (sym == libpointless.K_i):
				print gt("Current geometry: %sx%s") % \
				(plsetup.geometry.get_width(), plsetup.geometry.get_height())
				print gt("Current resolution: '%s'") % renderer.get_resolution()
				print gt("List of possible resolutions:")
				l=renderer.get_resolutions()
				print  "\n".join(l)
			elif (sym == libpointless.K_PLUS):
				## FIXME, international keyboards, DK keyboard ('+'='shift+0')
				renderer.increase_resolution()
			elif (sym == libpointless.K_MINUS):
				renderer.decrease_resolution()
			elif (sym == libpointless.K_f) and (mod == libpointless.KMOD_CTRL):
				renderer.toggle_fullscreen()
	
def init_renderer():
	"""Initialize renderer"""
	global renderer
	renderer = Renderer()
	if ploptions.print_resolutions:
		l=renderer.get_resolutions()
		if l:
			print gt("List of possible resolutions:")
			current = renderer.get_resolution()
			for r in l:
				if r == current: print " *", 
				else: print "  ", 
				print  r
		sys.exit()
	if ploptions.wanted_resolution:
		if not renderer.set_resolution(ploptions.wanted_resolution):
			print gt("Resolution '%s' not supported.") % \
				ploptions.wanted_resolution;
			sys.exit()

# --------------------------------------------------------------------------- #
# Main loop

def main():
	plsetup.process_args()
	pllog.debug(gt("CVS id for this particular version %s") % __id__)
	init_renderer()
	## Run slideshow. Returns true on resize etc, and false when exit
	## has been requested
	try:
		if PROFILE:
			profile.run("pointless.run_slideshow()", "pointless.prof")
			s = pstats.Stats('pointless.prof')
			s.sort_stats('cumulative')
			s.print_stats()
		else:
			while(run_slideshow()): pass
		return 0
	except (PllParseError), e:
		print e
		return 1

# --------------------------------------------------------------------------- #

if __name__ == '__main__':
	main()

# --------------------------------------------------------------------------- #

