#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                       #
#   MapServer REST API is a python wrapper around MapServer which       #
#   allows to manipulate a mapfile in a RESTFul way. It has been        #
#   developped to match as close as possible the way the GeoServer      #
#   REST API acts.                                                      #
#                                                                       #
#   Copyright (C) 2011-2013 Neogeo Technologies.                        #
#                                                                       #
#   This file is part of MapServer Rest API.                            #
#                                                                       #
#   MapServer Rest API is free software: you can redistribute it        #
#   and/or modify it under the terms of the GNU General Public License  #
#   as published by the Free Software Foundation, either version 3 of   #
#   the License, or (at your option) any later version.                 #
#                                                                       #
#   MapServer Rest API 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 General Public License for more details.                        #
#                                                                       #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

import os
import os.path

import functools
import yaml

import mapscript

from webapp import KeyExists

import metadata

class MetadataMixin(object):

    def __getattr__(self, attr):
        if hasattr(self, "ms") and hasattr(metadata, attr):
            return functools.partial(getattr(metadata, attr), self.ms)
        raise AttributeError("'%s' object has no attribute '%s'" %
                             (type(self).__name__, attr))

class Layer(MetadataMixin):
    def __init__(self, backend):
        self.ms = backend

    def update(self, name, enabled, metadata):
        self.ms.name = name
        self.ms.template = "foo.html" # TODO: Support html format response

        self.enable(enabled)
        self.update_metadatas(metadata)

    def enable(self, enabled=True):
        requests = ["GetCapabilities", "GetMap", "GetFeatureInfo", "GetLegendGraphic"]
        self.ms.status = mapscript.MS_ON if enabled else mapscript.MS_OFF
        self.set_metadata("wms_enable_request", " ".join(('%s' if enabled else "!%s") % c for c in requests))

    def get_type_name(self):
        return {
            0: "POINT",
            1: "LINESTRING",
            2: "POLYGON",
            3: "RASTER",
            4: "ANNOTATION",
            }[self.ms.type]

    def get_proj4(self):
        return self.ms.getProjection()

    def get_wkt(self):
        return tools.proj4_to_wkt(self.ms.getProjection())

    def get_authority(self):
        return tools.wkt_to_authority(self.get_wkt())

    def get_authority_name(self):
        return self.get_authority()[0]

    def get_authority_code(self):
        return self.get_authority()[1]

    def get_extent(self):
        extent = self.ms.getExtent()
        return stores.Extent(extent.minx, extent.miny, extent.maxx, extent.maxy)

    def get_latlon_extent(self):
        rect = mapscript.rectObj(*self.get_extent())
        res = rect.project(mapscript.projectionObj(self.get_proj4()),
                           mapscript.projectionObj('+init=epsg:4326'))
        return stores.Extent(rect.minx, rect.miny, rect.maxx, rect.maxy)

    def get_fields(self):
        fields = self.get_metadata("gml_include_items", "")

        if fields == "all":
            # TODO: Get fields from feature type
            raise NotImplemented()
        elif not fields:
            return []
        else:
            fields = fields.split(",")
        return fields

    def iter_fields(self):
        return iter(self.get_fields())

    def iter_classes(self):
        for i in xrange(self.ms.numclasses):
            yield Class(self.ms.getClass(i))

    def get_styles(self):
        return set(clazz.ms.group for clazz in self.iter_classes())

    def iter_styles(self):
        return iter(self.get_styles())

    def get_SLD(self):
        return self.ms.generateSLD().decode("LATIN1").encode("UTF8")


    def add_style_sld(self, mf, s_name, new_sld):

        # Because we do not want to apply the sld to real layers by mistake
        # we need to rename it to something we are sure is not used.
        sld_layer_name = "__mra_tmp_template"

        # Most xml parsers will have trouble with the kind of mess we get as sld.
        # Mostly because we haven't got the proper declarations, we fallback to
        # an html parser, which luckily is much more forgiving.
        from xml.dom.minidom import parseString
        xmlsld = parseString(new_sld)

        try:
            xmlsld.firstChild.getElementsByTagName("NamedLayer")[0]\
                .getElementsByTagName("Name")[0].firstChild.data = sld_layer_name
        except:
            raise ValueError("Bad sld (No NamedLayer/Name)")

        # Remove encoding ?
        # @wapiflapi Mapscript ne gère pas les espaces...
        new_sld = xmlsld.toxml()

        ms_template_layer = self.ms.clone()
        ms_template_layer.name = sld_layer_name
        mf.ms.insertLayer(ms_template_layer)

        try:
            ms_template_layer.applySLD(new_sld, sld_layer_name)
        except:
            raise ValueError("Unable to access storage.")

        for i in xrange(ms_template_layer.numclasses):
            ms_class = ms_template_layer.getClass(i)
            ms_class.group = s_name
            self.ms.insertClass(ms_class)

        mf.ms.removeLayer(ms_template_layer.index)

    def set_default_style(self, mf):

        if self.ms.type == mapscript.MS_LAYER_POINT:
            self.ms.tolerance = 8
            self.ms.toleranceunits = 6
            s_name = 'default_point'
        elif self.ms.type == mapscript.MS_LAYER_LINE:
            self.ms.tolerance = 8
            self.ms.toleranceunits = 6
            s_name = 'default_line'
        elif self.ms.type == mapscript.MS_LAYER_POLYGON:
            self.ms.tolerance = 0
            self.ms.toleranceunits = 6
            s_name = 'default_polygon'
        else:
            return

        try:
            style = open(os.path.join(os.path.dirname(__file__), "%s.sld" % s_name)).read()
        except IOError, OSError:
            return

        self.add_style_sld(mf, s_name, style)
        self.ms.classgroup = s_name

    def remove_style(self, s_name):
        for c_index in reversed(xrange(self.ms.numclasses)):
            c = self.ms.getClass(c_index)
            if c.group == s_name:
                self.ms.removeClass(c_index)
                break
        else:
            raise KeyError(s_name)


