/**********************************************************
  Copyright (c) 2006, 
    Lee Feigenbaum ( lee AT thefigtrees DOT net )
	Elias Torres   ( elias AT torrez DOT us )
  All rights reserved.

	Permission is hereby granted, free of charge, to any person obtaining a copy of
	this software and associated documentation files (the "Software"), to deal in
	the Software without restriction, including without limitation the rights to
	use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
	of the Software, and to permit persons to whom the Software is furnished to do
	so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
**********************************************************/

// our namespace
var CalendarDemo = {};

/**
 * TODO
 *
 * + Eliminate the dependency on a global log(...) function
 * + Eliminate the dependency on the YAHOO connection object
 * + Add a JSON configuration section for SPARQL service, modules, 
 *   data sources, transformations, non-RDF data wrappers, etc.
 *
 */
 
//-----------------
// Graph management
//  The CalendarDemo paradigm involves discovering new URIs which
//  are included in subsequent SPARQL queries to build a larger and
//  larger RDF dataset. In some cases, we also build up a collection
//  of named graphs, and queries make use of the named graphs and triple
//  provenance to infer semantic links which might not otherwise 
//  exist. 

CalendarDemo.urls = [];
CalendarDemo.named_urls = [];

/**
 * addUrl adds URLs to the growing RDF dataset used in SPARQL queries (FROM clause).
 * addNamedUrl adds a URL as a named graph for subsequent SPARQL queries (FROM NAMED clause).
 */
CalendarDemo.addUrl = function(u) { CalendarDemo._addUrl(u, CalendarDemo.urls); }
CalendarDemo.addNamedUrl = function(u) { CalendarDemo._addUrl(u, CalendarDemo.named_urls); }
CalendarDemo._addUrl = function(u, a) {
    for (var i = 0; i < a.length; i++)
        if (a[i] == u) return;
    a.push(u); 
}

/**
 * The badUrls hash maintains (as keys) URLs that are known not to resolve successfully
 * to graphs. We initialize it with some URLs that we know to be bad, yet are present
 * in FOAF files used in the demo. The list also grows automatically at runtime as 
 * needed. Similarly, goodUrls are known to be resolvable, and so are not rechecked
 * when a query fails.
 *
 * These hashes are maintained via the markUrlBad and markUrlGood functions.
 */
CalendarDemo.badUrls = {
    "http://www.pocketsoap.com/weblog/foaf.rdf": true,
    "http://www.snellspace.com/public/snellspace.rdf": true,
    "http://diveintomark.org/public/foaf.rdf": true
};
CalendarDemo.goodUrls = {};

CalendarDemo.markUrlBad = function(u) {
    function removeFromArray(u, a) {
        var removed = true;
        while (removed) {
            removed = false;
            for (var i = 0; i < a.length; i++) {
                if (u == a[i]) {
                    log("removing bad url: " + a[i]);
                    a.splice(i, 1);
                    removed = true;
                    break;
                }
            }
        }
    }
    removeFromArray(u, CalendarDemo.urls);
    removeFromArray(u, CalendarDemo.named_urls);
    
    CalendarDemo.badUrls[u] = true;
}

CalendarDemo.markUrlGood = function(u) {
    CalendarDemo.goodUrls[u] = true;
}

/**
 * eliminateBadUrls performs a HEAD request for each URL that we know about 
 * that we have not already previously checked. It uses the HTTP response
 * code from the request to call either markUrlBad or markUrlGood as
 * appropriate. eliminateBadUrls takes a callback function that is invokved
 * once all the URLs have been checked (to allow a client to retry a query
 * that failed initially. The function returns true if a pruning process 
 * is underway (and the callback can be expected), and returns false if
 * the callback will not be called.
 */
CalendarDemo.eliminatingBadUrls = {checksLeft: 0, cbs: []};

