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

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

from Sketch import Point, Rotation, Translation, Trafo, NullPoint, NullUndo

from blend import Blend, MismatchError, BlendTrafo
import color



class Pattern:

    is_procedural = 1
    is_Empty = 0
    is_Solid = 0
    is_Gradient = 0
    is_RadialGradient = 0
    is_AxialGradient = 0
    is_ConicalGradient = 0
    is_Hatching = 0
    is_Tiled = 0
    is_Image = 0

    name = ''

    def __init__(self, duplicate = None):
	pass

    def SetName(self, name):
	self.name = name

    def Name(self):
	return self.name

    def Execute(self, device, rect = None):
	pass

    def Transform(self, trafo, rects = None):
	# This method is usually called by a primitives Transform method.
	return NullUndo

    def Duplicate(self):
	return self.__class__(duplicate = self)

    Copy = Duplicate

class EmptyPattern_(Pattern):

    is_procedural = 0
    is_Empty = 1

    def Duplicate(self):
	return self

    Copy = Duplicate

    def Blend(self, *args):
	return self

    def SaveToFile(self, file):
	file.EmptyPattern()

    def __str__(self):
	return 'EmptyPattern'

EmptyPattern = EmptyPattern_()

class SolidPattern(Pattern):

    is_procedural = 0
    is_Solid = 1

    def __init__(self, color = None, duplicate = None):
	if duplicate is not None:
	    self.color = duplicate.color
	elif color is not None:
	    self.color = color
	else:
	    raise ValueError,'SolidPattern be must created with color argument'

    def __cmp__(self, other):
	if self.__class__ == other.__class__:
	    return cmp(self.color, other.color)
	else:
	    return cmp(id(self), id(other))

    def __str__(self):
	return 'SolidPattern(%s)' % `self.color`

    def Execute(self, device, rect = None):
	device.SetFillColor(self.color)

    def Blend(self, other, frac1, frac2):
	if other.__class__ == self.__class__:
	    return SolidPattern(Blend(self.color, other.color, frac1, frac2))
	else:
	    raise MismatchError

    def Color(self):
	return self.color

    def SaveToFile(self, file):
	file.SolidPattern(self.color)

class GradientPattern(Pattern):

    is_Gradient = 1

    def __init__(self, gradient, duplicate = None):
	if duplicate is not None:
	    Pattern.__init__(self, duplicate = duplicate)
	    self.gradient = duplicate.gradient.Duplicate()
	elif gradient:
	    self.gradient = gradient
	else:
	    raise ValueError,\
		  'GradientPattern must be created with gradient argument'

    def Gradient(self):
	return self.gradient

    def SetGradient(self, gradient):
	undo = (self.SetGradient, self.gradient)
	self.gradient = gradient
	return undo


class LinearGradient(GradientPattern):

    is_AxialGradient = 1

    def __init__(self, gradient = None, direction = Point(0, -1),
		 border = 0, duplicate = None):
	GradientPattern.__init__(self, gradient,
				 duplicate = duplicate)
	self.direction = direction
	self.border = border
	if duplicate is not None:
	    if duplicate.__class__ == self.__class__:
		self.direction = duplicate.direction
		self.border = duplicate.border
	    elif duplicate.__class__ == ConicalGradient:
		self.direction = duplicate.direction
	    elif duplicate.__class__ == RadialGradient:
		self.border = duplicate.border

    def SetDirection(self, dir):
	undo = (self.SetDirection, self.direction)
	self.direction = dir
	return undo

    def Direction(self):
	return self.direction

    def Border(self):
	return self.border

    def SetBorder(self, border):
	undo = (self.SetBorder, self.border)
	self.border = border
	return undo

    def Transform(self, trafo, rects = None):
	dx, dy = self.direction
	dx, dy = trafo.DTransform(dy, -dx)
	dir = Point(dy, -dx).normalized()
	if dir * trafo.DTransform(self.direction) < 0:
	    dir = -dir
	return self.SetDirection(dir)

    def Execute(self, device, rect):
	if device.has_axial_gradient:
	    self.execute_axial_gradient(device, rect)
	    return

	SetFillColor = device.SetFillColor
	FillRectangle = device.FillRectangle
	steps = device.gradient_steps

	colors = self.gradient.Sample(steps)
	SetFillColor(colors[0])
	apply(device.FillRectangle, tuple(rect))

	device.PushTrafo()
	vx, vy = self.direction
	angle = atan2(vy, vx) - pi / 2
	center = rect.center()
	rot = Rotation(angle, center)
	left, bottom, right, top = rot(rect)
	device.Concat(rot)
	device.Translate(center)
	height = top - bottom
	miny = -height / 2
	height = height * (1.0 - self.border)
	width = right - left
	dy = height / steps
	y = height / 2
	x = width / 2
	for i in range(steps):
	    SetFillColor(colors[i])
	    FillRectangle(-x, y, +x, miny)
	    y = y - dy
	device.PopTrafo()

    def execute_axial_gradient(self, device, rect):
	vx, vy = self.direction
	angle = atan2(vy, vx) - pi / 2
	center = rect.center()
	rot = Rotation(angle, center)
	left, bottom, right, top = rot(rect)
	height = (top - bottom) * (1.0 - self.border)
	trafo = rot(Translation(center))
	device.AxialGradient(self.gradient, trafo(0, height / 2),
			     trafo(0, -height / 2))

    def Blend(self, other, frac1, frac2):
	if other.__class__ == self.__class__:
	    gradient = other.gradient
	    dir = other.direction
	    border = other.border
	elif other.__class__ == SolidPattern:
	    gradient = other.Color()
	    dir = self.direction
	    border = self.border
	else:
	    raise MismatchError
	return LinearGradient(Blend(self.gradient, gradient, frac1, frac2),
			      frac1 * self.direction + frac2 * dir,
			      frac1 * self.border + frac2 * border)

    def SaveToFile(self, file):
	file.LinearGradientPattern(self.gradient, self.direction, self.border)


