# -*- indent-tabs-mode: t -*-

""" 
Core objects for \module{soya.pudding}

"""

__revision__ = '$Revision: 1.1 $'

__doc_classes__ = ['Base', 'Control', 'InputControl', 'RootWidget']
__doc_functions__ = []

import soya
from soya.opengl import *
import soya.sdlconst as sdl

import traceback

import unittest

import sys

#import soya.pudding as pudding
from soya.pudding import PuddingError, ANCHOR_LEFT, ANCHOR_TOP, ANCHOR_RIGHT, ANCHOR_BOTTOM

if sys.version_info < (2,4):
	def sorted(iterable, cmp=None, key=None, reverse=False):
		"return a sorted copy of its input"
		seq = list(iterable)
		if reverse:
				seq.reverse()        # preserve stability
		if key is not None:
				seq = [(key(elem), i, elem) for i, elem in enumerate(seq)]
		seq.sort(cmp)
		if key is not None:
				seq = [elem for (key, i, elem) in seq]
		if reverse:
				seq.reverse()
		return seq


class LateBindingProperty(object):
	""" http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/408713 """

	def __init__(self, getname=None, setname=None, delname=None,
							 doc=None):
		self.getname = getname
		self.setname = setname
		self.delname = delname
		self.__doc__ = doc

	def __get__(self, obj, default = None):
		if obj is None:
			return self
		if self.getname is None:
			raise AttributeError('unreadable attribute')
		try:
			fget = getattr(obj, self.getname)
		except TypeError:
			raise TypeError("you have passed %s instead of a string" % self.getname)
		except AttributeError:
			raise TypeError('%s object does not have a %s method' %
												(type(obj).__name__, self.getname))
		return fget()

	def __set__(self, obj, value):
		if self.setname is None:
			raise AttributeError("can't set attribute")
		try:
			fset = getattr(obj, self.setname)
		except AttributeError:
			raise TypeError('%s object does not have a %s method' %
												(type(obj).__name__, self.setname))
		fset(value)

	def __delete__(self, obj):
		if self.delname is None:
			raise AttributeError("can't delete attribute")
		try:
			fdel = getattr(obj, self.delname)
		except AttributeError:
			raise TypeError('%s object does not have a %s method' %
												(type(obj).__name__, self.delname))
		fdel()


class Base(object):
	""" The base class for all widgets. Note a Base control doesnt render 
	anything to the screen or it does it in a fashion where position and size 
	are not relevant. For graphical controls subclass \class{soya.pudding.Control}
	instead 
	"""

	def __get_child__(self):
		return self._child

	def __set_child__(self, child):
		if child == self:
			raise AttributeError('child cannot be self!')
			
		self._child = child
		if self._child.parent != self:
			self._child.parent = self

		self.on_set_child(child)
	
	def __get_parent__(self):
		return self._parent

	def __set_parent__(self, parent):
		if parent != None and not issubclass(parent.__class__, Base):
			raise PuddingError('parent is not a subclass of soya.pudding.Base!')
			
		self._parent = parent

	child     = LateBindingProperty('__get_child__', '__set_child__', 
											doc = "child object")
	parent    = LateBindingProperty('__get_parent__', '__set_parent__', 
											doc = "parent object")
	
	def __init__(self, parent=None, **kwargs):
		self._parent = parent
		
		self._child = None

		if self.parent:
			if issubclass( self.parent.__class__, Container):
				self.parent.add_child(self)
			else:
				self.parent.child = self

		for arg,value in kwargs.iteritems():
			if hasattr(self, arg):
				setattr(self, arg, value)
			else:
				raise AttributeError('%s has no attribute %s, ejector seat triggered'
														 % (self, arg))

		self.on_init()
	
	def process_event(self, event):
		""" process one event. returning False means that the event has not been
		handled and should be passed on to other widgets. returning True means
		that the event has been handled and the event should no longer be 
		propogated """

		if self._child: 
			return self._child.process_event(event)

		return False

	def begin_round(self):
		""" soya begin_round event """
		if self.child:
			self.child.begin_round()

	def advance_time(self, proportion):
		""" soya advance_time event """
		if self.child:
			self.child.advance_time(proportion)

	def end_round(self):
		""" soya.end_round event """
		if self.child:
			self.child.end_round()

	def on_init(self):
		""" event occurs at the end of initialisation for user processing """
		pass

	def on_set_child(self, child):
		""" event triggered when the child attribute is set """
		pass


