dojo.provide("dojox.data.AppStore");

dojo.require("dojox.atom.io.Connection");
dojo.require("dojo.data.util.simpleFetch");
dojo.require("dojo.data.util.filter");

dojo.experimental("dojox.data.AppStore");

dojo.declare("dojox.data.AppStore",
	null,{

	url: "",			// So the parser can instantiate the store via markup.
	urlPreventCache: false, 	// Whether or not to pass the preventCache parameter to the connection
	xmethod: false,			// Whether to use X-Method-Override for PUT/DELETE.
	
	_atomIO: null,
	_feed: null,
	_requests: null,
	_processing: null,
	
	_updates: null,
	_adds: null,
	_deletes: null,
	
	constructor: function(/*Object*/args){
		//	summary:
		//		The APP data store.  
		//	description:
		//		The APP Store is instantiated either in markup or programmatically by supplying a
		//		url of the Collection to be used.
		//
		//	args:
		//		An anonymous object to initialize properties.  It expects the following values:
		//		url:		The url of the Collection to load.
		//		urlPreventCache:	Whether or not to append on cache prevention params (as defined by dojo.xhr*)
		
		if(args && args.url){
			this.url = args.url;
		}
		if(args && args.urlPreventCache){
			this.urlPreventCache = args.urlPreventCache;
		}
		if(!this.url){
			throw new Error("A URL is required to instantiate an APP Store object");
		}
	},
	
	_setFeed: function(feed, data) {
		//	summary: 
		//		Sets the internal feed using a dojox.atom.io.model.Feed object.
		//	description:
		//		Sets the internal feed using a dojox.atom.io.model.Feed object.  Also adds
		//		a property to the entries to track that they belong to this store. It 
		//		also parses stored requests (since we were waiting on a callback) and 
		//		executes those as well.
		//
		//	feed: dojox.atom.io.model.Feed object
		//		The Feed to use for this data store.
		//	data: unused
		//		Signature for this function is defined by AtomIO.getFeed, since this is a callback.
		this._feed = feed;
		var i;
		for(i=0; i<this._feed.entries.length; i++){
			this._feed.entries[i].store = this;
		}
		if(this._requests){
			for(i=0; i<this._requests.length; i++) {
				var request = this._requests[i];
				if(request.request && request.fh && request.eh) {
					this._finishFetchItems(request.request, request.fh, request.eh);
				} else if(request.clear) {
					this._feed = null;	
				} else if(request.add) {
					this._feed.addEntry(request.add);
				} else if(request.remove){
					this._feed.removeEntry(request.remove);
				}
			}
		}
		this._requests = null;
	},
	
	_getAllItems: function(){
		//	summary:
		//		Function to return all entries in the Feed as an array of items.
		//	description:
		//		Function to return all entries in the Feed as an array of items.
		//
		//	returns:
		//		Array of all entries in the feed.
		var items = [];
		for(var i=0; i<this._feed.entries.length; i++){
			items.push(this._feed.entries[i]);
		}
		return items; //array
	},
	
	_assertIsItem: function(/* item */ item){
		//	summary:
		//		This function tests whether the item is an item.
		//	description:
		//		This function tests whether the item passed in is indeed an item 
		//		in the store.
		//
		//	item:
		//		The item to test for being contained by the store.
		if(!this.isItem(item)){ 
			throw new Error("This error message is provided when a function is called in the following form: "
				+ "getAttribute(argument, attributeName).  The argument variable represents the member "
				+ "or owner of the object. The error is created when an item that does not belong "
				+ "to this store is specified as an argument.");
		}
	},

	_assertIsAttribute: function(/* String */ attribute){
		//	summary:
		//		This function tests whether the item is an attribute.
		//	description:
		//		This function tests whether the item passed in is indeed a valid 
		//		'attribute' like type for the store.
		//	attribute: 
		//		The attribute to test for being contained by the store.
		//
		// returns: Returns a boolean indicating whether this is a valid attribute.
		if(typeof attribute !== "string"){ 
			throw new Error("The attribute argument must be a string. The error is created "
			+ "when a different type of variable is specified such as an array or object.");
		}

		for(var key in dojox.atom.io.model._actions){
			if(key == attribute){
				return true;
			}
		}
		return false;
	},
	
	_addUpdate: function(/* Object */ update){
		//	summary:
		//		Internal function to add an updated entry to our updates array
		//	description:
		//		Internal function to add an updated entry to our updates array
		//
		//	update: dojox.atom.io.model.Entry object
		//		The updated Entry we've changed.
		if(!this._updates){
			this._updates = [update];
		}else{
			this._updates.push(update);
		}
	},

/***************************************
     dojo.data.api.Read API
***************************************/
	
	getValue: function(	/* item */ item, 
						/* attribute-name-string */ attribute, 
						/* value? */ defaultValue){
		//	summary: 
		//      See dojo.data.api.Read.getValue()
		var values = this.getValues(item, attribute);
		return (values.length > 0)?values[0]:defaultValue; //Object || int || Boolean
	},

	getValues: function(/* item */ item, 
						/* attribute-name-string */ attribute){
		//	summary: 
		//		See dojo.data.api.Read.getValues()

		this._assertIsItem(item);
		var flag = this._assertIsAttribute(attribute);

		if(flag) {
			if((attribute === "author" || attribute === "contributor" || attribute === "link") && item[attribute+"s"]){
				return item[attribute+"s"];
			}
			if(attribute === "category" && item.categories){
				return item.categories;
			}
			if(item[attribute]){
				item = item[attribute];
				if(item.declaredClass == "dojox.atom.io.model.Content"){
					return [item.value];			
				}
				return [item] ;
			}
		}
		return []; //Array
	},

	getAttributes: function(/* item */ item){
		//	summary: 
		//		See dojo.data.api.Read.getAttributes()
		this._assertIsItem(item);
		var attributes = [];
		for(var key in dojox.atom.io.model._actions){
			if(this.hasAttribute(item, key)){
				attributes.push(key);
			}
		}
		return attributes; //Array
	},

	hasAttribute: function(	/* item */ item,
							/* attribute-name-string */ attribute){
		//	summary: 
		//		See dojo.data.api.Read.hasAttribute()
		return this.getValues(item, attribute).length > 0;
	},

	containsValue: function(/* item */ item, 
							/* attribute-name-string */ attribute, 
							/* anything */ value){
		//	summary: 
		//		See dojo.data.api.Read.containsValue()
		var regexp = undefined;
		if(typeof value === "string"){
		   regexp = dojo.data.util.filter.patternToRegExp(value, false);
		}
		return this._containsValue(item, attribute, value, regexp); //boolean.
	},

	_containsValue: function(	/* item */ item, 
								/* attribute-name-string */ attribute, 
								/* anything */ value,
								/* RegExp?*/ regexp,
								/* Boolean?*/ trim){
		//	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'
		var values = this.getValues(item, attribute);
		for(var i = 0; i < values.length; ++i){
			var possibleValue = values[i];
			if(typeof possibleValue === "string" && regexp){
				if(trim) {
					possibleValue = possibleValue.replace(new RegExp(/^\s+/),""); // START
					possibleValue = possibleValue.replace(new RegExp(/\s+$/),""); // END
				}
				possibleValue = possibleValue.replace(/\r|\n|\r\n/g, "");
				return (possibleValue.match(regexp) !== null);
			}else{
				//Non-string matching.
				if(value === possibleValue){
					return true; // Boolean
				}
			}
		}
		return false; // Boolean
	},

	isItem: function(/* anything */ something){
		//	summary: 
		//		See dojo.data.api.Read.isItem()
		return something && something.store && something.store === this; //boolean
	},

	isItemLoaded: function(/* anything */ something){
		//	summary: 
		//		See dojo.data.api.Read.isItemLoaded()
		return this.isItem(something);
	},

	loadItem: function(/* Object */ keywordArgs){
		//	summary: 
		//		See dojo.data.api.Read.loadItem()
		this._assertIsItem(keywordArgs.item);
	},
	
	_fetchItems: function(request, fetchHandler, errorHandler) {
		//	summary: Fetch items (Atom entries) that match to a query
		//	description: Fetch items (Atom entries) that match to a query
		//	request:
		//		A request object
		//	fetchHandler:
		//		A function to call for fetched items
		//	errorHandler:
		//		A function to call on error
		if(this._feed){
			this._finishFetchItems(request, fetchHandler, errorHandler);
		}else{
			var flag = false;
			if(!this._requests){
				this._requests = [];
				flag = true;
			}
			this._requests.push({request: request, fh: fetchHandler, eh: errorHandler});
			if(flag){
				this._atomIO = new dojox.atom.io.Connection(false, this.urlPreventCache);
				this._atomIO.getFeed(this.url,this._setFeed, null, this);
			}
		}
	},
		
	_finishFetchItems: function(request, fetchHandler, errorHandler){
		// summary: Internal function for finishing a fetch request.
		// description: Internal function for finishing a fetch request.  Needed since the feed
		//				might not have been loaded, so we finish the fetch in a callback.
		//
		//	request:
		//		A request object
		//	fetchHandler:
		//		A function to call for fetched items
		//	errorHandler:
		//		A function to call on error
		var items = null;
		var arrayOfAllItems = this._getAllItems();
		if(request.query){
			var ignoreCase = request.queryOptions ? request.queryOptions.ignoreCase : false; 
			items = [];

			//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 = {};
			var key;
			var value;
			for(key in request.query){
				value = request.query[key]+'';
				if(typeof value === "string"){
					regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase);
				}
			}

			for(var i = 0; i < arrayOfAllItems.length; ++i){
				var match = true;
				var candidateItem = arrayOfAllItems[i];
				for(key in request.query){
					value = request.query[key]+'';
					if (!this._containsValue(candidateItem, key, value, regexpList[key], request.trim)){
						match = false;
					}
				}
				if(match){
					items.push(candidateItem);
				}
			}
		}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 listsand sort without affecting each other.
			if(arrayOfAllItems.length> 0){
				items = arrayOfAllItems.slice(0,arrayOfAllItems.length); 
			}
		}
		try {
			fetchHandler(items, request);
		} catch(e){
			errorHandler(e, request);
		}
	},

	getFeatures: function(){
		//	summary: 
		//		See dojo.data.api.Read.getFeatures()
		return {
			'dojo.data.api.Read': true,
			'dojo.data.api.Write': true,
			'dojo.data.api.Identity': true
		};
	},
	
	close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
		//	summary:
		//		See dojo.data.api.Read.close()
		// nothing to do here!
		this._feed = null;
	},

	getLabel: function(/* item */ item){
		//	summary: 
		//		See dojo.data.api.Read.getLabel()
		if(this.isItem(item)){
			return this.getValue(item, "title", "No Title");
		}
		return undefined;
	},

	getLabelAttributes: function(/* item */ item){
		//	summary: 
		//		See dojo.data.api.Read.getLabelAttributes()
		return ["title"];
	},

