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

from math import sin, cos, atan2, hypot, pi, fmod, floor

from Sketch.const import ArcArc, ArcChord, ArcPieSlice, ConstraintMask, \
     AlternateMask

from Sketch import _, Point, Polar, Trafo, SingularMatrix, Rect, UnionRects, \
     CreateMultiUndo, NullUndo, RegisterCommands

import graphics

from Sketch.UI.command import AddCmd
import Sketch.UI.skpixmaps
pixmaps = Sketch.UI.skpixmaps.PixmapTk

import handle
from base import Primitive, RectangularPrimitive, RectangularCreator, Creator,\
     Editor
from bezier import PolyBezier
from blend import Blend
from properties import DefaultGraphicsProperties

from Sketch import _sketch



#
#	Ellipse
#

# helper function for snapping (might be useful elsewhere too):
def snap_to_line(start, end, p):
    if start != end:
	result = [(abs(start - p), start), (abs(end - p), end)]
	v = end - start
	length = abs(v)
	r = (v * (p - start)) / (length ** 2)
	if 0 <= r <= 1.0:
	    p2 = start + r * v
	    result.append((abs(p2 - p), p2))
	return min(result)
    else:
	return (abs(start - p), start)



class Ellipse(RectangularPrimitive):

    is_Ellipse = 1
    is_curve = 1
    is_clip = 1
    has_edit_mode = 1

    commands = RectangularPrimitive.commands[:]

    def __init__(self, trafo = None, start_angle = 0.0, end_angle = 0.0,
		 arc_type = ArcPieSlice, properties = None, duplicate = None):
	if duplicate is not None:
	    self.start_angle = duplicate.start_angle
	    self.end_angle = duplicate.end_angle
	    self.arc_type = duplicate.arc_type
	else:
	    self.start_angle = start_angle
	    self.end_angle = end_angle
	    self.arc_type = arc_type
	RectangularPrimitive.__init__(self, trafo, properties = properties,
				      duplicate = duplicate)
	self.normalize()

    def DrawShape(self, device, rect = None, clip = 0):
	Primitive.DrawShape(self, device)
	device.SimpleEllipse(self.trafo, self.start_angle, self.end_angle,
			     self.arc_type, rect, clip)

    def SetAngles(self, start_angle, end_angle):
	undo = (self.SetAngles, self.start_angle, self.end_angle)
	self.start_angle = start_angle
	self.end_angle = end_angle
	self.normalize()
	self._changed()
	return undo

    def Angles(self):
        return self.start_angle, self.end_angle

    def SetArcType(self, arc_type):
	if arc_type == self.arc_type:
	    return NullUndo
	undo = (self.SetArcType, self.arc_type)
	self.arc_type = arc_type
	self._changed()
	return undo

    def ArcType(self):
        return self.arc_type

    AddCmd(commands, 'EllipseArc', _("Arc"), SetArcType, args = ArcArc)
    AddCmd(commands, 'EllipseChord', _("Chord"), SetArcType, args = ArcChord)
    AddCmd(commands, 'EllipsePieSlice', _("Pie Slice"), SetArcType,
	   args = ArcPieSlice)

    def normalize(self):
	pi2 = 2 * pi
	self.start_angle = fmod(self.start_angle, pi2)
	if self.start_angle < 0:
	    self.start_angle = self.start_angle + pi2
	self.end_angle = fmod(self.end_angle, pi2)
	if self.end_angle < 0:
	    self.end_angle = self.end_angle + pi2

    def Paths(self):
        path = _sketch.approx_arc(self.start_angle, self.end_angle,
                                  self.arc_type)
	path.Transform(self.trafo)
        return (path,)

    def AsBezier(self):
        return PolyBezier(paths = self.Paths(),
                          properties = self.properties.Duplicate())

    def Hit(self, p, rect, device, clip = 0):
	return device.SimpleEllipseHit(p, self.trafo, self.start_angle,
				       self.end_angle, self.arc_type,
				       self.properties, self.Filled() or clip,
				       ignore_outline_mode = clip)

    def Blend(self, other, p, q):
	blended = RectangularPrimitive.Blend(self, other, p, q)
	if self.start_angle != self.end_angle \
	   or other.start_angle != other.end_angle:
	    blended.start_angle = p * self.start_angle + q * other.start_angle
	    blended.end_angle = p * self.end_angle + q * other.end_angle
	    if self.start_angle == self.end_angle:
		blended.arc_type = other.arc_type
	    elif other.start_angle == other.end_angle:
		blended.arc_type = self.arc_type
	    else:
		if self.arc_type == other.arc_type:
		    blended.arc_type = self.arc_type
		# The rest of the arc type blends is quite arbitrary
		# XXX: are these rules acceptable? Maybe we should blend
		# the ellipses as bezier curves if the arc types differ
		elif self.arc_type == ArcArc or other.arc_type == ArcArc:
		    blended.arc_type = ArcArc
		elif self.arc_type == ArcChord or other.arc_type == ArcChord:
		    blended.arc_type = ArcChord
		else:
		    blended.arc_type = ArcPieSlice
	return blended

    def GetSnapPoints(self):
	t = self.trafo
	start_angle = self.start_angle; end_angle = self.end_angle
	if self.start_angle == self.end_angle:
	    a = Point(t.m11, t.m21)
	    b = Point(t.m12, t.m22)
	    c = t.offset()
	    return [c, c + a, c - a, c + b, c - b]
	else:
	    points = [t(Polar(start_angle)), t(Polar(end_angle)), t.offset()]
	    if end_angle < start_angle:
		end_angle = end_angle + 2 * pi
	    pi2 = pi / 2
	    angle = pi2 * (floor(start_angle / pi2) + 1)
	    while angle < end_angle:
		points.append(t(Polar(1, angle)))
		angle = angle + pi2
	    return points

    def Snap(self, p):
	try:
	    r, phi = self.trafo.inverse()(p).polar()
	    start_angle = self.start_angle; end_angle = self.end_angle
	    p2 = self.trafo(Polar(1, phi))
	    if start_angle == end_angle:
		result = (abs(p - p2), p2)
	    else:
		result = []
		if phi < 0:
		    phi = phi + 2 * pi
		if start_angle < end_angle:
		    between = start_angle <= phi <= end_angle
		else:
		    between = start_angle <= phi or phi <= end_angle
		if between:
		    result.append((abs(p - p2), p2))
		start = self.trafo(Polar(self.start_angle))
		end = self.trafo(Polar(self.end_angle))
		if self.arc_type == ArcArc:
		    result.append((abs(start - p), start))
		    result.append((abs(end - p), end))
		elif self.arc_type == ArcChord:
		    result.append((snap_to_line(start, end, p)))
		elif self.arc_type == ArcPieSlice:
		    center = self.trafo.offset()
		    result.append(snap_to_line(start, center, p))
		    result.append(snap_to_line(end, center, p))
		result = min(result)
	    return result
	except SingularMatrix:
	    # XXX this case could be handled better.
	    return (1e200, p)


    def update_rects(self):
	trafo = self.trafo
	start = trafo.offset()
        # On some systems, atan2 can raise a ValueError if both
        # parameters are 0. In that case, the actual value the of angle
        # is not important since in the computation of p below, the
        # coordinate depending on the angle will always be 0 because
        # both trafo coefficients are 0. So set the angle to 0 in case
        # of an exception.
        try:
            phi1 = atan2(trafo.m12, trafo.m11)
        except ValueError:
            phi1 = 0
        try:
            phi2 = atan2(trafo.m22, trafo.m21)
        except ValueError:
            phi2 = 0
	p = Point(trafo.m11 * cos(phi1) + trafo.m12 * sin(phi1),
		  trafo.m21 * cos(phi2) + trafo.m22 * sin(phi2))
	self.coord_rect = r = Rect(start + p, start - p)
	if self.properties.HasLine():
	    width = self.properties.line_width
	    r = r.grown(width / 2 + 1)
	    # add the bounding boxes of arrows
	    if self.arc_type == ArcArc:
		pi2 = pi / 2
		arrow1 = self.properties.line_arrow1
		if arrow1 is not None:
		    pos = trafo(Polar(1, self.start_angle))
		    dir = trafo.DTransform(Polar(1, self.start_angle - pi2))
		    r = UnionRects(r, arrow1.BoundingRect(pos, dir, width))
		arrow2 = self.properties.line_arrow2
		if arrow2 is not None:
		    pos = trafo(Polar(1, self.end_angle))
		    dir = trafo.DTransform(Polar(1, self.end_angle + pi2))
		    r = UnionRects(r, arrow2.BoundingRect(pos, dir, width))
	self.bounding_rect = r

    def Info(self):
	trafo = self.trafo
	w = hypot(trafo.m11, trafo.m21)
	h = hypot(trafo.m12, trafo.m22)
	dict = {'center': trafo.offset(), 'radius': w, 'axes': (w, h)}
	if w == h:
	    text = _("Circle radius %(radius)[length], "
                     "center %(center)[position]")
        else:
            text = _("Ellipse axes %(axes)[size], center %(center)[position]")
        return text, dict

    def SaveToFile(self, file):
	Primitive.SaveToFile(self, file)
	file.Ellipse(self.trafo, self.start_angle, self.end_angle,
		     self.arc_type)

    def Editor(self):
	return EllipseEditor(self)

    context_commands = ('EllipseArc', 'EllipseChord', 'EllipsePieSlice')

