# Sketch - A Python-based interactive drawing program
# Copyright (C) 1998, 1999, 2000 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 Sketch import const
CHANGED = const.CHANGED
from Sketch.connector import Publisher
from Sketch import CreateListUndo, UndoAfter, NullUndo, SketchInternalError, _
from Sketch.warn import pdebug, INTERNAL

from pattern import SolidPattern, EmptyPattern
from color import StandardColors
from blend import Blend


class Style(Publisher):

    is_dynamic = 0
    name = ''

    def __init__(self, name = '', duplicate = None, **kw):
	if duplicate is not None:
	    self.__dict__ = duplicate.__dict__.copy()
	    if hasattr(self, 'fill_pattern'):
		self.fill_pattern = self.fill_pattern.Copy()
	    if hasattr(self, 'line_pattern'):
		self.line_pattern = self.line_pattern.Copy()
	else:
	    if name:
		self.name = name
	    for key, value in kw.items():
		setattr(self, key, value)

    def SetProperty(self, prop, value):
	dict = self.__dict__
	if dict.has_key(prop):
	    undo = (self.SetProperty, prop, dict[prop])
	else:
	    undo = (self.DelProperty, prop)
        if prop == 'fill_pattern' or prop == 'line_pattern':
            value = value.Copy()
	dict[prop] = value
	self.issue(CHANGED, self)
	return undo

    def DelProperty(self, prop):
	undo = (self.SetProperty, prop, getattr(self, prop))
	delattr(self, prop)
	self.issue(CHANGED, self)
	return undo

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

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

    def Name(self):
	return self.name

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

    def AsDynamicStyle(self):
	result = self.Copy()
	result.is_dynamic = 1
	return result

    def AsUndynamicStyle(self):
	result = self.Copy()
	if self.is_dynamic:
	    del result.is_dynamic
	    del result.name
	return result

    def SaveToFile(self, file):
	if self.is_dynamic:
	    file.DynamicStyle(self)

    def IsEmpty(self):
	return not self.__dict__

def FillStyle(pattern):
    return Style(fill_pattern = pattern)

EmptyFillStyle = Style(fill_pattern = EmptyPattern)

def LineStyle(color = None, width = 0, cap  = const.CapButt,
	      join  = const.JoinMiter, dashes = None,
	      arrow1 = None, arrow2 = None):
    return Style(line_pattern = SolidPattern(color), line_width = width,
		 line_cap = cap, line_join = join, line_dashes = dashes,
		 line_arrow1 = arrow1, line_arrow2 = arrow2)

SolidLine = LineStyle
EmptyLineStyle = Style(line_pattern = EmptyPattern)

