# Copyright (c) 2001, 2002, 2003 by Intevation GmbH
# Authors:
# Jan-Oliver Wagner <jan@intevation.de>
# Bernhard Herzog <bh@intevation.de>
# Jonathan Coles <jonathan@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

"""
Functions to save a session to a file
"""

__version__ = "$Revision: 1.37 $"

import os

import Thuban.Lib.fileutil

from Thuban.Model.layer import Layer, RasterLayer

from Thuban.Model.classification import \
    ClassGroupDefault, ClassGroupSingleton, ClassGroupRange, ClassGroupMap
from Thuban.Model.transientdb import AutoTransientTable, TransientJoinedTable
from Thuban.Model.table import DBFTable, FIELDTYPE_STRING
from Thuban.Model.data import DerivedShapeStore, ShapefileStore

from Thuban.Model.xmlwriter import XMLWriter
from postgisdb import PostGISConnection, PostGISShapeStore

def relative_filename(dir, filename):
    """Return a filename relative to dir for the absolute file name absname.

    This is almost the same as the function in fileutil, except that dir
    can be an empty string in which case filename will be returned
    unchanged.
    """
    if dir:
        return Thuban.Lib.fileutil.relative_filename(dir, filename)
    else:
        return filename


def unify_filename(filename):
    """Return a 'unified' version of filename

    The .thuban files should be as platform independent as possible.
    Since they must contain filenames the filenames have to unified. We
    unify on unix-like filenames for now, which means we do nothing on a
    posix system and simply replace backslashes with slashes on windows
    """
    if os.name == "posix":
        return filename
    elif os.name == "nt":
        return "/".join(filename.split("\\"))
    else:
        raise RuntimeError("Unsupported platform for unify_filename: %s"
                           % os.name)

def sort_data_stores(stores):
    """Return a topologically sorted version of the sequence of data containers

    The list is sorted so that data containers that depend on other data
    containers have higher indexes than the containers they depend on.
    """
    if not stores:
        return []
    processed = {}
    result = []
    todo = stores[:]
    while todo:
        # It doesn't really matter which if the items of todo is
        # processed next, but if we take the first one, the order is
        # preserved to some degree which makes writing some of the test
        # cases easier.
        container = todo.pop(0)
        if id(container) in processed:
            continue
        deps = [dep for dep in container.Dependencies()
                    if id(dep) not in processed]
        if deps:
            todo.append(container)
            todo.extend(deps)
        else:
            result.append(container)
            processed[id(container)] = 1
    return result


