/*
 * Copyright (C) 2009  Camptocamp
 *
 * This file is part of MapFish
 *
 * MapFish is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MapFish 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with MapFish.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @requires OpenLayers/Layer/Vector.js
 * @requires OpenLayers/Control/ModifyFeature.js
 * @requires OpenLayers/Strategy/BBOX.js
 * @requires core/Protocol/MapFish.js
 * @requires widgets/toolbar/Toolbar.js
 * @requires widgets/data/FeatureReader.js
 * @requires widgets/data/LayerStoreMediator.js
 * @requires widgets/editing/FeatureProperties.js
 */

Ext.namespace('mapfish.widgets', 'mapfish.widgets.editing');

/**
 * Class: mapfish.widgets.editing.FeatureEditingPanel
 * This class provides a panel for editing features.
 *
 * Inherits from:
 * - {Ext.Panel}
 */

/**
 * Constructor: mapfish.widgets.editing.FeatureEditingPanel
 *
 * Parameters:
 * config - {Object} Config object, see the possible Ext.Panel
 *     config options, and those specific to this panel
 *     documented below.
 */
mapfish.widgets.editing.FeatureEditingPanel = Ext.extend(Ext.Panel, {

    /**
     * Constant: COMBO_NONE_VALUE
     * The value of the none entry in the combo.
     */
    COMBO_NONE_VALUE: "__combo_none_value__",

    /**
     * Constant: COMBO_NONE_NAME
     * The name (label) of the none entry in the combo.
     */
    COMBO_NONE_NAME: OpenLayers.i18n("mf.editing.comboNoneName"),

    /**
     * APIProperty: map
     * {<OpenLayers.Map>} OpenLayers Map object.
     */
    map: null,

    /**
     * APIProperty: layerConfig
     * {Object} Hash of layers with config parameters.
     *
     * Example:
     * (code start)
     * {
     *     campfacilities: {
     *         text: "Camps",
     *         protocol: new mapfish.Protocol.MapFish({url: "camps"}),
     *         featuretypes: {
     *             geometry: {
     *                 type: OpenLayers.Geometry.MultiPolygon
     *             },
     *             // See the documentation in the
     *             // mapfish.widgets.editing.FeatureProperties classes for more
     *             // details
     *             properties: [
     *                 new mapfish.widgets.editing.StringProperty(
     *                     {name: 'comment'}),
     *                 new mapfish.widgets.editing.IntegerProperty(
     *                     {name: 'status'}),
     *                 new mapfish.widgets.editing.ComboProperty(
     *                     {name: '_type', url: 'campfacilitytypes'}),
     *                 new mapfish.widgets.editing.IntegerProperty(
     *                     {name: 'name', showInGrid: true}),
     *                 new mapfish.widgets.editing.FloatProperty(
     *                     {name: 'camp_id'})
     *             ]
     *         }
     *     },
     *     refugees: {
     *         ...
     *     }
     * }
     * (end)
     */
    layerConfig: null,

    /**
     * Property: combo
     * {Ext.form.ComboBox} The combo box to select the layer to edit.
     */
    combo: null,

    /**
     * APIProperty: comboConfig
     * {Object} Optional config parameters for layer selection combo
     */
    comboConfig: null,

    /**
     * Property: form
     * {Ext.FormPanel} The form to edit attributes.
     */
    form: null,

    /**
     * Property: store
     * {Ext.data.Store} The feature store for the grid.
     */
    store: null,

    /**
     * Property: layerStoreMediator
     * {<mapfish.widgets.data.LayerStoreMediator>} The layer store
     * mediator, it updates the store each time features are modified,
     * added to or removed from the layer.
     */
    layerStoreMediator: null,

    /**
     * Property: grid
     * {Ext.grid.GridPanel} The grid to store the edited features.
     */
    grid: null,

    /**
     * Property: menu
     * {Ext.menu.Menu} Context menu.
     */
    menu: null,

    /**
     * Property: layer
     * {<OpenLayers.Layer.Vector>} The vector layer.
     */
    layer: null,

    /**
     * Property: currentLayerId
     * {String} The identifier of the current edited layer.
     */
    currentLayerId: null,

    /**
     * Property: modifyControl
     * {<OpenLayers.Control.ModifyFeature>}  The modify feature
     * control.
     */
    modifyFeatureControl: null,

    /**
     * Property: drawFeatureControl
     * {<OpenLayers.Control.DrawFeature>} The draw feature control.
     */
    drawFeatureControl: null,

    /**
     * Property: importBtn
     * {Ext.Button} The import button.
     */
    importBtn: null,

    /**
     * Property: commitBtn
     * {Ext.Button} Rhe commit button.
     */
    commitBtn: null,

    /**
     * Property: deleteBtn
     * {Ext.Button} The delete button.
     */
    deleteBtn: null,

    /**
     * Property: attributesFormDefaults
     * {Ext.data.Record} A record representing default attributes
     * in the form.
     */
    attributesFormDefaults: null,

    /**
     * Method: initComponent
     * Initialize the component.
     */
    initComponent: function() {
        if (!this.map) {
            OpenLayers.Console.error(
                "map option for FeatureEditingPanel missing");
        }
        if (!this.layerConfig) {
            OpenLayers.Console.error(
                "layerConfig option for FeatureEditingPanel missing");
        }

        this.layout = 'form';
        this.tbar = this.createToolbar();

        mapfish.widgets.editing.FeatureEditingPanel.superclass.initComponent.apply(this);

        this.add(this.createLayerCombo());

        this.on("destroy", this.destroyResources, this);

        this.on("enable", this.setUp, this);
        this.on("disable", this.tearDown, this);

        // for accordion
        this.on('expand', this.setUp, this);
        this.on('collapse', this.tearDown, this);

        // for tabs
        this.on('activate', this.setUp, this);
        this.on('deactivate', this.tearDown, this);

        this.addEvents('beforecommit', 'commit');
    },

    /**
     * Method: createToolbar
     * Create the toolbar with the editing tools.
     *
     * Returns:
     * {<mapfish.widgets.toolbar.Toolbar>} MapFish toolbar.
     */
    createToolbar: function() {
        this.importBtn = new Ext.Button({
            text: OpenLayers.i18n("mf.editing.import"),
            tooltip: OpenLayers.i18n("mf.editing.importTooltip"),
            disabled: true,
            handler: function() {
                this.refreshFeatures();
            },
            scope: this
        });
        this.commitBtn = new Ext.Button({
            text: OpenLayers.i18n("mf.editing.commit"),
            tooltip: OpenLayers.i18n("mf.editing.commitTooltip"),
            disabled: true,
            handler: function() {
                this.commitFeatures();
            },
            scope: this
        });
        this.deleteBtn = new Ext.Button({
            text: OpenLayers.i18n("mf.editing.delete"),
            tooltip: OpenLayers.i18n("mf.editing.deleteTooltip"),
            disabled: true,
            handler: function() {
                this.deleteFeatures();
            },
            scope: this
        });
        var buttons = [
            this.importBtn
        ,
            this.commitBtn
        , '-',
            this.deleteBtn
        ];
        return new mapfish.widgets.toolbar.Toolbar({
            items: buttons,
            map: this.map
        });
    },

    /**
     * Method: refreshFeatures
     * Refresh the vector layor.
     */
    refreshFeatures: function() {
        // we created the layer ourself so we're assured there's
        // only one strategy configured into it
        this.layer.strategies[0].update({force: true});
    },

    /**
     * Method: commitFeatures
     * Commit the modified features.
     */
    commitFeatures: function() {
        // callback func called on each update and create
        function onUpdateCreate(resp) {
            // if the modify feature control has a selected feature,
            // unselect it
            if (this.modifyFeatureControl.feature) {
                this.modifyFeatureControl.unselectFeature(
                    {feature: this.modifyFeatureControl.feature}
                );
            }
            var toDestroy = (resp.reqFeatures instanceof Array) ?
                            resp.reqFeatures : [resp.reqFeatures];
            this.layer.destroyFeatures(toDestroy);
            this.layer.addFeatures(resp.features);
            if (resp.last) {
                this.fireEvent("commit");
            }
        }
        // callback func called on each delete
        function onDelete(resp) {
            var features = resp.reqFeatures;
            if (!(features instanceof Array)) {
                features = [features];
            }
            this.layer.destroyFeatures(features);
            if (resp.last) {
                this.fireEvent("commit");
            }
        }
        this.fireEvent("beforecommit");
        this.layer.protocol.commit(this.layer.features, {
            "create": {
                scope: this,
                callback: onUpdateCreate
            },
            "update": {
                scope: this,
                callback: onUpdateCreate
            },
            "delete": {
                scope: this,
                callback: onDelete
            }
        });
    },

    /**
     * Method: deleteFeatures
     * Delete the features that are in the selected features array of the layer.
     */
    deleteFeatures: function() {
        var feature;
        for (var i = this.layer.selectedFeatures.length - 1; i >= 0; i--) {
            feature = this.layer.selectedFeatures[i];
            // if the modify feature control has a selected feature,
            // and it is the current feature, unselect it
            if (this.modifyFeatureControl.feature == feature) {
                this.modifyFeatureControl.selectControl.unselect(feature);
            }
            if (feature.state == OpenLayers.State.INSERT) {
                // feature was created as part of the current "transaction",
                // so just destroy it right away
                this.layer.destroyFeatures([feature]);
            } else {
                feature.state = OpenLayers.State.DELETE;
                // add it to the store
                this.layerStoreMediator.featureStoreMediator.addFeatures(feature);
                // and redraw it so it gets the proper styling
                this.layer.drawFeature(feature);
            }
        }
    },

    /**
     * Method: createLayerCombo
     * Create a combobox to let user choose the layer to edit.
     *
     * Returns:
     * {Ext.form.ComboxBox} A combobox
     */
    createLayerCombo: function() {
        var data = [[this.COMBO_NONE_VALUE, this.COMBO_NONE_NAME]];
        for (var i in this.layerConfig) {
            data.push([i, this.layerConfig[i].text]);
        }
        var store = new Ext.data.SimpleStore({
            fields: ['value', 'text'],
            data : data
        });
        var comboConfig = OpenLayers.Util.applyDefaults({
            fieldLabel: OpenLayers.i18n("mf.editing.comboLabel"),
            name: "editingLayer",
            hiddenName: "editingLayer",
            displayField: "text",
            valueField: "value",
            mode: "local",
            triggerAction: "all",
            editable: false,
            store: store,
            listeners: {
                select: function(combo, record, index) {
                    this.prepareSwitchLayer(record.data.value);
                },
                scope: this
            }
        }, this.comboConfig);
        this.combo = new Ext.form.ComboBox(comboConfig);
        return this.combo;
    },

    /**
     * Method: prepareSwitchLayer
     * Called when the user selects a layer in the combobox.
     *
     * Parameters:
     * id - {String} The layer id (key in layerConfig)
     */
    prepareSwitchLayer: function(id) {
        if (this.isDirty()) {
            Ext.Msg.confirm(
                OpenLayers.i18n("mf.editing.confirmMessageTitle"),
                OpenLayers.i18n("mf.editing.confirmMessage"),
                function(btn, text){
                    if (btn == "yes") {
                        this.switchLayer(id);
                    } else {
                        this.combo.setValue(this.currentLayerId);
                    }
                },
                this
            );
        } else {
            this.switchLayer(id);
        }
    },

    /**
     * Method: switchLayer
     *
     * Parameters:
     * id - {String} The layer id (key in layerConfig)
     */
    switchLayer: function(id) {
        if (id != this.COMBO_NONE_VALUE) {
            var config = this.layerConfig[id];
            this.configureLayer(config);
            this.createStore(config);
            this.createModifyFeatureControl();
            this.createDrawFeatureControl(config);
            this.createLayerStoreMediator();
            this.importBtn.enable();
            this.createForm(config);
            this.createGrid(config);
        } else {
            this.destroyAllResources();
        }
        this.currentLayerId = id;
    },

    /**
     * Method: configureLayer
     *
     * Parameters:
     * config - {Object} The layer configuration.
     */
    configureLayer: function(config) {
        var layer = this.layer;
        if (!layer) {
            layer = this.layer = this.createLayer();
        }
        // we don't want to destroy the protocol when the layer
        // is destroyed
        config.protocol.autoDestroy = false;
        layer.protocol = config.protocol;
        if (OpenLayers.Util.indexOf(this.map.layers, layer) < 0) {
            this.map.addLayer(layer);
        }
        layer.destroyFeatures();
    },

    /**
     * Method: createLayer
     * Create the vector layer.
     */
    createLayer: function() {
        var layer = new OpenLayers.Layer.Vector(
            OpenLayers.Util.createUniqueID("mf.ediding"), {
            strategies: [this.createStrategy()],
            styleMap: this.createStyleMap(),
            displayInLayerSwitcher: false
        });
        layer.events.register(
            "featureselected", this, this.onFeatureselected
        );
        layer.events.register(
            "featureunselected", this, this.onFeatureunselected
        );
        layer.events.register(
            "featureremoved", this, this.onFeatureremoved
         );
        layer.events.register(
            "featuremodified", this, this.onFeaturemodified
        );
        return layer;
    },

    /**
     * Method: destroyLayer
     * Destroy the vector layer.
     */
    destroyLayer: function() {
        var layer = this.layer;
        if (layer) {
            layer.destroy();
            this.layer = null;
        }
    },

    /**
     * Method: createStyleMap
     * Create a style map for the vector layer.
     *
     * Returns:
     * {<OpenLayer.StyleMap>} The style map.
     */
    createStyleMap: function() {
        var styleMap = new OpenLayers.StyleMap();
        // create a styleMap for the vector layer so that features
        // have different styles depending on their state
        var context = function(feature) {
            return {
                state: feature.state || OpenLayers.State.UNKNOWN
            };
        };
        var lookup = {};
        lookup[OpenLayers.State.UNKNOWN] = {
        };
        lookup[OpenLayers.State.UPDATE] = {
            fillColor: "green",
            strokeColor: "green"
        };
        lookup[OpenLayers.State.DELETE] = {
            fillColor: "red",
            strokeColor: "red",
            fillOpacity: 0.2,
            strokeOpacity: 0.3,
            display: ""
        };
        lookup[OpenLayers.State.INSERT] = {
            fillColor: "green",
            strokeColor: "green"
        };
        styleMap.addUniqueValueRules("default", "state", lookup, context);
        styleMap.addUniqueValueRules("delete", "state", lookup, context);
        return styleMap;
    },

    /**
     * Method: createStrategy
     * Create a BBOX strategy for the vector layer.
     *
     * Returns:
     * {<OpenLayers.Strategy.BBOX>}
     */
    createStrategy: function() {
        return new OpenLayers.Strategy.BBOX({
            autoActivate: false
        });
    },

    /**
     * Method: onFeatureselected
     *
     * Parameters:
     * obj - {Object} Object with a feature property
     */
    onFeatureselected: function(obj) {
        var f = obj.feature;
        this.deleteBtn.enable();
        this.selectInGrid(f);
        this.editAttributes(f);
    },

    /**
     * Method: onFeatureunselected
     *
     * Parameters:
     * obj - {Object} Object with a feature property
     */
    onFeatureunselected: function(obj) {
        this.deleteBtn.disable();
        this.unselectInGrid();
        this.form.getForm().reset();
        this.form.setDisabled(true);
    },

    /**
     * Method: createStore
     * Create the store containing the edited features.
     */
    createStore: function(config) {
        this.destroyStore();
        var fields = [];
        var properties = config.featuretypes.properties;
        for (var i = 0; i < properties.length; i++) {
            fields.push(properties[i].getRecordType());
        }
        var store = new Ext.data.GroupingStore({
            reader: new mapfish.widgets.data.FeatureReader(
                {}, fields
            ),
            groupField: "state"
        });
        store.on("add", this.updateCommitBtnState, this);
        store.on("remove", this.updateCommitBtnState, this);
        store.on("clear", this.updateCommitBtnState, this);
        store.on("load", this.updateGridSelection, this);
        this.store = store;
    },

    /**
     * Method: destroyStore
     * Destroy the feature store.
     */
    destroyStore: function() {
        var store = this.store;
        if (store) {
            // for unknown reason this method is in Ext's API
            // doc, use it anyway as it's the safest method to
            // unregister listeners registered in the store
            store.destroy();
            this.store = null;
        }
    },

    /**
     * Method: updateCommitBtnState
     * Enable or disable the commit button based on whether there
     * are records or store in the store.
     */
    updateCommitBtnState: function(store) {
        this.commitBtn.setDisabled(!(store.getCount() > 0));
    },

    /**
     * Method: updateGridSelection
     * Make the selection in the grid reflect what's selected in
     * the layer.
     */
    updateGridSelection: function(store, records, options) {
        for (var i = 0; i < records.length; i++) {
            var feature = records[i].data.feature;
            if (OpenLayers.Util.indexOf(
                    this.layer.selectedFeatures, feature) >= 0) {
                this.selectInGrid(feature);
            }
        }
    },

    /**
     * Method: createModifyFeatureControl
     * Create a modify feature control.
     */
    createModifyFeatureControl: function() {
        this.destroyModifyFeatureControl();
        var ctrl = new OpenLayers.Control.ModifyFeature(this.layer, {
            mode: OpenLayers.Control.ModifyFeature.RESHAPE |
                  OpenLayers.Control.ModifyFeature.DRAG,
            title: OpenLayers.i18n("mf.editing.selectModifyFeature")
        });
        this.getTopToolbar().addControl(ctrl, {
            iconCls: 'modifyfeature',
            toggleGroup: this.getId() + 'map'
        });
        ctrl.activate();
        this.modifyFeatureControl = ctrl;
    },

    /**
     * Method: destroyModifyFeatureControl
     * Destroy the modify feature control.
     */
    destroyModifyFeatureControl: function() {
        var ctrl = this.modifyFeatureControl;
        if (ctrl) {
            this.getTopToolbar().removeControl(ctrl);
            ctrl.destroy();
            this.modifyFeatureControl = null;
        }
    },

    /**
     * Method: onFeatureremoved
     *
     * Parameters:
     * obj - {Object} Object with a feature property
     */
    onFeatureremoved: function(obj) {
        if (this.modifyFeatureControl &&
            obj.feature == this.modifyFeatureControl.feature) {
            this.modifyFeatureControl.feature = null;
        }
    },

    /**
     * Method: onFeaturemodified
     *
     * Parameters:
     * obj - {Object} Object with a feature property
     */
     onFeaturemodified: function(obj) {
         var feature = obj.feature;
         if (feature.state != OpenLayers.State.INSERT) {
             feature.state = OpenLayers.State.UPDATE;
         }
     },

    /**
     * Method: createDrawFeatureControl
     * Create a modify feature control.
     *
     * Parameters:
     * config -  {Object} A layer config object.
     */
    createDrawFeatureControl: function(config) {
        this.destroyDrawFeatureControl();
        var title, handler, multi = false, iconCls;
        switch (config.featuretypes.geometry.type) {
            case OpenLayers.Geometry.MultiPoint:
                multi = true;
            case OpenLayers.Geometry.Point:
                title = OpenLayers.i18n("mf.editing.drawPointTitle");
                handler = OpenLayers.Handler.Point;
                iconCls = "drawpoint";
                break;
            case OpenLayers.Geometry.MultiPolygon:
                multi = true;
            case OpenLayers.Geometry.Polygon:
                title = OpenLayers.i18n("mf.editing.drawPolygonTitle");
                handler = OpenLayers.Handler.Polygon;
                iconCls = "drawpolygon";
                break;
            case OpenLayers.Geometry.MultiLineString:
                multi = true;
            case OpenLayers.Geometry.LineString:
                title = OpenLayers.i18n("mf.editing.drawLineTitle");
                handler = OpenLayers.Handler.Path;
                iconCls = "drawline";
                break;
        }
        var ctrl =  new OpenLayers.Control.DrawFeature(this.layer, handler, {
            title: title,
            featureAdded: OpenLayers.Function.bind(this.onFeatureadded, this),
            handlerOptions: {
                multi: multi
            }
        });
        this.getTopToolbar().addControl(ctrl, {
            iconCls: iconCls,
            toggleGroup: this.getId() + 'map'
        });
        this.drawFeatureControl = ctrl;
    },

    /**
     * Method: destroyDrawFeatureControl
     * Destroy the draw feature control.
     */
    destroyDrawFeatureControl: function() {
        var ctrl = this.drawFeatureControl;
        if (ctrl) {
            this.getTopToolbar().removeControl(ctrl);
            ctrl.destroy();
            this.drawFeatureControl = null;
        }
    },

    /**
     * Method: onFeatureadded
     *
     * Parameters:
     * feature - {<OpenLayers.Feature.Vector>}
     */
    onFeatureadded: function(feature) {
        feature.state = OpenLayers.State.INSERT;
        // HACK: at this point "featuresadded" has alreay been triggered,
        // so for the layer store mediator to see the INSERT state of
        // that feature we trigger "featuresadded" again
        this.layer.events.triggerEvent("featuresadded", {
            features: [feature]
        });
        this.modifyFeatureControl.selectControl.select(feature);
        this.form.getForm().loadRecord(
            this.attributesFormDefaults
        );
        this.modifyFeatureControl.activate();
    },

    /**
     * Method: createLayerStoreMediator
     * Create the layer store mediator.
     */
    createLayerStoreMediator: function() {
        this.destroyLayerStoreMediator();
        var lsm = new mapfish.widgets.data.LayerStoreMediator({
            store: this.store,
            layer: this.layer,
            filter: function(feature) {
                return feature.state != null &&
                       feature.state != OpenLayers.State.UNKNOWN;
            }
        });
        lsm.activate();
        this.layerStoreMediator = lsm;
    },

    /**
     * Method: destroyLayerStoreMediator
     * Destroy the layer store mediator.
     */
    destroyLayerStoreMediator: function() {
        var lsm = this.layerStoreMediator;
        if (lsm) {
            lsm.deactivate();
            this.layerStoreMediator = null;
        }
    },

    /**
     * Method: destroyAllResources
     * Called when "None" is select in the combo.
     */
    destroyAllResources: function() {
        this.destroyResources();
        this.destroyForm();
        this.destroyGrid();
    },

    /**
     * Method: destroyResources
     * Called when the feature editing panel is destroyed, takes care
     * of destroying all the resources that aren't destroyed by the
     * Ext.Panel destroy method.
     */
    destroyResources: function() {
        // note: the components that are actually added to the feature
        // editing panel are destroyed in the destroy method of
        // Ext.Panel
        this.destroyStore();
        this.destroyModifyFeatureControl();
        this.destroyDrawFeatureControl();
        this.destroyLayerStoreMediator();
        this.destroyLayer();
    },

    /**
     * Method: setUp
     * Set up the feature editing panel.
     */
    setUp: function() {
        if (this.layer &&
            this.currentLayerId != this.COMBO_NONE_VALUE) {
            this.map.addLayer(this.layer)
        }
        if (this.modifyFeatureControl) {
            this.modifyFeatureControl.activate();
        }
    },

    /**
     * Method: tearDown
     * Tear down the feature editing panel.
     */
    tearDown: function() {
        if (this.modifyFeatureControl) {
            this.modifyFeatureControl.deactivate();
        }
        if (this.drawFeatureControl) {
            this.drawFeatureControl.deactivate();
        }
        var layer = this.layer;
        if (layer &&
            OpenLayers.Util.indexOf(this.map.layers, layer) >= 0) {
            this.map.removeLayer(layer)
        }
    },

    /**
     * Method: createForm
     * Create the feature attributes form for a given layer.
     *
     * Parameters:
     * config - {Object} The layer config object.
     */
    createForm: function(config) {
        if (!config.featuretypes) {
            OpenLayers.Console.error(
                "no featuretypes exist for the given layer"
            );
            return;
        }
        var form = this.form;
        if (!form) {
            var form = new Ext.FormPanel({
                title: OpenLayers.i18n("mf.editing.formTitle"),
                disabled: true,
                items: [{}], // hack to avoid JS errors if no items are provided
                trackResetOnLoad: true, // to detect attributes updates
                bodyStyle: 'padding: 5px',
                buttons: [{
                    text: 'ok',
                    handler: function() {
                        if (!this.currentlyEditedFeature ||
                            !this.form.getForm().isDirty()) {
                            return;
                        }
                        this.updateFeatureAttributes(this.currentlyEditedFeature);
                    },
                    scope: this
                }]
            });
            this.add(form);
            this.form = form;
            this.doLayout();
        }
        // clear current form (remove all items)
        var items = this.form.items.items
        for (var i = items.length - 1; i >=0; --i) {
            var field = items[i];
            form.getForm().remove(field);
            form.remove(field);
        }
        var properties = config.featuretypes.properties;
        var defaults = {};
        for (i = 0; i < properties.length; i++) {
            var property = properties[i];
            form.add(property.getExtField());
            defaults[property.name] = property.defaultValue;
        }
        form.doLayout();
        this.attributesFormDefaults = new Ext.data.Record(defaults);
        form.getForm().loadRecord(this.attributesFormDefaults);
    },

    /**
     * Method: destroyForm
     * Destroy the feature attributes form.
     */
    destroyForm: function() {
        var form = this.form;
        if (form) {
            form.destroy();
            this.form = null;
        }
    },

    /**
     * Method: updateFeatureAttributes
     * Loops into the attributes form and updates the OL feature
     *     attributes and data properties
     *
     * Parameters:
     * feature - {<OpenLayers.Feature>} The feature whose attributes are
     *     to be updated
     */
    updateFeatureAttributes: function(feature) {
        var item, value, i, items = this.form.items.items;
        for (i = 0; i < items.length; i++) {
            item = items[i];
            if (!item.getValue) {
                // prevent errors if this is not a form field
                continue;
            }
            if (item.isDirty && item.isDirty()) {
                value = item.getValue();
                feature.attributes[item.name] = value;
                feature.data[item.name] = value;
            }
        }
        if (feature.state != OpenLayers.State.INSERT) {
            feature.state = OpenLayers.State.UPDATE;
        }
        var fsm = this.layerStoreMediator.featureStoreMediator;
        fsm.addFeatures([feature]);
    },

    /**
     * Method: createGrud
     * Create a feature grid for the given layer.
     *
     * Parameters:
     * config - {Object} Layer config object.
     */
    createGrid: function(config) {
        var grid = this.grid;
        if (grid) {
            this.destroyGrid();
        }
        if (!config.featuretypes) {
            OpenLayers.Console.error(
                "no featuretypes exist for the given layer");
            return;
        }
        if (!config.featuretypes.properties) {
            OpenLayers.Console.error(
                "no featuretypes properties are given for layer");
            return;
        }
        var columns = [{
            header: OpenLayers.i18n("mf.editing.gridIdHeader"),
            dataIndex: "fid"
        }];
        Ext.each(config.featuretypes.properties, function(property) {
            if (property.showInGrid) {
                columns.push({
                    // FIXME it should use i18n instead of using the text property
                    header: property.label || property.name,
                    dataIndex: property.name
                });
            }
        });
        columns.push({
            header: OpenLayers.i18n("mf.editing.gridStateHeader"),
            dataIndex: "state"
        });
        grid = new Ext.grid.GridPanel({
            title: OpenLayers.i18n("mf.editing.gridTitle"),
            height: 200,
            store: this.store,
            view: new Ext.grid.GroupingView({
                groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Features" : "Feature"]})'
            }),
            columns: columns
        });
        grid.on("rowcontextmenu", this.onContextClick, this);
        this.add(grid);
        this.doLayout();
        this.grid = grid;
    },

    /**
     * Method: destroyGrid
     * Destroy the feature grid.
     */
    destroyGrid: function() {
        if (this.grid) {
            this.grid.destroy();
            this.grid = null;
        }
    },

    /**
     * Method: selectInGrid
     * Selects and focuses on the row of a given feature in the featureGrid
     *
     * Parameters:
     * feature - {<OpenLayers.Feature>}
     */
    selectInGrid: function(feature) {
        if (this.store && this.grid) {
            var index = this.store.findBy(function(rec, id) {
                return rec.get("feature") == feature;
            });
            var record = this.store.getAt(index);
            this.grid.getSelectionModel().selectRecords([record]);
            if (index != -1) {
                this.grid.getView().focusRow(index);
            }
        }
    },

    /**
     * Method: unselectInGrid
     */
    unselectInGrid: function() {
        if (this.grid) {
            this.grid.getSelectionModel().clearSelections();
        }
    },

    /**
     * Method: editAttributes
     * Shows/enable the editing attributes form and loads the
     *     feature attributes in the corresponding fields
     *
     * Parameters
     * feature - {<OpenLayers.Feature.Vector>}
     */
    editAttributes: function(feature) {
        this.currentlyEditedFeature = feature;
        this.form.getForm().reset();

        // use the feature store read to get a record object corresponding
        // to the edited feature
        var obj = this.store.reader.readRecords([feature]);
        var record = obj.records[0];

        this.form.getForm().loadRecord(record);
        this.form.enable();
    },

    /**
     * Method: onContextClick
     * Is called when user right clicks on a feature grid row
     *     Shows a contextual menu
     *
     * Parameters
     * grid - {<Ext.grid.GridPanel>}
     * index - {<Ext.Record>}
     * e - {<Ext.Event>}
     */
    onContextClick: function(grid, index, e) {
        var menu = this.menu;
        if (!menu) { // create context menu on first right click
            menu = new Ext.menu.Menu({
                id: 'grid-ctx',
                items: [{
                    text: OpenLayers.i18n('mf.editing.onContextClickMessage'),
                    scope: this,
                    handler: function() {
                        this.modifyFeatureControl.selectControl.unselectAll();
                        var feature = this.ctxRecord.data.feature;
                        this.modifyFeatureControl.selectControl.select(feature);
                        this.modifyFeatureControl.activate();
                    }
                }]
            });
            menu.on('hide', this.onContextHide, this);
            this.menu = menu;
        }
        e.stopEvent();
        if (this.ctxRow) {
            Ext.fly(this.ctxRow).removeClass('x-node-ctx');
            this.ctxRow = null;
        }
        this.ctxRow = grid.view.getRow(index);
        this.ctxRecord = grid.store.getAt(index);
        Ext.fly(this.ctxRow).addClass('x-node-ctx');
        menu.showAt(e.getXY());
    },

    /**
     * Method: onContextHide
     * Hides the context menu
     */
    onContextHide : function(){
        if(this.ctxRow) {
            Ext.fly(this.ctxRow).removeClass('x-node-ctx');
            this.ctxRow = null;
        }
    },

    /**
     * Method: isDirty
     * Checks for unsaved (uncommited) changes
     *
     * Returns
     * {Boolean} - true is there are no unsaved changes
     */
    isDirty: function() {
        return (this.store &&
                this.store.getCount() > 0);
    },

    /**
     * APIMethod: setWindowOnbeforeunload
     * Convenience method that sets window.onbeforeunload so that
     * when going away from the page a confirm window is displayed
     * if the store includes uncommitted changes.
     */
    setWindowOnbeforeunload: function() {
        window.onbeforeunload = OpenLayers.Function.bind(
            function(e) {
                if (this.isDirty()) {
                    return OpenLayers.i18n("mf.editing.onBeforeUnloadMessage");
                }
            },
            this
        );
    }
});

Ext.reg('featureediting', mapfish.widgets.editing.FeatureEditingPanel);