class Layergroup(object):

    def __init__(self, name, mapfile):
        self.name = name
        self.mapfile = mapfile

    def iter_layers(self):
        return self.mapfile.iter_layers(meta={"wms_group_name":self.name})

    def get_layers(self):
        return list(self.iter_layers())

    def add_layer(self, layer):
        layer.ms.group = self.name
        layer.set_metadata("wms_group_name", self.name)
        for k, v in self.mapfile.get_mra_metadata("layergroups")[self.name]:
            layer.set_metadata("wms_group_%s" % k, v)
        self.mapfile.move_layer_down(layer.ms.name)

    def add(self, *args):
        for layer in args:
            if isinstance(layer, basestring):
                layer = self.mapfile.get_layer(layer)
            self.add_layer(layer)

    def remove_layer(self, layer):
        layer.ms.group = None
        for mkey in layer.get_metadata_keys():
            # (We really do not want to use iter_metadata_keys())
            if mkey.startswith("wms_group_"):
                layer.del_metadata(mkey)

    def remove(self, *args):
        for layer in args:
            if isinstance(layer, basestring):
                layer = mapfile.get_layer(layer)
            self.remove_layer(layer)

    def clear(self):
        # Remove all the layers from this group.
        for layer in self.mapfile.iter_layers(attr={"group": self.name}):
            self.remove_layer(layer)

    def get_latlon_extent(self):
        layers = self.get_layers()
        if not layers:
            return stores.Extent(0, 0, 0, 0)

        extent = layers[0].get_latlon_extent()
        for layer in layers[1:]:
            e = layer.get_latlon_extent()
            extent.addX(e.minX(), e.maxX())
            extent.addY(e.minY(), e.maxY())

        return extent