class RadialGradient(GradientPattern):

    is_RadialGradient = 1

    def __init__(self, gradient = None, center = Point(0.5, 0.5),
		 border = 0, duplicate = None):
	GradientPattern.__init__(self, gradient,
				 duplicate = duplicate)
	self.center = center
	self.border = border
	if duplicate is not None:
	    if duplicate.__class__ == self.__class__:
		self.center = duplicate.center
		self.border = duplicate.border
	    elif duplicate.__class__ == ConicalGradient:
		self.center = duplicate.center
	    elif duplicate.__class__ == LinearGradient:
		self.border = duplicate.border

    def SetCenter(self, center):
	undo = (self.SetCenter, self.center)
	self.center = center
	return undo

    def Center(self):
	return self.center

    def Border(self):
	return self.border

    def SetBorder(self, border):
	undo = (self.SetBorder, self.border)
	self.border = border
	return undo

    def Transform(self, trafo, rects = None):
	if rects:
	    r1, r2 = rects
	    left, bottom, right, top = r1
	    cx, cy = self.center
	    cx = cx * right + (1 - cx) * left
	    cy = cy * top   + (1 - cy) * bottom
	    cx, cy = trafo(cx, cy)
	    left, bottom, right, top = r2
	    len = right - left
	    if len:
		cx = (cx - left) / len
	    else:
		cx = 0
	    len = top - bottom
	    if len:
		cy = (cy - bottom) / len
	    else:
		cy = 0
	    center = Point(cx, cy)
	else:
	    center = self.center

	return self.SetCenter(center)

    def Execute(self, device, rect):
	if device.has_radial_gradient:
	    self.execute_radial(device, rect)
	    return
	steps = device.gradient_steps
	cx, cy = self.center
	cx = cx * rect.right + (1 - cx) * rect.left
	cy = cy * rect.top   + (1 - cy) * rect.bottom
	radius = max(hypot(rect.left - cx, rect.top - cy),
		     hypot(rect.right - cx, rect.top - cy),
		     hypot(rect.right - cx, rect.bottom - cy),
		     hypot(rect.left - cx, rect.bottom - cy))
	color = self.gradient.ColorAt
	SetFillColor = device.SetFillColor
	FillCircle = device.FillCircle
	SetFillColor(color(0))
	apply(device.FillRectangle, tuple(rect))
	radius = radius * (1.0 - self.border)
	dr = radius / steps
	device.PushTrafo()
	device.Translate(cx, cy)
	center = NullPoint
	for i in range(steps):
	    SetFillColor(color(float(i) / (steps - 1)))
	    FillCircle(center, radius)
	    radius = radius - dr
	device.PopTrafo()

    def execute_radial(self, device, rect):
	cx, cy = self.center
	cx = cx * rect.right + (1 - cx) * rect.left
	cy = cy * rect.top   + (1 - cy) * rect.bottom
	radius = max(hypot(rect.left - cx, rect.top - cy),
		     hypot(rect.right - cx, rect.top - cy),
		     hypot(rect.right - cx, rect.bottom - cy),
		     hypot(rect.left - cx, rect.bottom - cy))
	radius = radius * (1.0 - self.border)
	device.RadialGradient(self.gradient, (cx, cy), radius, 0)

    def Blend(self, other, frac1, frac2):
	if other.__class__ == self.__class__:
	    gradient = other.gradient
	    center = other.center
	    border = other.border
	elif other.__class__ == SolidPattern:
	    gradient = other.Color()
	    center = self.center
	    border = self.border
	else:
	    raise MismatchError
	return RadialGradient(Blend(self.gradient, gradient, frac1, frac2),
			      frac1 * self.center + frac2 * center,
			      frac1 * self.border + frac2 * border)

    def SaveToFile(self, file):
	file.RadialGradientPattern(self.gradient, self.center, self.border)



