# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2002 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

#
# Class Compound
#
# a baseclass for objects that contain other graphics objects
# (primitives or other compounds)
#

from types import ListType, TupleType, IntType, InstanceType
import operator

from app import _, SketchError, UnionRects, EmptyRect, _sketch
from app import NullUndo, CreateListUndo, Undo

from base import GraphicsObject, Bounded, CHANGED
from blend import Blend, MismatchError
from properties import EmptyProperties

from selinfo import prepend_idx, select_range, build_info, list_to_tree2, \
		list_to_tree_sliced



class Compound(GraphicsObject):

	has_edit_mode = 0
	is_Group	  = 0
	is_Compound	  = 1
	can_be_empty  = 0
	is_Page       = 0

	allow_traversal = 0

	def __init__(self, objects = None, duplicate = None):
		GraphicsObject.__init__(self, duplicate = duplicate)
		if duplicate is not None:
			objects = []
			for obj in duplicate.objects:
				objects.append(obj.Duplicate())
			self.objects = objects
		elif objects:
			self.objects = objects
		else:
			self.objects = []
		self.changing_children = 0
		self.set_parent()

	def Destroy(self):
		self.destroy_objects()
		GraphicsObject.Destroy(self)

	def destroy_objects(self):
		for obj in self.objects:
			obj.Destroy()
		self.objects = []

	def SetParent(self, parent):
		if parent is self.parent:
			return
		GraphicsObject.SetParent(self, parent)
		if parent is not None:
			self.set_parent()
		else:
			self.unset_parent()

	def set_parent(self):
		for child in self.objects:
			child.SetParent(self)

	def unset_parent(self):
		for child in self.objects:
			child.SetParent(None)

	def SelectionInfo(self, child = None):
		info = GraphicsObject.SelectionInfo(self)
		if info and child is not None:
			path = info[0]
			return (path + (_sketch.IdIndex(self.objects, child),),
					child)
		return info

	def ChildChanged(self, child):
		self.del_lazy_attrs()
		if self.changing_children:
			return
		self.issue_changed()

	def SetDocument(self, doc):
		for obj in self.objects:
			obj.SetDocument(doc)
		GraphicsObject.SetDocument(self, doc)
		self.set_parent()

	def disconnect_objects(self):
		for obj in self.objects:
			obj.Disconnect()

	def Disconnect(self):
		self.disconnect_objects()
		self.unset_parent()

	def connect_objects(self):
		for obj in self.objects:
			obj.Connect()

	def Connect(self):
		self.set_parent()
		self.connect_objects()

	def set_objects(self, new_objs):
		if self.document is not None:
			self.document.AddClearRect(self.bounding_rect)
		self.disconnect_objects()
		self.destroy_objects()
		self.objects = new_objs
		for obj in self.objects:
			if self.document is not None:
				obj.SetDocument(self.document)
			obj.SetParent(self)
		# XXX no connect here ?
		self.del_lazy_attrs()
		if self.document is not None:
			self.document.AddClearRect(self.bounding_rect)

	def UntieFromDocument(self):
		for obj in self.objects:
			obj.UntieFromDocument()

	def TieToDocument(self):
		for obj in self.objects:
			obj.TieToDocument()

	def load_AppendObject(self, object):
		self.objects.append(object)

	def load_Done(self):
		pass

	def __getitem__(self, idx):
		if type(idx) == IntType:
			return self.objects[idx]
		elif type(idx) == TupleType:
			if len(idx) > 1:
				return self.objects[idx[0]][idx[1:]]
			elif len(idx) == 1:
				return self.objects[idx[0]]
		raise ValueError, 'invalid index %s' % `idx`

	def GetObjects(self):
		# XXX should this return a copy of self.objects?
		return self.objects

	def del_lazy_attrs(self):
		Bounded.del_lazy_attrs(self)
		return (self.del_lazy_attrs,)

	def update_rects(self):
		# XXX: should we raise an exception here if self.objects is empty?
		boxes = map(lambda o: o.coord_rect, self.objects)
		if boxes:
			self.coord_rect = reduce(UnionRects, boxes, boxes[0])
		else:
			self.coord_rect = EmptyRect

		boxes = map(lambda o: o.bounding_rect, self.objects)
		if boxes:
			self.bounding_rect = reduce(UnionRects, boxes, boxes[0])
		else:
			self.bounding_rect = EmptyRect

	def SelectSubobject(self, p, rect, device, path = None, *rest):
		return self

	def Insert(self, obj, at):
		raise SketchError('Cannot insert in compound')

	def Remove(self, *args, **kw):
		raise SketchError('Cannot remove from compound')

	RemoveSlice = Remove
	RemoveObjects = Remove

	ReplaceChild = Remove

	def MoveObjectsToTop(self, infolist):
		raise SketchError('Cannot rearrange objects in compound')

	MoveObjectsToBottom = MoveObjectsToTop
	MoveObjectsDown = MoveObjectsToTop
	MoveObjectsUp = MoveObjectsToTop
	def move_objects_to_top(self, infolist, to_bottom = 0):
		return infolist, NullUndo

	def DuplicateObjects(self, infolist, offset):
		raise SketchError('Cannot duplicate objects in compound')

	def ForAll(self, func):
		self.changing_children = 1
		try:
			return map(func, self.objects)
		finally:
			self.changing_children = 0

	def WalkHierarchy(self, func):
		for obj in self.objects:
			if obj.is_Compound:
				obj.WalkHierarchy(func)
			else:
				func(obj)

	def begin_change_children(self):
		self.changing_children = self.changing_children + 1
		return (self.end_change_children,)

	def end_change_children(self):
		self.changing_children = self.changing_children - 1
		if not self.changing_children:
			self._changed()
		return (self.begin_change_children,)

	def ForAllUndo(self, func):
		if self.objects:
			undo = [self.begin_change_children()]
			undo = undo + map(func, self.objects)
			undo.append(self.end_change_children())
			return CreateListUndo(undo)
		else:
			return NullUndo

	def FilterAll(self, func):
		return filter(func, self.GetObjects())

	def NumObjects(self):
		return len(self.objects)

	def Info(self):
		return _("Compound with %d objects") % len(self.objects)

	def AddStyle(self, style):
		undo = self.ForAllUndo(lambda o, style = style: o.AddStyle(style))
		return undo

	def Properties(self):
		return EmptyProperties

	def SetProperties(self, **kw):
		self.del_lazy_attrs()
		func = lambda o, kw = kw: apply(o.SetProperties, (), kw)
		undo = self.ForAllUndo(func)
		return undo

	def Hit(self, p, rect, device):
		test = rect.overlaps
		objects = self.objects
		for obj_idx in range(len(objects) - 1, -1, -1):
			obj = objects[obj_idx]
			if test(obj.bounding_rect):
				if obj.Hit(p, rect, device):
					return obj_idx + 1
		return 0

	def Transform(self, trafo):
		self.del_lazy_attrs()
		undo = self.ForAllUndo(lambda o, t = trafo: o.Transform(t))
		return undo

	def Translate(self, offset):
		undo = self.ForAllUndo(lambda o, p = offset: o.Translate(p))
		return undo

	def DrawShape(self, device, rect = None):
		if rect:
			test = rect.overlaps
			for o in self.objects:
				if test(o.bounding_rect):
					o.DrawShape(device, rect)
		else:
			for obj in self.objects:
				obj.DrawShape(device)

	def PickObject(self, point, rect, device):
		objects = self.objects[:]
		objects.reverse()
		test = rect.overlaps
		for obj in objects:
			if test(obj.bounding_rect):
				if obj.is_Compound:
					result = obj.PickObject(point, rect, device)
					if result:
						break
				elif obj.Hit(point, rect, device):
					result =  obj
					break
		else:
			result = None

		return result

	def Blend(self, other, frac1, frac2):
		try:
			objs = self.objects
			oobjs = other.objects
			blended = []
			for i in range(min(len(objs), len(oobjs))):
				blended.append(Blend(objs[i], oobjs[i], frac1, frac2))
			return Compound(blended)
		except:
			raise MismatchError

	def GetObjectHandle(self, multiple):
		return self.ForAll(lambda o: o.GetObjectHandle(1))


	def SelectFirstChild(self):
		if self.allow_traversal and self.objects:
			return self.objects[0]

	def SelectLastChild(self):
		if self.allow_traversal and self.objects:
			return self.objects[-1]

	def SelectNextChild(self, child, idx):
		if self.allow_traversal and len(self.objects) > idx + 1:
			return self.objects[idx + 1]

	def SelectPreviousChild(self, child, idx):
		if self.allow_traversal and len(self.objects) > idx - 1 and idx > 0:
			return self.objects[idx - 1]