class TestBase(unittest.TestCase):
	""" tes class for base """
	klass = Base

	def setUp(self):
		self.parent = Base

	# we use test1Create to force this test to run first
	def test1Create(self):
		""" testing creation """
		base = self.klass()

	def testChild(self):
		""" testing as child """
		base = self.parent()
		child = self.klass(base)

	def testBeginRound(self):
		""" testing begin round function """
		self.klass().begin_round()

	def testEndRound(self):
		""" testing end round function """
		self.klass().end_round()

	def testAdvanceTime(self):
		""" testing advance_time function """
		self.klass().advance_time(0.1)

class Control(Base):
	""" The main graphical base class for all widgets."""

	def __get_left__(self):
		return self._left

	def __set_left__(self, left):
		self._left = left

	def __get_top__(self):
		return self._top

	def __set_top__(self, top):
		self._top = top

	def __get_width__(self):
		return self._width

	def __set_width__(self, width):
		self._width = width

	def __get_height__(self):
		return self._height

	def __set_height__(self, height):
		self._height = height
	
	def __get_right__(self):
		return self.parent.width - self._left - self._width

	# not sure which imlpementation is correct..
	#def __set_right__(self, val):
	#  self._left = self.parent.width - self._width - val

	def __set_right__(self, val):
		self._width = self.parent.width - self._left - val

	def __get_bottom__(self):
		return self.parent.height - self._top - self._height

	# not sure which imlpementation is correct..
	#def __set_bottom__(self, val):
	#  self._top = self.parent.height - self._width - val

	def __set_bottom__(self, val):
		self._height = self.parent.height - self._top - val

	def __get_screen_left__(self):
		return self._screen_left

	def __get_screen_top__(self):
		return self._screen_top

	def __get_screen_right__(self):
		return self._screen_left + self._width

	def __get_screen_bottom__(self):
		return self._screen_top + self._height

	def __get_visible__(self):
		return self._visible

	def __set_visible__(self, visible):
		if visible:
			self._visible = True
			self.on_show()
		else:
			self._visible = False
			self.on_hide()

	def __get_anchors__(self):
		return self._anchors

	def __set_anchors__(self, anchors):
		self._anchors = anchors

		self._anchor_left = self._left
		self._anchor_right = self.right
		self._anchor_top = self._top
		self._anchor_bottom = self.bottom


	left = LateBindingProperty('__get_left__', '__set_left__',
									doc = """distance from the left edge of 
									the screen to the left edge of the control""")
									
	top = LateBindingProperty('__get_top__', '__set_top__',
									doc = """distrance from the top edge of 
									the screen to the top edge of the control""")
									
	right = LateBindingProperty('__get_right__', '__set_right__', 
									doc = """distance from the right edge of 
									the screen to the right edge of the control""")
									
	bottom = LateBindingProperty('__get_bottom__', '__set_bottom__',
									doc = """distance from the bottom edge of 
									the screen to the bottom edge of the control""")

	width = LateBindingProperty('__get_width__', '__set_width__',
									doc = "width of the control")

	height = LateBindingProperty('__get_height__', '__set_height__',
									doc = "height of the control")

	screen_left   = LateBindingProperty('__get_screen_left__')
	screen_top    = LateBindingProperty('__get_screen_top__')
	screen_right  = LateBindingProperty('__get_screen_right__')
	screen_bottom = LateBindingProperty('__get_screen_bottom__')

	visible = LateBindingProperty('__get_visible__', '__set_visible__', 
									doc = "is the object visible")

	anchors = LateBindingProperty('__get_anchors__', '__set_anchors__')
 
	####### end of properties
	
	def __init__(self, parent=None, left=0, top=0, width=0, height=0, **kwargs):
		self._width = width
		self._height = height

		self._left = left
		self._top = top

		self._anchor_left = 0
		self._anchor_top = 0
		self._anchor_right = 0
		self._anchor_bottom = 0

		self._screen_left = left
		self._screen_top = top

		self._visible = True
		self._needs_update = True

		self._anchors = 0

		self.z_index = 0
	
		Base.__init__(self, parent, **kwargs)

	def process_event(self, event):
		""" process one event. returning False means that the event has not been
		handled and should be passed on to other widgets. returning True means
		that the event has been handled and the event should no longer be 
		propogated. """
		
		if not self._visible:
			return False

		if self._child: 
			return self._child.process_event(event)

		return False

	def start_render(self):
		""" sets up opengl state"""
		glPushMatrix()

		self._screen_left = self.left + self.parent._screen_left
		self._screen_top = self.top + self.parent._screen_top

		glTranslatef( self.left, self.top, 0.)

	def end_render(self):
		""" shuts down opengl state"""
		glPopMatrix()

	def render_self(self):
		""" renders the current object. ie dont render the children, render self """
		pass

	def render(self):
		""" render the whole object. setup and take down opengl, render self and 
		render all children """

		if self._visible:
			self.start_render()
			self.render_self()
			if self.child:
				self.child.render()
			self.end_render()

	def resize(self, left, top, width, height):
		""" set the position and size of the control """

		self._left = left
		self._top = top
		self._width = width
		self._height = height

		self.on_resize()

	def do_anchoring(self):
		""" move the control based on anchor flags """

		if self._anchors & ANCHOR_LEFT:
			self._left = self._anchor_left
		
		if self._anchors & ANCHOR_TOP:
			self._top = self._anchor_top

		if self._anchors & ANCHOR_RIGHT:
			if self._anchors & ANCHOR_LEFT:
				self.right = self._anchor_right
			else:
				self._left = self.parent.width - self._width - self._anchor_right

		if self._anchors & ANCHOR_BOTTOM:
			if self._anchors & ANCHOR_TOP:
				self.bottom = self._anchor_bottom
			else:
				self._top = self.parent.height - self._height - self._anchor_bottom

	def set_pos_bottom_right(self, right = None, bottom = None):
		""" whereas using .right and .bottom effect the width and height of the 
		control this will effect the left and the top """

		if right != None:
			self._left = self.parent.width - self._width - right

		if bottom != None:
			self._top = self.parent.height - self._height - bottom
	 
	def on_resize(self):
		""" event when the control is resized """

		self.do_anchoring()
	 
		if self.child:
			self.child.on_resize()

	def on_show(self):
		""" event when the control is made visible """
		pass

	def on_hide(self):
		""" event when the control is made invisible """
		pass


