
var GoogleSearch = {};

(function(){

    GoogleSearch.callback = {};
    var callbackCounter = 0;
    var beginRequestListener = function () {};
    var endRequestListener = function () {};
    var beginNetworkListener = function () {};
    var endNetworkListener = function () {};

    var outstandingRequests = 0;

    GoogleSearch.doSearch = function (query, args, callback) {

        // Allocate a callback slot.
        var callbackIndex = callbackCounter++;
        var callbackLocalName = "c"+callbackIndex;
        var callbackFullName = "GoogleSearch.callback."+callbackLocalName;
        args.callback = callbackFullName;
        delete args.context;

        args.q = query;
        if (! args.v) args.v = "1.0";

        var queryStringBits = [];
        for (var key in args) {
            var value = args[key];

            if (value === true) value = "1";
            else if (value === false) value = "0";

            queryStringBits.push([
                encodeURIComponent(key),
                encodeURIComponent(value)
            ].join('='));
        }

        var queryString = queryStringBits.join("&");
        var url = [
            "http://ajax.googleapis.com/ajax/services/search/web",
            queryString
        ].join("?");

        var scriptNode = document.createElement("script");
        scriptNode.type = "text/javascript";
        scriptNode.src = url;

        var realCallback = function (data, details, status) {
            // Call the caller's callback
            callback(data, details, status);

            // Clean up our temporary junk
            if (scriptNode.parentNode) {
                scriptNode.parentNode.removeChild(scriptNode);
            }
            GoogleSearch.callback[callbackLocalName] = undefined;
            outstandingRequests--;

            if (endRequestListener) {
                endRequestListener(args);
            }

            if (outstandingRequests == 0) {
                // We're now idle, so let's take the opportunity to
                // reset everything.
                GoogleSearch.callback = {};
                callbackCounter = 0;

                if (endNetworkListener) {
                    endNetworkListener();
                }
            }
        };

        if (outstandingRequests == 0) {
            if (beginNetworkListener) {
                beginNetworkListener();
            }
        }
        beginRequestListener(args);
        outstandingRequests++;

        GoogleSearch.callback[callbackLocalName] = realCallback;
        document.body.appendChild(scriptNode);
        
    };

})();



var SGAPI = {};

(function(){

    SGAPI.callback = {};

    var callbackCounter = 0;
    var beginRequestListener = function () {};
    var endRequestListener = function () {};
    var beginNetworkListener = function () {};
    var endNetworkListener = function () {};

    var outstandingRequests = 0;

    SGAPI.fetchResultsRaw = function (urls, args, callback) {
        if (urls.length) {

            // Allocate a callback slot.
            var callbackIndex = callbackCounter++;
            var callbackLocalName = "c"+callbackIndex;
            var callbackFullName = "SGAPI.callback."+callbackLocalName;
            args.callback = callbackFullName;

            args.q = urls.join(",");

            var queryStringBits = [];
            for (var key in args) {
                var value = args[key];

                if (value === true) value = "1";
                else if (value === false) value = "0";

                queryStringBits.push([
                                      encodeURIComponent(key),
                                      encodeURIComponent(value)
                                      ].join('='));
            }

            var queryString = queryStringBits.join("&");
            var url = [
                       "http://socialgraph.apis.google.com/lookup",
                       queryString
            ].join("?");

            var scriptNode = document.createElement("script");
            scriptNode.type = "text/javascript";
            scriptNode.src = url;

            var realCallback = function (data) {
                // Call the caller's callback
                callback(data);

                // Clean up our temporary junk
                if (scriptNode.parentNode) {
                    scriptNode.parentNode.removeChild(scriptNode);
                }
                SGAPI.callback[callbackLocalName] = undefined;
                outstandingRequests--;

                if (endRequestListener) {
                    endRequestListener(urls);
                }

                if (outstandingRequests == 0) {
                    // We're now idle, so let's take an opportunity to
                    // reset everything.
                    SGAPI.callback = [];
                    callbackCounter = 0;

                    if (endNetworkListener) {
                        endNetworkListener();
                    }
                }
            };

            if (outstandingRequests == 0) {
                if (beginNetworkListener) {
                    beginNetworkListener();
                }
            }
            beginRequestListener(urls);
            outstandingRequests++;


            SGAPI.callback[callbackLocalName] = realCallback;
            document.body.appendChild(scriptNode);
        }
        else {
            // If we've actually been given an empty list for some reason,
            // let's just return immediately.
            realCallback({
                canonical_mapping: {},
                nodes: {}
            });
        }

    };

    SGAPI.fetchResults = function (urls, args, callback) {
            var nextUrlIdx = 0;
            var urlCount = urls.length;

            var result = {
                canonical_mapping: {},
                nodes: {}
            };

            var fetchSomeResults;
            fetchSomeResults = function () {
                var someUrls = [];

                var stop = nextUrlIdx + 15;
                if (stop > urlCount) stop = urlCount;

                var stringLength = 0;

                while (1) {
                    var nextUrl = urls[nextUrlIdx];
                    someUrls.push(nextUrl);
                    stringLength += nextUrl.length + 1;
                    nextUrlIdx++;
                    if (stringLength >= 600) break;
                    if (nextUrlIdx >= stop) break;
                }

                SGAPI.fetchResultsRaw(someUrls, args, function (data) {
                    for (url in data.canonical_mapping) {
                        result.canonical_mapping[url] = data.canonical_mapping[url];
                    }
                    for (url in data.nodes) {
                        result.nodes[url] = data.nodes[url];
                    }
                    if (nextUrlIdx == urlCount) {
                        // We're finished.
                        callback(result);
                    }
                    else {
                        // Need to fetch some more results
                        fetchSomeResults();
                    }
                });
            };

            fetchSomeResults();
    };

    SGAPI.setBeginRequestListener = function (callback) {
        beginRequestListener = callback;
    };
    SGAPI.setEndRequestListener = function (callback) {
        endRequestListener = callback;
    };
    SGAPI.setBeginNetworkListener = function (callback) {
        beginNetworkListener = callback;
    };
    SGAPI.setEndNetworkListener = function (callback) {
        endNetworkListener = callback;
    };

})();