/***************************************
     dojo.data.api.Identity API
***************************************/

	getIdentity: function(/* item */ item){
		//	summary: 
		//		See dojo.data.api.Identity.getIdentity()
		this._assertIsItem(item);
		return this.getValue(item, "id");
	},

	getIdentityAttributes: function(/* item */ item){
		 //	summary: 
		 //		See dojo.data.api.Identity.getIdentityAttributes()
		 return ["id"];
	},

	fetchItemByIdentity: function(keywordArgs){
		//	summary: 
		//		See dojo.data.api.Identity.fetchItemByIdentity()

		this._fetchItems({query:{id:keywordArgs.identity}, onItem: keywordArgs.onItem, scope: keywordArgs.scope},
			function(items, request) {
				var scope = request.scope;
				if(!scope){
					scope = dojo.global;
				}
				if(items.length < 1){
					request.onItem.call(scope, null);
				}else{
					request.onItem.call(scope, items[0]);
				}
		}, keywordArgs.onError);
	},

/***************************************
     dojo.data.api.Identity API
***************************************/

	newItem: function(/* Object? */ keywordArgs){
		//	summary: 
		//		See dojo.data.api.Write.newItem()
		var entry = new dojox.atom.io.model.Entry();
		var value = null;
		var temp = null;
		var i;
		for(var key in keywordArgs){
			if(this._assertIsAttribute(key)) {
				value = keywordArgs[key];
				switch(key){
					case "link":
						for(i in value) {
							temp = value[i];
							entry.addLink(temp.href,temp.rel,temp.hrefLang,temp.title,temp.type);
						}
						break;
					case "author":
						for(i in value) {
							temp = value[i];
							entry.addAuthor(temp.name, temp.email, temp.uri);
						}
						break;
					case "contributor":
						for(i in value) {
							temp = value[i];
							entry.addContributor(temp.name, temp.email, temp.uri);
						}
						break;
					case "category":
						for(i in value){
							temp = value[i];
							entry.addCategory(temp.scheme, temp.term, temp.label);
						}
						break;
					case "icon":
					case "id":
					case "logo":
					case "xmlBase":
					case "rights":
						entry[key] = value;
						break;
					case "updated":
					case "published":
					case "issued":
					case "modified":
						entry[key] = dojox.atom.io.model.util.createDate(value);
						break;
					case "content":
					case "summary":
					case "title":
					case "subtitle":
						entry[key] = new dojox.atom.io.model.Content(key);
						entry[key].value = value;
						break;
					default:
						entry[key] = value;
						break;
				}
			}
		}
		entry.store = this;
		entry.isDirty = true;

		if(!this._adds){
			this._adds = [entry];
		}else{
			this._adds.push(entry);
		}

		if(this._feed){
			this._feed.addEntry(entry);
		}else{
			if(this._requests){
				this._requests.push({add:entry});
			}else{
				this._requests = [{add:entry}];
				this._atomIO = new dojox.atom.io.Connection(false, this.urlPreventCache);
				this._atomIO.getFeed(this.url,dojo.hitch(this,this._setFeed));
			}
		}
		return true;
	},

	deleteItem: function(/* item */ item){
		//	summary: 
		//		See dojo.data.api.Write.deleteItem()
		this._assertIsItem(item);

		if(!this._deletes){
			this._deletes = [item];
		}else{
			this._deletes.push(item);
		}

		if(this._feed){
			this._feed.removeEntry(item);
		}else{
			if(this._requests){
				this._requests.push({remove:item});
			}else{
				this._requests = [{remove:item}];
				this._atomIO = new dojox.atom.io.Connection(false, this.urlPreventCache);
				this._atomIO.getFeed(this.url,dojo.hitch(this,this._setFeed));
			}
		}
		item = null;
		return true;
	},

	setValue: function(	/* item */ item, 
						/* string */ attribute,
						/* almost anything */ value){
		//	summary: 
		//		See dojo.data.api.Write.setValue()
		this._assertIsItem(item);
		
		var update = {item: item};
		if(this._assertIsAttribute(attribute)) {
			switch(attribute){
				case "link":
					update.links = item.links;
					this._addUpdate(update);
					item.links = null;
					item.addLink(value.href,value.rel,value.hrefLang,value.title,value.type);
					item.isDirty = true;
					return true;
				case "author":
					update.authors = item.authors;
					this._addUpdate(update);
					item.authors = null;
					item.addAuthor(value.name, value.email, value.uri);
					item.isDirty = true;
					return true;
				case "contributor":
					update.contributors = item.contributors;
					this._addUpdate(update);
					item.contributors = null;
					item.addContributor(value.name, value.email, value.uri);
					item.isDirty = true;
					return true;
				case "category":
					update.categories = item.categories;
					this._addUpdate(update);
					item.categories = null;
					item.addCategory(value.scheme, value.term, value.label);
					item.isDirty = true;
					return true;
				case "icon":
				case "id":
				case "logo":
				case "xmlBase":
				case "rights":
					update[attribute] = item[attribute];
					this._addUpdate(update);
					item[attribute] = value;
					item.isDirty = true;
					return true;
				case "updated":
				case "published":
				case "issued":
				case "modified":
					update[attribute] = item[attribute];
					this._addUpdate(update);
					item[attribute] = dojox.atom.io.model.util.createDate(value);
					item.isDirty = true;
					return true;
				case "content":
				case "summary":
				case "title":
				case "subtitle":
					update[attribute] = item[attribute];
					this._addUpdate(update);
					item[attribute] = new dojox.atom.io.model.Content(attribute);
					item[attribute].value = value;
					item.isDirty = true;
					return true;
				default:
					update[attribute] = item[attribute];
					this._addUpdate(update);
					item[attribute] = value;
					item.isDirty = true;
					return true;
			}
		}
		return false;
	},

	setValues: function(/* item */ item,
						/* string */ attribute, 
						/* array */ values){
		//	summary: 
		//		See dojo.data.api.Write.setValues()
		if(values.length === 0){
			return this.unsetAttribute(item, attribute);
		}
		this._assertIsItem(item);
		
		var update = {item: item};
		var value;
		var i;
		if(this._assertIsAttribute(attribute)) {
			switch(attribute){
				case "link":
					update.links = item.links;
					item.links = null;
					for(i in values) {
						value = values[i];
						item.addLink(value.href,value.rel,value.hrefLang,value.title,value.type);
					}
					item.isDirty = true;
					return true;
				case "author":
					update.authors = item.authors;
					item.authors = null;
					for(i in values) {
						value = values[i];
						item.addAuthor(value.name, value.email, value.uri);
					}
					item.isDirty = true;
					return true;
				case "contributor":
					update.contributors = item.contributors;
					item.contributors = null;
					for(i in values) {
						value = values[i];
						item.addContributor(value.name, value.email, value.uri);
					}
					item.isDirty = true;
					return true;
				case "categories":
					update.categories = item.categories;
					item.categories = null;
					for(i in values) {
						value = values[i];
						item.addCategory(value.scheme, value.term, value.label);
					}
					item.isDirty = true;
					return true;
				case "icon":
				case "id":
				case "logo":
				case "xmlBase":
				case "rights":
					update[attribute] = item[attribute];
					item[attribute] = values[0];
					item.isDirty = true;
					return true;
				case "updated":
				case "published":
				case "issued":
				case "modified":
					update[attribute] = item[attribute];
					item[attribute] = dojox.atom.io.model.util.createDate(values[0]);
					item.isDirty = true;
					return true;
				case "content":
				case "summary":
				case "title":
				case "subtitle":
					update[attribute] = item[attribute];
					item[attribute] = new dojox.atom.io.model.Content(attribute);
					item[attribute].values[0] = values[0];
					item.isDirty = true;
					return true;
				default:
					update[attribute] = item[attribute];
					item[attribute] = values[0];
					item.isDirty = true;
					return true;
			}
		}
		this._addUpdate(update);
		return false;
	},

	unsetAttribute: function(	/* item */ item, 
								/* string */ attribute){
		//	summary: 
		//		See dojo.data.api.Write.unsetAttribute()
		this._assertIsItem(item);
		if(this._assertIsAttribute(attribute)) {
			if(item[attribute] !== null) {
				var update = {item: item};
				switch(attribute){
					case "author":
					case "contributor":
					case "link":
						update[attribute+"s"] = item[attribute+"s"];
						break;
					case "category":
						update.categories = item.categories;
						break;
					default:
						update[attribute] = item[attribute];
						break;
				}
				item.isDirty = true;
				item[attribute] = null;
				this._addUpdate(update);
				return true;
			}
		}
		return false; // boolean
	},

	save: function(/* object */ keywordArgs){
		//	summary: 
		//		See dojo.data.api.Write.save()
		//	keywordArgs:
		//		{
		//			onComplete: function
		//			onError: function
		//			scope: object
		//		}
		var i;
		for(i in this._adds){
			this._atomIO.addEntry(this._adds[i], null, function(){}, keywordArgs.onError, false, keywordArgs.scope);
		}
			
		this._adds = null;
		
		for(i in this._updates){
			this._atomIO.updateEntry(this._updates[i].item, function(){}, keywordArgs.onError, false, this.xmethod, keywordArgs.scope);
		}
			
		this._updates = null;
		
		for(i in this._deletes){
			this._atomIO.removeEntry(this._deletes[i], function(){}, keywordArgs.onError, this.xmethod, keywordArgs.scope);
		}
			
		this._deletes = null;
		
		this._atomIO.getFeed(this.url,dojo.hitch(this,this._setFeed));
		
		if(keywordArgs.onComplete){
			var scope = keywordArgs.scope || dojo.global;
			keywordArgs.onComplete.call(scope);
		}
	},

	revert: function(){
		//	summary: 
		//		See dojo.data.api.Write.revert()
		var i;
		for(i in this._adds){
			this._feed.removeEntry(this._adds[i]);
		}
			
		this._adds = null;
		
		var update, item, key;
		for(i in this._updates){
			update = this._updates[i];
			item = update.item;
			for(key in update){
				if(key !== "item"){
					item[key] = update[key];
				}
			}
		}
		this._updates = null;
		
		for(i in this._deletes){
			this._feed.addEntry(this._deletes[i]);
		}
		this._deletes = null;
		return true;
	},

	isDirty: function(/* item? */ item){
		//	summary: 
		//		See dojo.data.api.Write.isDirty()
		if(item) {
			this._assertIsItem(item);
			return item.isDirty?true:false; //boolean
		}
		return (this._adds!==null || this._updates!==null); //boolean
	}
});
dojo.extend(dojox.data.AppStore,dojo.data.util.simpleFetch);
