using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Web;
using System.Xml;
 
namespace SemWeb.Remote {

	public class SparqlHttpSource : SelectableSource {
	
		string url;
		
		public SparqlHttpSource(string url) {
			this.url = url;
		}

		public bool Distinct { get { return true; } }
		
		public bool Contains(Statement template) {
			return Select(template, null, true);
		}
		
		public void Select(StatementSink sink) {
			Select(Statement.All, sink);
		}
		
		public void Select(Statement template, StatementSink sink) {
			Select(template, sink, false);
		}
		
		bool Select(Statement template, StatementSink sink, bool ask) {
			return Select(
				template.Subject == null ? null : new Entity[] { template.Subject },
				template.Predicate == null ? null : new Entity[] { template.Predicate },
				template.Object == null ? null : new Resource[] { template.Object },
				template.Meta == null ? null : new Entity[] { template.Meta },
				null,
				0,
				sink,
				ask
				);
		}
		
		public void Select(SelectFilter filter, StatementSink sink) {
			Select(filter.Subjects, filter.Predicates, filter.Objects, filter.Metas, filter.LiteralFilters, filter.Limit, sink, false);
		}
		
		bool Select(Entity[] subjects, Entity[] predicates, Resource[] objects, Entity[] metas, LiteralFilter[] litFilters, int limit, StatementSink sink, bool ask) {
			// TODO: Change meta into named graphs.  Anything but a null or DefaultMeta
			// meta returns no statements immediately.
			if (metas != null && (metas.Length != 1 || metas[0] != Statement.DefaultMeta))
				return false;
		
			string query;
			bool nonull = false;
		
			if (subjects != null && subjects.Length == 1
				&& predicates != null && predicates.Length == 1
				&& objects != null && objects.Length == 1) {
				query = "ASK WHERE { " + S(subjects[0], null) + " " + S(predicates[0], null) + " " + S(objects[0], null) + "}";
				nonull = true;
			} else {
				if (ask)
					query = "ASK";
				else
					query = "SELECT *";
				query += " WHERE { ";
				query += S(subjects, "subject");
				query += " ";
				query += S(predicates, "predicate");
				query += " ";
				query += S(objects, "object");
				query += " . ";
				query += SL(subjects, "subject");
				query += SL(predicates, "predicate");
				query += SL(objects, "object");
				query += " }";
				
				// TODO: Pass literal filters to server.
			}
			
			if (limit >= 1)
				query += " LIMIT " + limit;
			
			XmlDocument result = Load(query);
			
			if (ask || nonull) {
				foreach (XmlElement boolean in result.DocumentElement) {
					if (boolean.Name != "boolean") continue;
					bool ret = boolean.InnerText == "true";
					if (ask)
						return ret;
					else if (ret)
						sink.Add(new Statement(subjects[0], predicates[0], objects[0]));
					return false;
				}
				throw new ApplicationException("Invalid server response: No boolean node.");
			}
			
			XmlElement bindings = null;
			foreach (XmlElement e in result.DocumentElement)
				if (e.Name == "results")
					bindings = e;
			if (bindings == null)
				throw new ApplicationException("Invalid server response: No result node.");
			
			MemoryStore distinctCheck = null;
			if (bindings.GetAttribute("distinct") != "true")
				distinctCheck = new MemoryStore();
			
			Hashtable bnodes = new Hashtable();
			
			foreach (XmlNode bindingnode in bindings) {
				if (!(bindingnode is XmlElement)) continue;
				XmlElement binding = (XmlElement)bindingnode;
				Resource subj = GetBinding(binding, "subject", subjects, bnodes);
				Resource pred = GetBinding(binding, "predicate", predicates, bnodes);
				Resource obj = GetBinding(binding, "object", objects, bnodes);
				if (!(subj is Entity) || !(pred is Entity)) continue;
				Statement s = new Statement((Entity)subj, (Entity)pred, obj);
				if (distinctCheck != null && distinctCheck.Contains(s)) continue;
				if (litFilters != null && !LiteralFilter.MatchesFilters(s.Object, litFilters, this)) continue;
				if (!sink.Add(s)) return true;
				if (distinctCheck != null) distinctCheck.Add(s);
			}
			
			return true;
		}
		
