# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999 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


#
#	The Blend Group
#


from types import IntType, ListType
import operator

from Sketch.UI import command

from Sketch.const import SelectAdd
from Sketch import SketchInternalError, _
from Sketch import NullUndo, CreateListUndo, CreateMultiUndo, Undo, UndoAfter

import selinfo
from compound import Compound
from blend import Blend
from group import Group

SelectStart = 0
SelectEnd = 1


class BlendInterpolation(Compound):

    can_be_empty = 1
    is_BlendInterpolation = 1

    def __init__(self, steps = 2, start = None, end = None,
		 duplicate = None):
	if duplicate is not None:
	    self.steps = duplicate.steps
	    Compound.__init__(self, duplicate = duplicate)
	else:
	    self.steps = steps
	    if start is not None:
		Compound.__init__(self, self.compute_blend(start, end))
	    else:
		Compound.__init__(self)

    def SetParameters(self, steps):
	if steps < 2:
	    raise ValueError, 'steps must be >= 2'
	if self.steps != steps:
	    undo = (self.SetParameters, self.steps)
	    self.steps = steps
	    self.parent.RecomputeChild(self)
	else:
	    undo = NullUndo
	return undo

    def Steps(self):
	return self.steps

    def Recompute(self, start, end):
	self.set_objects(self.compute_blend(start, end))

    def compute_blend(self, start, end):
	steps = self.steps
	blended = []
	for step in range(1, steps):
	    fraction = float(step) / steps
	    blend = Blend(start, end, fraction)
	    blend.SetDocument(self.document)
	    blend.SetParent(None)
	    blended.append(blend)
	blended.reverse()
	return blended

    def set_objects(self, new_objs):
	# called by __init__ and recompute
	if self.document is not None:
	    self.document.AddClearRect(self.bounding_rect)
	for obj in self.objects:
	    obj.Destroy()
	self.objects = new_objs
	self.del_lazy_attrs()
	if self.document is not None:
	    self.document.AddClearRect(self.bounding_rect)
	self.issue_changed()

    def SaveToFile(self, file):
	if file.options.full_blend:
	    file.BeginBlendInterpolation(self.steps)
	    for obj in self.objects:
		obj.SaveToFile(file)
	    file.EndBlendInterpolation()
	else:
	    file.BlendInterpolation(self.steps)

    def AsGroup(self):
	return Group(self.objects[:])

    def Transform(self, trafo):
	if self.parent.changing_children:
	    return Compound.Transform(self, trafo)
	else:
	    return NullUndo

    def Translate(self, offset):
	if self.parent.changing_children:
	    Compound.Translate(self, offset)
	return NullUndo

    def Info(self):
	return _("Interpolation with %d steps") % self.steps