RegisterCommands(Ellipse)


class EllipseCreator(RectangularCreator):

    creation_text = _("Create Ellipse")

    def compute_trafo(self, state):
        start = self.drag_start
        end = self.drag_cur
        if state & AlternateMask:
            # start is the center of the ellipse
            if state & ConstraintMask:
                # end is a point of the periphery of a *circle* centered
                # at start
                radius = abs(start - end)
                self.trafo = Trafo(radius, 0, 0, radius, start.x, start.y)
            else:
                # end is a corner of the bounding box
                d = end - start
                self.trafo = Trafo(d.x, 0, 0, d.y, start.x, start.y)
        else:
            # the ellipse is inscribed into the rectangle with start and
            # end as opposite corners. 
            end = self.apply_constraint(self.drag_cur, state)
            d = (end - start) / 2
            self.trafo = Trafo(d.x, 0, 0, d.y, start.x + d.x, start.y + d.y)
            
    def MouseMove(self, p, state):
        # Bypass RectangularCreator
        Creator.MouseMove(self, p, state)
        self.compute_trafo(state)

    def ButtonUp(self, p, button, state):
        Creator.DragStop(self, p)
        self.compute_trafo(state)

    def DrawDragged(self, device, partially):
	device.DrawEllipse(self.trafo(-1, -1), self.trafo(1, 1))

    def CurrentInfoText(self):
        t = self.trafo
        data = {}
        if abs(round(t.m11, 2)) == abs(round(t.m22, 2)):
            text = _("Circle %(radius)[length], center %(center)[position]")
            data['radius'] = t.m11
        else:
            text = _("Ellipse %(size)[size], center %(center)[position]")
            data['size'] = (abs(t.m11), abs(t.m22))
        data['center'] = t.offset()
        return text, data

    def CreatedObject(self):
	return Ellipse(self.trafo,
                       properties = DefaultGraphicsProperties())


