#!/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 re
import mapscript
import urlparse
import stores
import metadata
import webapp

import functools

from webapp import KeyExists

import tools
from extensions import plugins

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))


def get_store_connection_string(info):
    cparam = info["connectionParameters"]
    if cparam.get("dbtype", "") == "postgis":
        # First mandatory
        url = "PG:dbname=%s port=%s host=%s " % (cparam["database"], cparam["port"], cparam["host"])
        # Then optionals:
        url += " ".join("%s=%s" % (p, cparam[p]) for p in ["user", "password"] if p in cparam)
        return url
    elif "url" in cparam:
        url = urlparse.urlparse(cparam["url"])
        if url.scheme != "file" or url.netloc:
            raise ValueError("Only local files are suported.")
        return tools.get_resource_path(url.path)
    else:
        raise ValueError("Unhandled type '%s'" % cparam.get("dbtype", "<unknown>"))


class Class(object):
    """
    """

    def __init__(self, backend):
        self.ms = backend


class Layer(MetadataMixin):
    """
    """

    def __init__(self, backend):
        self.ms = backend

    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_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)

    def update(self, metadata):
        self.update_metadatas(metadata)


class LayerGroup(object):
    """
    """

    # TODO: We need to handle the order of the layers in a group.

    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 LayerModel(MetadataMixin):
    """
    """

    def __init__(self, backend):
        self.ms = backend
        self.name = self.get_mra_metadata("name", None)

    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.ms.getProjection()),
                           mapscript.projectionObj('+init=epsg:4326'))
        return stores.Extent(rect.minx, rect.miny, rect.maxx, rect.maxy)

    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]


class FeatureTypeModel(LayerModel):
    """
    """

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

        ds = ws.get_datastore(ds_name)
        ft = ds[ft_name]
        self.name = ft_name

        # Set basic attributes.
        self.ms.name = "ft:%s:%s:%s" % (ws.name, 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"]:
        else:
            self.ms.connectiontype = mapscript.MS_SHAPEFILE
            url = urlparse.urlparse(cparam["url"])
            self.ms.data = tools.get_resource_path(url.path)

            # TODO: strip extention.
        #else:
        #    raise ValueError("Unhandled type '%s'" % info["dbtype"])

        # 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,
                                   "workspace": ws.name, "is_model": True})

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

        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, c_name, cs_name, metadata):

        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):

        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(object):
    # TODO
    pass