class ConicalGradient(GradientPattern):

    is_ConicalGradient = 1

    def __init__(self, gradient = None,
		 center = Point(0.5, 0.5), direction = Point(1, 0),
		 duplicate = None):
	GradientPattern.__init__(self, gradient, duplicate = duplicate)
	self.center = center
	self.direction = direction
	if duplicate is not None:
	    if duplicate.__class__ == self.__class__:
		self.center = duplicate.center
		self.direction = duplicate.direction
	    elif duplicate.__class__ == LinearGradient:
		self.direction = duplicate.direction
	    elif duplicate.__class__ == RadialGradient:
		self.center = duplicate.center

    def __set_center_and_dir(self, center, dir):
	undo = (self.__set_center_and_dir, self.center, self.direction)
	self.center = center
	self.direction = dir
	return undo

    def Transform(self, trafo, rects = None):
	dir = trafo.DTransform(self.direction).normalized()
	if rects:
	    r1, r2 = rects
	    left, bottom, right, top = r1
	    cx, cy = self.center
	    cx = cx * right + (1 - cx) * left
	    cy = cy * top   + (1 - cy) * bottom
	    cx, cy = trafo(cx, cy)
	    left, bottom, right, top = r2
	    len = right - left
	    if len:
		cx = (cx - left) / len
	    else:
		cx = 0
	    len = top - bottom
	    if len:
		cy = (cy - bottom) / len
	    else:
		cy = 0
	    center = Point(cx, cy)
	else:
	    center = self.center

	return self.__set_center_and_dir(center, dir)

    def SetCenter(self, center):
	undo = (self.SetCenter, self.center)
	self.center = center
	return undo

    def Center(self):
	return self.center

    def SetDirection(self, dir):
	undo = (self.SetDirection, self.direction)
	self.direction = dir
	return undo

    def Direction(self):
	return self.direction

    def Execute(self, device, rect):
	if device.has_conical_gradient:
	    self.execute_conical(device, rect)
	    return
	steps = device.gradient_steps
	cx, cy = self.center
	left, bottom, right, top = rect
	cx = cx * right + (1 - cx) * left
	cy = cy * top	+ (1 - cy) * bottom
	vx, vy = self.direction
	angle = atan2(vy, vx)
	rot = Rotation(angle, cx, cy)
	radius = max(hypot(left - cx, top - cy),
		     hypot(right - cx, top - cy),
		     hypot(right - cx, bottom - cy),
		     hypot(left-cx,bottom-cy)) + 10
	device.PushTrafo()
	device.Concat(rot)
	device.Translate(cx, cy)
	device.Scale(radius)
	colors = self.gradient.Sample(steps)
	SetFillColor = device.SetFillColor
	FillPolygon = device.FillPolygon
	da = pi / steps
        points = [(1, 0)]
        for i in range(steps):
            a = da * (i + 1)
            x = cos(a);	y = sin(a)
            points.insert(0, (x, y))
            points.append((x, -y))
        colors.reverse()
        SetFillColor(colors[0])
        FillPolygon(points)
        points.insert(0, (0, 0))
        for i in range(steps):
            SetFillColor(colors[i])
            del points[1]
            del points[-1]
            FillPolygon(points)
        device.PopTrafo()

    def execute_conical(self, device, rect):
	cx, cy = self.center
	left, bottom, right, top = rect
	cx = cx * right + (1 - cx) * left
	cy = cy * top	+ (1 - cy) * bottom
	angle = self.direction.polar()[1]
	device.ConicalGradient(self.gradient, (cx, cy), angle)

    def Blend(self, other, frac1, frac2):
	if other.__class__ == self.__class__:
	    gradient = other.gradient
	    dir = other.direction
	    center = other.center
	elif other.__class__ == SolidPattern:
	    gradient = other.Color()
	    dir = self.direction
	    center = self.center
	else:
	    raise MismatchError
	return ConicalGradient(Blend(self.gradient, gradient, frac1, frac2),
			       frac1 * self.center +  frac2 * center,
			       frac1 * self.direction + frac2 * dir)

    def SaveToFile(self, file):
	file.ConicalGradientPattern(self.gradient, self.center, self.direction)

        
