/**
 * @author adeitcher
 */
/*
 * This is an extension to Ext.data.Store that adds writing capability. It does so
 * by Ext.apply(Ext.data.Store.prototype,{...});
 * The primary changes are as follows:
 * 1) Modify commitChanges() to push all changes to the store if one is available, defined as:
 * 2) Add updateProxy config option. If this is not null, then commitChanges() will call:
 * 3) Add write() function with options to write changes.
 * 4) Add replaceWrite config boolean option. If set to true, then commitChanges() pushes
 *    the entire set of data out, rather than just the changes.
 *    
 * Additionally, the old modified[] list has been replaced by a journal[] that records 
 * all changes: add, remove, insert, update of any record. This allows for complete
 * commit or rollback, and pushing changes appropriately to the server.
 * 
 * Additionally, the reader must support the update option. In order to do so,
 * we have provided a modification of Ext.data.JsonReader. Ext.data.XmlReader will
 * come later.
 */

function include(arr, value){
	if ((arr && typeof(arr) != 'undefined') && (value && typeof(value) != 'undefined')) {
		for (var i=0, n=arr.length; i<n; ++i) {
			if (arr[i].id && typeof(arr[i].id) != 'undefined' && arr[i].id === value){
				return true;
			}
		}
	}
	return false;
}

Ext.ux.WriteStore = function(config) {
	Ext.ux.WriteStore.superclass.constructor.call(this,config);
    
	// make sure each load cleans the journal
	this.on('load',function(){this.journal=[];});
}
Ext.extend(Ext.ux.WriteStore,Ext.data.Store,
{
	// to keep track of changes
	journal : [],
	maxId : 0,
	replaceWrite: false,
	
	// fixed types
	types : {change: 'u', add: 'c', remove: 'd'},
	
	// the added config is automatically there - these are defaults
	updateProxy : null,
	
	// if you want to replace all, set options.replace = true
	write : function(options) {
		if (this.fireEvent('beforewrite',this,options) != false) {
			// get the appropriate records
			var records = this.writeRecords(options);
			
			// get unique records array
			var uniq = [];
			uniq.push(records[0]);
			for (var i = 1; i < records.length; ++i){
        		if (!include(uniq, records[i].id)){
            		uniq.push(records[i]);
        		}
    		}
			// structure the params we need for the update to the server
			var params = {
				data: this.reader.write(uniq)
			}
			// add any options
			Ext.applyIf(params, options);
            this.updateProxy.update(params, this.writeHandler, this);			
		}
	},
	
	writeRecords : function(options) {
		var data = [];
		var tmp;
		// we take only the journal, unless we have explicitly asked to replace all
		
		// sure, we could use a trinary operator, but this may get more complex in the future,
		// and if-then-else is cleaner to understand
		
		// to be supported later
		if ((options != null && options.replace == true) || this.replaceWrite == true) {
			// get the actual data in the record
			var rs = this.data.getRange(0);
			for (var i=0; i<rs.length; i++) {
				var elm = rs[i];
				if (elm != null) {
					data[i] = elm.data;
				}
			}
		} else {
			// get the actual data in the record
			for (var i=0; i<this.journal.length; i++) {
				if (this.journal[i] != null) {
					data[i] = {
						type: this.journal[i].type,
						data: this.journal[i].record.data
					};
				}
			}
		}

		return(data);
	},

	// handle the results	
	writeHandler: function(o,success,response) {
		if (success)
			this.fireEvent("write",this,o,response);
		else
			this.fireEvent("writeexception",this,o,response);
	},
	
	// these all need to be modified to keep track of real changes
    add : function(records){
        records = [].concat(records);
        for(var i = 0, len = records.length; i < len; i++){
            records[i].join(this);
        }
        var index = this.data.length;
        this.data.addAll(records);
		for (var i=0; i<records.length; i++) {
			this.journal.push({type: this.types.add, index: index+i, record: records[i]});
		}
        this.fireEvent("add", this, records, index);
    },

    
    remove : function(record){
        var index = this.data.indexOf(record);
        this.data.removeAt(index);
		this.journal.push({type: this.types.remove, index: index, record: record});
        this.fireEvent("remove", this, record, index);
    },

    
    removeAll : function(){
		// record that all objects have been removed
		for (var i=0,len = this.data.getCount(); i<len; i++) {
			this.journal.push({type: this.types.remove, index: i, record: this.data[i]});			
		}
        this.data.clear();
        this.fireEvent("clear", this);
    },

    
    insert : function(index, records){
        records = [].concat(records);
        for(var i = 0, len = records.length; i < len; i++){
            this.data.insert(index, records[i]);
            records[i].join(this);
        }
		for (var i=0; i<records.length; i++) {
			this.journal.push({type: this.types.add, index: index+i, record: records[i]});
		}
        this.fireEvent("add", this, records, index);
    },
	
    commitChanges : function(){
		// commit the changes and clean out
		var m = this.journal.slice(0);
		
		// only changes need commitment
		for (var i=0, len=m.length; i<len; i++) {
			if (m[i].type == this.types.change) {
				m[i].record.commit();
			}
		}
		
		// now write the changes to persistent storage
		this.on('write',function(){
			this.journal = [];
		},this,{single: true})
		if (this.updateProxy != null) {
			this.write();
		} else {
			this.journal = [];
		}
    },

    rejectChanges : function(){
		// back out the changes in reverse order
        var m = this.journal.slice(0).reverse();
        this.journal = [];
        for(var i = 0, len = m.length; i < len; i++){
			var jType = m[i].type;
			if (jType == this.types.change) {
				// reject the change
				m[i].record.reject();
			} else if (jType == this.types.add) {
				// undo the add
				this.data.removeAt(m[i].index);
			} else if (jType == this.types.remove) {
				// put it back
				this.data.insert(m[i].index,m[i].data);
			}
        }
    },

	/*
	 *  The next three are for changes affected directly to a record
	 *  Ideally, this should never happen: all changes go through the Store, 
	 *  and are passed through after being recorded. However, in an object-oriented paradigm,
	 *  it is accepted that you can gain access to the direct object, unlike a SQL paradigm.
	 *  Thus, we need to put events on the Record directly.
	 */
	
	// if we edited a record directly, we need to update the journal	
    afterEdit : function(record){
        this.journal.push({type: this.types.change, index: this.data.indexOf(record), record: record});
        this.fireEvent("update", this, record, Ext.data.Record.EDIT);
    },

	// if we rejected a change to a record directly, we need to remove it from the journal
    afterReject : function(record){
		// find the last edit we had, and remove it
		for (var i=this.journal.length-1; i>=0; i++) {
			if (this.journal[i].type == this.types.change && this.journal[i].record == record) {
				this.journal = this.journal.splice(i,1);
				break;
			}
		}
        this.fireEvent("update", this, record, Ext.data.Record.REJECT);
    },

    // if we committed a change to a record directly, we still keep it in the journal
    afterCommit : function(record){
        this.fireEvent("update", this, record, Ext.data.Record.COMMIT);
    },	

	getNextId : function() {
		// send back the maxId + 1
		return(this.getMaxId()+1);
	},
	
	getMaxId : function() {
		var maxId = 1000;
		if (this.data != null) {
			var records = this.data.getRange(0);
			for (var i=0; i<records.length; i++) {
				if (records[i].id > maxId)
					maxId = records[i].id;
			}
		}
		return(maxId);
	}
});


