/*
* Ext JS Library 2.0
* Copyright(c) 2006-2007, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*
*******************************************
* Steve 'Cutter' Blades (CutterBl) no.junkATcutterscrossingDOTcom
* http://blog.cutterscrossing.com
* 
* @@@ Ext.data.CFQueryReader v 1.2 [07.29.09] @@@
*
* Inspired by the CFJsonReader, originally writtin by John Wilson (Daemach)
* http://extjs.com/forum/showthread.php?t=21408&highlight=cfjsonreader
* 
* This Custom Data Reader will take the JSON return of a ColdFusion
* Query object, rather returned straight up, or via the ColdFusion
* QueryConvertForGrid() method, or even as a key within a struct.
* 
* The CFQueryReader constructor takes two arguments
* @meta			: object containing key/value pairs for each record (See ArrayReader Config Options)
* @recordType	: field mapping object
* ------------------------------------------------------------------------------------
* REVISION: [01.16.10]
* Removed the 'read' method, as it is identical to that of the JsonReader, the base of
* the ArrayReader, which is the base of this class (so, it's already baked in)
* ------------------------------------------------------------------------------------
* REVISION: [11.17.09]
* The 3.0.3 update to ExtJs changed the base Ext.data.JsonReader class (which is
* extended by Ext.data.ArrayReader, which is extended by the CFQueryReader), and
* renamed a key function from getJsonAccessor() to createAccessor(). I've added
* code to compensate, while maintaining backwards compatibility.
* ------------------------------------------------------------------------------------
* REVISION: [11.09.09]
* One of the frameworks will return lower cased column names, so we include some code
* that uppercases them in the references used to dynamically generate the methods
* for getting column data. This doesn't change the actual JSON object, just by ref
* ------------------------------------------------------------------------------------
* REVISION: [07.29.09]
* Small revisions to support Ext 3.0+, with backwards compatibility for Ext 2.x
* ------------------------------------------------------------------------------------
* REVISON: [05.12.09]
* Tested, and fully functional, with Ext JS 2.2, and a fresh (20:30 CDT) build of Ext 3.0
* ------------------------------------------------------------------------------------
* REVISON: [03.10.09]
* Through the 'meta' argument, you can now define which JSON node is the 'root',
* 'success', or the 'totalProperty', as well as the 'id'. 
* 
* If 'root' is undefined in the meta then it will look for a 'QUERY' node in the 
* object root, otherwise it will assume the object root is the root of the JSON object.
* 
* if the object root contains a 'TOTALROWCOUNT' node, it will apply it's value
* to the totalRowCount. If a 'totalProperty' is defined in the meta, it will
* take the node of that name value, and apply it's value to the totalRowCount. If
* neither is available, it will automatically count all records processed, and 
* apply that count to the value of totalRowCount.
* 
* With this revision, we also remove the requirement for using uppercase values for
* 'mapping' definitions
* -------------------------------------------------------------------------------------
* 
* The recordType object allows you to alias the returned ColdFusion column 
* name (which is always passed in upper case) to any 'name' you wish, as
* well as assign a data type, which your ExtJS app will attempt to cast
* whenever the value is referenced.
* 
* ColdFusion's JSON return, for a ColdFusion Query object, will appear in the
* following format:
* 
* {"COLUMNS":["INTVENDORTYPEID","STRVENDORTYPE","INTEXPENSECATEGORIESID",
* "STREXPENSECATEGORIES"],"DATA" :[[2,"Carpet Cleaning",1,"Cleaining"],
* [1,"Cleaning Service",1,"Cleaining"]]}
* 
* The ColdFusion JSON return on any query that is first passed through
* ColdFusion's QueryConvertForGrid() method will return the object in the
* following format:
* 
* {"TOTALROWCOUNT":3, "QUERY":{"COLUMNS":["MYIDFIELD","DATA1","DATA2"],
* "DATA":[[1,"Bob","Smith"],[6,"Jim","Brown"]]}}
* 
* The Ext.data.CFQueryReader is designed to accomodate either format
* automatically. You would create your reader instance in much the same
* way as the CFJsonReader was created:
* 
* var myDataModel = [
* 	{name: 'myIdField', mapping: 'myIdField'},
* 	{name: 'data1', mapping: 'data1'},
* 	{name: 'data2', mapping: 'data2'}
* ];
* 
* var myCFReader =  new Ext.data.CFQueryReader({id:'myIdField'},myDataModel);
* 
* Notice that the 'id' meta value mirrors the alias 'name' of the record's field.
* 
* You could also define your reader within the Store configuration:
* 
* var myStore = new Ext.data.Store({
* 	reader: new Ext.data.CFQueryReader({
* 		id:'myIdField', // matches the 'name' attribute of your id field
* 		root:'rootOfQueryJSON', // Case Specific
* 		totalProperty:'someTotalsNode' // Case Specific
* 	},[
* 		{name: 'myIdField', mapping: 'myIdField'},
* 		{name: 'data1', mapping: 'data1'},
* 		{name: 'data2', mapping: 'data2'}
* 	]),
* 	url:'/path/to/some/remote/proxy.cfc',
* 	baseParams:{
* 		method:'myMethod',
* 		returnFormat:'JSON'
* 		someMethodArg: numericValue
* 		anotherMethodArg: 'stringValue'
* 	}
* });
*/

