/**
 * Copyright (c) 2008-2011 The Open Planning Project
 * 
 * Published under the GPL license.
 * See https://github.com/opengeo/gxp/raw/master/license.txt for the full text
 * of the license.
 */

/**
 * @requires util.js
 * @requires plugins/LayerSource.js
 * @requires OpenLayers/Layer/WMS.js
 * @requires OpenLayers/Format/WMSCapabilities/v1_1_0.js
 * @requires OpenLayers/Format/WMSCapabilities/v1_1_1.js
 * @requires OpenLayers/Format/WMSCapabilities/v1_3_0.js
 * @requires OpenLayers/Protocol/WFS/v1_1_0.js
 * @requires GeoExt/data/WMSCapabilitiesReader.js
 * @requires GeoExt/data/WMSCapabilitiesStore.js
 * @requires GeoExt/data/WMSDescribeLayerStore.js
 * @requires GeoExt/data/AttributeReader.js
 * @requires GeoExt/data/AttributeStore.js
 */

/**
 * The WMSCapabilities and WFSDescribeFeatureType formats parse the document and
 * pass the raw data to the WMSCapabilitiesReader/AttributeReader.  There,
 * records are created from layer data.  The rest of the data is lost.  It
 * makes sense to store this raw data somewhere - either on the OpenLayers
 * format or the GeoExt reader.  Until there is a better solution, we'll
 * override the reader's readRecords method  here so that we can have access to
 * the raw data later.
 * 
 * The purpose of all of this is to get the service title, feature type and
 * namespace later.
 * TODO: push this to OpenLayers or GeoExt
 */
(function() {
    function keepRaw(data) {
        var format = this.meta.format;
        if (typeof data === "string" || data.nodeType) {
            data = format.read(data);
            // cache the data for the single read that readRecord does
            var origRead = format.read;
            format.read = function() {
                format.read = origRead;
                return data;
            };
        }
        // here is the new part
        this.raw = data;
    }
    Ext.intercept(GeoExt.data.WMSCapabilitiesReader.prototype, "readRecords", keepRaw);
    GeoExt.data.AttributeReader &&
        Ext.intercept(GeoExt.data.AttributeReader.prototype, "readRecords", keepRaw);
})();

/** api: (define)
 *  module = gxp.plugins
 *  class = WMSSource
 */

/** api: (extends)
 *  plugins/LayerSource.js
 */
Ext.namespace("gxp.plugins");

/** api: constructor
 *  .. class:: WMSSource(config)
 *
 *    Plugin for using WMS layers with :class:`gxp.Viewer` instances. The
 *    plugin issues a GetCapabilities request to create a store of the WMS's
 *    layers.
 */   
/** api: example
 *  Configuration in the  :class:`gxp.Viewer`:
 *
 *  .. code-block:: javascript
 *
 *    defaultSourceType: "gxp_wmssource",
 *    sources: {
 *        "opengeo": {
 *            url: "http://suite.opengeo.org/geoserver/wms"
 *        }
 *    }
 *
 *  A typical configuration for a layer from this source (in the ``layers``
 *  array of the viewer's ``map`` config option would look like this:
 *
 *  .. code-block:: javascript
 *
 *    {
 *        source: "opengeo",
 *        name: "world",
 *        group: "background"
 *    }
 *
 * An optional 'getFeatureInfo' property can also be passed to
 * customize the sort order, visibility, & labels for layer attributes.
 * A sample 'getFeatureInfo' configuration would look like this:
 *
 *  .. code-block:: javascript
 *
 *    {
 *        fields: ["twn_name","pop1990"]
 *        propertyNames: {"pop1990": "1990 Population",  "twn_name": "Town"}
 *    }
 *
 *  Within the 'getFeatureInfo' configuration, the 'fields' property determines sort
 *  order & visibility (any attributes not included are not displayed) and
 *  'propertyNames'  specifies the labels for the attributes.
 *
 *  For initial programmatic layer configurations, to leverage lazy loading of
 *  the Capabilities document, it is recommended to configure layers with the
 *  fields listed in :obj:`requiredProperties`.
 */