class TestControl(TestBase):
	""" test class for control """
	klass = Control

	def setUp(self):
		TestBase.setUp(self)

		self.parent = RootWidget

	def getControl(self):
		try:
			return self.klass(self.parent(), left = 10, top = 10, width = 100, height = 100)
		except TypeError, inst:
			raise TypeError('%s.__init__ got unexpected keyword argument %s' %
												(self.klass.__name__, inst)) 

	def testEvent(self):
		""" testing sending event """
		self.getControl().process_event([sdl.MOUSEMOTION, 0, 0, 0, 0]), True

	def testRender(self):
		""" testing calling render """
		self.getControl().render()

	def testOnResize(self):
		""" testing calling on_resize """
		self.getControl().on_resize()

	def testOnShow(self):
		""" testing calling on_show """
		self.getControl().on_show()

	def testOnHide(self):
		""" testing calling on_hide """
		self.getControl().on_hide()

	def testLTWHAttr(self):
		""" testing setting left/top/width/height """
		self.left = 10
		self.top = 10
		self.width = 10
		self.height = 10
		
	def testBeginRound(self):
		""" testing calling begin_round """
		self.getControl().begin_round()

	def testEndRound(self):
		""" testing calling end_round """
		self.getControl().end_round()

	def testAdvanceTime(self):
		""" testing calling advance_time """
		self.getControl().advance_time(0.1)