// Json data writer extension to Reader
Ext.ux.JsonWriterReader = function(meta, recordType){
    meta = meta || {};
    Ext.ux.JsonWriterReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ext.ux.JsonWriterReader, Ext.data.JsonReader, {

	// write - input objects, write out JSON    
	write : function(records) {
		// hold our new structure
		var obj = {};
		// need to do this way rather than literal, because the property name is a variable
		obj[this.meta.root] = records;
		var j = Ext.util.JSON.encode(obj);
		if (!j) {
			throw{message: "JsonWriter.write: unable to encode records into Json"};
		}
		return(j);
	}
	
});
// XML data writer extension to Reader
Ext.ux.XmlWriterReader = function(meta, recordType){
    meta = meta || {};
    Ext.ux.XmlWriterReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ext.ux.XmlWriterReader, Ext.data.XmlReader, {

	// write - input objects, write out XML    
	write : function(records) {
		
	}
	
});

Ext.ux.ObjectReader = function(meta, recordType){
    meta = meta || {};
    Ext.ux.ObjectReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ext.ux.ObjectReader, Ext.data.DataReader, {
    read : function(response){
        var o = response;
        if(!o) {
            throw {message: "ObjectReader.read: object not found"};
        }
        if(o.metaData){
            delete this.ef;
            this.meta = o.metaData;
            this.recordType = Ext.data.Record.create(o.metaData.fields);
            this.onMetaChange(this.meta, this.recordType, o);
        }
        return this.readRecords(o);
    },

    // private function a store will implement
    onMetaChange : function(meta, recordType, o){

    },

    simpleAccess: function(obj, subsc) {
    	return obj[subsc];
    },

	clone : function(o) {
		// nothing with null
		if(o == null) return o;
	
		var n = null;				
		// depends on type
		var t = typeof(o);
		if(o instanceof Array) {
			n = new Array();
			for (var i=0; i<o.length; i++)
				n[i] = this.clone(o[i]);
		} else if (o instanceof Object) {
			var n = new Object();
			for(var i in o) {
				n[i] = this.clone(o[i]);
			}
		} else {
			n = o;
		}
		return n;
	},
	
    readRecords : function(o){
		o = this.clone(o);
        this.objectData = o;
        var s = this.meta, Record = this.recordType,
            f = Record.prototype.fields, fi = f.items, fl = f.length;

		var getId = function(){return(null)};
		if (this.meta.id) {
			var idField = this.meta.id;
			getId = function(r) {
				return(rec[idField]);
			}
		}

        var records = [];
	    for(var i = 0; i < o.length; i++){
	        var record = new Record(o[i], getId(o[i]));
	        records[i] = record;
	    }
	    return {
	        success : true,
	        records : records,
	        totalRecords : records.length
	    };
    },

	// write - input objects, write out cloned objects    
	write : function(records) {
		// clone so we do not confuse objects
		return(this.clone(records));
	}
});