class HatchingPattern(Pattern):

    is_Hatching = 1

    def __init__(self, foreground = None, background = None,
		 direction = Point(1, 0),
		 spacing = 5.0, width = 0.5, duplicate = None):
	if duplicate is not None:
	    self.foreground = duplicate.foreground
	    self.background = duplicate.background
	    self.spacing = duplicate.spacing
	    self.width = duplicate.width
	    self.direction = duplicate.direction
	elif foreground:
	    self.foreground = foreground
	    if not background:
		background = color.StandardColors.white
	    self.background = background
	    self.spacing = spacing
	    self.width = width
	    self.direction = direction
	else:
	    raise ValueError,\
		  'HatchingPattern must be created with color argument'

    def SetDirection(self, dir):
	undo = (self.SetDirection, self.direction)
	self.direction = dir
	return undo

    def Direction(self):
	return self.direction

    def SetSpacing(self, spacing):
	undo = (self.SetSpacing, self.spacing)
	self.spacing = spacing
	return undo

    def Spacing(self):
	return self.spacing

    def Width(self):
        return self.width

    def Transform(self, trafo, rects = None):
	# XXX: should spacing be transformed as well? Should the pattern be
	# transformed at all?
	dir = trafo.DTransform(self.direction).normalized()
	return self.SetDirection(dir)

    def SetForeground(self, foreground):
	undo = (self.SetForeground, self.foreground)
	self.foreground = foreground
	return undo

    def Foreground(self):
	return self.foreground

    def SetBackground(self, color):
	undo = (self.SetBackground, self.background)
	self.background = color
	return undo

    def Background(self):
	return self.background

    def Execute(self, device, rect):
	left, bottom, right, top = rect
	dy = self.spacing
	if dy > 0:
	    device.SetFillColor(self.background)
	    device.FillRectangle(left, top, right, bottom)
	    device.PushTrafo()
	    vx, vy = self.direction
	    angle = atan2(vy, vx)
	    center = rect.center()
	    rot = Rotation(angle, center)
	    left, bottom, right, top = rot(rect)
	    device.Concat(rot)
	    device.Translate(center)
	    height = top - bottom
	    width = right - left
	    steps = int(height / dy + 1)
	    y = height / 2
	    x = width / 2
	    device.SetLineColor(self.foreground)
	    device.SetLineAttributes(self.width)
	    drawline = device.DrawLineXY
	    for i in range(steps):
		drawline(-x, y, +x, y)
		y = y - dy
	    device.PopTrafo()
	else:
	    device.SetFillColor(self.foreground)
	    device.FillRectangle(left, bottom, right, top)

    def Blend(self, other, frac1, frac2):
	if other.__class__ == self.__class__:
	    fg = other.foreground
	    bg = other.background
	    dir = other.direction
	    spacing = other.spacing
	    width = other.width
	elif other.__class__ == SolidPattern:
	    fg = bg = other.Color()
	    dir = self.direction
	    spacing = self.spacing
	    width = self.width
	else:
	    raise MismatchError
	return HatchingPattern(Blend(self.foreground, fg, frac1, frac2),
			       Blend(self.background, bg, frac1, frac2),
			       frac1 * self.direction + frac2 * dir,
			       frac1 * self.spacing + frac2 * spacing,
			       frac1 * self.width + frac2 * width)

    def SaveToFile(self, file):
	file.HatchingPattern(self.foreground, self.background,
			     self.direction, self.spacing, self.width)


class ImageTilePattern(Pattern):

    is_Tiled = 1
    is_Image = 1
    data = None

    def __init__(self, data = None, trafo = None, duplicate = None):
	if duplicate is not None:
	    data = duplicate.data
	    self.trafo = duplicate.trafo
	else:
	    if trafo is None:
		#width, height = data.size
		trafo = Trafo(1, 0, 0, -1, 0, 0)
	    self.trafo = trafo
	self.data = data

    def set_transformation(self, trafo):
	undo = (self.set_transformation, self.trafo)
	self.trafo = trafo
	return undo

    def Transform(self, trafo, rects = None):
	if rects:
	    r1, r2 = rects
	    trafo = trafo(Translation(r1.left, r1.top))
	    trafo = Translation(-r2.left, -r2.top)(trafo)
	return self.set_transformation(trafo(self.trafo))

    def Execute(self, device, rect):
	device.TileImage(self.data,
			 Translation(rect.left, rect.top)(self.trafo))

    def Blend(self, other, frac1, frac2):
	if self.__class__ == other.__class__:
	    if self.data is other.data:
		return self.__class__(self.data,
				      BlendTrafo(self.trafo, other.trafo,
						 frac1, frac2))
	raise MismatchError

    def SaveToFile(self, file):
	file.ImageTilePattern(self.data, self.trafo)
