﻿/** @class TNRIS.XmlShapeLayer
@augments TNRIS.ShapeLayer
*/

TNRIS.XmlShapeLayer = function(config) {
    Ext.applyIf(this, {
        id: Ext.id(),
        shapeLineColor: new VEColor(0,150,150,0.6),
        shapeFillColor: new VEColor(0,150,150,0.6),
        shapeIcon: '<div class="globePushpin"><img src="images/pushPins/greenGlobePin.png" /></div>'
    });
    
    TNRIS.XmlShapeLayer.superclass.constructor.call(this, config);

    
    Ext.applyIf(this.config, {
        columnConfigs: []
    });    
    
    this.addedRecords = [];
    this.deletedRecords = [];
    this.newFieldID = -1; // new fields added to shapes are initially given this bogus ID.  When the database provides a true ID, shape data will be remapped to the correct ID
    
    this.addEvents({'fieldchange' : true});
    
    this.on('fieldchange', this.onFieldChange, this);
};

Ext.extend(TNRIS.XmlShapeLayer, TNRIS.ShapeLayer, 
    /** @scope TNRIS.UserShapeLayer */
    {
    /** field modification event types */
    FIELDMOD: 'mod',
    FIELDADD: 'add',
    FIELDDELETE: 'delete',
    
    /** returns true if layer points should be refected with each change of view extent */
    isDisplayedDynamically: function() {
        // TODO - calculate this based on shape count?
        return false;
    },

    isEditable: function() {
        // TODO : calculate editability based on user role & other settings)
        return true;
    },
    
    /** whether layers can be listed in the grid control - HACK */
    queryable: function(value) {
        return 1;
    },

    // TODO: implement backend for xml queries to support smartable
    smartable: function(value) {
        return false;
    },
    
    xmlLayerType: function() {
        return this.defaultMethod('xmlLayerType');
    },
    
    /************************************
    * Data functions
    *************************************/

    importFromKml: function(url) {
        if (this.map() == null) {
            throw "The layer must be added to the map prior to importing data";
        }
        var layer = this.getShapeLayer();
        layer.DeleteAllShapes();
        try {
            var spec = new VEShapeSourceSpecification(VEDataType.ImportXML, url, layer);
            this.map().ImportShapeLayerData(spec, this.onImportFromKml.bind(this), true);
        } catch (e) {
            console.log("Unable to import kml: " + e);
        }
    },
    
    onImportFromKml: function(layer) {
        var total = layer.GetShapeCount();
        var fieldName = this.addField('kmlDescription');
        for (i = 0; i < total; i++) {
            var shape = layer.GetShapeByIndex(i);
            this.addShape(shape, fieldName);
        }
    },

    getUSPName: function(cmd) {
        return 'findUserSpatialRecords';
    },

    createRecord: function(args, createNew) {
        var constructor = this.getRecordConstructor(createNew);
        return new constructor(args);
    },
    
    startShape: function(shape) {
        if (this.isEditable()) {
            this.shapeInProgress = shape;
            this.getShapeLayer().AddShape(shape);
            this.applyShapeStyling(shape);
        } else {
            throw new Exception("The " + this.name() + " layer is not editable by this user");
        }
    },
    
    endShape: function() {
        if (null == this.shapeInProgress) {
            throw "There is no shapeInProgress.  XmlShapeLayer#endShape can only be called after startShape";
        }
        this.addShape(this.shapeInProgress);
        this.shapeInProgress = null;
    },
    
    cancelShape: function(shape) {
        this.getShapeLayer().DeleteShape(shape);
        this.shapeInProgress = null;
    },
    
    addShape: function(shape) {
        return this.AddShape(shape);
    },
    
    // Virtual Earth Naming convention starts functions with capital letter
    AddShape: function(veShape, savedDescriptionField) {
        if (this.isEditable()) {
            if (veShape.GetID() == null) {
                this.getShapeLayer().AddShape(veShape);
            }
            var record = this.createRecord({
                'shapeType': veShape.GetType(),
                'shapePoints': veShape.GetPoints()
            }, true);
            record.id = Ext.id(null, '');
            if (typeof(savedDescriptionField) != "undefined") {
                record.set(savedDescriptionField, veShape.GetDescription);
            }
            this.addedRecords.push(record);
            veShape.SetDescription(this.getShapeDescription(record));
            this.applyShapeStyling(veShape);
            this.stores.each(function(store) {
                store.add(record);
            }, this);
            this.cacheShape(record, veShape);
            this.shapeChanged();
        }
    },
    
    deleteShape: function(shape) {
        return this.DeleteShape(shape);
    },
    
    // Virtual Earth Naming convention starts functions with capital letter
    DeleteShape: function(shape) {
        if (this.isEditable()) {
            var record = this.getRecordByShape(shape);
            this.removeRecords([record]);
        }
    },
    
    removeRecords: function(records) {
        Ext.each(records, function(record) {
            this.stores.each(function(store) {
                //HACK: error out when there is no record on the grid store to delete //Ragu
                if (store.indexOf(record) >=0) {
                    store.remove(record);
                }
            }, this);
    
            // delete map shape if present
            var shape = this.getShapeByRecord(record);
            if (null != shape) {
                var layer = this.getShapeLayer();
                var mapShape = layer.GetShapeByID(shape.GetID());
                if (null != mapShape) {
                    this.uncacheShape(shape);
                    layer.DeleteShape(mapShape);
                }
            }
            
            // commit changes
            if (this.isPersisted()) {
                this.deletedRecords.push(record.id);
            } else {
                var idx = this.addedRecords.indexOf(record);
                if (idx >= 0) {
                    this.addedRecords.splice(idx,1);
                }
            }
        }, this);
        this.shapeChanged();
    },
    
    modifyShapes: function(shapes) {
        var shapeStore = this.getShapeStore();
        Ext.each(shapes, function(shape) {
            if (shape.GetShapeLayer() == this.getShapeLayer()) {
                var record = this.getRecordByShape(shape);
                record.set('shapePoints', shape.GetPoints());
                this.modifyRecord(record, shapeStore, true);
            } 
            return true;
        }, this);
        this.shapeChanged();
    },
    
    /** propogate modifed records across stores and persist if needed */
    modifyRecord: function(record, _store, skipNotify) {
        this.stores.each(function(store) {
            if (store != _store) {
                var r = store.getById(record.id);
                r.beginEdit();
                $H(record.getChanges()).each(function(item) {
                    r.set(item.key, item.value);
                });
                r.endEdit();
            } 
        }, this);
        if (!skipNotify) {
            this.shapeChanged();
        }
    },
    
    editShape: function(args) {
        var recordId = args[0];
        var record = this.getShapeStore().getById(recordId);
        var win = this.getShapeEditor();
        win.load(recordId, this.getShapeColumns(record));
        win.show();
    },
    
    shapeChanged: function() {
        if (this.isPersisted()) {
            this.fireEvent('beforedataload', "Saving...");
            this.commitShapeData(function() {
                this.fireEvent('dataload');
                this.reload();
            }.bind(this));
        } 
    },
    
    /** update clientfields associated with a shape
    @param {string} recordId - identifier within layer#getShapeStore() for modified shape
    @param {Ext.data.Record[]} - an array of modified fieldName & fieldValue pairs)
    */
    updateShapeData: function(recordId, fields) {
        if (fields == null || fields.length == 0) {
            return;
        }
        var record = this.getShapeStore().getById(recordId);
        if (null == record) {
            throw "An unknown record was modified in layer " + this.name();
        }
        
        // This is a batch modification of fields (adding and removing fields) - suspend events until the modifications are complete, then fire
        this.suspendEvents();
        
        var activeFields = [];
        fields.each(function(mod) {
        // if field doesn't exist, layer.updateClientMetadata
            if (mod.get('dbName') == null) {
                mod.set('dbName', this.addField(mod.get('guiName')));
            } else {
                this.modifyField(mod.get('dbName'), mod.get('guiName'));
            }
            activeFields.push(mod.get('dbName'));
        // if field does exist, record.set(...)
            record.set(mod.get('dbName'), mod.get('value'));
        }, this);
        
        // delete fields
        var deletedFields = this.deleteMissingFields(activeFields);
        Ext.each(deletedFields, function(field) {
            record.set(field, null);
        }, this);
        
        this.resumeEvents();
        
        // update other stores
        this.modifyRecord(record, this.getShapeStore(), true);
        
        this.shapeChanged();
        if (this.dirtyMetadata) {
            this.fireEvent('fieldchange', TNRIS.XmlShapeLayer.FIELDADD);
        }
    },
    
    addField: function(fieldName) {
        if (null == this.config.columnConfigs) {
            this.config.columnConfigs = [];
        }
        var fieldId = "newField" + this.newFieldID;
        this.config.columnConfigs.push({
            dbName: fieldId,
            guiName: fieldName
        });
        this.newFieldID--;
        this.dirtyMetadata = true;
        this.fireEvent('fieldchange', TNRIS.XmlShapeLayer.FIELDADD, fieldId);
        return fieldId;
    },
    
    /* delete fields from the layers metadata
    @param {Array[String]} fields the list of column dbNames to keep
    @returns {Array[String]} dbNames of deleted columns
    */
    deleteMissingFields: function(fields) {
        var deleted = [];
        for (i = 0; i < this.config.columnConfigs.length; i++) {
            if (fields.indexOf(this.config.columnConfigs[i].dbName) == -1) {
                var dbName = this.config.columnConfigs[i].dbName;
                deleted.push(dbName);
                this.config.columnConfigs.splice(i,1);
                this.dirtyMetadata = true;
                this.fireEvent('fieldchange', TNRIS.XmlShapeLayer.FIELDDELETE, dbName);
            }
        }
        return deleted;
    },
    
    deleteField: function(dbName) {
        for(i = 0; i <  this.config.columnConfigs.length; i++) {
            if (this.config.columnConfigs[i].dbName == dbName) {
                this.config.columnConfigs.splice(i,1);
                this.dirtyMetadata = true;
            }
        }
        this.fireEvent('fieldchange', TNRIS.XmlShapeLayer.FIELDDELETE, dbName);
    },
    
    modifyField: function(dbName, guiName) {
        var column = this.getColumnConfig(dbName);
        if (column.guiName != guiName) {
            column.guiName = guiName;
            this.dirtyMetadata = true;
        }
        this.fireEvent('fieldchange', TNRIS.XmlShapeLayer.FIELDMOD, dbName);
    },
    
    onFieldChange: function(operation, dbName) {
        // fields added or removed mean a new record type is created
        this.clearRecordConstructor();
        
        // persist this metadata change if possible
        if (this.isPersisted()) {
            this.commit();
        } else {
            // update all existing records to include this new field
            if (operation == TNRIS.XmlShapeLayer.FIELDADD && dbName) {
                this.stores.each(function(store) {
                    store.each(function(record) {
                        if (record.get(dbName) == null) {
                            record.set(dbName, "");
                        }
                    }, this);
                }, this);
            }
            
            // refresh shapes
            this.refresh();
        }
    },
    
    getColumnConfig: function(dbName) {
        var result = null;
        if (null != dbName) {
            Ext.each(this.config.columnConfigs, function(config) {
                if (config.dbName == dbName) {
                    result = config;
                    return false;
                } else {
                    return true;
                }
            }, this);
        }
        return result;
    },
    
    getShapeEditor: function() {
        var editor = Ext.getCmp('ShapeEditorWindow');
        if (null == editor) {
            editor = new TNRIS.XmlLayerUserDataWindow({id: 'ShapeEditorWindow', layer: this});
        }
        return editor;
    },
    
    getShapeFieldName: function(dbName) {
        var name = "<UNKNOWN>";
        Ext.each(this.config.columnConfigs,function(config) {
            if (config.dbName == dbName) {
                name = config.guiName;
                return false;
            } else {
                return true;
            }
        }, this);
        return name;
    },
    
    getColumnConfigs: function() {
        return this.config.columnConfigs || [];
    },
    
    getShapeDescriptionField: function(config, record) {
        var key = config.dbName;
        var value = record.get(config.dbName);
        return String.format('<span class="fieldName">{0}</span>:<span class="fieldValue">{1}</span>', this.getShapeFieldName(key),value);
    },
    
    getShapeDescription: function(record) {
        var cmpId = this.getId();
        var recordId = record.id;
        var link = String.format('<div class="linkButton"><a href="#" onclick="ShapeLayerFunctionProxy(\'editShape\', \'{0}\', [\'{1}\'])">Add Shape Properties</a></div>',cmpId, recordId);
        return TNRIS.XmlShapeLayer.superclass.getShapeDescription.call(this, record) + link;
    },
    
    getShapeDescriptionId: function(record) {
        return 'ushape_' + this.layerId() + '_' + record.id;
    },
    
    /** create a collection of the fields for this record
    @param {Ext.data.Record} record the record for a shape holding client fields and shape data
    @returns {Array[2]} a 2 dimensional array of field name and field value
    */
    getShapeColumns: function(record) {
        // TODO implement id, name, value
        // filter out nonclient data fields
        // add values for given shape
        var results = [];
        this.config.columnConfigs && this.config.columnConfigs.each(function(config) {
            var column = Object.clone(config);
            column.value = record.get(column.dbName);
            results.push(column);
        }, this);
        return results;
    },
    
    persistedFields: function(record) {
        var fields = {};
        $H(record.data).each(function(pair) {
            switch (pair.key) {
                case 'shapeId': // not persisted
                    break;
                default:
                    fields[this.getColumnMapping(pair.key)] = pair.value;
                    break;
            }
        }.bind(this));
        return fields;
    },
    
    getColumnMapping: function(oldName) {
        if (null != this.columnMapping && this.columnMapping[oldName] != null) {
            return this.columnMapping[oldName];
        } else {
            return oldName;
        }
    },
    
    onMetadataChange: function(serverResults) {
        if (null == serverResults.columnConfigs) {
            return;
        }
        this.columnMapping = {};
        Ext.each(this.config.columnConfigs, function(oldColumn) {
            Ext.each(serverResults.columnConfigs, function(newColumn) {
                if (newColumn.guiName == oldColumn.guiName) {
                    this.columnMapping[oldColumn.dbName] = newColumn.dbName;
                    return false;
                } else {
                    return true;
                }
            }, this);
        }, this);
        Ext.apply(this.config.columnConfigs, serverResults.columnConfigs);
    },
    
    loadShapeData: function(gridData) {
        TNRIS.XmlShapeLayer.superclass.loadShapeData.call(this, gridData);
        if (!this.isPersisted()) {
            this.addedRecords = this.addedRecords.concat(this.getShapeStore().getRange());
        }
    },
    
    commitShapeData: function(callback) {
        var values = {layer_id: this.layerId(), add: [], modify: [], remove: []};
        
        this.addedRecords.each(function(record) {
            values.add.push(this.persistedFields(record));
            return true;
        }.bind(this));
        
        values.remove = this.deletedRecords.slice(0);
        
        var modified = this.getModifiedRecords();
        Ext.each(modified, function(record) {
            values.modify.push(this.persistedFields(record));
            return true;
        }, this);
        
        if (values.add.length > 0 || values.modify.length > 0 || values.remove.length > 0 ) {
            PageMethods.saveLayerShapeData(values, function(results) {
                // TODO error checking & callback
                
                // if successful:
                this.addedRecords.length = 0;
                this.deletedRecords.length = 0;
                // this.commitChanges not necessary since stores will load with server data now
                callback("success");
            }.bind(this));
        } else {
            callback("success");
        }
    },
    
    /** @returns the store that holds modified records for commitShapeData */
    getModifiedRecords: function() {
        // this assumes all stores are kept in sync prior to the call
        return this.stores.first().getModifiedRecords();
    },
    
    commitChanges: function() {
        this.stores.each(function(store) {
            store.commitChanges();
        },this);
    },

/***********************************************
*  Grid Functions
***********************************************/
    /** return colum specs for the layer's grid data */
    gridColumnSpecs: function() {
        var gridStore = this.getGridStore();
        if (null == gridStore || null == gridStore.fields) {
            return [];
        }
        
        var fields = [];

        Ext.each(this.getColumnConfigs(), function(config) {
            fields.push( { header: config.guiName, dataIndex: config.dbName, editor: new Ext.form.TextArea({allowBlank: true}) });
        }, this);
        return fields;
    },
    
        /** whether to make all map and grid stores synchronize current data */
    synchronizeStores: function() {
        return true;
    }

});

