if(!dojo._hasResource["bepixeld.DWRReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["bepixeld.DWRReadStore"] = true;
dojo.provide("bepixeld.DWRReadStore");

dojo.require("dojo.data.util.simpleFetch");
dojo.require("dojo.data.api.Identity");
dojo.require("dojo.data.api.Read");
dojo.require("dojo.data.util.filter");

dojo.declare("bepixeld.DWRReadStore", null,{
    // The DWRReadStore implements the dojo.data.api.Read API and reads
    // data from the server using DWR	    
	constructor: function(params){	
	    this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true, 'dojo.data.api.Notification':true};        
	    this._loadMethod = params.loadMethod;
		if(typeof this._loadMethod == "string")
			this._loadMethod = eval(this._loadMethod);
	    this._loadArgument = params.loadArgument;
		if(typeof this._loadArgument == "string")
			try {
				this._loadArgument = eval(this._loadArgument);
			} catch(ex){
				console.dir(ex);
			}
		this._fillRecursive = params.fillRecursive;
	    this._items = [];
	    this._loadComplete = false;
	    this._loadInProgress = false;
	    this._identifierAttr = params.identifier || Number;		
		this._labelAttr = params.label;	
		this._pending = {
			_newItems:{}, 
			_modifiedItems:{}, 
			_deletedItems:{}
		};
		this._datatypeMap = params.typeMap || {};
		if(!this._datatypeMap['Date']){
			//If no default mapping for dates, then set this as default.
			//We use the dojo.date.stamp here because the ISO format is the 'dojo way'
			//of generically representing dates.
			this._datatypeMap['Date'] = {
											type: Date,
											deserialize: function(value){
												return dojo.date.stamp.fromISOString(value);
											}
										};										
		}
	
		this._arrayOfAllItems = [];
		this._arrayOfTopLevelItems = [];
		this._itemsByIdentity = null;
		this._queuedFetches = [];		
		
		this._storeRefPropName="_S";
		this._itemNumPropName="_0";
		this._rootItemPropName="_RI";
		this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity		
	},	
	
	loadMethod: "",

	loadArgument: null,
	
	identifier: "",
	
	label: "",
	
	typeMap: "",
	
	lastKeywordArgs: null,
	
	fillRecursive: true,
	
	_assert: function(/* boolean */ condition){
		if(!condition) {
			throw new Error("assertion failed in DWRReadStore");
		}
	},
	
    getFeatures: function(){
        return this._features; 
    },

	_containsValue: function(	/* item */ item, 
								/* attribute-name-string */ attribute, 
								/* anything */ value,
								/* RegExp?*/ regexp){
		//	summary: 
		//		Internal function for looking at the values contained by the item.
		//	description: 
		//		Internal function for looking at the values contained by the item.  This 
		//		function allows for denoting if the comparison should be case sensitive for
		//		strings or not (for handling filtering cases where string case should not matter)
		//	
		//	item:
		//		The data item to examine for attribute values.
		//	attribute:
		//		The attribute to inspect.
		//	value:	
		//		The value to match.
		//	regexp:
		//		Optional regular expression generated off value if value was of string type to handle wildcarding.
		//		If present and attribute values are string, then it can be used for comparison instead of 'value'
		return dojo.some(this.getValues(item, attribute), function(possibleValue){
			if(possibleValue !== null && !dojo.isObject(possibleValue) && regexp){
				if(possibleValue.toString().match(regexp)){
					return true; // Boolean
				}
			}else if(value === possibleValue){
				return true; // Boolean
			}
		});
	},
	
	containsValue: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ value){	
		var regexp = undefined;
		if(typeof value === "string"){
			regexp = dojo.data.util.filter.patternToRegExp(value, false);
		}
		return this._containsValue(item, attribute, value, regexp); //boolean.
	},
	
	_fetchItems: function(args,findCallback,errorCallback){
	    var self = this;
		this.lastKeywordArgs = args;
		// Function to apply filter to data set
	    var filter = function(requestArgs, arrayOfItems){
			var items = [];
			if(requestArgs.query){
				var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false; 

				//See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
				//same value for each item examined.  Much more efficient.
				var regexpList = {};
				for(var key in requestArgs.query){
					var value = requestArgs.query[key];
					if(typeof value === "string"){
						regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
					}
				}

				for(var i = 0; i < arrayOfItems.length; ++i){
					var match = true;
					var candidateItem = arrayOfItems[i];
					if(candidateItem === null){
						match = false;
					}else{
						for(var key in requestArgs.query) {
							var value = requestArgs.query[key];
							if (!self._containsValue(candidateItem, key, value, regexpList[key])){
								match = false;
							}
						}
					}
					if(match){
						items.push(candidateItem);
						self.onNew(candidateItem,null);
					}
				}
				findCallback(items, requestArgs);
			}else{
				// We want a copy to pass back in case the parent wishes to sort the array. 
				// We shouldn't allow resort of the internal list, so that multiple callers 
				// can get lists and sort without affecting each other.  We also need to
				// filter out any null values that have been left as a result of deleteItem()
				// calls in ItemFileWriteStore.
				for(var i = 0; i < arrayOfItems.length; ++i){
					var item = arrayOfItems[i];
					if(item !== null){
						items.push(item);
					}
				}
				findCallback(items, requestArgs);
			}
		};

		// If the data has already been return the previously loaded data
	    if(this._loadComplete){
	        filter(args, this._getItemsArray(args.queryOptions));
	    }else{
	        if(this._loadMethod ){
	            // If a new request comes in before the previous one
	            // is finished, we will ignore the request. 
	            if(this._loadInProgress){
					this._queuedFetches.push({args: args, filter: filter});
				}else{
	                this._loadInProgress = true;
	                var callback = function(data){
	                    try{
							if(!dojo.isObject(data) || data.length === "undefined") {
								data = [data];
							}
	                        self._items = data;
							self._fillSubItems(data);
	                        self._loadComplete = true;
	                        self._loadInProgress = false;
	                        filter(args, self._getItemsArray(args.queryOptions));
							self._handleQueuedFetches();
	                    }catch(e){
	                        self._loadComplete = true;
	                        self._loadInProgress = false;
	                        errorCallback(e, args);
	                    }
	                };
	                if(this._loadArgument){
	                	var arg = this._loadArgument;
	                	if(dojo.isFunction(arg)){
	                		arg = arg();
	                	}
	                	this._loadMethod(arg, callback);
					} else {
	                	this._loadMethod(callback);
	              	}
	            }
	        }else{
	            errorCallback(new Error("DWR Method is undefined."), args);
	        }
	    }
	},
	
	_handleQueuedFetches: function(){
		//	summary: 
		//		Internal function to execute delayed request in the store.
		//Execute any deferred fetches now.
		if (this._queuedFetches.length > 0) {
			for(var i = 0; i < this._queuedFetches.length; i++){
				var fData = this._queuedFetches[i];
				var delayedQuery = fData.args;
				var delayedFilter = fData.filter;
				if(delayedFilter){
					delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions)); 
				}else{
					this.fetchItemByIdentity(delayedQuery);
				}
			}
			this._queuedFetches = [];
		}
	},
	
	_getItemsArray: function(/*object?*/queryOptions){
		//	summary: 
		//		Internal function to determine which list of items to search over.
		//	queryOptions: The query options parameter, if any.
		if(queryOptions && queryOptions.deep) {
			return this._arrayOfAllItems; 
		}
		return this._arrayOfTopLevelItems;
	},
	
	getValue: function(item, attribute, defaultValue){
		var values = this.getValues(item, attribute);
		return (values.length > 0)?values[0]:defaultValue; // mixed
    },
	
	getValues: function(/* item */ item, /* attribute-name-string */ attribute){
		this._assertIsItem(item);
		this._assertIsAttribute(attribute);
		return item[attribute] || []; // Array
	},
	
	getAttributes: function(/* item */ item){
		//	summary: 
		//		See dojo.data.api.Read.getAttributes()
		this._assertIsItem(item);
		var attributes = [];
		for(var key in item){
			// Save off only the real item attributes, not the special id marks for O(1) isItem.
			if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){
				attributes.push(key);
			}
		}
		return attributes; // Array
	},
	
	_fillSubItems: function _fillSubItems(data){
		var self = this;
		function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){
			for(var i = 0; i < self._arrayOfAllItems.length; i++) {
				if(self._arrayOfAllItems[i] === anItem) { return; }
			}
			self._arrayOfAllItems.push(anItem);
			if(self._fillRecursive) {
				for(var attribute in anItem){
					var valueForAttribute = anItem[attribute];
					if(valueForAttribute){
						if(dojo.isArray(valueForAttribute)){
							var valueArray = valueForAttribute;
							for(var k = 0; k < valueArray.length; ++k){
								var singleValue = valueArray[k];
								if(valueIsAnItem(singleValue)){
									addItemAndSubItemsToArrayOfAllItems(singleValue);
								}
							}
						}else{
							if(valueIsAnItem(valueForAttribute)){
								addItemAndSubItemsToArrayOfAllItems(valueForAttribute);
							}
						}
					}
				}
			}
		}
		
		function valueIsAnItem(/* anything */ aValue){
			var isItem = (
				(aValue != null) &&
				(typeof aValue == "object") &&
				(!dojo.isArray(aValue)) &&
				(!dojo.isFunction(aValue)) &&
				(aValue.constructor == Object) &&
				(typeof aValue._reference == "undefined") && 
				(typeof aValue._type == "undefined") && 
				(typeof aValue._value == "undefined")
			);
			return isItem;
		}
		
		// We need to do some transformations to convert the data structure
		// that we read from the file into a format that will be convenient
		// to work with in memory.

		// Step 1: Walk through the object hierarchy and build a list of all items
		var item;
		var i;
		this._arrayOfAllItems = [];
		this._arrayOfTopLevelItems = data;
		for(i = 0; i < data.length; i++) {
			item = data[i];
			addItemAndSubItemsToArrayOfAllItems(item);
			item[this._rootItemPropName]=true;
		}
		
		
		// Step 2: Walk through all the attribute values of all the items, 
		// and replace single values with arrays.  For example, we change this:
		//		{ name:'Miss Piggy', pets:'Foo-Foo'}
		// into this:
		//		{ name:['Miss Piggy'], pets:['Foo-Foo']}
		// 
		// We also store the attribute names so we can validate our store  
		// reference and item id special properties for the O(1) isItem
		var allAttributeNames = {};
		var key;

		for(i = 0; i < this._arrayOfAllItems.length; ++i){
			item = this._arrayOfAllItems[i];
			for(key in item){
				if (key !== this._rootItemPropName)
				{
					var value = item[key];
					if(value !== null){
						if(!dojo.isArray(value)){
							item[key] = [value];
						}
					}else{
						item[key] = [null];
					}
				}
				allAttributeNames[key]=key;
			}
		}
		
		// Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName
		// This should go really fast, it will generally never even run the loop.
		while(allAttributeNames[this._storeRefPropName]){
			this._storeRefPropName += "_";
		}
		while(allAttributeNames[this._itemNumPropName]){
			this._itemNumPropName += "_";
		}
		while(allAttributeNames[this._reverseRefMap]){
			this._reverseRefMap += "_";
		}
		
		// Step 4: Some data files specify an optional 'identifier', which is 
		// the name of an attribute that holds the identity of each item. 
		// If this data file specified an identifier attribute, then build a 
		// hash table of items keyed by the identity of the items.
		var arrayOfValues;
		var identifier = this._identifierAttr; // <-----
		if(identifier !== Number){
			this._itemsByIdentity = {};
			for(i = 0; i < this._arrayOfAllItems.length; ++i){
				item = this._arrayOfAllItems[i];
				arrayOfValues = item[identifier];
				var identity = arrayOfValues[0];
				if(!this._itemsByIdentity[identity]){
					this._itemsByIdentity[identity] = item;
				}else{
					throw new Error("bepixeld.DWRReadStore:  The json data provided by the creation arguments is malformed.  Items within the list have identifier: [" + identifier + "].  Value collided: [" + identity + "]");
				}
			}
		}
		
		// Step 5: Walk through all the items, and set each item's properties 
		// for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true.
		for(i = 0; i < this._arrayOfAllItems.length; ++i){
			item = this._arrayOfAllItems[i];
			item[this._storeRefPropName] = this;
			item[this._itemNumPropName] = i;
		}
		
		// Step 6: We walk through all the attribute values of all the items,
		// looking for type/value literals and item-references.
		//
		// We replace item-references with pointers to items.  For example, we change:
		//		{ name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
		// into this:
		//		{ name:['Kermit'], friends:[miss_piggy] } 
		// (where miss_piggy is the object representing the 'Miss Piggy' item).
		//
		// We replace type/value pairs with typed-literals.  For example, we change:
		//		{ name:['Nelson Mandela'], born:[{_type:'Date', _value:'July 18, 1918'}] }
		// into this:
		//		{ name:['Kermit'], born:(new Date('July 18, 1918')) } 
		//
		// We also generate the associate map for all items for the O(1) isItem function.
		for(i = 0; i < this._arrayOfAllItems.length; ++i){
			item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
			for(key in item){
				arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}]
				for(var j = 0; j < arrayOfValues.length; ++j) {
					value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}}
					if(value !== null && typeof value == "object"){
						if(value._type && value._value){
							var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber'
							var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}}
							if(!mappingObj){ 
								throw new Error("bepixeld.DWRReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'");
							}else if(dojo.isFunction(mappingObj)){
								arrayOfValues[j] = new mappingObj(value._value);
							}else if(dojo.isFunction(mappingObj.deserialize)){
								arrayOfValues[j] = mappingObj.deserialize(value._value);
							}else{
								throw new Error("bepixeld.DWRReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function");
							}
						}
						if(value._reference){
							var referenceDescription = value._reference; // example: {name:'Miss Piggy'}
							if(!dojo.isObject(referenceDescription)){
								// example: 'Miss Piggy'
								// from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]}
								arrayOfValues[j] = this._itemsByIdentity[referenceDescription];
							}else{
								// example: {name:'Miss Piggy'}
								// from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] }
								for(var k = 0; k < this._arrayOfAllItems.length; ++k){
									var candidateItem = this._arrayOfAllItems[k];
									var found = true;
									for(var refKey in referenceDescription){
										if(candidateItem[refKey] != referenceDescription[refKey]){ 
											found = false; 
										}
									}
									if(found){ 
										arrayOfValues[j] = candidateItem; 
									}
								}
							}
							if(this.referenceIntegrity){
								var refItem = arrayOfValues[j];
								if(this.isItem(refItem)){
									this._addReferenceToMap(refItem, item, key);
								}
							}
						}else if(this.isItem(value)){
							//It's a child item (not one referenced through _reference).  
							//We need to treat this as a referenced item, so it can be cleaned up
							//in a write store easily.
							if(this.referenceIntegrity){
								this._addReferenceToMap(value, item, key);
							}
						}
					}
				}
			}
		}
	},
	
	_addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){
		 //	summary:
		 //		Method to add an reference map entry for an item and attribute.
		 //	description:
		 //		Method to add an reference map entry for an item and attribute. 		 //
		 //	refItem:
		 //		The item that is referenced.
		 //	parentItem:
		 //		The item that holds the new reference to refItem.
		 //	attribute:
		 //		The attribute on parentItem that contains the new reference.
		 
		 //Stub function, does nothing.  Real processing is in ItemFileWriteStore.
	},
	
	hasAttribute: function(	/* item */ item, /* attribute-name-string */ attribute){		
		return this.getValues(item, attribute).length > 0;
	},
	
	isItem: function(/* anything */ something){
		if(something && something[this._storeRefPropName] === this){
			if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){
				return true;
			}
		}
		return false; // Boolean
	},
	
	isItemLoaded: function(/* anything */ something) {
		return this.isItem(something);
	},
	
	loadItem: function(/* object */ keywordArgs){
		this._assertIsItem(keywordArgs.item);
	},
	
	getLabel: function(item){
		if(this._labelAttr && this.isItem(item)){
			return this.getValue(item,this._labelAttr); //String
		}
		return undefined; //undefined
    },
	
	getLabelAttributes: function(/* item */ item){
		if(this._labelAttr){
			return [this._labelAttr]; //array
		}
		return null; //null
	}, 
	
    close: function(request){
         // We do not have any special cleanup requirements
    },
    
    getIdentity: function(/* item */ item){
    	if(item === null) {
    		return null;
    	}
		if(this._identifierAttr === Number){
			return item[this._itemNumPropName]; // Number
		}else{
			var arrayOfValues = item[this._identifierAttr];
			if(arrayOfValues){
				return arrayOfValues[0]; // Object || String
			}
		}
		return null; // null
	},
	
	fetchItemByIdentity: function(/* Object */ keywordArgs){
		// Hasn't loaded yet, we have to trigger the load.
		if(!this._loadComplete){
			var self = this;			
			if(this._loadMethod ){
	            // If a new request comes in before the previous one
	            // is finished, we will ignore the request. 
	            if(this._loadInProgress){
					this._queuedFetches.push({args: keywordArgs});
				}else{
	                this._loadInProgress = true;
	                var callback = function(data){
						var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
	                    try{
							if(!dojo.isObject(data) || data.length === "undefined") {
								data = [data];
							}
	                        self._items = data;
							self._fillSubItems(data);
	                        self._loadComplete = true;
	                        self._loadInProgress = false;
							var item = self._getItemByIdentity(keywordArgs.identity);
							if(keywordArgs.onItem){
								keywordArgs.onItem.call(scope, item);
							}
							self._handleQueuedFetches();
							self.onNew(item,null);
	                    }catch(e){
	                        self._loadInProgress = false;
							self._loadComplete = true;
	                        if(keywordArgs.onError){								
								keywordArgs.onError.call(scope, e);
							}
	                    }
	                };
					if(this._loadArgument) {
	                	var arg = this._loadArgument;
	                	if(dojo.isFunction(arg)){
	                		arg = arg();
	                	}
	                	this._loadMethod(arg, callback);					
	                } else {
	                	this._loadMethod(callback);
	                }
	            }
	        }else{
	        	if(keywordArgs.onError){
					var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
					keywordArgs.onError.call(scope, new Error("DWR Method is undefined."));
				}
	        }		
		}
		else {
			// Already loaded.  We can just look it up and call back.
			var item = this._getItemByIdentity(keywordArgs.identity);
			if(keywordArgs.onItem){
				var scope =  keywordArgs.scope?keywordArgs.scope:dojo.global;
				keywordArgs.onItem.call(scope, item);
			}
		}
	},
	
	_getItemByIdentity: function(/* Object */ identity){
		//	summary:
		//		Internal function to look an item up by its identity map.
		var item = null;
		if(this._itemsByIdentity){
			item = this._itemsByIdentity[identity];
		}else{
			item = this._arrayOfAllItems[identity];
		}
		if(item === undefined){
			item = null;
		}
		return item; // Object
	},
	
	getIdentityAttributes: function(/* item */ item){
		if(this._identifierAttr === Number){
			return null; // null
		}else{
			return [this._identifierAttr]; // Array
		}
	},
	
	_forceLoad: function(){
		//	summary: 
		//		Internal function to force a load of the store if it hasn't occurred yet.  This is required
		//		for specific functions to work properly.  		
	},
	
	_assertIsItem: function(/* item */ item){
		if(!this.isItem(item)){ 			
			throw new Error("bepixeld.DWRReadStore: Invalid item argument.");
		}
	},

	_assertIsAttribute: function(/* attribute-name-string */ attribute){
		if(typeof attribute !== "string"){ 
			throw new Error("bepixeld.DWRReadStore: Invalid attribute argument.");
		}
	},
	
	itemToObject:function(item) {
		var jsItem = {};
		if (this.isItem(item)) {
			var attributes = this.getAttributes(item);
			if (attributes) {
				var i;
				for (i = 0; i < attributes.length; i++) {
					var value = this.getValue(item, attributes[i]);
					if (this.isItem(value)) {
						value = this.itemToObject(value);
					}
					jsItem[attributes[i]] = value;
				}
			}
		}
		return jsItem;
	},
	
	/* dojo.data.api.Notification */

	onSet: function(/* item */ item, 
					/*attribute-name-string*/ attribute, 
					/*object | array*/ oldValue,
					/*object | array*/ newValue){
		// summary: See dojo.data.api.Notification.onSet()
		
		// No need to do anything. This method is here just so that the 
		// client code can connect observers to it. 
	},

	onNew: function(/* item */ newItem, /*object?*/ parentInfo){
		// summary: See dojo.data.api.Notification.onNew()
		
		// No need to do anything. This method is here just so that the 
		// client code can connect observers to it. 
	},

	onDelete: function(/* item */ deletedItem){
		// summary: See dojo.data.api.Notification.onDelete()
		
		// No need to do anything. This method is here just so that the 
		// client code can connect observers to it. 
	}
});
dojo.extend(bepixeld.DWRReadStore,dojo.data.util.simpleFetch);

}