class EditableCompound(Compound):

	allow_traversal = 1

	def SelectSubobject(self, p, rect, device, path = None, *rest):
		test = rect.overlaps
		if path is None:
			return self
		if path:
			path_idx = path[0]
			path = path[1:]
		else:
			path_idx = -1
			path = None
		objects = self.objects
		for obj_idx in range(len(objects) - 1, -1, -1):
			obj = objects[obj_idx]
			if test(obj.bounding_rect) and obj.Hit(p, rect, device):
				if obj_idx == path_idx:
					result = obj.SelectSubobject(p, rect, device, path)
				else:
					result = obj.SelectSubobject(p, rect, device)
				return prepend_idx(obj_idx, result)
		return None

	def Insert(self, obj, at):
		# Insert OBJ into the object hierarchy at the position described
		# by AT. AT should be either an integer or a tuple of integers.
		# OBJ can be a graphics object of a list of such objects.
		#
		# If AT is a tuple of 2 or more ints, self's child at AT[0] has
		# to be a compound object and its Insert method is called with
		# OBJ and AT[1:] as arguments.
		#
		# If AT is an int or a singleton of one int, insert OBJ at that
		# position in self's children. If OBJ is a graphics object, this
		# works just like a list objects insert method (insert(AT,
		# OBJ)). If its a list of graphics objects this method
		# effectively assigns that list to the slice AT:AT.
		#
		# As a side effect, this method calls the following methods of
		# the inserted objects:
		#
		#	    obj.SetDocument(self.document)
		#	    obj.SetParent(self)
		#	    obj.Connect()
		#
		# Return a tuple (SELINFO, UNDO), where SELINFO is selection
		# info for the inserted objects at their new positions, and UNDO
		# is the appropriate undo info.
		#
		# If self is modified directly, issue a CHANGED message.
		undo_info = None
		try:
			if type(at) == TupleType and at:
				if len(at) == 1:
					at = at[0]
				else:
					child = at[0]
					at = at[1:]
					sel_info, undo_info = self.objects[child].Insert(obj, at)
					sel_info = prepend_idx(child, sel_info)
					return (sel_info, undo_info)
			if type(at) != IntType or at > len(self.objects):
				at = len(self.objects)
			if type(obj) == InstanceType:
				self.objects.insert(at, obj)
				obj.SetDocument(self.document)
				obj.SetParent(self)
				obj.Connect()
				sel_info = build_info(at, obj)
				undo_info = (self.Remove, obj, at)
			else:
				self.objects[at:at] = obj
				for o in obj:
					# XXX: should we have undo info for these:
					o.SetDocument(self.document)
					o.SetParent(self)
					o.Connect()
				sel_info = select_range(at, obj)
				undo_info = (self.RemoveSlice, at, at + len(obj))
			self._changed()
			return (sel_info, undo_info)
		except:
			if undo_info is not None:
				Undo(undo_info)
			raise

	def _insert_with_undo(self, obj, at):
		# The same as the Insert method but return only the undo info.
		return self.Insert(obj, at)[1]

	def do_remove_child(self, idx):
		obj = self.objects[idx]
		del self.objects[idx]
		obj.Disconnect()
		obj.SetParent(None)
		self._changed()
		return (self._insert_with_undo, obj, idx)

	def Remove(self, obj, idx = None):
		if type(idx) == TupleType:
			if len(idx) == 1:
				idx = idx[0]
			else:
				return self.objects[idx[0]].Remove(obj, idx[1:])
		if idx is None:
			idx = self.objects.index(obj)
		elif self.objects[idx] is not obj:
			raise ValueError, 'Compound.Remove(): invalid index'
		return self.do_remove_child(idx)

	def RemoveSlice(self, min, max):
		objs = self.objects[min:max]
		self.objects[min:max] = []
		for obj in objs:
			obj.Disconnect()
			obj.SetParent(None)
		self._changed()
		return (self._insert_with_undo, objs, min)

	def RemoveObjects(self, infolist):
		if not infolist:
			return NullUndo
		sliced = list_to_tree_sliced(infolist)
		sliced.reverse() # important!
		undo = [self.begin_change_children()]
		try:
			for start, end in sliced:
				if type(end) == IntType:
					undo.append(self.RemoveSlice(start, end))
				elif type(end) == ListType:
					undo.append(self.objects[start].RemoveObjects(end))
				else:
					undo.append(self.Remove(end, start))
			undo.append(self.end_change_children())
			return CreateListUndo(undo)
		except:
			Undo(CreateListUndo(undo))
			raise

	def ReplaceChild(self, child, object):
		# replace self's child child with object. Return undo info
		idx = self.objects.index(child)
		self.objects[idx] = object
		object.SetParent(self)
		object.SetDocument(self.document)
		child.SetParent(None)
		self._changed()
		return (self.ReplaceChild, object, child)

	def permute_objects(self, permutation):
		# permutation must be a list of ints and len(permutation) must be
		# equal to len(self.objects). permutation[i] is the index of the
		# object that is moved to index i in the result.
		objects = self.objects
		length = len(objects)
		identity = range(length)
		if permutation == identity:
			return NullUndo
		if len(objects) != len(permutation):
			raise ValueError, 'len(permutation) != len(self.objects)'

		result = map(operator.getitem, [objects] * length, permutation)
		inverse = [0] * length
		map(operator.setitem, [inverse] * length, permutation, identity)
		self.objects = result
		self._changed()
		return (self.permute_objects, inverse)

	def move_objects_to_top(self, infolist, to_bottom = 0):
		# Implement the public methods MoveToTop (if to_bottom is false)
		# and MoveToBottom (if to_bottom is true).
		sliced = list_to_tree_sliced(infolist)
		sliced.reverse()

		undo = [self.begin_change_children()]
		selection = []
		idxs = []
		permutation = range(len(self.objects))
		try:
			for start, end in sliced:
				if type(end) == IntType:
					# a contiguous range of self's children (start:end)
					idxs[:0] = permutation[start:end]
					del permutation[start:end]
				elif type(end) == ListType:
					# children of self.objects[start]
					child = self.objects[start]
					sel, undo_info = child.move_objects_to_top(end, to_bottom)
					if undo_info is not NullUndo:
						undo.append(undo_info)
					selection = selection + prepend_idx(start, sel)
				else:
					# a single object (self.object[start])
					idxs.insert(0, start)
					del permutation[start]

			if idxs:
				# direct children of self are involved: apply the
				# permutation
				if to_bottom:
					permutation = idxs + permutation
				else:
					permutation = permutation + idxs
				undo_info = self.permute_objects(permutation)
				if undo_info is not NullUndo:
					undo.append(undo_info)
			# finished:
			undo.append(self.end_change_children())
			if len(undo) <= 2:
				# We haven't really done anything (undo has length 2),
				# so we just pass the selection info back unchanged
				selection = infolist
				undo = NullUndo
			else:
				# We have done something, so figure out the new
				# selection info
				undo = CreateListUndo(undo)

				if to_bottom:
					selection = selection \
								+ select_range(0, self.objects[:len(idxs)])
				else:
					min = len(self.objects) - len(idxs)
					selection = selection \
								+ select_range(min, self.objects[min:])
			return (selection, undo)
		except:
			# Ooops, something's gone wrong. Undo everything we've done
			# so far... (hmm, this currently fails to undo everything if
			# undo.append(undo_info) fails... (the undo_info involved
			# would be lost))
			Undo(CreateListUndo(undo))
			raise

	def MoveObjectsToTop(self, infolist):
		return self.move_objects_to_top(infolist)

	def MoveObjectsToBottom(self, infolist):
		return self.move_objects_to_top(infolist, to_bottom = 1)

	def MoveObjectsDown(self, infolist):
		sliced = list_to_tree_sliced(infolist)
		undo = [self.begin_change_children()]
		selection = []
		permutation = range(len(self.objects))
		objects = self.objects
		try:
			for start, end in sliced:
				if type(end) == IntType:
					if start > 0:
						temp = permutation[start:end]
						del permutation[start:end]
						permutation[start - 1:start - 1] = temp
						selection = selection +select_range(start - 1,
															objects[start:end])
					else:
						selection = selection +select_range(start,
															objects[start:end])

				elif type(end) == ListType:
					sel, undo_info = objects[start].MoveObjectsDown(end)
					if undo_info is not NullUndo:
						undo.append(undo_info)
					selection = selection + prepend_idx(start, sel)
				else:
					if start > 0:
						del permutation[start]
						permutation.insert(start - 1, start)
						selection.append(build_info(start - 1, objects[start]))
					else:
						selection.append(build_info(start, objects[start]))

			undo_info = self.permute_objects(permutation)
			if undo_info is not NullUndo:
				undo.append(undo_info)
			undo.append(self.end_change_children())
			if len(undo) <= 2:
				undo = NullUndo
				selection = infolist
			else:
				undo = CreateListUndo(undo)
			return (selection, undo)
		except:
			Undo(CreateListUndo(undo))
			raise

	def MoveObjectsUp(self, infolist):
		sliced = list_to_tree_sliced(infolist)
		sliced.reverse()
		undo = [self.begin_change_children()]
		selection = []
		permutation = range(len(self.objects))
		objects = self.objects
		max = len(objects)
		try:
			for start, end in sliced:
				if type(end) == IntType:
					if end < max:
						temp = permutation[start:end]
						del permutation[start:end]
						permutation[start + 1:start + 1] = temp
						selection = selection + select_range(start + 1,
															objects[start:end])
					else:
						selection = selection + select_range(start,
															objects[start:end])

				elif type(end) == ListType:
					sel, undo_info = objects[start].MoveObjectsUp(end)
					if undo_info is not NullUndo:
						undo.append(undo_info)
					selection = selection + prepend_idx(start, sel)
				else:
					if start < max - 1:
						del permutation[start]
						permutation.insert(start + 1, start)
						selection.append(build_info(start + 1, objects[start]))
					else:
						selection.append(build_info(start, objects[start]))

			undo_info = self.permute_objects(permutation)
			if undo_info is not NullUndo:
				undo.append(undo_info)
			undo.append(self.end_change_children())
			if len(undo) <= 2:
				undo = NullUndo
				selection = infolist
			else:
				undo = CreateListUndo(undo)
			return (selection, undo)
		except:
			Undo(CreateListUndo(undo))
			raise

	def DuplicateObjects(self, infolist, offset):
		infolist = list_to_tree2(infolist)

		objects = self.objects
		undo = [self.begin_change_children()]
		selection = []
		added = 0
		try:
			for idx, obj in infolist:
				idx = idx + added
				if type(obj) == ListType:
					# duplicate in subobj
					sel, undoinfo = objects[idx].DuplicateObjects(obj, offset)
					undo.append(undoinfo)
					selection = selection + prepend_idx(idx, sel)
				else:
					obj = obj.Duplicate()
					obj.Translate(offset)
					sel, undoinfo = self.Insert(obj, idx + 1)
					undo.append(undoinfo)
					selection.append(sel)
					added = added + 1
			undo.append(self.end_change_children())
			return (selection, CreateListUndo(undo))
		except:
			Undo(CreateListUndo(undo))
			raise