class Mapfile(MetadataMixin):

    def __init__(self, path, create=False):
        self.path = path
        self.filename = os.path.basename(self.path)
        self.name = os.path.splitext(self.filename)[0]

        if create and os.path.exists(self.path):
            raise KeyExists(self.filename)

        if create:
            self.ms = mapscript.mapObj()
        else:
            self.ms = mapscript.mapObj(self.path)

    def save(self, path=None):
        self.ms.save(path or self.path)

    def rawtext(self):
        open(self.path, "r").read()


    # Layers:

    def iter_ms_layers(self, attr={}, meta={}, mra={}):
        def check(f, v):
            return f(v) if callable(f) else f == v

        for l in xrange(self.ms.numlayers):
            ms_layer = self.ms.getLayer(l)
            if not all(check(checker, getattr(ms_layer, k, None)) for k, checker in attr.iteritems()):
                continue
            if not all(check(checker, metadata.get_metadata(ms_layer, k, None)) for k, checker in meta.iteritems()):
                continue
            if not all(check(checker, metadata.get_mra_metadata(ms_layer, k, None)) for k, checker in mra.iteritems()):
                continue
            yield ms_layer

    def iter_layers(self, **kwargs):
        for ms_layer in self.iter_ms_layers(**kwargs):
            yield Layer(ms_layer)

    def get_layer(self, l_name):
        try:
            return next(self.iter_layers(attr={"name": l_name}))
        except StopIteration:
            raise KeyError(l_name)

    def has_layer(self, l_name):
        try:
            self.get_layer(l_name)
        except KeyError:
            return False
        else:
            return True

    def create_layer(self, model, l_name, l_enabled, l_metadata={}):
        if self.has_layer(l_name):
            raise KeyExists(l_name)

        # Create the layer.
        layer = Layer(mapscript.layerObj(self.ms))

        dflt_metadata = {
            "wms_title": l_name,
            "wms_abstract": l_name,
            "wms_bbox_extended": "true",
            # TODO: Add other default values as above:
            # wms_keywordlist, wms_keywordlist_vocabulary, wms_keywordlist_<vocabulary>_items
            # wms_srs, wms_dataurl_(format|href), ows_attribution_(title|onlineresource)
            # ows_attribution_logourl_(href|format|height|width), ows_identifier_(authority|value)
            # ows_authorityurl_(name|href), ows_metadataurl_(type|href|format)
            }
        for k, v in dflt_metadata.iteritems():
            l_metadata.setdefault(k, v)
        l_metadata["wms_name"] = l_name

        # Update layer.
        layer.update(l_name, l_enabled, l_metadata)

        # Configure the layer according to the model.
        model.configure_layer(layer, l_enabled)

        # Set default style.
        layer.set_default_style(self)

    def delete_layer(self, l_name):
        layer = self.get_layer(l_name)
        self.ms.removeLayer(layer.ms.index)

    def move_layer_up(self, l_name):
        layer = self.get_layer(l_name)
        self.ms.moveLayerUp(layer.ms.index)

    def move_layer_down(self, l_name):
        layer = self.get_layer(l_name)
        self.ms.moveLayerDown(layer.ms.index)


    # Layergroups

    def create_layergroup(self, lg_name, mra_metadata={}):
        with self.mra_metadata("layergroups", {}) as layergroups:
            if lg_name in layergroups:
                raise KeyExists(lg_name)
            layergroups[lg_name] = mra_metadata
        return LayerGroup(lg_name, self)

    def iter_layergroups(self):
        return (LayerGroup(name, self) for name in self.get_mra_metadata("layergroups", {}).iterkeys())

    def get_layergroup(self, lg_name):
        if lg_name in self.get_mra_metadata("layergroups", {}):
            return LayerGroup(lg_name, self)
        else:
            raise KeyError(lg_name)

    def add_to_layergroup(self, lg_name, *args):
        lg = self.get_layergroup(lg_name)
        lg.add(*args)

    def remove_from_layergroup(self, lg_name, *args):
        lg = self.get_layergroup(lg_name)
        lg.remove(*args)

    def delete_layergroup(self, lg_name):
        layer_group = self.get_layergroup(lg_name)
        # Remove all the layers from this group.
        layer_group.clear()
        # Remove the group from mra metadata.
        with self.mra_metadata("layergroups", {}) as layergroups:
            del layergroups[lg_name]




# Workspaces are special Mapfiles that are composed of LayerModels
# which are layers that can be used to configure other layers.


class LayerModel(Layer):
    def __init__(self, ws, *args, **kwargs):
        Layer.__init__(self, *args, **kwargs)
        self.ws = ws
        self.name = self.get_mra_metadata("name", None)