class SessionSaver(XMLWriter):

    """Class to serialize a session into an XML file.

    Applications built on top of Thuban may derive from this class and
    override or extend the methods to save additional information. This
    additional information should take the form of additional attributes
    or elements whose names are prefixed with a namespace. To define a
    namespace derived classes should extend the write_session method to
    pass the namespaces to the default implementation.
    """


    def __init__(self, session):
        XMLWriter.__init__(self)
        self.session = session
        # Map object ids to the ids used in the thuban files
        self.idmap = {}

    def get_id(self, obj):
        """Return the id used in the thuban file for the object obj"""
        return self.idmap.get(id(obj))

    def define_id(self, obj, value = None):
        if value is None:
            value = "D" + str(id(obj))
        self.idmap[id(obj)] = value
        return value

    def has_id(self, obj):
        return self.idmap.has_key(id(obj))

    def prepare_filename(self, filename):
        """Return the string to use when writing filename to the thuban file

        The returned string is a unified version (only slashes as
        directory separators, see unify_filename) of filename expressed
        relative to the directory the .thuban file is written to.
        """
        return unify_filename(relative_filename(self.dir, filename))

    def write(self, file_or_filename):
        XMLWriter.write(self, file_or_filename)

        self.write_header("session", "thuban-1.0.dtd")
        self.write_session(self.session)
        self.close()

    def write_session(self, session, attrs = None, namespaces = ()):
        """Write the session and its contents

        By default, write a session element with the title attribute and
        call write_map for each map contained in the session.

        The optional argument attrs is for additional attributes and, if
        given, should be a mapping from attribute names to attribute
        values. The values should not be XML-escaped yet.

        The optional argument namespaces, if given, should be a sequence
        of (name, URI) pairs. The namespaces are written as namespace
        attributes into the session element. This is mainly useful for
        derived classes that need to store additional information in a
        thuban session file.
        """
        if attrs is None:
            attrs = {}
        attrs["title"] = session.title
        for name, uri in namespaces:
            attrs["xmlns:" + name] = uri
        # default name space
        attrs["xmlns"] = \
               "http://thuban.intevation.org/dtds/thuban-1.0rc1.dtd"
        self.open_element("session", attrs)
        self.write_db_connections(session)
        self.write_data_containers(session)
        for map in session.Maps():
            self.write_map(map)
        self.close_element("session")

    def write_db_connections(self, session):
        for conn in session.DBConnections():
            if isinstance(conn, PostGISConnection):
                self.write_element("dbconnection",
                                   {"id": self.define_id(conn),
                                    "dbtype": "postgis",
                                    "host": conn.host,
                                    "port": conn.port,
                                    "user": conn.user,
                                    "dbname": conn.dbname})
            else:
                raise ValueError("Can't handle db connection %r" % conn)

    def write_data_containers(self, session):
        containers = sort_data_stores(session.DataContainers())
        for container in containers:
            if isinstance(container, AutoTransientTable):
                # AutoTransientTable instances are invisible in the
                # thuban files. They're only used internally. To make
                # sure that containers depending on AutoTransientTable
                # instances refer to the right real containers we give
                # the AutoTransientTable instances the same id as the
                # source they depend on.
                self.define_id(container,
                               self.get_id(container.Dependencies()[0]))
                continue

            idvalue = self.define_id(container)
            if isinstance(container, ShapefileStore):
                self.define_id(container.Table(), idvalue)
                filename = self.prepare_filename(container.FileName())
                self.write_element("fileshapesource",
                                   {"id": idvalue, "filename": filename,
                                    "filetype": "shapefile"})
            elif isinstance(container, DerivedShapeStore):
                shapesource, table = container.Dependencies()
                self.write_element("derivedshapesource",
                                   {"id": idvalue,
                                    "shapesource": self.get_id(shapesource),
                                    "table": self.get_id(table)})
            elif isinstance(container, PostGISShapeStore):
                conn = container.DBConnection()
                self.write_element("dbshapesource",
                                   {"id": idvalue,
                                    "dbconn": self.get_id(conn),
                                    "tablename": container.TableName()})
            elif isinstance(container, DBFTable):
                filename = self.prepare_filename(container.FileName())
                self.write_element("filetable",
                                   {"id": idvalue,
                                    "title": container.Title(),
                                    "filename": filename,
                                    "filetype": "DBF"})
            elif isinstance(container, TransientJoinedTable):
                left, right = container.Dependencies()
                left_field = container.left_field
                right_field = container.right_field
                self.write_element("jointable",
                                   {"id": idvalue,
                                    "title": container.Title(),
                                    "right": self.get_id(right),
                                    "rightcolumn": right_field,
                                    "left": self.get_id(left),
                                    "leftcolumn": left_field,
                                    "jointype": container.JoinType()})
            else:
                raise ValueError("Can't handle container %r" % container)


    def write_map(self, map):
        """Write the map and its contents.

        By default, write a map element element with the title
        attribute, call write_projection to write the projection
        element, call write_layer for each layer contained in the map
        and finally call write_label_layer to write the label layer.
        """
        self.open_element('map title="%s"' % self.encode(map.title))
        self.write_projection(map.projection)
        for layer in map.Layers():
            self.write_layer(layer)
        self.write_label_layer(map.LabelLayer())
        self.close_element('map')

    def write_projection(self, projection):
        """Write the projection.
        """
        if projection and len(projection.params) > 0:
            attrs = {"name": projection.GetName()}
            epsg = projection.EPSGCode()
            if epsg is not None:
                attrs["epsg"] = epsg
            self.open_element("projection", attrs)
            for param in projection.params:
                self.write_element('parameter value="%s"' % 
                                   self.encode(param))
            self.close_element("projection")

    def write_layer(self, layer, attrs = None):
        """Write the layer.

        The optional argument attrs is for additional attributes and, if
        given, should be a mapping from attribute names to attribute
        values. The values should not be XML-escaped yet.
        """

        if attrs is None:
            attrs = {}

        attrs["title"]   = layer.title
        attrs["visible"] = ("false", "true")[int(layer.Visible())]

        if isinstance(layer, Layer):
            attrs["shapestore"]   = self.get_id(layer.ShapeStore())

            lc = layer.GetClassification()
            attrs["stroke"] = lc.GetDefaultLineColor().hex()
            attrs["stroke_width"] = str(lc.GetDefaultLineWidth())
            attrs["fill"] = lc.GetDefaultFill().hex()

            self.open_element("layer", attrs)
            self.write_projection(layer.GetProjection())
            self.write_classification(layer)
            self.close_element("layer")
        elif isinstance(layer, RasterLayer):
            attrs["filename"] = self.prepare_filename(layer.filename)
            self.open_element("rasterlayer", attrs)
            self.write_projection(layer.GetProjection())
            self.close_element("rasterlayer")

    def write_classification(self, layer, attrs = None):
        """Write Classification information."""

        if attrs is None:
            attrs = {}

        lc = layer.GetClassification()

        field = layer.GetClassificationColumn()

        #
        # there isn't a classification of anything so do nothing
        #
        if field is None: return

        attrs["field"] = field
        attrs["field_type"] = str(layer.GetFieldType(field))
        self.open_element("classification", attrs)

        for g in lc:
            if isinstance(g, ClassGroupDefault):
                open_el  = 'clnull label="%s"' % self.encode(g.GetLabel())
                close_el = 'clnull'
            elif isinstance(g, ClassGroupSingleton):
                if layer.GetFieldType(field) == FIELDTYPE_STRING:
                    value = self.encode(g.GetValue())
                else:
                    value = str(g.GetValue())
                open_el  = 'clpoint label="%s" value="%s"' \
                           % (self.encode(g.GetLabel()), value)
                close_el = 'clpoint'
            elif isinstance(g, ClassGroupRange):
                open_el  = 'clrange label="%s" range="%s"' \
                          % (self.encode(g.GetLabel()), str(g.GetRange()))
                close_el = 'clrange'
            else:
                assert False, _("Unsupported group type in classification")
                continue

            data = g.GetProperties()
            dict = {'stroke'      : data.GetLineColor().hex(),
                    'stroke_width': str(data.GetLineWidth()),
                    'fill'        : data.GetFill().hex()}

            self.open_element(open_el)
            self.write_element("cldata", dict)
            self.close_element(close_el)

        self.close_element("classification")

    def write_label_layer(self, layer):
        """Write the label layer.
        """
        labels = layer.Labels()
        if labels:
            self.open_element('labellayer')
            for label in labels:
                self.write_element(('label x="%g" y="%g" text="%s"'
                                    ' halign="%s" valign="%s"')
                                % (label.x, label.y, 
                                   self.encode(label.text), 
                                   label.halign,
                                   label.valign))
            self.close_element('labellayer')



def save_session(session, file, saver_class = None):
    """Save the session session to a file.

    The file argument may either be a filename or an open file object.

    The optional argument saver_class is the class to use to serialize
    the session. By default or if it's None, the saver class will be
    SessionSaver.

    If writing the session is successful call the session's
    UnsetModified method
    """
    if saver_class is None:
        saver_class = SessionSaver
    saver = saver_class(session)
    saver.write(file)

    # after a successful save consider the session unmodified.
    session.UnsetModified()