// Higher-level API for dealing with people searches.

var PeopleSearch = {};

(function () {

    PeopleSearch.findPeople = function (queryString, callback) {

        // First we do a Google Search for the query string to find pages
        // about whatever was entered.
        
        var searchArgs = {
            rsz: "large"
        };
        
        GoogleSearch.doSearch(queryString, searchArgs, function (result) {
            if (! result) {
                callback(undefined, makeError(500, "Search returned an invalid result"));
                return;
            }
            if (result.responseStatus != 200) {
                callback(undefined, makeError(result.responseStatus, result.responseDetails));
            }
            
            var items = result.responseData.results;
            
            // Extract just the URLs, keeping the result order so that we can
            // order the people results by it. We also track URLs that we've
            // not yet attached to people -- which at this point is all of them --
            // so that they can be returned a Page results later.
            var resultURLs = [];
            var justPageURLs = {};
            
            for (var i = 0; i < items.length; i++) {
                var url = items[i].url;
                resultURLs.push(url);
                justPageURLs[url] = items[i];
            }

            // Now we take the URLs we found and feed them into the Social Graph API
            // to find out:
            //  * Attributes such as names, usernames, profile URLs and so forth
            //  * Other URLs that represent the same person where we may find the attributes above.
            
            var sgapiArgs = {
                fme: true,
                sgn: true
            };
            
            SGAPI.fetchResults(resultURLs, sgapiArgs, function (result) {

                // Turn all of the node objects in the response into instances of our SocialNode class
                for (key in result.nodes) {
                    result.nodes[key] = new SocialNode(key, result.nodes[key]);
                }
                
                var people = [];

                for (var i = 0; i < resultURLs.length; i++) {
                    var url = resultURLs[i];
                    if (! justPageURLs[url]) continue; // We've already processed this URL.
                    var canonURL = result.canonical_mapping[url];
                    var primaryNode = result.nodes[canonURL];
                    
                    // Wha? Oh well.
                    if (! primaryNode) continue;
                    
                    if (! primaryNode.looksLikePerson()) continue;
                    delete justPageURLs[url];
                    
                    var nodes = getNodeMapForPrimaryNode(primaryNode, result.nodes);
                    
                    // Now we remove any other nodes we encountered from the "justPageURLs"
                    // map so we won't process them again if they came up later in the search
                    // results.
                    for (claimedNodeKey in nodes) {
                        var claimedNode = nodes[claimedNodeKey];
                        var claimedNodeURLs = claimedNode.getAllURLs();
                        for (var claimedNodeURLIndex = 0; claimedNodeURLIndex < claimedNodeURLs.length; claimedNodeURLIndex++) {
                            var claimedNodeURL = claimedNodeURLs[claimedNodeURLIndex];
                            delete justPageURLs[claimedNodeURL];
                        }
                    }

                    var person = new Person(primaryNode, nodes);
                    people.push(person);

                }

                // Anything left over in justPageURLs is a Page rather than a Person.
                var pages = [];
                
                for (var i = 0; i < items.length; i++) {
                    var url = items[i].url;
                    if (justPageURLs[url]) {
                        pages.push(new Page(items[i]));
                    }
                }

                callback(new PeopleSearchResult(people, pages), undefined);
                
            });
        
        });
    
    };

    /* PeopleSearchResult class */
    var PeopleSearchResult = function (people, pages) {
        this.people = people;
        this.pages = pages;
    };
    PeopleSearchResult.prototype = {};

    /* Person class */
    var Person = function (primaryNode, nodes) {
        this.primaryNodeKey = primaryNode.getKey();
        this.nodes = nodes;
    };
    Person.prototype = {};
    
    Person.prototype.getName = function () {
        var names = this.getNames();
        if (names && names[0]) {
            return names[0];
        }
        else {
            return null;
        }
    };
    Person.prototype.getNames = function () {
        var allNames = removeDuplicatesFromList(getAllResponses(this.nodes, "getName")).sort(compareFullNamesForQuality);
        return allNames;
    };
    Person.prototype.getUsernames = function () {
        var usernames = [];
        for (key in this.nodes) {
            if (key.match(/sgn:/)) {
                var result;
                if (result = key.match(/\?ident=(.*)$/)) {
                    usernames.push(result[1]);
                }
            }
        }
        return removeDuplicatesFromList(usernames).sort(compareUsernamesForQuality);
    };
    Person.prototype.getDisplayNames = function () {
        var names = this.getNames();
        var usernames = this.getUsernames();

        return removeDuplicatesFromList(concatenateLists(names, usernames));        
    };
    Person.prototype.getDisplayName = function () {
        var displayNames = this.getDisplayNames();
        if (displayNames && displayNames[0]) {
            return displayNames[0];
        }
        else {
            return null;
        }
    };
    Person.prototype.getPhotoURLs = function () {
        return getAllResponses(this.nodes, "getPhotoURL");
    };
    Person.prototype.getNodes = function () {
        var ret = [];
        for (key in this.nodes) {
            ret.push(this.nodes[key]);
        }
        return ret;
    };

    /* SocialPage class */
    var SocialNode = function (key, sgapiNode) {
        if (! sgapiNode) throw "sgapiNode is required";
        this.key = key;
        this.sgapiNode = sgapiNode;
    };
    SocialNode.prototype = {};
    SocialNode.prototype.getKey = function () {
        return this.key;
    };
    SocialNode.prototype.getClaimedNodeKeys = function () {
        return this.sgapiNode.claimed_nodes;
    };
    SocialNode.prototype.looksLikePerson = function () {
        if (this.sgapiNode.claimed_nodes.length > 0) return true;
        
        for (dummy in this.sgapiNode.attributes) {
            return true;
        }
    };
    SocialNode.prototype.getAllURLs = function () {
        var ret = [];
        var interestingAttributes = ["url", "profile", "atom", "rss", "blog"];
        for (var i = 0; i < interestingAttributes.length; i++) {
            if (this.sgapiNode.attributes[interestingAttributes[i]]) {
                ret.push(this.sgapiNode.attributes[interestingAttributes[i]]);
            }
        }
        return ret;
    };
    SocialNode.prototype.getName = function () {
        return this.sgapiNode.attributes.fn;
    };
    SocialNode.prototype.getPhotoURL = function () {
        return this.sgapiNode.attributes.photo;
    };
    SocialNode.prototype.getPrimaryURL = function () {
        return this.sgapiNode.attributes.url ? this.sgapiNode.attributes.url : this.key;
    };
    SocialNode.prototype.getSGNDomain = function () {
        var key = this.key;
        if (matches = key.match(/^sgn:\/\/([^\/]+)\//)) {
            return matches[1];
        }
        else {
            return null;
        }
    };
    SocialNode.prototype.getIconURL = function () {
        var key = this.key;
        var icon;
        var domain;
        if (domain = this.getSGNDomain()) {
            icon = domainFavicons[domain] ? domainFavicons[domain] : undefined;
            if (icon === true) icon = 'http://'+domain+'/favicon.ico';
        }
        else if (key.match(/^xmpp:/)) {
            icon = "http://xmpp.org/favicon.ico";
        }
        else if (key.match(/^callto:/)) {
            icon = "http://skype.com/favicon.ico";
        }
        return icon ? icon : 'other.png';
    };
    SocialNode.prototype.getUsername = function () {
        if (this.key.match(/sgn:/)) {
            var result;
            if (result = this.key.match(/\?ident=(.*)$/)) {
                return result[1];
            }
            else if (result = this.key.match(/\?pk=(.*)$/)) {
                return result[1];
            }
        }
        return this.key;
    };
    SocialNode.prototype.hasAssociatedIdentNode = function () {
        return this.sgapiNode.attributes.ident_is ? true : false;
    };

    /* Page class */
    var Page = function (searchResult) {
        this.searchResult = searchResult;
    };
    Page.prototype = {};

    /* Helper Methods */
    
    function makeError(code, message) {
        var ret = new Error();
        ret.name = code;
        ret.message = message;
        return ret;
    };

    function getNodeMapForPrimaryNode(primaryNode, resultNodes) {
        var nodes = {};
        populateNodeMapForPrimaryNode(primaryNode, resultNodes, nodes);
        return nodes;
    }

    function populateNodeMapForPrimaryNode(primaryNode, resultNodes, nodes) {
        var sgapiNode = primaryNode.sgapiNode;
        var primaryNodeKey = primaryNode.getKey();
        
        // If we've already visited this node, return immediately.
        if (nodes[primaryNodeKey]) return;
        
        nodes[primaryNodeKey] = primaryNode;
        
        var claimedNodeKeys = primaryNode.getClaimedNodeKeys();
        
        for (var i = 0; i < claimedNodeKeys.length; i++) {
            var claimedNodeKey = claimedNodeKeys[i];
            var claimedNode = resultNodes[claimedNodeKey];
            if (claimedNode) {
                populateNodeMapForPrimaryNode(claimedNode, resultNodes, nodes);
            }
        }
    }

    function compareFullNamesForQuality(a, b) {
        var qualityA = fullNameQualityScore(a);
        var qualityB = fullNameQualityScore(b);
        
        if (qualityA > qualityB) return -1;
        if (qualityB > qualityA) return 1;
        
        // Shorter forms prefered, so (for example) we get Alex Smith before Alexander Smith.
        if (a.length < b.length) return -1;
        if (b.length < a.length) return 1;
        
        return 0;
    }

    function fullNameQualityScore(name) {
        var score = 0;
        // If it contains at least one space
        if (name.match(/ /)) {
            score += 2;
        }
        
        // If it contains capital letters
        if (name.match(/[A-Z]/)) {
            score++;
        }
        
        // Don't want abbreviations marked with a dot
        if (name.match(/\./)) {
            score --;
        }

        return score;
    }

    function compareUsernamesForQuality(a, b) {
        var qualityA = usernameQualityScore(a);
        var qualityB = usernameQualityScore(b);
        
        if (qualityA > qualityB) return -1;
        if (qualityB > qualityA) return 1;
        
        // Shorter forms prefered, so (for example) we get Alex Smith before Alexander Smith.
        if (a.length < b.length) return -1;
        if (b.length < a.length) return 1;
        
        return 0;
    }

    function usernameQualityScore(name) {
        var score = 0;

        // If it contains capital letters
        if (name.match(/[A-Z]/)) {
            score += 2;
        }
        
        // If it contains letters at all
        if (name.match(/[a-zA-Z]/)) {
            score++;
        }
        
        // Prefer usernames without underscores
        if (name.match(/_/)) {
            score--;
        }

        // If it's all numeric, push it right to the bottom
        if (name.match(/^\d+$/)) {
            score -= 5;
        }

        // If it contains a slash, push it right to the bottom
        // (Facebook idents are odd-looking.)
        if (name.match(/\//)) {
            score -= 4;
        }

        return score;
    }

    function getAllResponses(list, method) {
        var ret = [];
        for (key in list) {
            var item = list[key];
            if (item) {
                var response = item[method]();
                if (response) ret.push(response);
            }
        }
        return ret;
    }

    function removeDuplicatesFromList(list) {
        var seen = {};
        var ret = [];
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            if (! seen[item]) ret.push(item);
            seen[item] = true;
        }
        return ret;
    }

    function concatenateLists(list1, list2) {
        var ret = [];
        for (var i = 0; i < list1.length; i++) {
            ret.push(list1[i]);
        }
        for (var i = 0; i < list2.length; i++) {
            ret.push(list2[i]);
        }
        return ret;    
    }

    var domainFavicons = {
        'livejournal.com': true,
        'last.fm': true,
        'myspace.com': true,
        'digg.com': true,
        'facebook.com': true,
        'linkedin.com': 'http://www.linkedin.com/favicon.ico',
        'jaiku.com': true,
        'meetup.com': true,
        'tribe.net': true,
        'youtube.com': true,
        'orkut.com': true,
        'aol.com': true,
        'reddit.com': true,
        'twitter.com': true,
        'del.icio.us': true,
        'ma.gnolia.com': true,
        'zooomr.com': true,
        'picasaweb.google.com': true,
        'amazon.com': true,
        'dopplr.com': true,
        'upcoming.yahoo.com': true,
        'bloglines.com': true,
        'yelp.com': true,
        'disqus.com': 'http://media.disqus.com/images/dsq-favicon-16x16.ico',
        'flickr.com': true,
        'friendfeed.com': true,
        'pownce.com': 'http://pownce.com/img/favicon.ico',
        'slideshare.net': 'http://www.slideshare.net/favicon.ico',
        'vox.com': 'http://static.vox.com/.shared/images/favicon.ico',
        'dodgeball.com': 'http://www.dodgeball.com/static/4021100690-favicon.ico',
        'blogger.com': 'http://www.blogger.com/favicon.ico',
        'reader.google.com': 'http://www.google.com/favicon.ico',
        'profiles.google.com': 'http://www.google.com/favicon.ico',
        'stumbleupon.com': 'http://www.stumbleupon.com/favicon.ico',
        'tumblr.com': 'http://www.tumblr.com/images/favicon.gif',
        'wordpress.com': true
    };

})();



if (window.loadFirebugConsole) window.loadFirebugConsole();

(function () {

    var queryField;
    var searchButton;
    var peopleResultList;
    var pageResultList;
    var currentQuery;

    var initialize = function () {
    
        queryField = document.getElementById("queryfield");
        searchButton = document.getElementById("searchbutton");

        personResultList = new ResultList("People", document.getElementById("personresults"));
        pageResultList = new ResultList("Other Search Results", document.getElementById("pageresults"));

        queryField.disabled = false;
        searchButton.disabled = false;
        queryField.focus();

        var initialQuery = getQueryFromLocation();
        if (initialQuery) {
            startQuery(initialQuery);
        }
        
        setInterval(function () {
            var query = getQueryFromLocation();
            if (query && query != currentQuery) {
                startQuery(query);
            }
        }, 50);

        var startQueryFromField = function () {
            var newQuery = queryField.value;
            window.location.hash = '#'+escape(newQuery);
            //startQuery(newQuery);
        };
        
        queryField.addEventListener("keypress", function (evt) {
            if (evt.keyCode == 13) {
                startQueryFromField();
                return false;
            }
        }, false);
        searchButton.addEventListener("click", function (evt) {
            startQueryFromField();
        }, true);
    };
    
    var startQuery = function (query) {
        currentQuery = query;
        queryField.value = query;
        personResultList.clearResults();
        pageResultList.clearResults();
        
        if (! query) {
            return;
        }
        
        // Keep a local reference to the query so that if results come back
        // out of turn we can just keep the current results.
        var thisQuery = query;

        var callback = function (result) {
            if (thisQuery != currentQuery) return;

            var people = result.people;
            for (var i = 0; i < people.length; i++) {
                var person = people[i];
                var personResult = new PersonResult(person);
                personResultList.addResult(personResult);
            }

            var pages = result.pages;
            for (var i = 0; i < pages.length; i++) {
                var page = pages[i];
                var pageResult = new PageResult(page);
                pageResultList.addResult(pageResult);
            }

        };

        PeopleSearch.findPeople(query, callback);

    };

    /* PersonResult */
    var PersonResult = function (person) {
        this.person = person;
        var rootElem = this.domElem = document.createElement("div");
        rootElem.className = "personresult";
        
        rootElem.appendChild(makePhotoHolder(person.getPhotoURLs(), 100, 100));
        
        var detailsElem = document.createElement("div");
        detailsElem.className = "personresultdetails";
        rootElem.appendChild(detailsElem);
        
        var nameElem = document.createElement("div");
        nameElem.className = "personresultname";
        var otherNames = person.getDisplayNames();
        var displayName = otherNames.shift();
        if (displayName) {
            nameElem.appendChild(document.createTextNode(displayName));
        }
        else {
            nameElem.innerHTML = "(name unknown)";
            nameElem.className += " nameless";
        }
        detailsElem.appendChild(nameElem);
        
        if (otherNames.length) {
            var akaElem = document.createElement("div");
            akaElem.className = "personresultaka";
            akaElem.appendChild(document.createTextNode("aka "+otherNames.join(", ")));
            detailsElem.appendChild(akaElem);
        }
        
        var nodesContainerElem = document.createElement("div");
        nodesContainerElem.className = "personresultnodescontainer collapsed";
        
        var nodesExpanderElem = document.createElement("div");
        nodesExpanderElem.className = "expander";
        nodesExpanderElem.innerHTML = "(toggle expanded view)";
        nodesExpanderElem.title = "Toggle Expanded View";
        var expanded = false;
        nodesExpanderElem.addEventListener("click", function (evt) {
            if (expanded) {
                nodesContainerElem.className = "personresultnodescontainer collapsed";
                expanded = false;
            }
            else {
                nodesContainerElem.className = "personresultnodescontainer expanded";
                expanded = true;
            }
        }, true);
        nodesContainerElem.appendChild(nodesExpanderElem);
        
        var nodesElem = document.createElement("ul");
        nodesElem.className = "personresultnodes";
        
        
        var nodes = person.getNodes();
        for (var ni = 0; ni < nodes.length; ni++) {
            var node = nodes[ni];
            if (node.hasAssociatedIdentNode()) continue;
            var nodeElem = document.createElement("li");
            var nodeLinkElem = document.createElement("a");
            var domain = node.getSGNDomain();
            var username = node.getUsername();
            var titleText = username + (domain ? " @ "+domain : "");
            
            if (domain == "mboxsha1") continue;
            
            nodeLinkElem.href = node.getPrimaryURL();
            nodeLinkElem.setAttribute("title", titleText);
            nodeLinkElem.appendChild(document.createTextNode(username));
            nodeLinkElem.style.backgroundImage = 'url('+node.getIconURL()+')';
            
            nodeElem.appendChild(nodeLinkElem);
            nodesElem.appendChild(nodeElem);
        }
        
        nodesContainerElem.appendChild(nodesElem);
        detailsElem.appendChild(nodesContainerElem);

        var clearElem = document.createElement("div");
        clearElem.style.clear = 'both';
        detailsElem.appendChild(clearElem);
        
    };
    PersonResult.prototype = {};
    PersonResult.prototype.getDOMElement = function () {
        return this.domElem;
    };

    /* PageResult */
    var PageResult = function (page) {
        this.page = page;
        var rootElem = this.domElem = document.createElement("div");
        rootElem.className = "pageresult";
        
        var headingElem = document.createElement("div");
        headingElem.className = "pageresultheading";
        rootElem.appendChild(headingElem);
        
        var linkElem = document.createElement("a");
        linkElem.href = page.searchResult.url;
        linkElem.innerHTML = page.searchResult.title;
        headingElem.appendChild(linkElem);
        
        var bodyElem = document.createElement("p");
        bodyElem.innerHTML = page.searchResult.content;
        rootElem.appendChild(bodyElem);
        
        var footerElem = document.createElement("div");
        footerElem.className = "pageresultfooter";
        footerElem.innerHTML = page.searchResult.visibleUrl + (page.searchResult.cacheUrl ? ' - ' : '');
        if (page.searchResult.cacheUrl) {
            var cacheLinkElem = document.createElement("a");
            cacheLinkElem.href = page.searchResult.cacheUrl;
            cacheLinkElem.innerHTML = "Cached";
            footerElem.appendChild(cacheLinkElem);
        }
        rootElem.appendChild(footerElem);
    };
    PageResult.prototype = {};
    PageResult.prototype.getDOMElement = function () {
        return this.domElem;
    };

    /* ResultList */
    var ResultList = function (titleHTML, elem) {
        this.titleHTML = titleHTML;
        this.elem = elem;
        this.items = [];
        this.listElem = null;
        this.clearResults();
    };
    ResultList.prototype = {};
    
    ResultList.prototype.clearResults = function () {
        this.listElem = null;
        this.elem.style.display = 'none';
        this.elem.innerHTML = "";
        this.items = [];
    };
    
    ResultList.prototype.addResult = function (result) {
        if (! this.listElem) {
            // Do initial setup of the element
            var heading = document.createElement("h2");
            heading.innerHTML = this.titleHTML;
            var list = document.createElement("ul");
            this.elem.appendChild(heading);
            this.elem.appendChild(list);
            list.className = "resultslist";
            this.elem.style.display = '';
            this.listElem = list;
        }
        
        this.items.push(result);
        
        var itemElem = result.getDOMElement();
        
        var li = document.createElement("li");
        li.appendChild(itemElem);
        this.listElem.appendChild(li);
    };

    function getQueryFromLocation() {
        if (window.location.hash) {
            return decodeURIComponent(window.location.hash.substr(1));
        }
        else {
            return null;
        }
    }
    
    function makePhotoHolder(urls, size) {
        var ret = document.createElement("div");
        ret.className = "photoholder";
        ret.style.width = size+"px";
        ret.style.height = size+"px";
        ret.style.overflow = 'hidden';
        ret.style.position = "relative";
        
        var imageElems = [];
        for (var i = 0; i < urls.length; i++) {
            var imageElem = document.createElement("img");
            imageElem.src = urls[i];
            imageElem.alt = "";
            imageElem.onload = function (evt) {
                var image = evt.target;
                
                // Scale it to fit in our box, and center it.
                
                var sourceWidth = image.width;
                var sourceHeight = image.height;
                var maxSize = size;
                var ourWidth = null;
                var ourHeight = null;
                var aspect = sourceWidth / sourceHeight;

                if (sourceWidth <= maxSize && sourceHeight <= maxSize) {
                    // No resizing required!
                    ourWidth = sourceWidth;
                    ourHeight = sourceHeight;
                }
                else if (sourceWidth >= sourceHeight) {
                    // The image is wider than it is tall, or it's square
                    ourWidth = maxSize;
                    ourHeight = maxSize / aspect;
                }
                else {
                    // The image is taller than it is wide
                    ourWidth = maxSize * aspect;
                    ourHeight = maxSize;
                }

                image.setAttribute("width", ourWidth);
                image.setAttribute("height", ourHeight);

                // Now how much do we need to offset them to center them in the box?
                var x = (maxSize / 2) - (ourWidth / 2);
                var y = (maxSize / 2) - (ourHeight / 2);
                image.style.position = "absolute";
                image.style.left = x+"px";
                image.style.top = y+"px";
                image.style.display = '';

            };
            imageElem.style.display = 'none';
            imageElems.push(imageElem);
        }

        var currentImage = 0;
        var showImage = function (idx) {
            ret.innerHTML = "";
            if (imageElems[idx]) {
                ret.appendChild(imageElems[idx]);
                currentImage = idx;
            }
        };
        var showNextImage = function () {
            var newImage = currentImage + 1;
            if (! imageElems[newImage]) newImage = 0;
            showImage(newImage);
        };

        showImage(0);
        ret.addEventListener("click", showNextImage, true);
        
        return ret;
    }

    var doneonload = false;
    window.onload = document.onload = function () {
        if (doneonload) return;
        initialize();
        doneonload = true;
    }

})();


