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

dojo.require("dojo.data.util.simpleFetch");
dojo.require("dojo.data.util.filter");
dojo.require("dojo.date.stamp");

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

dojo.declare("dojox.data.AtomReadStore", null, {
	//	summary:
	//		A read only data store for Atom XML based services or documents
	//	description:
	//		A data store for Atom XML based services or documents.	This store is still under development
	//		and doesn't support wildcard filtering yet.	Attribute filtering is limited to category or id.

	constructor: function(/* object */ args){
		//	summary:
		//		Constructor for the AtomRead store.
		//	args:
		//		An anonymous object to initialize properties.	It expects the following values:
		//		url:			The url to a service or an XML document that represents the store
		//		unescapeHTML:	A boolean to specify whether or not to unescape HTML text
		//		sendQuery:		A boolean indicate to add a query string to the service URL

		if(args){
			this.url = args.url;
			this.rewriteUrl = args.rewriteUrl;
			this.label = args.label || this.label;
			this.sendQuery = (args.sendQuery || args.sendquery || this.sendQuery);
			this.unescapeHTML = args.unescapeHTML;
			if("urlPreventCache" in args){
				this.urlPreventCache = args.urlPreventCache?true:false;
						}
		}
		if(!this.url){
			throw new Error("AtomReadStore: a URL must be specified when creating the data store");
		}
	},

	//Values that may be set by the parser.
	//Ergo, have to be instantiated to something
	//So the parser knows how to set them.
	url: "",

	label: "title",

	sendQuery: false,

	unescapeHTML: false,

	//Configurable preventCache option for the URL.
	urlPreventCache: false,

	/* dojo.data.api.Read */

	getValue: function(/* item */ item, /* attribute || attribute-name-string */ attribute, /* value? */ defaultValue){
		//	summary:
		//		Return an attribute value
		//	description:
		//		'item' must be an instance of an object created by the AtomReadStore instance.
		//		Accepted attributes are id, subtitle, title, summary, content, author, updated,
		//		published, category, link and alternate
		//	item:
		//		An item returned by a call to the 'fetch' method.
		//	attribute:
		//		A attribute of the Atom Entry
		//	defaultValue:
		//		A default value
		//	returns:
		//		An attribute value found, otherwise 'defaultValue'
		this._assertIsItem(item);
		this._assertIsAttribute(attribute);
		this._initItem(item);
		attribute = attribute.toLowerCase();
		//If the attribute has previously been retrieved, then return it
		if(!item._attribs[attribute] && !item._parsed){
			this._parseItem(item);
			item._parsed = true;
		}
		var retVal = item._attribs[attribute];

		if(!retVal && attribute == "summary"){
			var content = this.getValue(item, "content");
			var regexp = new RegExp("/(<([^>]+)>)/g", "i");
			var text = content.text.replace(regexp,"");
			retVal = {
				text: text.substring(0, Math.min(400, text.length)),
				type: "text"
			};
			item._attribs[attribute] = retVal;
		}

		if(retVal && this.unescapeHTML){
			if((attribute == "content" || attribute == "summary" || attribute == "subtitle") && !item["_"+attribute+"Escaped"]){
				retVal.text = this._unescapeHTML(retVal.text);
				item["_"+attribute+"Escaped"] = true;
			}
		}
		return retVal ? dojo.isArray(retVal) ? retVal[0]: retVal : defaultValue;
	},

	getValues: function(/* item */ item, /* attribute || attribute-name-string */ attribute){
		//	summary:
		//		Return an attribute value
		//	description:
		//		'item' must be an instance of an object created by the AtomReadStore instance.
		//		Accepted attributes are id, subtitle, title, summary, content, author, updated,
		//		published, category, link and alternate
		//	item:
		//		An item returned by a call to the 'fetch' method.
		//	attribute:
		//		A attribute of the Atom Entry
		//	returns:
		//		An array of values for the attribute value found, otherwise 'defaultValue'
		this._assertIsItem(item);
		this._assertIsAttribute(attribute);
		this._initItem(item);
		attribute = attribute.toLowerCase();
		//If the attribute has previously been retrieved, then return it
		if(!item._attribs[attribute]){
			this._parseItem(item);
		}
		var retVal = item._attribs[attribute];
		return retVal ? ((retVal.length !== undefined && typeof(retVal) !== "string") ? retVal : [retVal]) : undefined;
	},

	getAttributes: function(/* item */ item){
		//	summary:
		//		Return an array of attribute names
		// 	description:
		//		'item' must be have been created by the AtomReadStore instance.
		//		tag names of child elements and XML attribute names of attributes
		//		specified to the element are returned along with special attribute
		//		names applicable to the element including "tagName", "childNodes"
		//		if the element has child elements, "text()" if the element has
		//		child text nodes, and attribute names in '_attributeMap' that match
		//		the tag name of the element.
		//	item:
		//		An XML element
		//	returns:
		//		An array of attributes found
		this._assertIsItem(item);
		if(!item._attribs){
			this._initItem(item);
			this._parseItem(item);
		}
		var attrNames = [];
		for(var x in item._attribs){
			attrNames.push(x);
		}
		return attrNames; //array
	},

	hasAttribute: function(/* item */ item, /* attribute || attribute-name-string */ attribute){
		//	summary:
		//		Check whether an element has the attribute
		//	item:
		//		'item' must be created by the AtomReadStore instance.
		//	attribute:
		//		An attribute of an Atom Entry item.
		//	returns:
		//		True if the element has the attribute, otherwise false
		return (this.getValue(item, attribute) !== undefined); //boolean
	},

	containsValue: function(/* item */ item, /* attribute || attribute-name-string */ attribute, /* anything */ value){
		//	summary:
		//		Check whether the attribute values contain the value
		//	item:
		//		'item' must be an instance of a dojox.data.XmlItem from the store instance.
		//	attribute:
		//		A tag name of a child element, An XML attribute name or one of
		//		special names
		//	returns:
		//		True if the attribute values contain the value, otherwise false
		var values = this.getValues(item, attribute);
		for(var i = 0; i < values.length; i++){
			if((typeof value === "string")){
				if(values[i].toString && values[i].toString() === value){
					return true;
				}
			}else if(values[i] === value){
				return true; //boolean
			}
		}
		return false;//boolean
	},

	isItem: function(/* anything */ something){
		//	summary:
		//		Check whether the object is an item (XML element)
		//	item:
		//		An object to check
		// 	returns:
		//		True if the object is an XML element, otherwise false
		if(something && something.element && something.store && something.store === this){
			return true; //boolean
		}
		return false; //boolran
	},

	isItemLoaded: function(/* anything */ something){
		//	summary:
		//		Check whether the object is an item (XML element) and loaded
		//	item:
		//		An object to check
		//	returns:
		//		True if the object is an XML element, otherwise false
		return this.isItem(something); //boolean
	},

	loadItem: function(/* object */ keywordArgs){
		//	summary:
		//		Load an item (XML element)
		//	keywordArgs:
		//		object containing the args for loadItem.	See dojo.data.api.Read.loadItem()
	},

	getFeatures: function(){
		//	summary:
		//		Return supported data APIs
		//	returns:
		//		"dojo.data.api.Read" and "dojo.data.api.Write"
		var features = {
			"dojo.data.api.Read": true
		};
		return features; //array
	},

	getLabel: function(/* item */ item){
		//	summary:
		//		See dojo.data.api.Read.getLabel()
		if((this.label !== "") && this.isItem(item)){
			var label = this.getValue(item,this.label);
			if(label && label.text){
				return label.text;
			}else if(label){
				return label.toString();
			}else{
				return undefined;
			}
		}
		return undefined; //undefined
	},

	getLabelAttributes: function(/* item */ item){
		//	summary:
		//		See dojo.data.api.Read.getLabelAttributes()
		if(this.label !== ""){
			return [this.label]; //array
		}
		return null; //null
	},

	getFeedValue: function(attribute, defaultValue){
		// summary:
		//		Non-API method for retrieving values regarding the Atom feed,
		//		rather than the Atom entries.
		var values = this.getFeedValues(attribute, defaultValue);
		if(dojo.isArray(values)){
			return values[0];
		}
		return values;
	},

	getFeedValues: function(attribute, defaultValue){
		// summary:
		//		Non-API method for retrieving values regarding the Atom feed,
		//		rather than the Atom entries.
		if(!this.doc){
			return defaultValue;
		}
		if(!this._feedMetaData){
			this._feedMetaData = {
				element: this.doc.getElementsByTagName("feed")[0],
				store: this,
				_attribs: {}
			};
			this._parseItem(this._feedMetaData);
		}
		return this._feedMetaData._attribs[attribute] || defaultValue;
	},

	_initItem: function(item){
		// summary:
		//		Initializes an item before it can be parsed.
		if(!item._attribs){
			item._attribs = {};
		}
	},

	_fetchItems: function(request, fetchHandler, errorHandler){
		// summary:
		//		Retrieves the items from the Atom XML document.
		var url = this._getFetchUrl(request);
		if(!url){
			errorHandler(new Error("No URL specified."));
			return;
		}
		var localRequest = (!this.sendQuery ? request : null); // use request for _getItems()

		var _this = this;
		var docHandler = function(data){
			_this.doc = data;
			var items = _this._getItems(data, localRequest);
			var query = request.query;
			if(query){
				if(query.id){
					items = dojo.filter(items, function(item){
						return (_this.getValue(item, "id") == query.id);
					});
				}else if(query.category){
					items = dojo.filter(items, function(entry){
						var cats = _this.getValues(entry, "category");
						if(!cats){
							return false;
						}
						return dojo.some(cats, "return item.term=='"+query.category+"'");
					});
				}
			}

			if(items && items.length > 0){
				fetchHandler(items, request);
			}else{
				fetchHandler([], request);
			}
		};

		if(this.doc){
			docHandler(this.doc);
		}else{
			var getArgs = {
				url: url,
				handleAs: "xml",
				preventCache: this.urlPreventCache
			};
			var getHandler = dojo.xhrGet(getArgs);
			getHandler.addCallback(docHandler);

			getHandler.addErrback(function(data){
				errorHandler(data, request);
			});
		}
	},

	_getFetchUrl: function(request){
		if(!this.sendQuery){
			return this.url;
		}
		var query = request.query;
		if(!query){
			return this.url;
		}
		if(dojo.isString(query)){
			return this.url + query;
		}
		var queryString = "";
		for(var name in query){
			var value = query[name];
			if(value){
				if(queryString){
					queryString += "&";
				}
				queryString += (name + "=" + value);
			}
		}
		if(!queryString){
			return this.url;
		}
		//Check to see if the URL already has query params or not.
		var fullUrl = this.url;
		if(fullUrl.indexOf("?") < 0){
			fullUrl += "?";
		}else{
			fullUrl += "&";
		}
		return fullUrl + queryString;
	},

	_getItems: function(document, request){
		// summary:
		//		Parses the document in a first pass
		if(this._items){
			return this._items;
		}
		var items = [];
		var nodes = [];

		if(document.childNodes.length < 1){
			this._items = items;
			console.log("dojox.data.AtomReadStore: Received an invalid Atom document. Check the content type header");
			return items;
		}

		var feedNodes = dojo.filter(document.childNodes, "return item.tagName && item.tagName.toLowerCase() == 'feed'");

		var query = request.query;

		if(!feedNodes || feedNodes.length != 1){
			console.log("dojox.data.AtomReadStore: Received an invalid Atom document, number of feed tags = " + (feedNodes? feedNodes.length : 0));
			return items;
		}

		nodes = dojo.filter(feedNodes[0].childNodes, "return item.tagName && item.tagName.toLowerCase() == 'entry'");

		if(request.onBegin){
			request.onBegin(nodes.length, this.sendQuery ? request : {});
		}

		for(var i = 0; i < nodes.length; i++){
			var node = nodes[i];
			if(node.nodeType != 1 /*ELEMENT_NODE*/){
				continue;
			}
			items.push(this._getItem(node));
		}
		this._items = items;
		return items;
	},

	close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
		 //	summary:
		 //		See dojo.data.api.Read.close()
	},

/* internal API */

	_getItem: function(element){
		return {
			element: element,
			store: this
		};
	},

	_parseItem: function(item){
		var attribs = item._attribs;
		var _this = this;
		var text, type;

		function getNodeText(node){
			var txt = node.textContent || node.innerHTML || node.innerXML;
			if(!txt && node.childNodes[0]){
				var child = node.childNodes[0];
				if(child && (child.nodeType == 3 || child.nodeType == 4)){
					txt = node.childNodes[0].nodeValue;
				}
			}
			return txt;
		}
		function parseTextAndType(node){
			return {text: getNodeText(node),type: node.getAttribute("type")};
		}
		dojo.forEach(item.element.childNodes, function(node){
			var tagName = node.tagName ? node.tagName.toLowerCase() : "";
			switch(tagName){
				case "title":
					attribs[tagName] = {
						text: getNodeText(node),
						type: node.getAttribute("type")
					}; break;
				case "subtitle":
				case "summary":
				case "content":
					attribs[tagName] = parseTextAndType(node);
					break;
				case "author":
					var nameNode ,uriNode;
					dojo.forEach(node.childNodes, function(child){
						if(!child.tagName){
							return;
						}
						switch(child.tagName.toLowerCase()){
							case "name":
								nameNode = child;
								break;
							case "uri": 
								uriNode = child; 
								break;
						}
					});
					var author = {};
					if(nameNode && nameNode.length == 1){
						author.name = getNodeText(nameNode[0]);
					}
					if(uriNode && uriNode.length == 1){
						author.uri = getNodeText(uriNode[0]);
					}
					attribs[tagName] = author;
					break;
				case "id": 
					attribs[tagName] = getNodeText(node); 
					break;
				case "updated": 
					attribs[tagName] = dojo.date.stamp.fromISOString(getNodeText(node) );
					break;
				case "published": 
					attribs[tagName] = dojo.date.stamp.fromISOString(getNodeText(node));
					break;
				case "category":
					if(!attribs[tagName]){
						attribs[tagName] = [];
					}
					attribs[tagName].push({scheme:node.getAttribute("scheme"), term: node.getAttribute("term")});
					break;
				case "link":
					if(!attribs[tagName]){
						attribs[tagName] = [];
					}
					var link = {
						rel: node.getAttribute("rel"),
						href: node.getAttribute("href"),
						type: node.getAttribute("type")};
					attribs[tagName].push(link);

					if(link.rel == "alternate"){
						attribs["alternate"] = link;
					}
					break;
				default:
					break;
			}
		});
	},

	_unescapeHTML : function(text){
		//Replace HTML character codes with their unencoded equivalents, e.g. &#8217; with '
		text = text.replace(/&#8217;/m , "'").replace(/&#8243;/m , "\"").replace(/&#60;/m,">").replace(/&#62;/m,"<").replace(/&#38;/m,"&");
		return text;
	},

	_assertIsItem: function(/* item */ item){
		//	summary:
		//		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("dojox.data.AtomReadStore: Invalid item argument.");
		}
	},

	_assertIsAttribute: function(/* attribute-name-string */ attribute){
		//	summary:
		//		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.
		if(typeof attribute !== "string"){
			throw new Error("dojox.data.AtomReadStore: Invalid attribute argument.");
		}
	}
});
dojo.extend(dojox.data.AtomReadStore,dojo.data.util.simpleFetch);