/** @class Dialog box for adding/editing fields through the map
@constructor
@param {object} config
@config {TNRIS.ShapeLayer} layer A layer to query against
@config {TNRIS.SmartLayer} smartLayer an existing smart layer to edit
@config {function} onSuccess a call back to invoke after completion.  Passes the smartlayer.
*/
TNRIS.XmlLayerUserDataWindow = function(config) {
    Ext.apply(this, config);

    this.fieldRecordConstructor = Ext.data.Record.create(['dbName', 'guiName', 'order', 'dbType', 'isId', 'isName', 'isHidden', 'isGeometry', 'value']);

    this.fieldStore = new Ext.data.Store({
        reader: new Ext.data.JsonReader({}, this.fieldRecordConstructor)
    });
    
TNRIS.XmlLayerUserDataWindow.superclass.constructor.call(this, {
    title: 'Shape Properties',
    closable: false,
    closeAction: 'hide',
    items: new Ext.grid.EditorGridPanel({
        store: this.fieldStore,
        clicksToEdit: 1,
        autoExpandColumn: 'valueField',
        columns: [
            {header: '', width: 25, renderer: function(value, metadata, record, rowIndex, colIndex, store) {
                return '<span class="removeButton">[-]</span>';
            }},
            {tooltip: 'Field Name', editor: new Ext.form.TextField({allowBlank: false, emptyText: 'Field Name'}), dataIndex: 'guiName'},
            {tooltip: 'Field Value', id: 'valueField', editor: new Ext.form.TextArea({allowBlank: true}), dataIndex: 'value'}
        ],
        hideHeaders: true,
        trackMouseOver: false,
        listeners: { 'cellclick' : { scope: this, fn: this.onCellClick }}
    }),
    tbar: [
        new Ext.Toolbar.Button({
            iconCls: 'addFieldButton',
            cls: 'x-btn-text-icon',
            id: 'addFieldBtn',
            text: 'Add Field',
            handler: function(btn, e){ 
                var field = new this.fieldRecordConstructor({guiName: '', value: ''});
                this.fieldStore.add(field);
            },
            scope: this
        })
    ],
    buttonAlign: 'center',
    buttons: [
        { 
            id: 'okButton',
            text: 'Ok',
            scope: this,
            handler: this.onOK
        },{
            id: 'cancelButton',
            text: 'Cancel',
            scope: this,
            handler: function() {this.close();}
        }
    ],
    layout: 'fit',
    width: 400,
    height: 300,
    modal: true
});
    
};

Ext.extend(TNRIS.XmlLayerUserDataWindow, Ext.Window,
    /** @scope TNRIS.XmlLayerUserDataWindow */
    {
    load: function(shapeId, array) {
        this.shapeId = shapeId;
        this.fieldStore.loadData(array);
    },
    
    onCellClick: function(grid, rowIndex, columnIndex, e) {
        if (columnIndex != 0) {
            return;
        }
        var record = this.fieldStore.getAt(rowIndex);
        if (null != record) {
            this.fieldStore.remove(record);
        }
    },

    onOK: function() {
        var fields = this.fieldStore.getRange();
        this.layer.updateShapeData(this.shapeId, fields);
        this.close();
    }    
});

if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded(); 