class BlendGroup(Compound):

    is_Blend = 1
    is_Group = 1

    allow_traversal = 1 # allow selection of subobjects via M-Down etc.

    def __init__(self, steps = 0, start = None, end = None, duplicate = None):
	# Three different ways to instantiate this class:
	#
	# 1. Duplicating a BlendGroup: duplicate is not None.
	#
	# 2. Creating a BlendGroup from two normal graphics objects:
	#    start and end are not None.
	#
	# 3. Creating a BlendGroup from an sk file: steps is 0.

	if duplicate is not None:
	    # case 1
	    Compound.__init__(self, duplicate = duplicate)
	    #self.Connect()
	elif start is not None:
	    # case 2
	    inter = BlendInterpolation(steps, start, end)
	    Compound.__init__(self, [start, inter, end])
	    #self.Connect()
	else:
	    # case 3
	    Compound.__init__(self)

    def load_AppendObject(self, object):
	Compound.load_AppendObject(self, object)
	length = len(self.objects)
	if length > 2 and length & 1:
	    # last object was a control object
	    if len(self.objects[-2].objects) == 0:
		self.objects[-2].Recompute(self.objects[-3], self.objects[-1])

    def insert(self, obj, at):
	undo_info = None
	try:
	    if type(at) != IntType or at > len(self.objects):
		at = len(self.objects)
	    self.objects.insert(at, obj)
	    obj.SetDocument(self.document)
	    obj.SetParent(self)
	    obj.Connect()
	    undo_info = (self.remove, at)
	    self._changed()
	    return undo_info
	except:
	    if undo_info is not None:
		Undo(undo_info)
	    raise

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

    def do_remove_child(self, idx):
	undo = []
	try:
	    if idx % 2 == 0:
		# a control object
		if self.document is not None:
		    undo.append(self.document.AddClearRect(self.bounding_rect))
		if len(self.objects) > 3:
		    if idx == 0:
			undo.append(self.remove(1))
			undo.append(self.remove(0))
		    elif idx == len(self.objects) - 1:
			undo.append(self.remove(idx))
			undo.append(self.remove(idx - 1))
		    else:
			steps = self.objects[idx + 1].Steps() \
				+ self.objects[idx - 1].Steps()
			u = (UndoAfter, CreateMultiUndo(self.remove(idx + 1),
							self.remove(idx)),
			     self.objects[idx - 1].SetParameters(steps))
			undo.append(u)
		else:
		    # remove one of only two control objects -> Remove self
		    undo.append(self.parent.Remove(self))
		return CreateListUndo(undo)
	    else:
		# XXX implement this case
		raise ValueError, 'BlendGroup: cannot remove non control child'
	except:
	    Undo(CreateListUndo(undo))
	    raise

    def RemoveSlice(self, min, max):
	raise SketchInternalError('RemoveSlice not allowed for BlendGroup')

    def RemoveObjects(self, infolist):
	if not infolist:
	    return NullUndo
	sliced = selinfo.list_to_tree_sliced(infolist)
	sliced.reverse()
	undo = [self.begin_change_children()]
	try:
	    for start, end in sliced:
		if type(end) == IntType:
		    # > 1 adjacent children of self. XXX implement this
		    raise SketchInternalError('Deletion of multiple objects'
					      ' of BlendGroups not yet'
					      ' implemented')
		elif type(end) == ListType:
		    # remove grandchildren (children of child of self)
		    if start % 2 == 0:
			# control object changes. This should result in
			# a recompute() automatically.
			undo.append(self.objects[start].RemoveObjects(end))
		    else:
			pass
		else:
		    # a single child. If it's one of our control
		    # objects, remove self
		    undo.append(self.do_remove_child(start))
	    undo.append(self.end_change_children())
	    return CreateListUndo(undo)
	except:
	    Undo(CreateListUndo(undo))
	    raise

    def permute_objects(self, permutation):
	# for now, silently ignore this
	return NullUndo

    def DuplicateObjects(self, infolist, offset):
	# XXX: should allow duplication of the control objects by inserting
	# them into the parent.
	return [], NullUndo

    def ReplaceChild(self, child, object):
	idx = self.objects.index(child)
        if idx % 2 == 0:
            # the object is a control object
            undo = self.ReplaceChild, object, child
            self.objects[idx] = object
            object.SetParent(self)
            object.SetDocument(self.document)
            child.SetParent(None)
            self.ChildChanged(object)
            return undo
        else:
            raise SketchError('Cannot replace child')

    def SelectSubobject(self, p, rect, device, path = None, *rest):
	idx = self.Hit(p, rect, device) - 1
	obj = self.objects[idx]
	if idx % 2 == 0:
	    # a control object
	    if path:
		path_idx = path[0]
		path = path[1:]
		obj = obj.SelectSubobject(p, rect, device, path)
	    elif path == ():
		obj = obj.SelectSubobject(p, rect, device)
            info = selinfo.prepend_idx(idx, obj)
        else:
            # an interpolation object
            if path is None:
                info = self
            else:
                info = selinfo.prepend_idx(idx, obj)
	return info

    def ForAllUndo(self, func):
	# XXX: should we just change start and end and recompute?
	self.begin_change_children()
	undo = map(func, self.objects)
	self.end_change_children(ignore_child_changes = 1)
	idx = range(0, len(self.objects), 2)
	undo = map(operator.getitem, [undo] * len(idx), idx)
	return CreateListUndo(undo)

    def begin_change_children(self):
	self.child_has_changed = 0
	return Compound.begin_change_children(self)

    def end_change_children(self, ignore_child_changes = 0):
	if self.child_has_changed and not ignore_child_changes:
	    self.document.AddAfterHandler(self.recompute, (), self.depth())
	self.child_has_changed = 0
	return Compound.end_change_children(self)

    def ChildChanged(self, child):
	idx = self.objects.index(child)
	if idx % 2 == 0:
	    if self.changing_children:
		self.child_has_changed = 1
	    else:
		depth = self.depth(); recompute = self.recompute
		if idx > 0:
		    self.document.AddAfterHandler(recompute, (idx - 1,), depth)
		if idx < len(self.objects) - 1:
		    self.document.AddAfterHandler(recompute, (idx + 1,), depth)
	else:
	    Compound.ChildChanged(self, child)
	self.del_lazy_attrs()

    def recompute(self, idx):
	self.objects[idx].Recompute(self.objects[idx - 1], self.objects[idx+1])

    def RecomputeChild(self, child):
	self.recompute(self.objects.index(child))

    def SaveToFile(self, file):
	file.BeginBlendGroup()
	for obj in self.objects:
	    obj.SaveToFile(file)
	file.EndBlendGroup()

    def Info(self):
	return _("BlendGroup with %d control objects") \
	       % (len(self.objects) / 2 + 1)

    def CancelEffect(self):
	self.unset_parent()
	idx = range(0, len(self.objects), 2)
	return map(operator.getitem, [self.objects] * len(idx), idx)

    def SelectControl(self, relative, which):
	idx = self.objects.index(relative)
	if idx % 2 == 1:
	    # an interpolation
	    if which == SelectStart:
		idx = idx - 1
	    else:
		idx = idx + 1
	else:
	    # a control
	    if which == SelectStart:
		if idx > 0:
		    idx = idx - 2
	    else:
		if idx < len(self.objects) - 1:
		    idx = idx + 2
	self.document.SelectObject(self.objects[idx])

    def ExtendBlend(self, start, end, steps):
	idx = self.objects.index(start)
	if idx == 0:
	    inter = BlendInterpolation(steps, end, start)
	    return CreateMultiUndo(self.insert(inter, 0),
				   self.insert(end, 0))
	elif idx == len(self.objects) - 1:
	    inter = BlendInterpolation(steps, start, end)
	    return CreateMultiUndo(self.insert(inter, None),
				   self.insert(end, None))

    def Ungroup(self):
	objects = []
	for obj in self.objects:
	    if obj.__class__ is BlendInterpolation:
		objects.append(obj.AsGroup())
	    else:
		objects.append(obj)
	return objects


def CreateBlendGroup(start, end, steps):
    if isinstance(start.parent, BlendGroup):
	undo = start.parent.ExtendBlend(start, end, steps)
	result = start.parent
    elif isinstance(end.parent, BlendGroup):
	undo = end.parent.ExtendBlend(end, start, steps)
	result = end.parent
    else:
	result = BlendGroup(steps, start, end)
	undo = NullUndo
    return result, undo