class FeatureTypeModel(LayerModel):

    def update(self, ds_name, ft_name, metadata):
        ws = self.ws

        # Make sure the datastore exists.
        ds = ws.get_datastore(ds_name)

        ft = ds[ft_name]
        self.name = ft_name

        # Set basic attributes.
        self.ms.name = "ft:%s:%s" % (ds_name, ft_name)
        self.ms.status = mapscript.MS_OFF
        self.ms.type = ft.get_geomtype_mapscript()
        self.ms.setProjection(ft.get_proj4())
        self.ms.setExtent(*ft.get_extent())

        # Configure the connection to the store.
        # This is a little hacky as we have to translate stuff...
        info = ws.get_datastore_info(ds_name)
        cparam = info["connectionParameters"]
        if cparam.get("dbtype", None) in ["postgis", "postgres", "postgresql"]:
            self.ms.connectiontype = mapscript.MS_POSTGIS
            connection = "dbname=%s port=%s host=%s " % (cparam["database"], cparam["port"], cparam["host"])
            connection += " ".join("%s=%s" % (p, cparam[p]) for p in ["user", "password"] if p in cparam)
            self.ms.connection = connection
            self.ms.data = "%s FROM %s" % (ds[ft_name].get_geometry_column(), ft_name)
            self.set_metadata("ows_extent", "%s %s %s %s" %
                (ft.get_extent().minX(), ft.get_extent().minY(),
                ft.get_extent().maxX(), ft.get_extent().maxY()))
        #elif cpram["dbtype"] in ["shp", "shapefile"]:
        # TODO: clean up this fallback.
        else:
            self.ms.connectiontype = mapscript.MS_SHAPEFILE
            url = urlparse.urlparse(cparam["url"])
            self.ms.data = tools.get_resource_path(url.path)

        # Deactivate wms and wfs requests, because we are a virtual layer.
        self.set_metadatas({
            "wms_enable_request": "!GetCapabilities !GetMap !GetFeatureInfo !GetLegendGraphic",
            "wfs_enable_request": "!GetCapabilities !DescribeFeatureType !GetFeature",
            })

        # Update mra metadatas, and make sure the mandatory ones are left untouched.
        self.update_mra_metadatas(metadata)
        self.update_mra_metadatas({"name": ft_name, "type": "featuretype", "storage": ds_name})


    def configure_layer(self, layer, enabled=True):
        ws = self.ws

        plugins.extend("pre_configure_vector_layer", self, ws, layer)

        # We must also update all our personal attributes (type, ...)
        # because we might not have been cloned.

        layer.ms.type = self.ms.type
        layer.ms.setProjection(self.ms.getProjection())
        layer.ms.setExtent(self.ms.extent.minx, self.ms.extent.miny,
                           self.ms.extent.maxx, self.ms.extent.maxy)
        layer.ms.data = self.ms.data
        layer.ms.connectiontype = self.ms.connectiontype
        layer.ms.connection = self.ms.connection

        layer.update_mra_metadatas({
                "name": self.get_mra_metadata("name"),
                "type": self.get_mra_metadata("type"),
                "storage": self.get_mra_metadata("storage"),
                "workspace": self.get_mra_metadata("workspace"),
                "is_model": False,
                })

        layer.update_metadatas({
                "wfs_name": layer.get_metadata("wms_name"),
                "wfs_title": layer.get_metadata("wms_title"),
                "wfs_abstract": layer.get_metadata("wms_abstract"),
                })

        if enabled:
            layer.set_metadata("wfs_enable_request",
                               "GetCapabilities GetFeature DescribeFeatureType")

        # Configure the layer based on information from the store.
        ds = ws.get_datastore(self.get_mra_metadata("storage"))
        ft = ds[self.get_mra_metadata("name")]

        # Configure the different fields.
        field_names = []
        for field in ft.iterfields():
            layer.set_metadatas({
                "gml_%s_alias" % field.get_name(): field.get_name(),
                "gml_%s_type" % field.get_name(): field.get_type_gml(),
                # TODO: Add gml_<field name>_precision, gml_<field name>_width
                })
            field_names.append(field.get_name())

        geometry_column = ft.get_geometry_column()
        if geometry_column == None:
            geometry_column = "geometry"
        layer.set_metadatas({
            "ows_include_items": ",".join(field_names),
            "gml_include_items": ",".join(field_names),
            "gml_geometries": geometry_column,
            "gml_%s_type" % geometry_column: ft.get_geomtype_gml(),
            # TODO: Add gml_<geometry name>_occurances,
            "wfs_srs": "EPSG:4326",
            "wfs_getfeature_formatlist": "OGRGML,SHAPEZIP",
            })

        if ft.get_fid_column() != None:
            layer.set_metadatas({
                "wfs_featureid": ft.get_fid_column(),
                "gml_featureid": ft.get_fid_column(),
                })

        plugins.extend("post_configure_vector_layer", self, ws, ds, ft, layer)