CalendarDemo.eliminateBadUrls = function(cb) {
    function finishedCheckingAUrl() {
        CalendarDemo.eliminatingBadUrls.checksLeft -= 1;
        if (CalendarDemo.eliminatingBadUrls.checksLeft == 0) {
            // call all of the callbacks
            log("finished checking for bad urls. about to call callbacks: " + CalendarDemo.eliminatingBadUrls.cbs.length);
            for (var i = 0; i < CalendarDemo.eliminatingBadUrls.cbs.length; i++)
                if (CalendarDemo.eliminatingBadUrls.cbs[i] != null) {
                    CalendarDemo.eliminatingBadUrls.cbs[i]();
                } else {
                    log("skipping NULL callback");
                }
        } else {
            log("finished checking a url; urls left: " + CalendarDemo.eliminatingBadUrls.checksLeft);
        }
    }
    
    if (CalendarDemo.eliminatingBadUrls.checksLeft > 0) {
        // queue this callback up since we're already checking URLs
        log("alredy checking for bad URLs; adding a callback.");
        CalendarDemo.eliminatingBadUrls.cbs.push(cb);
        return true;
    }

    var toCheck = [];
    for (var i = 0; i < CalendarDemo.urls.length; i++) 
        if (!(CalendarDemo.urls[i] in CalendarDemo.goodUrls) && !(CalendarDemo.urls[i] in CalendarDemo.badUrls))
            toCheck.push(CalendarDemo.urls[i]);
    for (var i = 0; i < CalendarDemo.named_urls.length; i++) 
        if (!(CalendarDemo.named_urls[i] in CalendarDemo.goodUrls) && !(CalendarDemo.named_urls[i] in CalendarDemo.badUrls))
            toCheck.push(CalendarDemo.named_urls[i]);

    // will we learn anything new?
    if (toCheck.length == 0) {
        log("All known URLs have already been checked.");
        return false;
    }

    CalendarDemo.eliminatingBadUrls = {
        checksLeft: toCheck.length,
        cbs: [cb]
    };

    log("Beginning to check for bad URLs: " + toCheck.length);

    for (var i = 0; i < toCheck.length; i++) {
        YAHOO.util.Connect.asyncRequest('HEAD', toCheck[i],
            {failure: (function(u) { return function() { CalendarDemo.markUrlBad(u); finishedCheckingAUrl();}; })(toCheck[i]),
             success: (function(u) { return function() { CalendarDemo.markUrlGood(u); finishedCheckingAUrl();}; })(toCheck[i])
             }
        );
    }

    return true; // yes, we're checking
}

//-------------------------------
// people management

/**
 * We maintain a bit of state about people (foaf:Person) that we know
 * about, primarily to facilitate some primitive smushing of people
 * based on foaf IFPs (mbox, mbox_sha1sum)
 */
CalendarDemo.People = {};

CalendarDemo.People.people = []; // each element is {bnodeids: {ids as keys}, uris: {uris as keys}, ids: ={ids as keys}, name: <label>}

CalendarDemo.People.findIndexByKey= function(val, key) {
    for (var i = 0; i < CalendarDemo.People.people.length; i++) {
        if (val in CalendarDemo.People.people[i][key])
            return i;
    }
    return -1;
}
CalendarDemo.People.findIndexByUri = function(uri) { return CalendarDemo.People.findIndexByKey(uri, "uris"); }
CalendarDemo.People.findIndexById = function(id) { return CalendarDemo.People.findIndexByKey(id, "ids"); }
CalendarDemo.People.findIndexByBnodeId = function(bid) { return CalendarDemo.People.findIndexByKey(bid, "bnodeids"); }

/**
 * We mark a new bnode scope by erasing all state that tracked bnode IDs, since they're now meaningless.
 */
CalendarDemo.People.markNewBnodeScope = function() {
	for (var i = 0; i < CalendarDemo.People.people.length; i++) {
		CalendarDemo.People.people[i].bnodeids = {};
	}
};

/**
 * Builds up the array of people, performing smushing either by ID
 * (mbox or mbox_sha1sum) or by URI.
 */
