# 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

###Sketch Config
#type = Import
#class_name = 'SKLoader'
#rx_magic = '^##Sketch 1 *(?P<minor>[0-9]+)'
#tk_file_type = ('Skencil/Sketch Document', '.sk')
format_name = 'SK-1'
#standard_messages = 1
###End

(''"Skencil/Sketch Document")

from types import StringType, TupleType
import os, sys
from string import atoi

from Sketch.warn import warn, INTERNAL, pdebug, warn_tb

from Sketch import load, const, plugins, SketchLoadError, SketchError
from Sketch import CreateRGBColor, SolidPattern, HatchingPattern,EmptyPattern,\
     LinearGradient, ConicalGradient, RadialGradient, ImageTilePattern, \
     Style, MultiGradient, Trafo, Translation, Point, \
     GridLayer, GuideLayer, GuideLine, Arrow, CreatePath, StandardColors, \
     GetFont

from Sketch.load import GenericLoader

from Sketch.Graphics import pagelayout, plugobj, blendgroup, text, image, eps,\
     properties

base_style = Style()
base_style.fill_pattern = EmptyPattern
base_style.fill_transform = 1
base_style.line_pattern = SolidPattern(StandardColors.black)
base_style.line_width = 0.0
base_style.line_join = const.JoinMiter
base_style.line_cap = const.CapButt
base_style.line_dashes = ()
base_style.line_arrow1 = None
base_style.line_arrow2 = None
base_style.font = None
base_style.font_size = 12.0

# sanity check: does base_style have all properties?
for key in dir(properties.factory_defaults):
    if not hasattr(base_style, key):
	warn(INTERNAL, 'added default for property %s', key)
	setattr(base_style, key,
                getattr(properties.factory_defaults, key))

papersizes = [#    'A0', 'A1', 'A2',
    'A3', 'A4', 'A5', 'A6', 'A7',
    'letter', 'legal', 'executive',
    ]

        
