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

# This file contains various classes for reading a drawing from (ASCII)
# files.
#
# Classes:
#
# LoaderWithComposites
# GenericLoader(LoaderWithComposites)
#
#	Two classes that provide common functions for the format
#	specific classes.
#
# Functions
#
# load_drawing(filename)
#
#	Determines the type of the file (by reading a few lines) and
#	invokes the appropriate import filter. Return the newly created
#	document.
#

from types import StringType, TupleType
import os, string
import time

import config, plugins
from warn import warn, INTERNAL, pdebug
from const import ArcPieSlice

from Sketch.Graphics import document, layer, group, text
from Sketch import _, PolyBezier, Rectangle, Ellipse, Trafo, Translation, \
     Style, PropertyStack, EmptyPattern, Document, SketchLoadError, \
     ImageData, Image


doc_class = Document


class EmptyCompositeError(SketchLoadError):
    pass


# The loaders usually intercept exceptions raised while reading and
# raise a SketchLoadError instead. Setting the following variable to
# true will prohibit this. (useful only for debugging a loader)
#
# XXX this is better done by using warn_tb(INTERNAL,...) before
# reraising the exception as a SketchLoadError.
_dont_handle_exceptions = 0


class LoaderWithComposites:

    guess_continuity = 0

    def __init__(self):
	self.composite_stack = None
	self.composite_items = []
	self.composite_class = None
	self.composite_args = None
	self.object = None

    def __push(self):
	self.composite_stack = (self.composite_class,
				self.composite_args,
				self.composite_items,
				self.composite_stack)

    def __pop(self):
	(self.composite_class, self.composite_args,
	 self.composite_items, self.composite_stack) = self.composite_stack

    def begin_composite(self, composite_class, args = (), kw = None):
	self.__push()
	self.composite_class = composite_class
	self.composite_args = (args, kw)
	self.composite_items = []
	self.object = None

    def end_composite(self):
        if self.composite_class:
            args, kw = self.composite_args
            if not kw:
                kw = {}
            composite = apply(self.composite_class, args, kw)
            # We treat Plugins specially here and in check_object so
            # that we can be a bit more lenient with plugin objects.
            # They should not be empty (after all they'd be invisible
            # then) but they might. If they're empty they're simply
            # ignored
            if self.composite_items or composite.can_be_empty \
                   or composite.is_Plugin:
                append = composite.load_AppendObject
                for item in self.composite_items:
                    if self.check_object(item):
                        append(item)
                composite.load_Done()
                self.__pop()
                self.append_object(composite)
            else:
                self.__pop()
                raise EmptyCompositeError
        else:
            raise SketchLoadError('no composite to end')

    def end_all(self):
	while self.composite_stack:
	    self.end_composite()

    def check_object(self, object):
        # Return true if object is OK. Currently this just checks the
        # following things:
        #
        # - whether a bezier object has at least one path and all paths
        #   have at least length 1.
        # - for bezier objects, guess the continuity if
        #   self.guess_continuity is true.
        # - Whether a plugin object is not empty
        result = 1
        if object.is_Bezier:
            paths = object.Paths()
            if len(paths) >= 1:
                for path in paths:
                    if path.len == 0:
                        result = 0
                        break
            else:
                result = 0
            if result and self.guess_continuity:
                object.guess_continuity()
        elif object.is_Plugin:
            result = len(object.GetObjects())
        return result

    def append_object(self, object):
	self.composite_items.append(object)
	self.object = object

    def pop_last(self):
        # remove the last object in self.composite_items and return it
        object = None
        if self.composite_items:
            object = self.composite_items[-1]
            del self.composite_items[-1]
            if self.composite_items:
                self.object = self.composite_items[-1]
            else:
                self.object = None
        return object