gxp.plugins.WMSSource = Ext.extend(gxp.plugins.LayerSource, {
    
    /** api: ptype = gxp_wmssource */
    ptype: "gxp_wmssource",
    
    /** api: config[url]
     *  ``String`` WMS service URL for this source
     */

    /** private: config[restUrl]
     *  ``String`` Optional URL for rest configuration endpoint.  Note that this
     *  property is being added for a specific GeoNode case and it may be 
     *  removed if an alternate solution is chosen (like a specific 
     *  GeoNodeSource).  This is used where the rest config endpoint cannot
     *  be derived from the source url (e.g. source url "/geoserver" and rest
     *  config url "/other_rest_proxy").
     */

    /** api: config[baseParams]
     *  ``Object`` Base parameters to use on the WMS GetCapabilities
     *  request.
     */
    baseParams: null,

    /** private: property[format]
     *  ``OpenLayers.Format`` Optional custom format to use on the 
     *  WMSCapabilitiesStore store instead of the default.
     */
    format: null,
    
    /** private: property[describeLayerStore]
     *  ``GeoExt.data.WMSDescribeLayerStore`` additional store of layer
     *  descriptions. Will only be available when the source is configured
     *  with ``describeLayers`` set to true.
     */
    describeLayerStore: null,
    
    /** private: property[describedLayers]
     */
    describedLayers: null,
    
    /** private: property[schemaCache]
     */
    schemaCache: null,
    
    /** private: property[ready]
     *  ``Boolean``
     */
    ready: false,
    
    /** api: config[version]
     *  ``String``
     *  If specified, the version string will be included in WMS GetCapabilities
     *  requests.  By default, no version is set.
     */
    
    /** api: config[requiredProperties]
     *  ``Array(String)`` List of config properties that are required for each
     *  layer from this source to allow lazy loading, in addition to ``name``.
     *  Default is ``["title", "bbox"]``. When the source loads layers from a
     *  WMS that does not provide layers in all projections, ``srs`` should be
     *  included in this list. Fallback values are available for ``title`` (the
     *  WMS layer name), ``bbox`` (the map's ``maxExtent`` as array), and
     *  ``srs`` (the map's ``projection``, e.g. "EPSG:4326").
     */
    
    /** api: property[requiredProperties]
     *  ``Array(String)`` List of config properties that are required for a
     *  complete layer configuration, in addition to ``name``.
     */
    requiredProperties: ["title", "bbox"],
    
    /** private: method[constructor]
     */
    constructor: function(config) {
        // deal with deprecated forceLazy config option
        //TODO remove this before we cut a release
        if (config && config.forceLazy === true) {
            config.requiredProperties = [];
            delete config.forceLazy;
            if (window.console) {
                console.warn("Deprecated config option 'forceLazy: true' for layer source '" +
                    config.id + "'. Use 'requiredProperties: []' instead.");
            }
        }
        gxp.plugins.WMSSource.superclass.constructor.apply(this, arguments);
        if (!this.format) {
            this.format = new OpenLayers.Format.WMSCapabilities({keepData: true});
        }
    },

    /** api: method[init]
     *  :arg target: ``Object`` The object initializing this plugin.
     */
    init: function(target) {
        gxp.plugins.WMSSource.superclass.init.apply(this, arguments);
        this.target.on("authorizationchange", this.onAuthorizationChange, this);
    },

    /** private: method[onAuthorizationChange]
     *  Reload the store when the authorization changes.
     */
    onAuthorizationChange: function() {
        if (this.disabled !== true && this.store && this.url.charAt(0) === "/") {
            var lastOptions = this.store.lastOptions || {params: {}};
            Ext.apply(lastOptions.params, {
                '_dc': Math.random()
            });
            this.store.reload(lastOptions);
        }
    },

    /** private: method[destroy]
     */
    destroy: function() {
        this.target.un("authorizationchange", this.onAuthorizationChange, this);
        gxp.plugins.WMSSource.superclass.destroy.apply(this, arguments);
    },

    /** private: method[isLazy]
     *  :returns: ``Boolean``
     *
     *  The store for a lazy source will not be loaded upon creation.  A source
     *  determines whether or not it is lazy given the configured layers for
     *  the target.  If the layer configs have all the information needed to 
     *  construct layer records, the source can be lazy.
     */
    isLazy: function() {
        var lazy = true;
        var mapConfig = this.target.initialConfig.map;
        if (mapConfig && mapConfig.layers) {
            var layerConfig;
            for (var i=0, ii=mapConfig.layers.length; i<ii; ++i) {
                layerConfig = mapConfig.layers[i];
                if (layerConfig.source === this.id) {
                    lazy = this.layerConfigComplete(layerConfig);
                    if (lazy === false) {
                        break;
                    }
                }
            }
        }
        return lazy;
    },
    
    /** private: method[layerConfigComplete]
     *  :returns: ``Boolean``
     *
     *  A layer configuration is considered complete if it has a title and a
     *  bbox.
     */
    layerConfigComplete: function(config) {
        var lazy = true;
        if (!Ext.isObject(config.capability)) {
            var props = this.requiredProperties;
            for (var i=props.length-1; i>=0; --i) {
                lazy = !!config[props[i]];
                if (lazy === false) {
                    break;
                }
            } 
        }
        return lazy;
    },

    /** api: method[createStore]
     *
     *  Creates a store of layer records.  Fires "ready" when store is loaded.
     */
    createStore: function() {
        var baseParams = this.baseParams || {
            SERVICE: "WMS",
            REQUEST: "GetCapabilities"
        };
        if (this.version) {
            baseParams.VERSION = this.version;
        }

        var lazy = this.isLazy();
        
        this.store = new GeoExt.data.WMSCapabilitiesStore({
            // Since we want our parameters (e.g. VERSION) to override any in the 
            // given URL, we need to remove corresponding paramters from the 
            // provided URL.  Simply setting baseParams on the store is also not
            // enough because Ext just tacks these parameters on to the URL - so
            // we get requests like ?Request=GetCapabilities&REQUEST=GetCapabilities
            // (assuming the user provides a URL with a Request parameter in it).
            url: this.trimUrl(this.url, baseParams),
            baseParams: baseParams,
            format: this.format,
            autoLoad: !lazy,
            layerParams: {exceptions: null},
            listeners: {
                load: function() {
                    // The load event is fired even if a bogus capabilities doc 
                    // is read (http://trac.geoext.org/ticket/295).
                    // Until this changes, we duck type a bad capabilities 
                    // object and fire failure if found.
                    if (!this.store.reader.raw || !this.store.reader.raw.service) {
                        this.fireEvent("failure", this, "Invalid capabilities document.");
                    } else {
                        if (!this.title) {
                            this.title = this.store.reader.raw.service.title;                        
                        }
                        if (!this.ready) {
                            this.ready = true;
                            this.fireEvent("ready", this);
                        } else {
                            this.lazy = false;
                            //TODO Here we could update all records from this
                            // source on the map that were added when the
                            // source was lazy.
                        }
                    }
                    // clean up data stored on format after parsing is complete
                    delete this.format.data;
                },
                exception: function(proxy, type, action, options, response, error) {
                    delete this.store;
                    var msg, details = "";
                    if (type === "response") {
                        if (typeof error == "string") {
                            msg = error;
                        } else {
                            msg = "Invalid response from server.";
                            // special error handling in IE
                            var data = this.format && this.format.data;
                            if (data && data.parseError) {
                                msg += "  " + data.parseError.reason + " - line: " + data.parseError.line;
                            }
                            var status = response.status;
                            if (status >= 200 && status < 300) {
                                // TODO: consider pushing this into GeoExt
                                var report = error && error.arg && error.arg.exceptionReport;
                                details = gxp.util.getOGCExceptionText(report);
                            } else {
                                details = "Status: " + status;
                            }
                        }
                    } else {
                        msg = "Trouble creating layer store from response.";
                        details = "Unable to handle response.";
                    }
                    // TODO: decide on signature for failure listeners
                    this.fireEvent("failure", this, msg, details);
                    // clean up data stored on format after parsing is complete
                    delete this.format.data;
                },
                scope: this
            }
        });
        if (lazy) {
            this.lazy = lazy;
            this.ready = true;
            this.fireEvent("ready", this);
        }
    },
    
    /** private: method[trimUrl]
     *  :arg url: ``String``
     *  :arg params: ``Object``
     *
     *  Remove all parameters from the URL's query string that have matching
     *  keys in the provided object.  Keys are compared in a case-insensitive 
     *  way.
     */
    trimUrl: function(url, params, respectCase) {
        var urlParams = OpenLayers.Util.getParameters(url);
        params = OpenLayers.Util.upperCaseObject(params);
        var keys = 0;
        for (var key in urlParams) {
            ++keys;
            if (key.toUpperCase() in params) {
                --keys;
                delete urlParams[key];
            }
        }
        return url.split("?").shift() + (keys ? 
            "?" + OpenLayers.Util.getParameterString(urlParams) :
            ""
        );
    },
    
    /** private: method[createLazyLayerRecord]
     *  :arg config: ``Object`` The application config for this layer.
     *  :returns: ``GeoExt.data.LayerRecord``
     *
     *  Create a minimal layer record
     */
    createLazyLayerRecord: function(config) {
        config = Ext.apply({}, config);
        
        var srs = config.srs || this.target.map.projection;
        config.srs = {};
        config.srs[srs] = true;
        
        var bbox = config.bbox || this.target.map.maxExtent || OpenLayers.Projection.defaults[srs].maxExtent;
        config.bbox = {};
        config.bbox[srs] = {bbox: bbox};
        
        var record;
        if (this.store && this.store instanceof GeoExt.data.WMSCapabilitiesStore) {
            record = new this.store.recordType(config);
        } else {
            record = new GeoExt.data.LayerRecord(config);
        }
        record.setLayer(new OpenLayers.Layer.WMS(
            config.title || config.name,
            config.url || this.url, {
                layers: config.name,
                transparent: "transparent" in config ? config.transparent : true,
                cql_filter: config.cql_filter,
                format: config.format
            }, {
                projection: srs,
                eventListeners: {
                  tileloaded: this.countAlive,
                  tileerror: this.countAlive,
                  scope: this
                }
            }
        ));
        record.json = config;
        return record;
    },

    countAlive: function(evt) {
        if (!('_alive' in evt.object.metadata)) {
            evt.object.metadata._alive = 0;
            evt.object.events.register('loadend', this, this.removeDeadLayer);
        }
        evt.object.metadata._alive += (evt.type == 'tileerror' ? -1 : 1);
    },

    removeDeadLayer: function(evt) {
        evt.object.events.un({
            'tileloaded': this.countAlive,
            'tileerror': this.countAlive,
            'loadend': this.removeDeadLayer,
            scope: this
        });
        if (evt.object.metadata._alive === 0) {
            this.target.mapPanel.map.removeLayer(evt.object);
            if (window.console) {
              console.debug('Unavailable layer ' + evt.object.name + ' removed.');
            }
        }
        delete evt.object.metadata._alive;
    },

    /** api: method[createLayerRecord]
     *  :arg config:  ``Object``  The application config for this layer.
     *  :returns: ``GeoExt.data.LayerRecord`` or null when the source is lazy.
     *
     *  Create a layer record given the config. Applications should check that
     *  the source is not :obj:`lazy`` or that the ``config`` is complete (i.e.
     *  configured with all fields listed in :obj:`requiredProperties` before
     *  using this method. Otherwise, it is recommended to use the asynchronous
     *  :meth:`gxp.Viewer.createLayerRecord` method on the target viewer
     *  instead, which will load the source's store to complete the
     *  configuration if necessary.
     */
    createLayerRecord: function(config) {
        var record, original;
        var index = this.store.findExact("name", config.name);
        if (index > -1) {
            original = this.store.getAt(index);
        } else if (Ext.isObject(config.capability)) {
            original = this.store.reader.readRecords({capability: {
                request: {getmap: {href: this.trimUrl(this.url, this.baseParams)}},
                layers: [config.capability]}
            }).records[0];
        } else if (this.layerConfigComplete(config)) {
            original = this.createLazyLayerRecord(config);
        }
        if (original) {

            var layer = original.getLayer().clone();

            /**
             * TODO: The WMSCapabilitiesReader should allow for creation
             * of layers in different SRS.
             */
            var projection = this.getMapProjection();
            
            // If the layer is not available in the map projection, find a
            // compatible projection that equals the map projection. This helps
            // us in dealing with the different EPSG codes for web mercator.
            var layerProjection = this.getProjection(original);
            if (layerProjection) {
                layer.addOptions({projection: layerProjection});
            }

            var projCode = (layerProjection || projection).getCode(),
                bbox = original.get("bbox"), maxExtent;

            // determine maxExtent in map projection
            if (bbox && bbox[projCode]){
                maxExtent = OpenLayers.Bounds.fromArray(bbox[projCode].bbox, layer.reverseAxisOrder());
            } else {
                var llbbox = original.get("llbbox");
                if (llbbox) {
                    llbbox[0] = Math.max(llbbox[0], -180);
                    llbbox[1] = Math.max(llbbox[1], -90);
                    llbbox[2] = Math.min(llbbox[2], 180);
                    llbbox[3] = Math.min(llbbox[3], 90);
                    maxExtent = OpenLayers.Bounds.fromArray(llbbox).transform("EPSG:4326", projection);
                }
            }
            
            // update params from config
            layer.mergeNewParams({
                STYLES: config.styles,
                FORMAT: config.format,
                TRANSPARENT: config.transparent,
                CQL_FILTER: config.cql_filter
            });
            
            var singleTile = false;
            if ("tiled" in config) {
                singleTile = !config.tiled;
            } else {
                // for now, if layer has a time dimension, use single tile
                if (original.data.dimensions && original.data.dimensions.time) {
                    singleTile = true;
                }
            }

            layer.setName(config.title || layer.name);
            layer.addOptions({
                attribution: layer.attribution || config.attribution,
                maxExtent: maxExtent,
                restrictedExtent: maxExtent,
                singleTile: singleTile,
                ratio: config.ratio || 1,
                visibility: ("visibility" in config) ? config.visibility : true,
                opacity: ("opacity" in config) ? config.opacity : 1,
                buffer: ("buffer" in config) ? config.buffer : 1,
                dimensions: original.data.dimensions,
                transitionEffect: singleTile ? 'resize' : null,
                minScale: config.minscale,
                maxScale: config.maxscale
            });
            
            // data for the new record
            var data = Ext.applyIf({
                title: layer.name,
                group: config.group,
                infoFormat: config.infoFormat,
                getFeatureInfo:  config.getFeatureInfo,
                source: config.source,
                properties: "gxp_wmslayerpanel",
                fixed: config.fixed,
                selected: "selected" in config ? config.selected : false,
                restUrl: this.restUrl,
                layer: layer
            }, original.data);
            
            // add additional fields
            var fields = [
                {name: "source", type: "string"}, 
                {name: "group", type: "string"},
                {name: "properties", type: "string"},
                {name: "fixed", type: "boolean"},
                {name: "selected", type: "boolean"},
                {name: "restUrl", type: "string"},
                {name: "infoFormat", type: "string"},
                {name: "getFeatureInfo"}
            ];
            original.fields.each(function(field) {
                fields.push(field);
            });

            var Record = GeoExt.data.LayerRecord.create(fields);
            record = new Record(data, layer.id);
            record.json = config;

        } else {
            if (window.console && this.store.getCount() > 0 && config.name !== undefined) {
                console.warn("Could not create layer record for layer '" + config.name + "'. Check if the layer is found in the WMS GetCapabilities response.");
            }
        }
        return record;
    },
    
    /** api: method[getProjection]
     *  :arg layerRecord: ``GeoExt.data.LayerRecord`` a record from this
     *      source's store
     *  :returns: ``OpenLayers.Projection`` A suitable projection for the
     *      ``layerRecord``. If the layer is available in the map projection,
     *      the map projection will be returned. Otherwise an equal projection,
     *      or null if none is available.
     *
     *  Get the projection that the source will use for the layer created in
     *  ``createLayerRecord``. If the layer is not available in a projection
     *  that fits the map projection, null will be returned.
     */
    getProjection: function(layerRecord) {
        var projection = this.getMapProjection();
        var compatibleProjection = projection;
        var availableSRS = layerRecord.get("srs");
        if (!availableSRS[projection.getCode()]) {
            compatibleProjection = null;
            var p, srs;
            for (srs in availableSRS) {
                if ((p=new OpenLayers.Projection(srs)).equals(projection)) {
                    compatibleProjection = p;
                    break;
                }
            }
        }
        return compatibleProjection;
    },
    
    /** private: method[initDescribeLayerStore]
     *  creates a WMSDescribeLayer store for layer descriptions of all layers
     *  created from this source.
     */
    initDescribeLayerStore: function() {
        var raw = this.store.reader.raw;
        if (this.lazy) {
            // When lazy, we assume that the server supports a DescribeLayer
            // request at the layer's url.
            raw = {
                capability: {
                    request: {
                        describelayer: {href: this.url}
                    }
                },
                version: this.version || "1.1.1"
            };
        }
        var req = raw.capability.request.describelayer;
        if (req) {
            var version = raw.version;
            if (parseFloat(version) > 1.1) {
                //TODO don't force 1.1.1, fall back instead
                version = "1.1.1";
            }
            var params = {
                SERVICE: "WMS",
                VERSION: version,
                REQUEST: "DescribeLayer"
            };
            this.describeLayerStore = new GeoExt.data.WMSDescribeLayerStore({
                url: this.trimUrl(req.href, params),
                baseParams: params
            });
        }
    },
    
    /** api: method[describeLayer]
     *  :arg rec: ``GeoExt.data.LayerRecord`` the layer to issue a WMS
     *      DescribeLayer request for
     *  :arg callback: ``Function`` Callback function. Will be called with
     *      an ``Ext.data.Record`` from a ``GeoExt.data.DescribeLayerStore``
     *      as first argument, or false if the WMS does not support
     *      DescribeLayer.
     *  :arg scope: ``Object`` Optional scope for the callback.
     *
     *  Get a DescribeLayer response from this source's WMS.
     */
    describeLayer: function(rec, callback, scope) {
        if (!this.describeLayerStore) {
            this.initDescribeLayerStore();
        }
        function delayedCallback(arg) {
            window.setTimeout(function() {
                callback.call(scope, arg);
            }, 0);
        }
        if (!this.describeLayerStore) {
            delayedCallback(false);
            return;
        }
        if (!this.describedLayers) {
            this.describedLayers = {};
        }
        var layerName = rec.getLayer().params.LAYERS;
        var cb = function() {
            var recs = Ext.isArray(arguments[1]) ? arguments[1] : arguments[0];
            var rec, name;
            for (var i=recs.length-1; i>=0; i--) {
                rec = recs[i];
                name = rec.get("layerName");
                if (name == layerName) {
                    this.describeLayerStore.un("load", arguments.callee, this);
                    this.describedLayers[name] = true;
                    callback.call(scope, rec);
                    return;
                } else if (typeof this.describedLayers[name] == "function") {
                    var fn = this.describedLayers[name];
                    this.describeLayerStore.un("load", fn, this);
                    fn.apply(this, arguments);
                }
            }
            // something went wrong (e.g. GeoServer does not return a valid
            // DescribeFeatureType document for group layers)
            delete describedLayers[layerName];
            callback.call(scope, false);
        };
        var describedLayers = this.describedLayers;
        var index;
        if (!describedLayers[layerName]) {
            describedLayers[layerName] = cb;
            this.describeLayerStore.load({
                params: {LAYERS: layerName},
                add: true,
                callback: cb,
                scope: this
            });
        } else if ((index = this.describeLayerStore.findExact("layerName", layerName)) == -1) {
            this.describeLayerStore.on("load", cb, this);
        } else {
            delayedCallback(this.describeLayerStore.getAt(index));
        }
    },

    /** private: method[fetchSchema]
     *  :arg url: ``String`` The url fo the WFS endpoint
     *  :arg typeName: ``String`` The typeName to use
     *  :arg callback: ``Function`` Callback function. Will be called with
     *      a ``GeoExt.data.AttributeStore`` containing the schema as first
     *      argument, or false if the WMS does not support DescribeLayer or the
     *      layer is not associated with a WFS feature type.
     *  :arg scope: ``Object`` Optional scope for the callback.
     *
     *  Helper function to fetch the schema for a layer of this source.
     */
    fetchSchema: function(url, typeName, callback, scope) {
        var schema = this.schemaCache[typeName];
        if (schema) {
            if (schema.getCount() == 0) {
                schema.on("load", function() {
                    callback.call(scope, schema);
                }, this, {single: true});
            } else {
                callback.call(scope, schema);
            }
        } else {
            schema = new GeoExt.data.AttributeStore({
                url: url,
                baseParams: {
                    SERVICE: "WFS",
                    //TODO should get version from WFS GetCapabilities
                    VERSION: "1.1.0",
                    REQUEST: "DescribeFeatureType",
                    TYPENAME: typeName
                },
                autoLoad: true,
                listeners: {
                    "load": function() {
                        callback.call(scope, schema);
                    },
                    scope: this
                }
            });
            this.schemaCache[typeName] = schema;
        }
    },
    
    /** api: method[getSchema]
     *  :arg rec: ``GeoExt.data.LayerRecord`` the WMS layer to issue a WFS
     *      DescribeFeatureType request for
     *  :arg callback: ``Function`` Callback function. Will be called with
     *      a ``GeoExt.data.AttributeStore`` containing the schema as first
     *      argument, or false if the WMS does not support DescribeLayer or the
     *      layer is not associated with a WFS feature type.
     *  :arg scope: ``Object`` Optional scope for the callback.
     *
     *  Gets the schema for a layer of this source, if the layer is a feature
     *  layer.
     */
    getSchema: function(rec, callback, scope) {
        if (!this.schemaCache) {
            this.schemaCache = {};
        }
        this.describeLayer(rec, function(r) {
            if (r && r.get("owsType") == "WFS") {
                var typeName = r.get("typeName");
                var url = r.get("owsURL");
                this.fetchSchema(url, typeName, callback, scope);
            } else if (!r) {
                // When DescribeLayer is not supported, we make the following
                // assumptions:
                // 1. URL of the WFS is the same as the URL of the WMS
                // 2. typeName is the same as the WMS Layer name
                this.fetchSchema(this.url, rec.get('name'), callback, scope);
            } else {
                callback.call(scope, false);
            }
        }, this);
    },
    
    /** api: method[getWFSProtocol]
     *  :arg record: :class:`GeoExt.data.LayerRecord`
     *  :arg callback: ``Function``
     *  :arg scope: ``Object``
     *  :returns: :class:`OpenLayers.Protocol.WFS`
     *
     *  Creates a WFS protocol for the given WMS layer record.
     */
    getWFSProtocol: function(record, callback, scope) {
        this.getSchema(record, function(schema) {
            var protocol = false;
            if (schema) {
                var geometryName;
                var geomRegex = /gml:((Multi)?(Point|Line|Polygon|Curve|Surface|Geometry)).*/;
                schema.each(function(r) {
                    var match = geomRegex.exec(r.get("type"));
                    if (match) {
                        geometryName = r.get("name");
                    }
                }, this);
                protocol = new OpenLayers.Protocol.WFS({
                    version: "1.1.0",
                    srsName: record.getLayer().projection.getCode(),
                    url: schema.url,
                    featureType: schema.reader.raw.featureTypes[0].typeName,
                    featureNS: schema.reader.raw.targetNamespace,
                    geometryName: geometryName
                });
            }
            callback.call(scope, protocol, schema, record);
        }, this);
    },

    /** api: method[getConfigForRecord]
     *  :arg record: :class:`GeoExt.data.LayerRecord`
     *  :returns: ``Object``
     *
     *  Create a config object that can be used to recreate the given record.
     */
    getConfigForRecord: function(record) {
        var config = Ext.applyIf(
                gxp.plugins.WMSSource.superclass.getConfigForRecord.apply(this, arguments),
                record.json
            ),
            layer = record.getLayer(),
            params = layer.params,
            options = layer.options;
        var name = config.name,
            raw = this.store.reader.raw;
        if (raw) {
            var capLayers = raw.capability.layers;
            for (var i=capLayers.length-1; i>=0; --i) {
                if (capLayers[i].name === name) {
                    config.capability = Ext.apply({}, capLayers[i]);
                    var srs = {};
                    srs[layer.projection.getCode()] = true;
                    // only store the map srs, because this list can be huge
                    config.capability.srs = srs;
                    break;
                }
            }
        }
        if (!config.capability) {
            if (layer.maxExtent) {
                config.bbox = layer.maxExtent.toArray();
            }
            config.srs = layer.projection.getCode();
        }
        return Ext.apply(config, {
            format: params.FORMAT,
            styles: params.STYLES,
            tiled: !options.singleTile,
            transparent: params.TRANSPARENT,
            cql_filter: params.CQL_FILTER,
            minscale: options.minScale,
            maxscale: options.maxScale,
            infoFormat: record.get("infoFormat"),
            attribution: layer.attribution
        });
    },

    disable: function() {
        this.disabled = true;
    },
    
    /** private: method[getState] */
    getState: function() {
        var state = gxp.plugins.WMSSource.superclass.getState.apply(this, arguments);
        return Ext.applyIf(state, {title: this.title});
    }
    
});

Ext.preg(gxp.plugins.WMSSource.prototype.ptype, gxp.plugins.WMSSource);