class MapfileWorkspace(Workspace):
    """ A workspace representing a whole mapfile.
    This is currently the only existing type of workspace,
    but there should be others that can handle subsets of
    the mapfile.
    """

    def __init__(self, mapfile):
        # We are obvliously the default workspace.
        self.name = mapfile.get_default_workspace_name()
        self.mapfile = mapfile

    def save(self):
        """Saves the workspace to disk, same as calling save on the
        associated mapfile.
        """
        self.mapfile.save()

    # 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)

    def get_store_info(self, st_type, name):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        info = self.mapfile.get_mra_metadata(st_type, {})[name].copy()
        info["name"] = name
        return info

    def iter_store_names(self, st_type):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        return self.mapfile.get_mra_metadata(st_type, {}).iterkeys()

    def iter_stores(self, st_type):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        return self.mapfile.get_mra_metadata(st_type, {}).iteritems()

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

    def update_store(self, st_type, name, configuration):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        with self.mapfile.mra_metadata(st_type, {}) as stores:
            stores[name].update(configuration)

    def delete_store(self, st_type, name):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        with self.mapfile.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("datastores", name)

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

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

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

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

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

    def delete_datastore(self, name):
        """Delete a datastore."""
        return self.delete_store("datastores", 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("coveragestores", name)

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

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

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

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

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

    def delete_coveragestore(self, name):
        """Delete a coveragestore."""
        return self.delete_store("coveragestores", name)

    # Feature types

    def iter_featuretypemodels(self, ds_name=None, **kwargs):
        kwargs.setdefault("mra", {}).update({"type":"featuretype", "is_model":True})
        if ds_name != None:
            kwargs["mra"].update({"storage":ds_name, "workspace":self.name})
        for ms_layer in self.mapfile.iter_ms_layers(**kwargs):
            yield FeatureTypeModel(ms_layer)

    def get_featuretypemodel(self, ft_name, ds_name):
        # Improvement: Use get by name combined with a coverage-specific naming.
        try:
            return next(self.iter_featuretypemodels(ds_name, mra={"name":ft_name}))
        except StopIteration:
            raise KeyError((ds_name, ft_name))

    def has_featuretypemodel(self, ft_name, ds_name):
        # Improvement: See get_featuretypemodel
        try:
            self.get_featuretypemodel(ft_name, ds_name)
        except KeyError:
            return False
        else:
            return True

    def create_featuretypemodel(self, ft_name, ds_name, metadata={}):
        if self.has_featuretypemodel(ft_name, ds_name):
            raise KeyExists(ft_name)

        ft = FeatureTypeModel(mapscript.layerObj(self.mapfile.ms))
        ft.update(self, ft_name, ds_name, metadata)
        return ft

    def update_featuretypemodel(self, ft_name, ds_name, metadata={}):
        ft = self.get_featuretypemodel(ft_name, ds_name)
        ft.update(self, ft_name, ds_name, metadata)

    def delete_featuretypemodel(self, ft_name, ds_name):
        try:
            next(self.mapfile.iter_layers(mra={"workspace":self.name, "type":"featuretype",
                                               "storage":ds_name, "name":ft_name}))
        except StopIteration:
            pass # No layers use our featuretyp, all OK.
        else:
            raise ValueError("The featuretype '%s' can't be delete because it is used." % ft_name)

        ft = self.get_featuretypemodel(ft_name, ds_name)
        self.mapfile.ms.removeLayer(ft.ms.index)

    # Coverages

    def iter_coveragemodels(self, cs_name=None, **kwargs):
        kwargs.setdefault("mra", {}).update({"type":"coverage", "is_model":True})
        if cs_name != None:
            kwargs["mra"].update({"storage":cs_name, "workspace":self.name})
        for ms_layer in self.mapfile.iter_ms_layers(**kwargs):
            yield CoverageModel(ms_layer)

    def get_coveragemodel(self, c_name, cs_name):
        # Improvement: Use get by name combined with a coverage-specific naming.
        try:
            return next(self.iter_coveragemodels(cs_name, mra={"name":c_name}))
        except StopIteration:
            raise KeyError((cs_name, c_name))

    def has_coveragemodel(self, c_name, cs_name):
        # Improvement: See get_coveragemodel
        try:
            self.get_coveragemodel(c_name, cs_name)
        except KeyError:
            return False
        else:
            return True

    def create_coveragemodel(self, c_name, cs_name, metadata={}):
        if self.has_coveragemodel(c_name, cs_name):
            raise KeyExists(c_name)

        c = CoverageModel(mapscript.layerObj(self.mapfile.ms))
        c.update(self, c_name, cs_name, metadata)
        return c

    def update_coveragemodel(self, c_name, cs_name, metadata={}):
        c = self.get_coveragemodel(c_name, cs_name)
        c.update(self, c_name, cs_name, metadata)

    def delete_coveragemodel(self, c_name, cs_name):
        try:
            next(self.mapfile.iter_layers(mra={"workspace":self.name, "type":"coverage",
                                               "storage":cs_name, "name":c_name}))
        except StopIteration:
            pass # No layers use our featuretyp, all OK.
        else:
            raise ValueError("The coverage '%s' can't be delete because it is used." % c_name)

        c = self.get_coveragemodel(c_name, cs_name)
        self.mapfile.ms.removeLayer(c.ms.index)

    # All the above :)

    def get_model(self, m_name, s_type, s_name):

        if s_type == "coverage":
            return self.get_coveragemodel(m_name, s_name)
        elif s_type == "featuretype":
            return self.get_featuretypemodel(m_name, s_name)
        else:
            raise ValueError("Bad storage type '%s'." % s_type)


def create_mapfile(path, map_name, data):
    if os.path.exists("%s.map" % path):
        raise KeyExists(map_name)

    mf = mapscript.mapObj()
    mf.name = map_name

    # The following could be defined in <mapfile.py>:

    mf.web.metadata.set("ows_name", map_name)
    mf.web.metadata.set("ows_title", data.get("title", map_name))
    mf.web.metadata.set("ows_abstract", data.get("abstract", ""))

    # Set default values:
    # It should be configurable to the future.

    # mf.web.metadata.set("ows_keywordlist", "")
    # mf.web.metadata.set("ows_keywordlist_vocabulary", "")
    # + ows_keywordlist_[vocabulary’s name]_items
    # mf.web.metadata.set("wms_onlineresource", "")
    # mf.web.metadata.set("wfs_onlineresource", "")
    # mf.web.metadata.set("wms_service_onlineresource", "")
    # mf.web.metadata.set("wfs_service_onlineresource", "")
    mf.web.metadata.set("wms_srs", "EPSG:4326")
    mf.web.metadata.set("wfs_srs", "EPSG:4326")
    mf.web.metadata.set("wms_bbox_extended", "true")
    # mf.web.metadata.set("wms_resx", "")
    # mf.web.metadata.set("wms_resy", "")

    mf.web.metadata.set("ows_schemas_location",
                        "http://schemas.opengeospatial.net")
    mf.web.metadata.set("ows_updatesequence", "foo")
    mf.web.metadata.set("ows_addresstype", "foo")
    mf.web.metadata.set("ows_address", "foo")
    mf.web.metadata.set("ows_city", "foo")
    mf.web.metadata.set("ows_stateorprovince", "foo")
    mf.web.metadata.set("ows_postcode", "foo")
    mf.web.metadata.set("ows_contactperson", "foo")
    mf.web.metadata.set("ows_contactposition", "foo")
    mf.web.metadata.set("ows_contactorganization", "foo")
    mf.web.metadata.set("ows_contactelectronicmailaddress", "foo")
    mf.web.metadata.set("ows_contactfacsimiletelephone", "foo")
    mf.web.metadata.set("ows_contactvoicetelephone", "foo")
    mf.web.metadata.set("wms_fees", "none")
    mf.web.metadata.set("wfs_fees", "none")
    mf.web.metadata.set("wms_accessconstraints", "none")
    mf.web.metadata.set("wfs_accessconstraints", "none")
    # mf.web.metadata.set("ows_attribution_logourl_format", "")
    # mf.web.metadata.set("ows_attribution_logourl_height", "")
    # mf.web.metadata.set("ows_attribution_logourl_href", "")
    # mf.web.metadata.set("ows_attribution_logourl_width", "")
    # mf.web.metadata.set("ows_attribution_onlineresource", "")
    # mf.web.metadata.set("ows_attribution_title", "")

    mf.web.metadata.set("wms_enable_request",
                        "GetCapabilities GetMap GetFeatureInfo GetLegendGraphic")
    mf.web.metadata.set("wfs_enable_request",
                        "GetCapabilities DescribeFeatureType GetFeature")
    mf.web.metadata.set("ows_sld_enabled", "true")
    mf.web.metadata.set("wms_getcapabilities_version", "1.3.0")
    mf.web.metadata.set("wfs_getcapabilities_version", "1.0.0")
    # mf.web.metadata.set("wms_getmap_formatlist", "")
    # mf.web.metadata.set("wms_getlegendgraphic_formatlist", "")
    mf.web.metadata.set("wms_feature_info_mime_type",
                        "application/vnd.ogc.gml,text/plain")
                        # TODO: text/html
    mf.web.metadata.set("wms_encoding", "UTF-8")
    mf.web.metadata.set("wfs_encoding", "UTF-8")

    # mf.web.metadata.set("wms_timeformat", "")
    # mf.web.metadata.set("wms_languages", "")
    # mf.web.metadata.set("wms_layerlimit", "")
    # mf.web.metadata.set("wms_rootlayer_abstract", "")
    # mf.web.metadata.set("wms_rootlayer_keywordlist", "")
    # mf.web.metadata.set("wms_rootlayer_title", "")
    # mf.web.metadata.set("wfs_maxfeatures", "")
    # mf.web.metadata.set("wfs_feature_collection", "")
    # mf.web.metadata.set("wfs_namespace_uri", "")
    # mf.web.metadata.set("wfs_namespace_prefix", "")

    mf.status = mapscript.MS_ON
    mf.setSize(256,256)
    mf.maxsize = 4096
    mf.resolution = 96
    mf.imagetype = 'png'
    mf.imagecolor.setRGB(255,255,255)
    mf.setProjection("init=epsg:4326")
    mf.setExtent(-180,-90,180,90)
    mf.units = mapscript.MS_DD

    mf.save("%s.map" % path)


class Mapfile(MetadataMixin):
    """
    """

    def __init__(self, path, root=None):

        if root != None:
            full_path = os.path.realpath(os.path.join(root, "%s.map" % path))
            if not full_path.startswith(root):
                raise IOError("mapfile '%s' outside root directory." % (path))
            path = full_path
        if isinstance(path, mapscript.mapObj):
            self.path = None
            self.ms = path
        else:
            self.path = path
            self.ms = mapscript.mapObj(self.path)

        self.filename = os.path.basename(self.path)

        # We have one workspace that represents the file.
        self.__default_workspace = MapfileWorkspace(self)

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

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

    def update(self, configuration):
        raise NotImplemented()

    # 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):
        kwargs.setdefault("mra", {}).update({"is_model": lambda x: x != True})
        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 move_layer_down(self, l_name):
        layer = self.get_layer(l_name)
        self.ms.moveLayerDown(layer.ms.index)

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

    def get_layer_wsm(self, l_name):
        layer = self.get_layer(l_name)
        ws = self.get_workspace(layer.get_mra_metadata("workspace"))
        model = ws.get_model(m_name=layer.get_mra_metadata("name"),
                             s_type=layer.get_mra_metadata("type"),
                             s_name=layer.get_mra_metadata("storage"))
        return layer, ws, model

    def create_layer(self, ws, model, l_name, l_enabled, l_metadata={}):
        # First create the layer, then configure it.

        if self.has_layer(l_name):
            raise KeyExists(l_name)

        # We still clone, because we have to start from something,
        # but everything should be configured() anyway.
        layer = Layer(model.ms.clone())
        self.ms.insertLayer(layer.ms)

        layer.ms.name = l_name
        layer.enable(l_enabled)

        # is queryable (by default):
        layer.ms.template = "foo.html" # TODO: Support html format response

        l_metadata["wms_name"] = l_name

        l_metadata.setdefault("wms_title", l_name)
        l_metadata.setdefault("wms_abstract", l_name)
        l_metadata.setdefault("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)

        # TODO: pass correct data (title, abstract, ...) to layer.update()
        layer.update(l_metadata)

        model.configure_layer(ws, layer, l_enabled)

        layer.set_default_style(self)

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

        self.ms.removeLayer(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.
        for layer in self.iter_layers(attr={"group": layer_group.name}):
            layer_group.remove(layer)
        # Remove the group from mra metadata.
        with self.mra_metadata("layergroups", {}) as layergroups:
            del layergroups[lg_name]

    # Styles:

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

    def get_styles(self):
        # The group name is the style's name.

        styles = set()
        for layer in self.iter_layers():
            for clazz in layer.iter_classes():
                styles.add(clazz.ms.group)

        return styles

    def get_style_sld(self, s_name):
        # Because styles do not really exist here, we first need to find
        # a layer that has the style we want.

        for layer in self.iter_layers():
            if s_name in layer.get_styles():
                break
        else:
            raise KeyError(s_name)

        # This is a ugly hack. We clone the layer and remove all the other styles.
        # Then we ask mapscript to generate the sld, but aprently for that the
        # cloned style needs to be in the mapfile... so be it.
        clone = layer.ms.clone()
        for c_index in reversed(xrange(clone.numclasses)):
            c = clone.getClass(c_index)
            if c.group != s_name:
                clone.removeClass(c_index)

        self.ms.insertLayer(clone)
        sld = Layer(clone).get_SLD()
        self.ms.removeLayer(clone.index)

        return sld

    # Workspaces:

    def get_default_workspace_name(self):
        return self.get_mra_metadata("default_workspace", "default")

    def set_default_workspace_name(self, name):
        self.set_mra_metadata("default_workspace", name)

    def iter_workspaces(self):
        """iter_ates over the workspaces managed by a mapfile.
        For current version, only "default" workspace is available which
        correspond to the current mapfile. The relationship between default
        workspace and mapfile is one to one.
        However there is currently work underway to change this...
        """
        yield self.get_default_workspace()

    def get_workspaces(self):
        """Gets workspaces from mapfile that match all the specified conditions.
        For the moment this is equivalent to iter_workspaces, because workpspaces
        are still a "virtual" notion and do not really exist.
        """
        return list(self.iter_workspaces())

    def get_workspace(self, name):
        if name != self.get_default_workspace_name():
            raise KeyError(name)
        return self.get_default_workspace()

    def get_default_workspace(self):
        return self.__default_workspace

    # Let"s delegate other stuff to the default workspace.
    # def __getattr__(self, name):
    #     if hasattr(self, "__default_workspace"):
    #         return getattr(self.__default_workspace, name)