class GenericLoader(LoaderWithComposites):

    format_name = ''

    base_style = None

    def __init__(self, file, filename, match):
	LoaderWithComposites.__init__(self)
	self.file = file
	self.filename = filename
	self.match = match
	self.style = Style()
	if self.base_style is not None:
	    self.prop_stack = PropertyStack(base=self.base_style.Duplicate())
	else:
	    self.prop_stack = PropertyStack()
	self.messages = {}

    def get_prop_stack(self):
	stack = self.prop_stack
	if not self.style.IsEmpty():
	    stack.load_AddStyle(self.style)
	stack.condense()
	if self.base_style is not None:
	    self.prop_stack = PropertyStack(base =self.base_style.Duplicate())
	else:
	    self.prop_stack = PropertyStack()
	self.style = Style()
	return stack

    def set_prop_stack(self, stack):
        self.prop_stack = stack

    def document(self, *args, **kw):
	self.begin_composite(doc_class, args, kw)

    def layer(self, *args, **kw):
	self.begin_layer_class(layer.Layer, args, kw)

    def end_layer(self):
	self.end_composite()

    def begin_layer_class(self, layer_class, args, kw = None):
	if issubclass(self.composite_class, layer.Layer):
	    self.end_composite()
	if issubclass(self.composite_class, doc_class):
	    self.begin_composite(layer_class, args, kw)
	else:
	    raise SketchLoadError('self.composite_class is %s, not a document',
				  self.composite_class)

    def bezier(self, paths = None):
	self.append_object(PolyBezier(paths = paths,
				      properties = self.get_prop_stack()))

    def rectangle(self, m11, m21, m12, m22, v1, v2, radius1 = 0, radius2 = 0):
	trafo = Trafo(m11, m21, m12, m22, v1, v2)
	self.append_object(Rectangle(trafo, radius1 = radius1,
				     radius2 = radius2,
				     properties = self.get_prop_stack()))

    def ellipse(self, m11, m21, m12, m22, v1, v2, start_angle = 0.0,
		end_angle = 0.0, arc_type = ArcPieSlice):
	self.append_object(Ellipse(Trafo(m11, m21, m12, m22, v1, v2),
				   start_angle, end_angle, arc_type,
				   properties = self.get_prop_stack()))
    def simple_text(self, str, trafo = None, valign = text.ALIGN_BASE,
		    halign = text.ALIGN_LEFT):
	if type(trafo) == TupleType:
	    if len(trafo) == 2:
		trafo = apply(Translation, trafo)
	    else:
		raise TypeError, "trafo must be a Trafo-object or a 2-tuple"
	self.append_object(text.SimpleText(text = str, trafo = trafo,
					   valign = valign, halign = halign,
					   properties = self.get_prop_stack()))

    def image(self, image, trafo):
	if type(trafo) == TupleType:
	    if len(trafo) == 2:
		trafo = apply(Translation, trafo)
	    else:
		raise TypeError, "trafo must be a Trafo-object or a 2-tuple"
        image = ImageData(image)
        self.append_object(Image(image, trafo = trafo))

    def begin_group(self, *args, **kw):
	self.begin_composite(group.Group, args, kw)

    def end_group(self):
	self.end_composite()

    def guess_cont(self):
	self.guess_continuity = 1

    def end_composite(self):
	isdoc = self.composite_class is doc_class
	LoaderWithComposites.end_composite(self)
	if isdoc:
	    self.add_meta(self.object)

    def add_meta(self, doc):
	doc.meta.fullpathname = self.filename
	dir, name = os.path.split(self.filename)
	doc.meta.directory = dir
	doc.meta.filename = name
	doc.meta.native_format = 0
	doc.meta.format_name = self.format_name

    def add_message(self, message):
	pdebug(('load', 'echo_messages'), message)
	self.messages[message] = self.messages.get(message, 0) + 1

    def Messages(self):
	messages = self.messages.items()
	list = []
	for message, count in messages:
	    if count > 1:
		list.append(_("%(message)s (%(count)d times)") % locals())
	    else:
		list.append(message)
	list.sort()
	return string.join(list, '\n')


class SimplifiedLoader(GenericLoader):

    def __init__(self, file, filename, match):
	GenericLoader.__init__(self, file, filename, match)
	self.lineno = 1 # first line has been read

    def readline(self):
	line = self.file.readline()
	self.lineno = self.lineno + 1
	return line

    def set_properties(self, **kw):
	style = self.style
	for key, value in kw.items():
	    setattr(style, key, value)

    def empty_line(self):
	self.style.line_pattern = EmptyPattern

    def empty_fill(self):
	self.style.fill_pattern = EmptyPattern



do_profile = 0
def load_drawing_from_file(file, filename = '', doc_class = None):
    # Note: the doc_class argument is only here for plugin interface
    # compatibility with 0.7 (especiall e.g. gziploader)
    line = file.readline()
    # XXX ugly hack for riff-based files, e.g. Corel's CMX. The length
    # might contain newline characters.
    if line[:4] == 'RIFF' and len(line) < 12:
        line = line + file.read(12 - len(line))
    #print line
    for info in plugins.import_plugins:
	match = info.rx_magic.match(line)
	if match:
	    loader = info(file, filename, match)
	    try:
		if do_profile:
		    import profile
		    warn(INTERNAL, 'profiling...')
		    prof = profile.Profile()
		    prof.runctx('loader.Load()', globals(), locals())
		    prof.dump_stats(os.path.join(info.dir,
						 info.module_name + '.prof'))
		    warn(INTERNAL, 'profiling... (done)')
		    doc = loader.object
		else:
                    #t = time.clock()
		    doc = loader.Load()
                    #print 'load in', time.clock() - t, 'sec.'
		messages = loader.Messages()
		if messages:
		    doc.meta.load_messages = messages
		return doc
	    finally:
		info.UnloadPlugin()
    else:
	raise SketchLoadError(_("unrecognised file type"))


def load_drawing(filename):
    if type(filename) == StringType:
	try:
	    file = open(filename, 'r')
	except IOError, value:
	    message = value.strerror
	    raise SketchLoadError(_("Cannot open %(filename)s:\n%(message)s")
				  % locals())
    else:
	# assume a file object. This does not happen at the moment and
	# SKLoder requires the filename for external objects.
	file = filename
	filename = ''
    return load_drawing_from_file(file, filename)