class CoverageModel(LayerModel):

    def update(self, ws, cs_name, c_name, metadata):
        ws = self.ws

        cs = ws.get_coveragestore(cs_name)
        self.name = c_name

        # Set basic attributes.
        self.ms.name = "c:%s:%s" % (cs_name, c_name)
        self.ms.status = mapscript.MS_OFF
        self.ms.type = mapscript.MS_LAYER_RASTER
        self.ms.setProjection(cs.get_proj4())
        self.ms.setExtent(*cs.get_extent())
        self.ms.setProcessingKey("RESAMPLE","AVERAGE")

        # Configure the connection to the store.
        # This is a little hacky as we have to translate stuff...
        info = ws.get_coveragestore_info(cs_name)
        cparam = info["connectionParameters"]

        #if cparam["dbtype"] in ["tif", "tiff"]:
        self.ms.connectiontype = mapscript.MS_RASTER
        url = urlparse.urlparse(cparam["url"])
        self.ms.data = tools.get_resource_path(url.path)
            # TODO: strip extention.
        #else:
        #    raise ValueError("Unhandled type '%s'" % cparam["dbtype"])

        # Deactivate wms and wcs requests, because we are a virtual layer.
        self.set_metadatas({
            "wms_enable_request": "!GetCapabilities !GetMap !GetFeatureInfo !GetLegendGraphic",
            "wcs_enable_request": "!GetCapabilities !DescribeCoverage !GetCoverage",
            })

        # Update mra metadatas, and make sure the mandatory ones are left untouched.
        self.update_mra_metadatas(metadata)
        self.update_mra_metadatas({"name": c_name, "type": "coverage", "storage": cs_name,
                                   "workspace": ws.name, "is_model": True})

    def configure_layer(self, ws, layer, enabled=True):
        ws = self.ws

        plugins.extend("pre_configure_raster_layer", self, ws, layer)

        # We must also update all our personal attributes (type, ...)
        # because we might not have been cloned.

        layer.ms.type = self.ms.type
        layer.ms.setProjection(self.ms.getProjection())
        layer.ms.setExtent(self.ms.extent.minx, self.ms.extent.miny,
                           self.ms.extent.maxx, self.ms.extent.maxy)
        layer.ms.setProcessingKey("RESAMPLE","AVERAGE")
        layer.ms.data = self.ms.data
        layer.ms.connectiontype = self.ms.connectiontype
        layer.ms.connection = self.ms.connection

        layer.update_mra_metadatas({
                "name": self.get_mra_metadata("name"),
                "type": self.get_mra_metadata("type"),
                "storage": self.get_mra_metadata("storage"),
                "workspace": self.get_mra_metadata("workspace"),
                "is_model": False,
                })

        layer.set_metadatas({
                "wfs_name": layer.get_metadata("wms_name"),
                "wfs_title": layer.get_metadata("wms_title"),
                "wfs_abstract": layer.get_metadata("wms_abstract"),
                # TODO: wfs_keywordlist, wcs_srs, wcs_getfeature_formatlist...
                })

        if enabled:
            layer.set_metadata("wcs_enable_request", "GetCapabilities GetCoverage DescribeCoverage")

        plugins.extend("post_configure_raster_layer", self, ws, layer)



