# 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