// extend the HttpProxy to write
Ext.ux.HttpWriteProxy = function(conn){
    Ext.ux.HttpWriteProxy.superclass.constructor.call(this, conn);
};

Ext.extend(Ext.ux.HttpWriteProxy,Ext.data.HttpProxy,
{
    update : function(params, callback, scope, arg){
        if(this.fireEvent("beforeupdate", this, params) !== false){
            var  o = {
                params : params || {},
                request: {
                    callback : callback,
                    scope : scope,
                    arg : arg
                },				
                callback : this.updateResponse,
				method: 'POST',
                scope: this
            };
            if(this.useAjax){
                Ext.applyIf(o, this.conn);
                if(this.activeRequest){
                    Ext.Ajax.abort(this.activeRequest);
                }
                this.activeRequest = Ext.Ajax.request(o);
            }else{
                this.conn.request(o);
            }
        }else{
            callback.call(scope||this, arg, false, null);
        }
    },

    updateResponse : function(o, success, response){
        delete this.activeRequest;
        if(!success){
            this.fireEvent("updateexception", this, o, response);
            o.request.callback.call(o.request.scope, o.request.arg, false, response);
            return;
        }
        this.fireEvent("update", this, o, o.request.arg);
        o.request.callback.call(o.request.scope, o.request.arg, true, response);
    }
});

/**
 * @class Ext.ux.StoreProxy
 * @extends Ext.data.DataProxy
 * An implementation of Ext.data.DataProxy that gets data from an existing store
 * @constructor
 * @param store the store which contains the data we want to load as an element of
 *   one of the Records in its store. 
 * @param field the name of the field within the record
 */
Ext.ux.StoreProxy = function(config){
    Ext.ux.StoreProxy.superclass.constructor.call(this);
	config = config || {};
    this.sourceStore = config.store;
	this.sourceField = config.field;
};


Ext.extend(Ext.ux.StoreProxy, Ext.data.DataProxy, {
    /**
     * Load data from the requested source (in this case another store), read the data object into
     * a block of Ext.data.Records directly, and
     * process that block using the passed callback.
     * @param {Object} params The only parameter looked at is ID, which is the ID of the record in the source store
     * @param {Ext.data.DataReader) reader This parameter is not used by the StoreProxy class.
     * @param {Function} callback The function into which to pass the block of Ext.data.records.
     * The function must be passed <ul>
     * <li>The Record block object</li>
     * <li>The "arg" argument from the load function</li>
     * <li>A boolean success indicator</li>
     * </ul>
     * @param {Object} scope The scope in which to call the callback
     * @param {Object} arg An optional argument which is passed to the callback as its second parameter.
     */
    load : function(params, reader, callback, scope, arg){
        params = params || {};
        var result;
        try {
			var record = this.sourceStore.getById(params.id);
			if (record != null && record != undefined) {
				this.loadId = params.id;
				var data = record.get(this.sourceField);
				result = reader.readRecords(data);
			}
        }catch(e){
			this.loadId = null;
            this.fireEvent("loadexception", this, arg, null, e);
            callback.call(scope, null, arg, false);
            return;
        }
        callback.call(scope, result, arg, true);
    },
	reconfig : function(config) {
		this.sourceStore = config.store;
		this.sourceField = config.field;
	},
    update : function(params, callback, scope, arg){
        if(this.fireEvent("beforeupdate", this, params) !== false){
	        try {
				var record = this.sourceStore.getById(this.loadId);
				if (record != null && record != undefined) {
					// make sure that toString() returns unique
					var s = Ext.util.JSON.encode(params.data);
					params.data.toString = function(){return(s)}
					record.set(this.sourceField,params.data);
				} else {
					throw "No record matches id "+this.loadId;
				}
	        }catch(e){
	            this.fireEvent("updateexception", this, arg, null, e);
	            callback.call(scope, params, null, arg);
	            return;
	        }
			this.fireEvent("update", this, arg);	
	        callback.call(scope, params, true, arg);
        }else{
            callback.call(scope||this, true, params, arg);
        }
    }   
});