class Workspace(Mapfile):

    def __init__(self, *args, **kwargs):
        Mapfile.__init__(self, *args, **kwargs)
        if self.name.endswith(".ws"):
            self.name = self.name[:-3]

    # Stores:
    def get_store(self, st_type, name):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        cparam = get_store_connection_string(self.get_store_info(st_type, name))
        if st_type == "datastores":
            return stores.Datastore(cparam)
        elif st_type == "coveragestores":
            return stores.Coveragestore(cparam)
        else:
            raise AssertionError("Unknown st_type '%s'." % st_type)

    def get_store_info(self, st_type, name):
        info = self.get_mra_metadata(st_type, {})[name].copy()
        info["name"] = name
        return info

    def iter_store_names(self, st_type):
        return self.get_mra_metadata(st_type, {}).iterkeys()

    def iter_stores(self, st_type):
        return self.get_mra_metadata(st_type, {}).iteritems()

    def create_store(self, st_type, name, configuration):
        with self.mra_metadata(st_type, {}) as stores:
            if name in stores:
                raise KeyExists(name)
            stores[name] = configuration

    def update_store(self, st_type, name, configuration):
        with self.mra_metadata(st_type, {}) as stores:
            stores[name].update(configuration)

    def delete_store(self, st_type, name):
        with self.mra_metadata(st_type, {}) as stores:
            del stores[name]

    # Datastores:

    def get_datastore(self, name):
        """Returns a store.Datastore object from the workspace."""
        return self.get_store("datastore", name)

    def get_datastore_info(self, name):
        """Returns info for a datastore from the workspace."""
        return self.get_store_info("datastore", name)

    def iter_datastore_names(self):
        """Return an iterator over the datastore names."""
        return self.iter_store_names("datastore")

    def iter_datastores(self):
        """Return an iterator over the datastore (names, configuration)."""
        return self.iter_stores("datastore")

    def create_datastore(self, name, configuration):
        """Creates a new datastore."""
        return self.create_store("datastore", name, configuration)

    def update_datastore(self, name, configuration):
        """Update a datastore."""
        return self.update_store("datastore", name, configuration)

    def delete_datastore(self, name):
        """Delete a datastore."""
        try:
            next(self.iter_featuretypemodels(name))
        except StopIteration:
            pass # No layers use our store, all OK.
        else:
            raise ValueError("The datastore '%s' can't be delete because it is used." % name)
        return self.delete_store("datastore", name)

    # Coveragestores (this is c/p from datastores):

    def get_coveragestore(self, name):
        """Returns a store.Coveragestore object from the workspace."""
        return self.get_store("coveragestore", name)

    def get_coveragestore_info(self, name):
        """Returns info for a coveragestore from the workspace."""
        return self.get_store_info("coveragestore", name)

    def iter_coveragestore_names(self):
        """Return an iterator over the coveragestore names."""
        return self.iter_store_names("coveragestore")

    def iter_coveragestores(self):
        """Return an iterator over the coveragestore (names, configuration)."""
        return self.iter_stores("coveragestore")

    def create_coveragestore(self, name, configuration):
        """Creates a new coveragestore."""
        return self.create_store("coveragestore", name, configuration)

    def update_coveragestore(self, name, configuration):
        """Update a coveragestore."""
        return self.update_store("coveragestore", name, configuration)

    def delete_coveragestore(self, name):
        """Delete a coveragestore."""
        try:
            next(self.iter_coveragemodels(name))
        except StopIteration:
            pass # No layers use our store, all OK.
        else:
            raise ValueError("The coveragestore '%s' can't be delete because it is used." % name)
        return self.delete_store("coveragestore", name)

    # LayerModels:

    def __ms2model(self, ms_layer, st_type=None):
        if st_type == "datatore" or ms_layer.name.startswith("ft:"):
            return FeatureTypeModel(self, ms_layer)
        elif st_type == "coveragestore" or ms_layer.name.startswith("c:"):
            return CoverageModel(self, ms_layer)
        else:
            raise ValueError("Badly named Layer Model '%s'." % ms_layer.name)

    def iter_layermodels(self, st_type=None, store=None, **kwargs):
        if st_type:
            kwargs.setdefault("mra", {})["type"] = st_type
        if store != None:
            kwargs.setdefault("mra", {})["storage"] = store
        for ms_layer in self.iter_ms_layers(*kwargs):
            yield self.__ms2model(ms_layer)

    def get_layermodel(self, st_type, store, name):
        try:
            return next(self.iter_layermodels(attr={"name":"%s:%s:%s" % (st_type, store, name)}))
        except StopIteration:
            raise KeyError((st_type, store, name))

    def has_layermodel(self, st_type, name, store):
        try:
            self.get_layermodel(st_type, name, store)
        except KeyError:
            return False
        else:
            return True

    def create_layermodel(self, st_type, store, name, metadata={}):
        if self.has_layermodel(st_type, store, name):
            raise KeyExists((st_type, store, name))
        ft = self.__ms2model(mapscript.layerObj(self.ms), st_type=st_type)
        ft.update(self, st_type, store, name, metadata)
        return ft

    def update_layermodel(self, st_type, store, name, metadata={}):
        ft.update(self, st_type, store, name, metadata)

    def delete_layermodel(self, st_type, ds_name, ft_name):
        model = self.get_layermodel(st_type, ds_name, ft_name)
        if model.get_mra_metadata("layers", []):
            raise ValueError("The %s '%s' can't be delete because it is used." % (st_type, ft_name))
        self.ms.removeLayer(model.ms.index)


    # Featuretypes

    def iter_featuretypemodels(self, ds_name=None, **kwargs):
        return self.iter_layermodels("featuretype", ds_name, **kwargs)

    def get_featuretypemodel(self, ds_name, ft_name):
        return self.get_layermodel("featuretype", ds_name, ft_name)

    def has_featuretypemodel(self, ds_name, ft_name):
        return self.has_layermodel("featuretype", ds_name, ft_name)

    def create_featuretypemodel(self, ds_name, ft_name, metadata={}):
        return self.create_layermodel("featuretype", ds_name, ft_name, metadata)

    def update_featuretypemodel(self, ds_name, ft_name, metadata={}):
        return self.update_layermodel("featuretype", ds_name, ft_name, metadata={})

    def delete_featuretypemodel(self, ds_name, ft_name):
        return self.delete_layermodel("featuretype", ds_name, ft_name)

    # Coverages

    def iter_coveragemodels(self, ds_name=None, **kwargs):
        return self.iter_layermodels("coverage", ds_name, **kwargs)

    def get_coveragemodel(self, ds_name, ft_name):
        return self.get_layermodel("coverage", ds_name, ft_name)

    def has_coveragemodel(self, ds_name, ft_name):
        return self.has_layermodel("coverage", ds_name, ft_name)

    def create_coveragemodel(self, ds_name, ft_name, metadata={}):
        return self.create_layermodel("coverage", ds_name, ft_name, metadata)

    def update_coveragemodel(self, ds_name, ft_name, metadata={}):
        return self.update_layermodel("coverage", ds_name, ft_name, metadata={})

    def delete_coveragemodel(self, ds_name, ft_name):
        return self.delete_layermodel("coverage", ds_name, ft_name)