CalendarDemo.People.addPerson = function(uri, bnodeId, id, name, selected) {
	if (uri && bnodeId)
		throw "Can't add a person with both a uri and bnodeId at once";
    var index = -1;
    var byId = id ? CalendarDemo.People.findIndexById(id) : -1;
    var byUri = uri ? CalendarDemo.People.findIndexByUri(uri) : -1;
	var byBid = bnodeId ? CalendarDemo.People.findIndexByBnodeId(bnodeId) : -1;
    if (byId != -1 && (byUri != -1 || byBid != -1)) {
        if (byId != (byUri == -1 ? byBid : byUri)) {
			var byUriOrBid = byUri == -1 ? byBid : byUri;
            log("smushing the following 2 together");
            log(JSON.stringify(CalendarDemo.People.people[byId]));
            log(JSON.stringify(CalendarDemo.People.people[byUriOrBid]));
            // smush these together, arbitrarily preferring the id
            var from = CalendarDemo.People.people[byUriOrBid];
            var to = CalendarDemo.People.people[byId];
            for (var u in from.uris) to.uris[u] = true;
			for (var b in from.uris) to.bnodeids[b] = true;
            for (var i in from.ids)  to.ids[i] = true;
            if (!to.name)
                to.name = from.name; // preserve info
            to.selected = to.selected || from.selected;
            CalendarDemo.People.people.splice(byUriOrBid, 1); // remove that element
            // if byUri was before byId, then we need to decrement it
            if (byUriOrBid < byId)
                byId = byId - 1;
            log("with result:");
            log(JSON.stringify(CalendarDemo.People.people[byId]));
        }
        index = byId;
    } else if (byId != -1) {
        CalendarDemo.People.people[byId].uris[uri] = true;
        index = byId;
    } else if (byUri != -1) {
        CalendarDemo.People.people[byUri].ids[id] = true;
        index = byUri;
	} else if (byBid != -1) {
		CalendarDemo.People.people[byBid].bnodeids[bnodeId] = true;
		index = byBid;
    } else {
        var p = {bnodeids: {}, uris: {}, ids: {}, name: '', selected: false};
        if (uri) p.uris[uri] = true;
        if (id)  p.ids[id] = true;
		if (bnodeId) p.bnodeids[bnodeId] = true;
        CalendarDemo.People.people.push(p);
        index = CalendarDemo.People.people.length - 1;
    }
    // copy the name if we have one now and didn't before
    if (name && !CalendarDemo.People.people[index].name)
        CalendarDemo.People.people[index].name = name;
    CalendarDemo.People.people[index].selected = CalendarDemo.People.people[index].selected || selected;
        
    return index;
}

/**
 * Produce a URL which wraps ICS files in the ics2rdf service to allow using
 * the ICS file as a source graph in our SPARQL queries.
 */
CalendarDemo.calendarAsRdf = function(u) {
    // if it's an ics file that isn't being fed through ics2rdf, then do that
    // otherwise, no-op
    if (u.indexOf('.ics') == (u.length-4) && u.indexOf('ics2rdf') == -1)
        //return 'http://localhost/ical/services/ics2rdf/index.py?ical=' + u;
        return 'http://torrez.us/services/ics2rdf/' + u;
    return u;
}

CalendarDemo.upcomingRssAsRdf = function(u) {
	if (u.indexOf('upcoming.org/syndicate/') != -1 && u.indexOf('xslfile') == -1)
		return 'http://www.w3.org/2000/06/webdata/xslt?transform=Submit&xslfile=http%3A%2F%2Fthefigtrees.net%2Flee%2Fsw%2Fdemos%2Fcalendar%2Fdata%2Fupcoming-rss2rdf.xsl&xmlfile=' + u;
	return u;
}

CalendarDemo.wrapNonRdfData = function(u) {
	return CalendarDemo.upcomingRssAsRdf(CalendarDemo.calendarAsRdf(u));
}