class PropertyStack:

    update_cache = 1

    def __init__(self, base = None, duplicate = None):
	if duplicate is not None:
	    self.stack = []
	    for layer in duplicate.stack:
		self.stack.append(layer.Duplicate())
	else:
	    if base is None:
		base = factory_defaults.Duplicate()
	    self.stack = [base]

    def __getattr__(self, attr):
	if self.update_cache:
	    cache = self.__dict__
	    stack = self.stack[:]
	    stack.reverse()
	    for layer in stack:
		cache.update(layer.__dict__)
	    self.update_cache = 0
	try:
	    return self.__dict__[attr]
	except KeyError:
	    raise AttributeError, attr

    def _clear_cache(self):
	self.__dict__ = {'stack' : self.stack}
	return (self._clear_cache,)

    def prop_layer(self, prop):
	# return property layer containing PROP
	for item in self.stack:
	    if hasattr(item, prop):
		return item
	# we should never reach this...
	raise SketchInternalError('unknown graphics property "%s"' % prop)

    def set_property(self, prop, value):
	layer = self.prop_layer(prop)
	if layer.is_dynamic:
	    if self.stack[0].is_dynamic:
		layer = Style()
		setattr(layer, prop, value)
		return self.add_layer(layer)
	    else:
		layer = self.stack[0]
	return layer.SetProperty(prop, value)

    def SetProperty(self, **kw):
	stack = self.stack
	undo = []
	append = undo.append
	if len(stack) == 1 and not stack[0].is_dynamic:
	    set = stack[0].SetProperty
	    for prop, value in kw.items():
		append(set(prop, value))
	else:
	    set = self.set_property
	    for prop, value in kw.items():
		append(set(prop, value))

	if len(self.stack) > 1:
	    undo_stack = (self.set_stack, self.stack[:])
	    if self.delete_shadowed_layers():
		undo.append(undo_stack)
	undo = CreateListUndo(undo)
	undo = (UndoAfter, undo, self._clear_cache())
	return undo

    def set_stack(self, stack):
	undo = (self.set_stack, self.stack)
	self.stack = stack
	self._clear_cache()
	return undo

    def delete_shadowed_layers(self):
	# check if some styles are completely hidden now
	stack = self.stack
	layers = []
	dict = {'name':1, 'is_dynamic':0}
	dict.update(stack[0].__dict__)
	length = len(dict)
	for layer in stack[1:]:
	    dict.update(layer.__dict__)
	    if length != len(dict):
		layers.append(layer)
	    length = len(dict)
	length = len(stack)
	stack[1:] = layers
	return length != len(stack)

    def add_layer(self, layer):
	undo = (self.set_stack, self.stack[:])
	self.stack.insert(0, layer)
	return undo
    load_AddStyle = add_layer

    def AddStyle(self, style):
	if style.is_dynamic:
	    undo = self.add_layer(style)
	    self.delete_shadowed_layers()
	    self._clear_cache()
	    return undo
	else:
	    return apply(self.SetProperty, (), style.__dict__)


    def HasFill(self):
	return self.fill_pattern is not EmptyPattern

    def IsAlgorithmicFill(self):
	return self.fill_pattern.is_procedural

    def ExecuteFill(self, device, rect = None):
	self.fill_pattern.Execute(device, rect)

    def HasLine(self):
	return self.line_pattern is not EmptyPattern

    def IsAlgorithmicLine(self):
	return self.line_pattern.is_procedural

    def ExecuteLine(self, device, rect = None):
	line_pattern = self.line_pattern
	if line_pattern is not EmptyPattern:
	    line_pattern.Execute(device, rect)
	    device.SetLineAttributes(self.line_width, self.line_cap,
				     self.line_join, self.line_dashes)

    def HasFont(self):
	return self.font is not None

    def ObjectChanged(self, object):
	if object in self.stack:
	    self._clear_cache()
	    return 1
	return 0

    def ObjectRemoved(self, object):
	if object in self.stack:
	    idx = self.stack.index(object)
	    undo = (self.set_stack, self.stack[:])
	    self.stack[idx] = self.stack[idx].AsUndynamicStyle()
	    pdebug('properties', 'made style undynamic')
	    return undo
	return NullUndo

    def Untie(self):
	info = []
	for i in range(len(self.stack)):
	    style = self.stack[i]
	    if style.is_dynamic:
		self.stack[i] = style.AsUndynamicStyle()
		info.append((i, style))
	self._clear_cache()
	return info

    def Tie(self, document, info):
	for i, style in info:
	    s = document.GetDynamicStyle(style.Name())
	    if s == style:
		self.stack[i] = s
	self._clear_cache()

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


    grow_join = [5.240843064, 0.5, 0.5]
    grow_cap = [None, 0.5, 0.5, 0.70710678]

    def GrowAmount(self):
	return self.line_width * max(self.grow_cap[self.line_cap],
				     self.grow_join[self.line_join])

    def Blend(self, other, frac1, frac2):
	result = {}
	for prop, func in blend_functions:
	    if func:
		result[prop] = func(getattr(self, prop), getattr(other, prop),
				    frac1, frac2)
	    else:
		result[prop] = getattr(self, prop)
	return PropertyStack(apply(Style, (), result))

    def Transform(self, trafo, rects):
	# XXX hardcoding which properties may need to be transformed is
	# not really a good idea, but it's significantly faster.
	undo = NullUndo
	if len(self.stack) == 1 and not self.stack[0].is_dynamic:
	    if self.fill_transform:
		pattern = self.fill_pattern
		if pattern.is_procedural:
		    undo = pattern.Transform(trafo, rects)
	elif self.fill_transform:
	    pattern = self.fill_pattern
	    if pattern.is_procedural:
		pattern = pattern.Duplicate()
		if pattern.Transform(trafo, rects) is not NullUndo:
		    undo = self.set_property('fill_pattern', pattern)
	if undo is not NullUndo:
	    undo = (UndoAfter, undo, self._clear_cache())
	return undo

    def CreateStyle(self, which_properties):
	properties = {}
	for prop in which_properties:
            if property_types[prop] == FontProperty and not self.HasFont():
                continue
            properties[prop] = getattr(self, prop)
	return apply(Style, (), properties)

    def DynamicStyleNames(self):
	names = []
	for style in self.stack:
	    if style.is_dynamic:
		names.append(style.Name())
	return names

    def condense(self):
	stack = self.stack
	last = stack[0]
	for style in stack[1:]:
	    if not last.is_dynamic and not style.is_dynamic:
		dict = style.__dict__.copy()
		dict.update(last.__dict__)
		last.__dict__ = dict
	    last = style
	length = len(stack)
	self.delete_shadowed_layers()

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


