# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307	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 Sketch import _, SketchError, UnionRects, EmptyRect, _sketch
from Sketch 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

    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