class Container(Control):
	""" Base class for child containers. Some child containers can handle clever
	resizing ( see soya.pudding.container ).
	
	The child attribute for containers is not used. 
	"""

	def __get_children__(self):
		return self._children

	def __set_children__(self, children):
		self._children = children

	def __get_padding__(self):
		return self._padding
	
	def __set_padding__(self, padding):
		self._padding = padding
	
	children  = LateBindingProperty('__get_children__', 
																	'__set_children__', 
																	doc = "children of the container")
	padding   = LateBindingProperty('__get_padding__', 
																	'__set_padding__', 
																	doc = "padding around child widgets")
	
	def __init__(self, parent=None, **kwargs):
		self._children = []
		self._padding = 0

		Control.__init__(self, parent, **kwargs)

	def apply(self, function, *args):
		""" utility function to call the same function for all children 
		with the same arguments """

		for child in self.children:
			getattr(child, function)(*args)

	def render_self(self):
		""" calls the render function of all children """

		# uncomment this to see where the sizers are really going
		#pudding.STYLE.draw_bordered_box(self.width, self.height, 
		#    background = (1.0, 0., 0., 1. ), border = (1.0,1.,1.,1.))

		for child in sorted(self.children, key=lambda child: child.z_index):
			child.render()

	def add_child(self, child, flags = 0):
		""" add a child to the container. The childs parent attribute will be 
		updated.
		control as it wants. valid flags are in soya.pudding.__init__.py. 

		"""

		"""
		examples:
		
		\\begin{verbatim}
			# create a label that is allowed to resize with the container
			my_container.add_child( soya.pudding.control.Label(None, 'some text'), 
															soya.pudding.EXPAND_BOTH)

			# create a label that is allowed to resize with the container but 
			# only horizontally
			my_container.add_child( soya.pudding.control.Label(None, 'some text'), 
															soya.pudding.EXPAND_HORIZ)

			# create a button that is not allowed to resize but sits in the middle 
			# of its space
			my_container.add_child( soya.pudding.control.Button(None, 'some text', 
																							width = 50, height = 20), 
															soya.pudding.CENTER_BOTH)

		\\end{verbatim}
		"""
			

		self.children.append(child)
		
		# add a property to the child for storing flags 
		# i get the feeling this is evil but the only other thing i can think to do 
		# is to create another list off container to store the child flags 
		child._container_flags = flags
		
		child.parent = self

		return child

	def get_child_index(self, child):
		""" return the index of child in children """
		for index, test_child in enumerate(self.children):
			if test_child == child:
				return index
		
	def get_child_options(self, child):
		""" get options set for the child """
		index = self.get_child_index(child)

		return self.children[index]._container_flags

	def set_child_options(self, child, flags = 0):
		""" set options for the child """

		index = self.get_child_index(child)

		self._children[index]._container_flags = flags

	def process_event(self, event):
		""" try passing the event to all children. 
		Returns True if one of the children handled the event """

		if not self._visible:
			return False

		for child in self.children:
			if child.process_event(event):
				return True

		return False

	def on_resize(self):
		""" Update anchors and then tell all children to resize themselves """

		self.do_anchoring()

		for child in self.children:
			child.on_resize()

	def begin_round(self):
		""" soya begin_round event """
		self.apply('begin_round')

	def advance_time(self, proportion):
		""" soya advance_time event """
		self.apply('advance_time', proportion)

	def end_round(self):
		""" soya.end_round event """
		self.apply('end_round')


class TestContainer(TestControl):
	klass = Container