Ext.data.CFQueryReader = function(meta, recordType){
    this.meta = meta || {};
	
	/**
     * @cfg {Array/Object} fields
     * <p>Either an Array of {@link Ext.data.Field Field} definition objects (which
     * will be passed to {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record}
     * constructor created from {@link Ext.data.Record#create}.</p>
     */
    this.recordType = Ext.isArray(recordType) ?
        Ext.data.Record.create(recordType) : recordType;
		
    /**
     * @cfg {String} idProperty [id] Name of the property within a row object
     * that contains a record identifier value.  Defaults to <tt>id</tt>
     */
    /**
     * @cfg {String} successProperty [success] Name of the property from which to
     * retrieve the success attribute. Defaults to <tt>success</tt>.  See
     * {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception}
     * for additional information.
     */
    /**
     * @cfg {String} totalProperty [total] Name of the property from which to
     * retrieve the total number of records in the dataset. This is only needed
     * if the whole dataset is not passed in one go, but is being paged from
     * the remote server.  Defaults to <tt>total</tt>.
     */
    /**
     * @cfg {String} root [undefined] <b>Required</b>.  The name of the property
     * which contains the Array of row objects.  Defaults to <tt>undefined</tt>.
     * An exception will be thrown if the root property is undefined. The data
     * packet value for this property should be an empty array to clear the data
     * or show no data.
     */
    Ext.applyIf(meta, {
        idProperty: 'id',
        successProperty: 'success',
        totalProperty: 'total'
    });

    //Ext.data.CFQueryReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ext.data.CFQueryReader, Ext.data.JsonReader, {
	/**
     * Used for un-phantoming a record after a successful database insert.  Sets the records pk along with new data from server.
     * You <b>must</b> return at least the database pk using the idProperty defined in your DataReader configuration.  The incoming
     * data from server will be merged with the data in the local record.
     * In addition, you <b>must</b> return record-data from the server in the same order received.
     * Will perform a commit as well, un-marking dirty-fields.  Store's "update" event will be suppressed.
     * @param {Record/Record[]} record The phantom record to be realized.
     * @param {Object/Object[]} data The new record data to apply.  Must include the primary-key from database defined in idProperty field.
     */
     realize: function(rs, data){
        if (Ext.isArray(rs)) {
            for (var i = rs.length - 1; i >= 0; i--) {
                // recurse
                if (Ext.isArray(data)) {
                    this.realize(rs.splice(i,1).shift(), data.splice(i,1).shift());
                }
                else {
                    // weird...rs is an array but data isn't??  recurse but just send in the whole invalid data object.
                    // the else clause below will detect !this.isData and throw exception.
                    this.realize(rs.splice(i,1).shift(), data);
                }
            }
        }
        else {
            // If rs is NOT an array but data IS, see if data contains just 1 record.  If so extract it and carry on.
            if (Ext.isArray(data) && data.length == 1) {
                data = data.shift();
            }
            if (!this.isData(data)) {
                // TODO: Let exception-handler choose to commit or not rather than blindly rs.commit() here.
                //rs.commit();
                throw new Ext.data.DataReader.Error('realize', rs);
            }
            rs.phantom = false; // <-- That's what it's all about
            rs._phid = rs.id;  // <-- copy phantom-id -> _phid, so we can remap in Store#onCreateRecords
            rs.id = this.getId(data);
            rs.data = data;

            rs.commit();
        }
    },
	
	/**
     * Decode a json response from server.
     * @param {String} action [Ext.data.Api.actions.create|read|update|destroy]
     * @param {Object} response
     * TODO: refactor code between JsonReader#readRecords, #readResponse into 1 method.
     * there's ugly duplication going on due to maintaining backwards compat. with 2.0.  It's time to do this.
     */
    readResponse : function(action, response) {
        var o = (response.responseText !== undefined) ? Ext.decode(response.responseText) : response;
        if(!o) {
            throw new Ext.data.JsonReader.Error('response');
        }

        var root = this.getRoot(o);
        if (action === Ext.data.Api.actions.create) {
            var def = Ext.isDefined(root);
            if (def && Ext.isEmpty(root)) {
                throw new Ext.data.JsonReader.Error('root-empty', this.meta.root);
            }
            else if (!def) {
                throw new Ext.data.JsonReader.Error('root-undefined-response', this.meta.root);
            }
        }

        // instantiate response object
        var res = new Ext.data.Response({
            action: action,
            success: this.getSuccess(o),
            data: (root) ? this.extractData(root, false) : [],
            message: this.getMessage(o),
            raw: o
        });

        // blow up if no successProperty
        if (Ext.isEmpty(res.success)) {
            throw new Ext.data.JsonReader.Error('successProperty-response', this.meta.successProperty);
        }
        return res;
    },
	
	readRecords : function(o){
        this.jsonData = o;
		var s = this.meta, 
		    Record = this.recordType,
            f = Record.prototype.fields, 
			fi = f.items, 
			fl = f.length,
			reset = false;
			
		if(!this.ef){
			this.buildExtractors(o);
		}
		
		var root = this.getRoot(o), c = root.length, totalRecords = c, 
			success = true, cols = this.getQueryRoot(o).COLUMNS;
		
		for (var i = 0;i < cols.length;i++){
			cols[i] = cols[i].toUpperCase();
		}
		
		if(s.totalProperty){
            v = parseInt(this.getTotal(o), 10);
            if(!isNaN(v)){
                totalRecords = v;
            }
        }
        if(s.successProperty){
            v = this.getSuccess(o);
            if(v === false || v === 'false'){
                success = false;
            }
        }
		
		return {
	        success: success,
	        records : this.extractData(root, true),
	        totalRecords : totalRecords
	    };
    },
	
	// private
    buildExtractors : function(o) {
        if(this.ef){
            return;
        }
        var s = this.meta, Record = this.recordType,
            f = Record.prototype.fields, fi = f.items, fl = f.length;
		
		// For backwards compatability with Ext 2.x
        if(typeof this.getJsonAccessor != "function"){
			this.getJsonAccessor = this.createAccessor;
		}
		
		if(s.totalProperty) {
            this.getTotal = this.getJsonAccessor(s.totalProperty);
        } else if(o.TOTALROWCOUNT) {
        	this.getTotal = this.getJsonAccessor('TOTALROWCOUNT');
        }
		
        if(s.successProperty) {
            this.getSuccess = this.getJsonAccessor(s.successProperty);
        }
        
        if (s.messageProperty) {
            this.getMessage = this.getJsonAccessor(s.messageProperty);
        }
		
		/*
         * Providing built in support for CF query objects within
         * structure objects, as is the case when a developer
         * uses the ConvertQueryForGrid() function of CF
         */
        if(s.root){
        	this.getRoot = this.getJsonAccessor(s.root + '.DATA');
        	this.getQueryRoot = this.getJsonAccessor(s.root);
        } else {
        	this.getRoot = (o.QUERY) ? this.getJsonAccessor('QUERY.DATA') : this.getJsonAccessor('DATA');
        	this.getQueryRoot = function(){
        		return (o.QUERY) ? o.QUERY : o;
        	};
        }
		
		var root = this.getRoot(o), c = root.length, 
			totalRecords = c, 
			success = true,
            cols = this.getQueryRoot(o).COLUMNS;
		
		for (var i = 0;i < cols.length;i++){
			cols[i] = cols[i].toUpperCase();
		}
		
        // Create an array location mappings according to the COLUMNS output
    	for(b=0;b < fl; b++){
        	var fMap = (fi[b].mapping !== undefined && fi[b].mapping !== null) ? fi[b].mapping : fi[b].name;
        	fi[b].mapArrLoc = cols.indexOf(fMap.toUpperCase());
        }
		
		// Create methods for getting a record's ID value
	    if (s.id || s.idProperty) {
       		var idMap = this.getFieldMap(s.id||s.idProperty,fi,fl);//this.getJsonAccessor(s.id || s.idProperty);
			this.getId = function(rec){
        		var r = rec[idMap];
        		return (r === undefined || r === "") ? null : r;
       		};
        } else {
        	this.getId = function(){return null;};
        }
		
        // Create extractor functions by field
        ef = [];
        for(var i = 0; i < fl; i++){
            ef[i] = function(rec){
            	var r = rec[fi[i].mapArrLoc];
            	return (r === undefined || r === "") ? null : r;
            };
        }
        this.ef = ef;
    },
	
	getFieldMap: function(key,items,len){
		for(var i=0;i < len;i++){
			if(items[i].name.toUpperCase() === key.toUpperCase()){
				return items[i].mapArrLoc;
			}
		}
		return;
	},

    /**
     * type-casts a single row of raw-data from server
     * @param {Object} data
     * @param {Array} items
     * @param {Integer} len
     * @private
     */
    extractValues : function(data, items, len) {
        var f, values = {};
        for(var j = 0; j < len; j++){
            f = items[j]; // field reference
            var k = (f.mapArrLoc !== undefined && f.mapArrLoc !== null) ? f.mapArrLoc : j; // get the array position within the row for the field ref
            values[f.name] = f.convert((data[k] !== undefined) ? data[k] : f.defaultValue, data); // build a values object by applying the current value to a key of the current 'field'
        }
        return values;
    }
});