class EllipseEditor(Editor):

    EditedClass = Ellipse

    selection = 0

    def ButtonDown(self, p, button, state):
	if self.selection == 1:
	    start = self.trafo(cos(self.start_angle), sin(self.start_angle))
	else:
	    start = self.trafo(cos(self.end_angle), sin(self.end_angle))
	Editor.DragStart(self, start)
	return p - start

    def apply_constraint(self, p, state):
	if state & ConstraintMask:
	    try:
		inverse = self.trafo.inverse()
		p2 = inverse(p)
		r, phi = p2.polar()
		pi12 = pi / 12
		angle = pi12 * floor(phi / pi12 + 0.5)
		pi2 = 2 * pi
		d1 = fmod(abs(phi - angle), pi2)
		if self.selection == 1:
		    selected_angle = self.end_angle
		else:
		    selected_angle = self.start_angle
		d2 = fmod(abs(phi - selected_angle), pi2)
		if d2 < d1:
		    phi = selected_angle
		else:
		    phi = angle
		p = self.trafo(Polar(r, phi))
	    except SingularMatrix:
		pass
	return p

    def MouseMove(self, p, state):
	p = self.apply_constraint(p, state)
	Editor.MouseMove(self, p, state)

    def ButtonUp(self, p, button, state):
	p = self.apply_constraint(p, state)
	Editor.DragStop(self, p)
	start_angle, end_angle, arc_type = self.angles()
	return CreateMultiUndo(self.object.SetAngles(start_angle, end_angle),
			       self.object.SetArcType(arc_type))

    def angles(self):
	start_angle = self.start_angle; end_angle = self.end_angle
	if self.arc_type == ArcChord:
	    arc_type = ArcChord
	else:
	    arc_type = ArcPieSlice

	try:
	    inverse = self.trafo.inverse()
	    p = inverse(self.drag_cur)
	    if self.selection == 1:
		start_angle = atan2(p.y, p.x)
	    elif self.selection == 2:
		end_angle = atan2(p.y, p.x)
	    if abs(p) > 1:
		arc_type = ArcArc
	except SingularMatrix:
	    pass
	if fmod(abs(start_angle - end_angle), 2 * pi) < 0.0001:
	    if self.selection == 1:
		start_angle = end_angle
	    else:
		end_angle = start_angle
	return (start_angle, end_angle, arc_type)

    def DrawDragged(self, device, partially):
	start_angle, end_angle, arc_type = self.angles()
	device.SimpleEllipse(self.trafo, start_angle, end_angle, arc_type)

    def GetHandles(self):
	trafo = self.trafo
	start_angle = self.start_angle; end_angle = self.end_angle
	p1 = trafo(cos(self.start_angle), sin(self.start_angle))
	if start_angle == end_angle:
	    return [handle.MakeNodeHandle(p1)]
	p2 = trafo(cos(self.end_angle), sin(self.end_angle))
	return [handle.MakeNodeHandle(p1), handle.MakeNodeHandle(p2)]

    def SelectHandle(self, handle, mode):
	self.selection = handle.index + 1

    def SelectPoint(self, p, rect, device, mode):
	return 0