class InputControl: 
	"""  This class should be used with multiple inheritance to create
	some standard events. call InputControl.process_event(self,event)
	from your widgets process_event call.
	
	Note the methods on_mouse*, on_key_*, on_focus and on_loose_focus """

	def __get_focus__(self):
		return self._focus

	def __set_focus__(self, val):
		if val:
			self.on_focus()
		elif self._focus:
			self.on_loose_focus()

		self._focus = val

	focus = LateBindingProperty('__get_focus__', '__set_focus__')

	def __init__(self):
		self._focus = False
			
	def process_event(self, event):
		""" process an individial event and then pass it on the correct event
		handler. if that handler returns True the event is assumed to of been 
		dealt with """

		if not self._visible:
			return False
		
		# if its a mouse event lets see if the cursor is over us
		if event[0] in [sdl.MOUSEMOTION, sdl.MOUSEBUTTONDOWN, sdl.MOUSEBUTTONUP]:
			return self.process_mouse_event(event)
		elif self.focus:
			if event[0] == sdl.KEYDOWN:
				if event[3] != 0:
					return self.on_key_down(event[3], event[2])
				else:
					return self.on_key_down(event[1], event[2])

			elif event[0] == sdl.KEYUP:
				return self.on_key_up(event[1], event[2])

		return False

	def process_mouse_event(self, event):
		""" process a mouse event. focus is set if the mouse is over the widget.
		the event handlers on mouse_* are called from here """

		if event[0] == sdl.MOUSEMOTION:
			x, y = event[1], event[2]
		else:
			x, y = event[2], event[3]


		if x > self.screen_left and x < self.screen_right \
					and y > self.screen_top and y < self.screen_bottom:

			# convert the coords to widget coords not screen coords
			x -= self.screen_left
			y -= self.screen_top

			if not self.focus:
				self.focus = True

			if event[0] == sdl.MOUSEMOTION:
				return self.on_mouse_over(x, y, event[1])
			elif event[0] == sdl.MOUSEBUTTONDOWN:
				return self.on_mouse_down(x, y, event[1])
			else:
				return self.on_mouse_up(x, y, event[1])

		else:
			if self._focus:
				self.focus = False

		return False

	def on_mouse_over(self, x, y, buttons):
		""" event triggered when the mouse moves over the control """
		pass

	def on_mouse_down(self, x, y, button):
		""" event triggered when a mouse button is pressed """
		pass

	def on_mouse_up(self, x, y, button):
		""" event triggered when a mouse button is released """
		pass

	def on_key_down(self, key, mods):
		""" event triggered when a key is pressed """
		pass

	def on_key_up(self, key, mods):
		""" event triggered when a key is released """
		pass

	def on_focus(self):
		""" event triggered when the control gets focus """
		pass

	def on_loose_focus(self):
		""" event triggered when the control looses focus """
		pass

class _TestInputControl(unittest.TestCase):
	def testOnMouseOver(self):  
		""" testing calling on_mouse_over """
		self.getControl().on_mouse_over(1, 1, 1)

	def testOnMouseDown(self):
		""" testing calling on_mouse_down """
		self.getControl().on_mouse_down(1, 1, 1)

	def testOnMouseUp(self):
		""" testing calling on_mouse_up """
		self.getControl().on_mouse_up(1, 1, 1)

	def testOnKeyDown(self):
		""" testing calling on_key_down """
		self.getControl().on_key_down(0, 0)

	def testOnKeyUp(self):
		""" testing calling on_key_up """
		self.getControl().on_key_up(0, 0)

	def testOnFocus(self):
		""" testing calling on_focus """
		self.getControl().on_focus()

	def testOnLooseFocus(self):
		""" testing calling on_loose_focus """
		self.getControl().on_loose_focus()


class RootWidget(Container):
	""" The root widget to be used with \module{soya.pudding}.

	If your display looks incorrect try resizing the window. If that corrects 
	the display then you need to call root_widget.on_resize() at some 
	point before the user gets control.

	"""

	def on_init(self):
		""" Declares self.cameras """

		self.cameras = []

	def widget_begin_round(self):
		""" Called at the beginning of every round """
		self.begin_round()
		
	def widget_advance_time(self, proportion):
		""" Called once or more per round """
		self.advance_time(proportion)
	
	def widget_end_round(self):
		""" Called at the end of every round """
		self.end_round()

	def start_render(self):
		""" Load the identity matrix for the root widget """
		# as we're the root we want to make sure we have the starting point
		glPushMatrix()

		self._screen_top = 0
		self._screen_left = 0
		
		glLoadIdentity()

		for camera in self.cameras:
			camera.render()

	def add_child(self, child):
		""" Add a child to the root widget. \class{RootWidget} also accepts cameras
		as children altho these are stored in .cameras """

		# find if something really is subclassed to a camera 
		# isinstance(child, soya.Camera) returns False 
		if isinstance(child, soya._soya._Camera):
			self.cameras.append(child)
		else:
			Container.add_child(self, child)

		return child

	def on_resize(self):
		""" Resize all cameras and children """

		for camera in self.cameras:
			camera.set_viewport(0, 0, self.width, self.height)

		Container.on_resize(self)


class TestRootWidget(unittest.TestCase):
	def test1Create(self):
		""" testing creating """
		r = RootWidget()