class SKLoader(GenericLoader):

    format_name = format_name

    base_style = base_style

    functions = ['document', 'layer', ('bezier', 'b'), ('rectangle', 'r'),
		 ('ellipse', 'e'),
		 'group', ('group', 'G'), 'endgroup', ('endgroup', 'G_'),
		 'guess_cont']

    def __init__(self, file, filename, match):
	GenericLoader.__init__(self, file, filename, match)
        if atoi(match.group('minor')) > 2:
            self.add_message(_("The file was created by a newer version of "
                          "Skencil or Sketch, there might be inaccuracies."))
	if self.filename:
	    self.directory = os.path.split(filename)[0]
	else:
	    self.directory = ''
	self.style_dict = {}
	self.id_dict = {}
	self.page_layout = None
	self.pattern = None
	self.gradient = None
	self.arrow1 = None
	self.arrow2 = None
	self.font = None
	self.font_size = 1.0
	self.color_cache = {}

    def __del__(self):
	pass

    def warn(self, level, *args, **kw):
	message = apply(warn, (level,) + args, kw)
	self.add_message(message)

    def get_func_dict(self):
	func_dict = {}
	for name in self.functions:
	    if type(name) == StringType:
		func_dict[name] = getattr(self, name)
	    else:
		func_dict[name[1]] = getattr(self, name[0])
	return func_dict

    functions.append('layout')
    def layout(self, format, orientation):
	if type(format) == StringType:
            if format not in papersizes:
                # The format is given by name but it's not one of the
                # standard papersizes. The file may be corrupted.
                self.add_message(_("Unknown paper format '%s', "
                                   "using A4 instead") % format)
                format = "A4"
            layout = pagelayout.PageLayout(format, orientation=orientation)
	else:
	    w, h = format
	    layout = pagelayout.PageLayout(width = w, height = h,
					   orientation = orientation)
	self.page_layout = layout

    functions.append('grid')
    def grid(self, geometry, visible = 0, color = None, name = None):
        if name is None:
            name = _("Grid")
	self.begin_layer_class(GridLayer,
			       (geometry, visible, self.convert_color(color),
				name))
	self.end_composite()

    def convert_color(self, color_spec):
	try:
	    c = self.color_cache.get(color_spec)
	    if c:
		return c
	    c = apply(CreateRGBColor, color_spec)
	    self.color_cache[color_spec] = c
	except:
	    # This should only happen if the color_spec is invalid
	    type, value = sys.exc_info()[:2]
	    warn(INTERNAL, 'Color allocation failed: %s: %s', type, value)
	    c = StandardColors.black
	return c

    functions.append('gl')
    def gl(self, colors):
	c = []
	for pos, color in colors:
	    c.append((pos, self.convert_color(color)))
	self.gradient = MultiGradient(c)

    functions.append('pe')
    def pe(self):
	self.pattern = EmptyPattern

    functions.append('ps')
    def ps(self, color):
	self.pattern = SolidPattern(self.convert_color(color))

    functions.append('pgl')
    def pgl(self, dx, dy, border = 0):
	if not self.gradient:
	    raise SketchLoadError(_("No gradient for gradient pattern"))
	self.pattern = LinearGradient(self.gradient, Point(dx, dy), border)

    functions.append('pgr')
    def pgr(self, dx, dy, border = 0):
	if not self.gradient:
	    raise SketchLoadError(_("No gradient for gradient pattern"))
	self.pattern = RadialGradient(self.gradient, Point(dx, dy), border)

    functions.append('pgc')
    def pgc(self, cx, cy, dx, dy):
	if not self.gradient:
	    raise SketchLoadError(_("No gradient for gradient pattern"))
	self.pattern = ConicalGradient(self.gradient,
				       Point(cx, cy), Point(dx, dy))

    functions.append('phs')
    def phs(self, color, background, dx, dy, dist, width):
	self.pattern = HatchingPattern(self.convert_color(color),
				       self.convert_color(background),
				       Point(dx, dy), dist, width)

    functions.append('pit')
    def pit(self, id, trafo):
        trafo = apply(Trafo, trafo)
	self.pattern = ImageTilePattern(self.id_dict[id], trafo)

    functions.append('fp')
    def fp(self, color = None):
	if color is None:
	    self.style.fill_pattern =  self.pattern
	else:
	    self.style.fill_pattern = SolidPattern(self.convert_color(color))

    functions.append('fe')
    def fe(self):
	self.style.fill_pattern =  EmptyPattern

    functions.append('ft')
    def ft(self, bool):
	self.style.fill_transform =  bool

    functions.append('lp')
    def lp(self, color = None):
	if color is None:
	    self.style.line_pattern = self.pattern
	else:
	    self.style.line_pattern = SolidPattern(self.convert_color(color))

    functions.append('le')
    def le(self):
	self.style.line_pattern = EmptyPattern

    functions.append('lw')
    def lw(self, width):
	self.style.line_width = width

    functions.append('lj')
    def lj(self, join):
	self.style.line_join = join

    functions.append('lc')
    def lc(self, cap):
        if not 1 <= cap <= 3:
            self.add_message('line cap corrected from %d to 1' % cap)
            cap = 1
        self.style.line_cap = cap

    functions.append('ld')
    def ld(self, dashes):
	self.style.line_dashes = dashes

    functions.append('la1')
    def la1(self, args = None):
	if args is not None:
	    self.style.line_arrow1 = apply(Arrow, args)
	else:
	    self.style.line_arrow1 = None

    functions.append('la2')
    def la2(self, args = None):
	if args is not None:
	    self.style.line_arrow2 = apply(Arrow, args)
	else:
	    self.style.line_arrow2 = None

    functions.append('dstyle')
    def dstyle(self, name = ''):
	if not name:
	    raise SketchLoadError(_("unnamed style"))
	style = self.style.AsDynamicStyle()
	style.SetName(name)
	self.style_dict[name] = style
	self.style = Style()

    functions.append(('use_style', 'style'))
    def use_style(self, name = ''):
	if not name:
	    raise SketchLoadError(_("unnamed style"))
	if not self.style.IsEmpty():
	    self.prop_stack.load_AddStyle(self.style)
	    self.style = Style()
	style = self.style_dict[name]
	self.prop_stack.load_AddStyle(style)

    functions.append('Fn')
    def Fn(self, name):
	self.style.font = GetFont(name)

    functions.append('Fs')
    def Fs(self, size):
	self.style.font_size = size

    functions.append('guide')
    def guide(self, pos, horizontal):
	if horizontal:
	    p = Point(0, pos)
	else:
	    p = Point(pos, 0)
	self.append_object(GuideLine(p, horizontal))

    functions.append('guidelayer')
    def guidelayer(self, *args, **kw):
	self.begin_layer_class(GuideLayer, args, kw)

    def bezier_load(self, line):
	bezier = self.object
	while 1:
	    try:
		bezier.paths[-1].append_from_string(line)
		line = bezier.paths[-1].append_from_file(self.file)
	    except:
		warn(INTERNAL, _("Error reading line %s"), `line`)
		line = self.file.readline()
	    if line[:2] == 'bC':
		bezier.paths[-1].load_close()
		line = self.file.readline()
	    if line[:2] == 'bn':
		bezier.paths = bezier.paths + (CreatePath(),)
		line = self.file.readline()
	    else:
		break
	    if line[:2] not in ('bs', 'bc'):
		break
	return line

    functions.append('txt')
    def txt(self, thetext, trafo, halign = text.ALIGN_LEFT,
	    valign = text.ALIGN_BASE):
	if len(trafo) == 2:
	    trafo = Translation(trafo)
	else:
	    trafo = apply(Trafo, trafo)
	object = text.SimpleText(text = thetext, trafo = trafo,
				 halign = halign, valign = valign,
				 properties = self.get_prop_stack())
	self.append_object(object)

    functions.append('im')
    def im(self, trafo, id):
	if len(trafo) == 2:
	    trafo = Translation(trafo)
	else:
	    trafo = apply(Trafo, trafo)
	self.append_object(image.Image(self.id_dict[id], trafo = trafo))

    functions.append('bm')
    def bm(self, id, filename = None):
	if filename is None:
	    from streamfilter import Base64Decode, SubFileDecode
	    decoder = Base64Decode(SubFileDecode(self.file, '-'))
	    data = image.load_image(decoder)
	else:
            data = image.load_image(os.path.join(self.directory, filename))
	self.id_dict[id] = data

    functions.append('eps')
    def eps(self, trafo, filename):
	if len(trafo) == 2:
	    trafo = Translation(trafo)
	else:
	    trafo = apply(Trafo, trafo)
	if not os.path.isabs(filename):
	    if self.directory:
		filename = os.path.join(self.directory, filename)
	    else:
		filename = os.path.join(os.getcwd(), filename)
	self.append_object(eps.EpsImage(filename = filename, trafo = trafo))

    functions.append('B')
    def B(self, *args, **kw):
	self.begin_composite(blendgroup.BlendGroup, args, kw)

    functions.append('B_')
    def B_(self):
	self.end_composite()

    functions.append('Bi')
    def Bi(self, *args, **kw):
	self.begin_composite(blendgroup.BlendInterpolation, args, kw)
	self.end_composite()

    group = GenericLoader.begin_group
    endgroup = GenericLoader.end_group

    functions.append('M')
    def M(self, *args, **kw):
	from Sketch.Graphics import maskgroup
	self.begin_composite(maskgroup.MaskGroup, args, kw)

    functions.append('M_')
    def M_(self):
	self.end_composite()

    functions.append('PT')
    def PT(self, *args, **kw):
	self.begin_composite(text.PathText, args, kw)

    functions.append('pt')
    def pt(self, thetext, *args):
        matrix = ()
        model = text.PATHTEXT_ROTATE
        start_pos = 0.0
        if args:
            if type(args[0]) == TupleType:
                matrix = args[0]
                args = args[1:]
            if args:
                model = args[0]
                if len(args) > 1:
                    start_pos = args[1]

	if matrix:
	    trafo = apply(Trafo, matrix)
	else:
	    trafo = None

	self.append_object(text.InternalPathText(thetext, trafo = trafo,
						 model = model,
                                                 start_pos = start_pos,
					 properties = self.get_prop_stack()))

    functions.append('PT_')
    def PT_(self):
	self.end_composite()

    functions.append('PC')
    def PC(self, class_name, *args, **kw):
        kw['loading'] = 1
	info = plugins.find_object_plugin(class_name)
	if info is not None:
	    try:
		theclass = info.Constructor()
		self.begin_composite(theclass, args, kw)
		return
	    except SketchError:
		pass
	# constructing the plugin object failed. Use an UnknownPlugin
	# object.
	self.add_message(_("Unknown Plugin: %s") % class_name)
	self.begin_composite(plugobj.UnknownPlugin, (class_name,) + args, kw)

    functions.append('PC_')
    def PC_(self):
	self.end_composite()

    #
    #	The loader driver
    #

    def Load(self):
	file = self.file
	if type(file) == StringType:
	    file = open(file, 'r')
	dict = self.get_func_dict()
	from Sketch import skread
	parse = skread.parse_sk_line2
	readline = file.readline
	bezier_load = self.bezier_load
	num = 1
	line = '#'
	if __debug__:
	    import time
	    start_time = time.clock()
	try:
	    line = readline()
	    while line:
		num = num + 1
		if line[0] == 'b' and line[1] in 'sc':
		    line = bezier_load(line)
		    continue
		#parse(line, dict)
                funcname, args, kwargs = parse(line)
                if funcname is not None:
                    function = dict.get(funcname)
                    if function is not None:
                        try:
                            apply(function, args, kwargs)
                        except TypeError:
                            tb = sys.exc_info()[2]
                            try:
                                if tb.tb_next is None:
                                    # the exception was raised by apply
                                    # and not within the function. Try to
                                    # invoke the function with fewer
                                    # arguments
                                    if call_function(function, args, kwargs):
                                        message = _("Omitted some arguments "
                                                    "for function %s")
                                    else:
                                        message = _("Cannot call function %s")
                                    self.add_message(message
                                                     % function.__name__)

                                else:
                                    raise
                            finally:
                                del tb
                    else:
                        self.add_message(_("Unknown function %s") % funcname)
                    
		line = readline()

	except (SketchLoadError, SyntaxError), value:
	    # a loader specific error occurred
            warn_tb(INTERNAL, 'error in line %d', num)
	    if load._dont_handle_exceptions:
		raise
	    else:
		raise SketchLoadError('%d:%s' % (num, value))
	except:
	    # An exception was not converted to a SketchLoadError.
	    # This should be considered a bug.
            warn_tb(INTERNAL, 'error in line %d:\n%s', num, `line`)
	    if load._dont_handle_exceptions:
		raise
	    else:
		raise SketchLoadError(_("error %s:%s in line %d:\n%s")
				      % (sys.exc_info()[:2] +(num, `line`)))

	self.end_all()
	if self.page_layout:
	    self.object.load_SetLayout(self.page_layout)
	for style in self.style_dict.values():
	    self.object.load_AddStyle(style)
	self.object.load_Completed()

	self.object.meta.native_format = 1

	if __debug__:
	    pdebug('timing', 'time:', time.clock() - start_time)
	return self.object


def call_function(function, args, kwargs):
    if hasattr(function, 'im_func'):
        args = (function.im_self,) + args
        function = function.im_func
    code = function.func_code
    if code.co_flags & 0x000C:
        # uses *args or **kwargs
        return 0
    args = args[:code.co_argcount]
    argnames = code.co_varnames[:code.co_argcount]
    for key in kwargs.keys():
        if key not in argnames:
            del kwargs[key]
    try:
        apply(function, args, kwargs)
    except:
        warn_tb(INTERNAL, 'Trying to call function %s with reduced arglist',
                function.func_name)
        return 0
    return 1