/** 
 * Uses our lists of graph URLs and named graph URLs for the given
 * query object.
 */
CalendarDemo.buildDataset = function(q) {
    var gs = {};
    var ngs = {};
    function addGraph(g) { 
        if (g && !(g in gs)) {
            gs[g] = true;
            if (!(g in CalendarDemo.badUrls))
                q.addDefaultGraph(CalendarDemo.wrapNonRdfData(g)); 
        }
    }
    function addNamedGraph(g) { 
        if (g && !(g in ngs)) {
            ngs[g] = true;
            if (!(g in CalendarDemo.badUrls))
                q.addNamedGraph(CalendarDemo.wrapNonRdfData(g)); 
        }
    }

    for (var i = 0; i < CalendarDemo.urls.length; i++)
        addGraph(CalendarDemo.urls[i]);
    for (var i = 0; i < CalendarDemo.named_urls.length; i++)
        addNamedGraph(CalendarDemo.named_urls[i]);

}

//---------------------------
// initialization 
//   (I would have liked to put this in an init file, but monket
//    seems to corner the market on onload events, so this was easier.

//CalendarDemo.sparql = new SPARQL.Service("http://localhost:2020/sparql");
CalendarDemo.sparql = new SPARQL.Service("http://sparql.org/sparql");
CalendarDemo.sparql.setPrefix("foaf", "http://xmlns.com/foaf/0.1/");
CalendarDemo.sparql.setPrefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
CalendarDemo.sparql.setPrefix("dc", "http://purl.org/dc/elements/1.1/");
CalendarDemo.sparql.setPrefix("ical", "http://www.w3.org/2002/12/cal/icaltzd#");
CalendarDemo.sparql.setPrefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#");


/**
 * A collection of functions to generate SPARQL strings for the queries we're 
 * interested in.
 */