# Finaly the global context:

class MRA(object):
    def __init__(self, config_path):
        try:
            self.config = yaml.load(open(config_path, "r"))
        except yaml.YAMLError as e:
            exit("Error in configuration file: %s" % e)

    def safe_path_join(self, root, *args):
        full_path = os.path.realpath(os.path.join(root, *args))
        if not full_path.startswith(os.path.realpath(root)):
            raise webapp.Forbidden(message="path '%s' outside root directory." % (args))
        return full_path

    def mk_path(self, path):
        dirs = os.path.dirname(path)
        if not os.path.isdir(dirs):
            os.makedirs(dirs)
        return path

    def get_path(self, *args):
        root = self.config["storage"]["root"]
        return self.safe_path_join(root, *args)

    def get_resouces_path(self, *args):
        root = self.config["storage"].get("resources", "resources")
        return self.get_path(root, *args)

    # Styles:

    def get_style_path(self, *args):
        root = self.config["storage"].get("styles", "styles")
        return self.get_resouces_path(root, *args)

    # Files:

    def get_file_path(self, *args):
        root = self.config["storage"].get("data", "data")
        return self.get_resouces_path(root, *args)

    # Available (get):

    def get_available_path(self, *args):
        root = self.config["storage"].get("available", "available")
        return self.get_path(root, *args)

    def get_available(self):
        path = self.get_available_path("layers.map")
        # TODO: Create the thing if not existing.
        return Mapfile(path)

    # Workspaces:

    def list_workspaces(self):
        for (root, subFolders, files) in os.walk(self.get_available_path()):
            for f in files:
                if f.endswith(".ws.map") and not f.startswith('.'):
                    yield f[:-7]

    def create_workspace(self, name):
        path = self.get_available_path("%s.ws.map" % name)
        return Workspace(self.mk_path(path), create=True)

    def get_workspace(self, name):
        path = self.get_available_path("%s.ws.map" % name)
        return Workspace(path)

    def delete_workspace(self, name):
        path = self.get_available_path("%s.ws.map" % name)


    # Services:

    def get_service_path(self, *args):
        root = self.config["storage"].get("services", "services")
        return self.get_path(root, *args)

    def get_services(self):
        pass

    def get_service(self, name):
        path = self.get_service_path("%s.map" % name)
        return Mapfile(path)