class _EmptyProperties:
    def HasFill(self):
	return 0
    HasLine = HasFill
    HasFont = HasFill

    def DynamicStyleNames(self):
	return []

EmptyProperties = _EmptyProperties()

#
#
#

factory_defaults = Style()
default_graphics_style = None # set below
default_text_style = None # set below
blend_functions = []
property_names = []
property_titles = {}
property_types = {}
transform_properties = []

def blend_number(n1, n2, frac1, frac2):
    return n1 * frac1 + n2 * frac2

LineProperty = 1
FillProperty = 2
FontProperty = 3
OtherProperty = -1

def _set_defaults(prop, title, short_title, type, value,
		  blend = None, transform = 0):
    factory_defaults.SetProperty(prop, value)
    property_names.append(prop)
    property_titles[prop] = (title, short_title)
    property_types[prop] = type
    blend_functions.append((prop, blend))
    if transform:
	transform_properties.append(prop)

black = StandardColors.black

# XXX the default properties should be defined by the user.
_set_defaults('fill_pattern', _("Fill Pattern"), _("Pattern"), FillProperty,
	      EmptyPattern, blend = Blend, transform = 1)
_set_defaults('fill_transform', _("Fill Transform Pattern"),
	      _("Transform pattern"), FillProperty, 1)
_set_defaults('line_pattern', _("Line Pattern"), _("Pattern"), LineProperty,
	      SolidPattern(black), blend = Blend, transform = 1)
_set_defaults('line_width', _("Line Width"), _("Width"), LineProperty, 1 ,
	      blend = blend_number)
_set_defaults('line_cap', _("Line Cap"), _("Cap"), LineProperty,
	      const.CapButt)
_set_defaults('line_join', _("Line Join"), _("Join"), LineProperty,
	      const.JoinMiter)
_set_defaults('line_dashes', _("Line Dashes"), _("Dashes"), LineProperty, ())
_set_defaults('line_arrow1', _("Line Arrow 1"), _("Arrow 1"), LineProperty,
	      None)
_set_defaults('line_arrow2', _("Line Arrow 2"), _("Arrow 2"), LineProperty,
	      None)
_set_defaults('font', _("Font"), _("Font"), FontProperty, None)
_set_defaults('font_size', _("Font Size"), _("Size"), FontProperty, 12,
	      blend = blend_number)

factory_text_style = factory_defaults.Copy()
factory_text_style.fill_pattern = SolidPattern(black)
factory_text_style.line_pattern = EmptyPattern
factory_text_style.font = None
default_graphics_style = factory_defaults.Copy()
default_text_style = factory_defaults.Copy()
default_text_style.fill_pattern = SolidPattern(black)
default_text_style.line_pattern = EmptyPattern
default_text_style.font = None


def FactoryTextStyle():
    import Sketch
    style = factory_text_style.Copy()
    if style.font is None:
        fontname = Sketch.config.preferences.default_font
        style.font = Sketch.GetFont(fontname)
    return style

def DefaultTextProperties():
    import Sketch
    if default_text_style.font is None:
        fontname = Sketch.config.preferences.default_font
        default_text_style.font = Sketch.GetFont(fontname)
    return PropertyStack(base = default_text_style.Copy())

def DefaultGraphicsProperties():
    return PropertyStack(base = default_graphics_style.Copy())

def set_graphics_defaults(kw):
    for key, value in kw.items():
        if not property_types[key] == FontProperty:
            if key in ('fill_pattern', 'line_pattern'):
                value = value.Copy()
            default_graphics_style.SetProperty(key, value)

def set_text_defaults(kw):
    for key, value in kw.items():
        if not property_types[key] == LineProperty:
            if key == 'fill_pattern':
                value = value.Copy()
            default_text_style.SetProperty(key, value)
    