CalendarDemo.Queries = {
    // given a set of people, find shared (foaf:)interests
    shared_interests: function(people) {
        var q;
        var query = "SELECT DISTINCT ?interest ?name WHERE { ";
        for (var i = 0; i < people.length; i++) {
            var p = people[i];
            
            // we disjoin all the ways of identifying this person
            // and sum the interests attached to each
            query += '{';
            var first = true;
            for (var uri in p.uris) {
                if (typeof(p.uris[uri]) == 'function') continue;
                if (first) {
                    q = ' ';
                    first = false;
                } else {
                    q = ' UNION ';
                }
                q += "{ #URI# foaf:interest ?interest . OPTIONAL {?interest dc:title ?name} . OPTIONAL {?interest rdfs:label ?name} .}";
                query += q.replace(/#URI#/g, '<'+uri+'>');
            }
            for (var id in p.ids) {
                if (typeof(p.ids[id]) == 'function') continue;
                if (first) {
                    q = ' ';
                    first = false;
                } else {
                    q = ' UNION ';
                }
                if (id.indexOf('@') != -1) {
                    q += "{ _:x foaf:mbox #ID# ; foaf:interest ?interest . OPTIONAL {?interest dc:title ?name} . OPTIONAL {?interest rdfs:label ?name} .}";
                    query += q.replace(/#ID#/g, '<'+id+'>');
                } else {
                    q += "{ _:x foaf:mbox_sha1sum #ID# ; foaf:interest ?interest . OPTIONAL {?interest dc:title ?name} . OPTIONAL {?interest rdfs:label ?name} .}";
                    query += q.replace(/#ID#/g, '"'+id+'"');
                }
             }
             query += '}.';
         }
         query += '}';
         return query;
    },
    // given a url, find other urls to build our dataset
    mine_url: function(url) { var q = 
        "SELECT DISTINCT ?url ?named_url " +
        "WHERE { " +
        "  { "+
        "    _:x rdfs:seeAlso ?named_url . "+
        "    ?named_url rdf:type ical:Vcalendar "+
        "  } "+ 
        "UNION " +
        "  { " +
        "    ?someone foaf:knows ?known . " +
        "    OPTIONAL { ?known rdfs:seeAlso ?url } . "+
        "  } " +
        "}"; 
        return q.replace(/#URL#/g, '<'+url+'>'); 
    },
    // find names, ids, and calendars for all the foaf:Persons in our dataset
    people_display: function() { var q =
        "SELECT ?who ?name ?id ?cal "+
        "WHERE { "+
        " ?who rdf:type foaf:Person . "+
        " OPTIONAL { ?who foaf:name ?name } . "+
        " OPTIONAL { ?who rdfs:label ?name } . "+
        " OPTIONAL { { ?who foaf:mbox ?id } UNION { ?who foaf:mbox_sha1sum ?id } } . "+
//        " OPTIONAL { ?who foaf:mbox_sha1sum ?id } . "+
        " OPTIONAL { ?who rdfs:seeAlso ?cal . ?cal rdf:type ical:Vcalendar } . "+
        "} ORDER BY ?name ";
        return q;
    },
    // for the given people, interests, dates, find all (upcoming.org) events fitting 
    // those interests that occur at a location and date at which all the relevant 
    // people will be present
    common_events: function(people, interests, date_regex) {
        var query = "PREFIX jfn: <java:com.hp.hpl.jena.query.function.library.>  SELECT DISTINCT ?title ?targetEvent ?interestGraph ?eventStart ?eventEnd ?sharedLocation WHERE {";
        // first, retrieve all events in the graphs of matching interests, along with their dates
        // and locations
        query += "{";
        query += "GRAPH ?interestGraph { ";
        query += "  ?targetEvent dc:title ?title; ical:dtstart ?eventStart; ical:dtend ?eventEnd; ical:x-calconnect-venue ?venue . ";
        query += "  ?venue ical:x-calconnect-location ?sharedLocation";
        query += "} .";
        query += "FILTER (";
        for (var i = 0; i < interests.length; i++) {
            if (i > 0)
                query += " || ";
            query += 'regex(str(?interestGraph), "' + interests[i] + '")';
            //we used to match on the graph exactly; now that we wrap the graphs we match
			//using a regexp (now that we have a substr function this could use that also)
			//query += "?interestGraph = <" + interests[i] + ">";
        }
        query += ").";
        query += "}";
        // now, go for each person
        for (var i = 0; i < people.length; i++) {
            var first = true;
            query += "{";
            var p = people[i];
            var graphVar = '?g' + i;
            var startVar = '?start' + i;
            var endVar = '?end' + i;
            var eventVar = '?event' + i;
            // build a list of patterns that could find calendar graphs
            // for this person
            for (var uri in p.uris) {
                if (typeof(p.uris[uri]) == 'function') continue;
                q = '';
                if (first) first = false; else q += " UNION ";
                q += "{ #URI# rdfs:seeAlso #GVAR# . "+
                     "  #GVAR# rdf:type ical:Vcalendar . } ";
                q = q.replace(/#GVAR#/g, graphVar);
                query += q.replace(/#URI#/g, '<'+uri+'>');
            }
            for (var id in p.ids) {
                if (typeof(p.ids[id]) == 'function') continue;
                q = '';
                if (first) first = false; else q += " UNION ";
                if (id.indexOf("@") != -1) {
                    q += "{ _:x foaf:mbox #ID# . "+
                         "  _:x rdfs:seeAlso #GVAR# . "+
                         "  #GVAR# rdf:type ical:Vcalendar . }";
                    q = q.replace(/#GVAR#/g, graphVar);
                    query += q.replace(/#ID#/g, '<'+id+'>');
                } else {
                    q += "{ _:x foaf:mbox_sha1sum #ID# . "+
                         "  _:x rdfs:seeAlso #GVAR# . "+
                         "  #GVAR# rdf:type ical:Vcalendar . }";
                    q = q.replace(/#GVAR#/g, graphVar);
                    query += q.replace(/#ID#/g, '"'+id+'"');
                }
            }
            query += "}."; // end of building ?gX variables
            // get calendar events from that graph
            query += "GRAPH " + graphVar + "_wrapped {";
            query += eventVar + " ical:dtstart " + startVar + ";";
            query +=            " ical:dtend " + endVar + ";";
            query +=            " ical:location ?sharedLocation . ";
            query += "}"; // end of pulling calendar events
            query += ". FILTER regex(str(" + graphVar + "_wrapped), str(" + graphVar + ")) .";
        }

        // this is a hack to match full datetimes to specific days
        if (date_regex)
            query += "FILTER (regex(str(?eventStart), '" + date_regex + "') || regex(str(?eventEnd), '" + date_regex + "')).";
        function day(v, a) { return "jfn:substr(str(?" + v + a + "), 0, 10)"; }
        // filter everything for dates that overlap
        query += "FILTER (";
        for (var i = 0; i < people.length; i++) {
            if (i > 0) query += " && ";
            query += "("; 
            query += day("start", i) + " >= " + day("eventStart", "");
            query += " && ";
            query += day("start", i) + " <= " + day("eventEnd", "");
            query += ") || (";
            query += day("end", i) + " >= " + day("eventStart", "");
            query += " && ";
            query += day("end", i) + " <= " + day("eventEnd", "");
			query += ") || (";
            query += day("eventStart", "") + " >= " + day("start", i);
            query += " && ";
            query += day("eventStart", "") + " <= " + day("end", i);
			query += ") || (";
            query += day("eventEnd", "") + " >= " + day("start", i);
            query += " && ";
            query += day("eventEnd", "") + " <= " + day("end", i);
            query += ")";
        }
        query += ").";
        query += " } ";
        return query;
    },
    // get calendar events for days that match the reg.exp. for the specified people
    calendars: function(people, date_regex) {
        var q;
        var query = "SELECT DISTINCT ?title ?start ?end ?name ?location ?g WHERE { {";
        var first = true;
        for (var i = 0; i < people.length; i++) {
            var p = people[i];
            log("adding SPARQL clauses for person: ");
            log(JSON.stringify(p));
            // build a list of patterns that could find calendar graphs
            // for this person
            for (var uri in p.uris) {
                if (typeof(p.uris[uri]) == 'function') continue;
                q = '';
                if (first) first = false; else q += " UNION ";
                q += "{ #URI# rdfs:seeAlso ?g . "+
                     "  ?g rdf:type ical:Vcalendar . "+
                     "  OPTIONAL { #URI# foaf:name ?name } . "+
                     "  OPTIONAL { #URI# rdfs:label ?name } . }";
                query += q.replace(/#URI#/g, '<'+uri+'>');
            }
            for (var id in p.ids) {
                if (typeof(p.ids[id]) == 'function') continue;
                q = '';
                if (first) first = false; else q += " UNION ";
                if (id.indexOf("@") != -1) {
                    q += "{ ?x foaf:mbox #ID# . "+
                         "  ?x rdfs:seeAlso ?g . "+
                         "  ?g rdf:type ical:Vcalendar . "+
                         "  OPTIONAL { ?x foaf:name ?name } . "+
                         "  OPTIONAL { ?x rdfs:label ?name } . }";
                    query += q.replace(/#ID#/g, '<'+id+'>');
                } else {
//                  //q = ' UNION '
                    q += "{ ?x foaf:mbox_sha1sum #ID# . "+
                         "  ?x rdfs:seeAlso ?g . "+
                         "  ?g rdf:type ical:Vcalendar . "+
                         "  OPTIONAL { ?x foaf:name ?name } . "+
                         "  OPTIONAL { ?x rdfs:label ?name } . }";
                    query += q.replace(/#ID#/g, '"'+id+'"');
                }
            }
        }
        query += "} . GRAPH ?g_wrapped {_:ev ical:summary ?title; ical:dtstart ?start; ical:dtend ?end; ical:location ?location. ";
        if (date_regex)
            query += "FILTER (regex(str(?start), '" + date_regex + "') || regex(str(?end), '" + date_regex + "')).";
        query += "}. FILTER regex(str(?g_wrapped), str(?g)) .";
        query += "}";
        return query;
    },
        
};

// a very rough, broken, and ad-hoc SPARQL query prettyprinter :-)
function sparql_pp(q) {
    var indentBy = 2;
    q = q.replace(/  +/g, " ");
    //q = q.replace(/{{/g, "{ {");
    //q = q.replace(/}}/g, "} }");
    q = q.replace(/([^\n])(PREFIX|SELECT|CONSTRUCT|ASK|WHERE|FROM)/g, "$1\n$2");
    q = q.replace(/{\s*([^\n])/mg, "{\n$1");
    // TODO -- there's some bizarre bug here which I can't figure out -- dunno
    // why this reg exp. fails with a \s* after the { and before the [^\n]
    q = q.replace(/{([^\n])/mg, "{\n$1");
    q = q.replace(/&&([^\n])/mg, "&&\n$1");
    q = q.replace(/\|\|([^\n])/mg, "||\n$1");
    q = q.replace(/([^\n])}/mg, "$1\n}");
    q = q.replace(/\.([^A-Za-z0-9_\n])/g, ".\n$1");
    // try to do some nice indenting
    var lines = q.split(/\n/);
    var indent = 0;
    for (var i = 0; i < lines.length; i++) {
        var padding = "";
        if (/^\s*}/.test(lines[i]))
            indent -= 1;
        for (var j = 0; j < indent * indentBy; j++) padding += " ";
        lines[i] = padding + lines[i];
        if (/{\s*$/.test(lines[i]))
            indent += 1;
    }
    q = lines.join("\n");
    return q;
}
//////////////////////////////////////////////////////////////////
// these are useful, yes they are.
function setElementText(idOrElt, text) {
    var elt;
    if (typeof(idOrElt) == "string")
        elt = document.getElementById(id);
    else 
        elt = idOrElt;
    while (elt.hasChildNodes())
        elt.removeChild(elt.lastChild);
    elt.appendChild(document.createTextNode(text));
}

function addElementText(id, text) {
    var elt = document.getElementById(id);
    elt.appendChild(document.createTextNode(text));
}

function getElementText(id) {
    var elt = document.getElementById(id);
    var text = '';
    for (var child = elt.firstChild; child != null; child = child.nextSibling)
        if (child.nodeType == 3) // text node
            text += child.nodeValue;
    return text; 
}
 
function executeXPath(xp, extractor, start) {
	var objs = [];
	var results = document.evaluate(xp, start || document, null, XPathResult.ANY_TYPE,null);
	var thisResult = results.iterateNext();
	while(thisResult != null) {
		objs.push(extractor ? extractor(thisResult) : thisResult);
		thisResult = results.iterateNext();
	}
	return objs;
}
function parseDateTime(value){
	try{
		if(/^(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):?(\d{2}):?(\d{2})/.test(value)){
			return new Date(Date.UTC(RegExp.$1, RegExp.$2-1, RegExp.$3, RegExp.$4, RegExp.$5, 0));
		}else if(/^(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):?(\d{2})-(\d{2}):?(\d{2})/.test(value)){
			return new Date(Date.UTC(RegExp.$1, RegExp.$2-1, RegExp.$3, RegExp.$4, RegExp.$5, 0));
		}else if(/^(\d{4})-?(\d{2})-?(\d{2})/.test(value)){
			return new Date(Date.UTC(RegExp.$1, RegExp.$2-1, RegExp.$3, 0, 0, 0));
		}else{ //todo error message
			throw new Error("Could not convert the given date.");
		}
	}catch(e){
		return Date.parseDate(value, "Y-m-d");
		throw new Error("'dateTime.iso8601' element could not be parsed.");    
	}
}