		string S(Resource[] r, string v) {
			if (r == null || r.Length != 1) return "?" + v;
			return S(r[0], null);
		}
		string SL(Resource[] r, string v) {
			if (r == null || r.Length <= 1) return "";
			StringBuilder ret = new StringBuilder();
			ret.Append("FILTER(");
			bool first = true;
			for (int i = 0; i < r.Length; i++) {
				if (r[i].Uri == null) continue;
				if (!first) ret.Append(" || "); first = false;
				ret.Append('?');
				ret.Append(v);
				ret.Append("=<");
				if (r[i].Uri != null)
					ret.Append(r[i].Uri);
				ret.Append('>');
			}
			ret.Append(").");
			if (first) return "";
			return ret.ToString();
		}
		
		string S(Resource r, string v) {
			if (r == null) {
				return v;
			} else if (r is Literal) {
				return r.ToString();
			} else if (r.Uri != null) {
				if (r.Uri.IndexOf('>') != -1)
					throw new ArgumentException("Invalid URI: " + r.Uri);
				return "<" + r.Uri + ">";
			} else {
				throw new ArgumentException("Blank node in select not supported.");
			}
		}
		
		Resource GetBinding(XmlElement binding, string v, Resource[] values, Hashtable bnodes) {
			if (values != null && values.Length == 1) return values[0];
			
			XmlElement b = (XmlElement)binding.FirstChild;
			while (b != null && b.GetAttribute("name") != v)
				b = (XmlElement)b.NextSibling;
			if (b == null)
				throw new ApplicationException("Invalid server response: Not all bindings present (" + v + "): " + binding.OuterXml);
			
			b = (XmlElement)b.FirstChild;
			if (b.Name == "uri")
				return new Entity(b.InnerText);
			else if (b.Name == "literal")
				return new Literal(b.InnerText); // datatype/lang
			else if (b.Name == "bnode") {
				string id = b.InnerText;
				if (bnodes.ContainsKey(id)) return (Entity)bnodes[id];
				Entity ret = new BNode();
				bnodes[id] = ret;
				return ret;
			}
			throw new ApplicationException("Invalid server response: " + b.OuterXml);
		}
		
		XmlDocument Load(string query) {
			string qstr = "query=" + System.Web.HttpUtility.UrlEncode(query);
			
			string method = "POST";
			
			System.Net.WebRequest rq;
			
			if (method == "GET") {
				string qurl = url + "?" + qstr;
				rq = System.Net.WebRequest.Create(qurl);
			} else {
				ASCIIEncoding encoding = new ASCIIEncoding(); // ?
				byte[] data = encoding.GetBytes(qstr);

				rq = System.Net.WebRequest.Create(url);
				rq.Method = "POST";
				rq.ContentType="application/x-www-form-urlencoded";
				rq.ContentLength = data.Length;
				
				using (Stream stream = rq.GetRequestStream())
					stream.Write(data, 0, data.Length);
			}
			
			System.Net.WebResponse resp = rq.GetResponse();
			
			string mimetype = resp.ContentType;
			if (mimetype.IndexOf(';') > -1)
				mimetype = mimetype.Substring(0, mimetype.IndexOf(';'));
			
			if (mimetype != "application/sparql-results+xml")
				throw new ApplicationException("The result of the query was not a SPARQL Results document.");

			XmlDocument ret = new XmlDocument();
			ret.Load(new StreamReader(resp.GetResponseStream(), System.Text.Encoding.UTF8));
						
			if (ret.DocumentElement.Name != "sparql")
				throw new ApplicationException("Invalid server response: Not a sparql results document.");
			
			return ret;
		}
		
		public Entity[] FindEntities(Statement[] graph) {
			string query = "SELECT ?entity WHERE { ";
			
			foreach (Statement s in graph) {
				query += S(s.Subject, "?entity");
				query += " ";
				query += S(s.Predicate, "?entity");
				query += " ";
				query += S(s.Object, "?entity");
				query += " . ";
				if (s.Meta != Statement.DefaultMeta) return new Entity[0];
			}
			
			query += "}";
			
			XmlDocument result = Load(query);
			
			XmlElement bindings = null;
			foreach (XmlElement e in result.DocumentElement)
				if (e.Name == "results")
					bindings = e;
			if (bindings == null)
				throw new ApplicationException("Invalid server response: No result node.");
			
			Hashtable bnodes = new Hashtable();
			ArrayList ret = new ArrayList();
			
			foreach (XmlElement binding in bindings) {
				Entity e = (Entity)GetBinding(binding, "entity", null, bnodes);
				ret.Add(e);
			}
			
			return (Entity[])ret.ToArray(typeof(Entity));
		}

	}
}

