// -*- coding: utf-8 -*-

/* minimal cookie parser, from http://www.quirksmode.org/js/cookies.html */

function createCookie(name,value,days) {
  if (days) {
    var date = new Date();
    date.setTime(date.getTime()+(days*24*60*60*1000));
    var expires = "; expires="+date.toGMTString();
  }
  else var expires = "";
  document.cookie = name+"="+value+expires+"; path=/";
}

function readCookie(name) {
  var nameEQ = name + "=";
  var ca = document.cookie.split(';');
  for(var i=0;i < ca.length;i++) {
    var c = ca[i];
    while (c.charAt(0)==' ') c = c.substring(1,c.length);
    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  }
  return null;
}

function eraseCookie(name) {
  createCookie(name,"",-1);
}

// not from quirksmode.
function listCookies() {
    var ca = document.cookie.split(';');
    var rv = [];
    for (var ii = 0; ii < ca.length; ii++) {
        var cookie = ca[ii];
        for (var jj = 0; (jj < cookie.length) && (cookie[jj] == ' '); jj++)
            ;
        rv.push(cookie.substring(jj, cookie.indexOf('=')));
    }
    return rv;
}


/* basic DHTML infrastructure  */

function inject_script(url) {
  /* because $.getScript DOES NOT WORK with Flickr, especially its broken text/plain content type */
  var newScript = document.createElement('script');
  newScript.type = 'text/javascript';
  newScript.src = url;
  document.body.appendChild(newScript);
}

window._knx_pendingScripts = {};

function inject_script_failsafe(url, label, failsafe) {
  inject_script(url);
  window._knx_pendingScripts[label] = setTimeout(failsafe, 45000);
}

function inject_script_ack(label) {
    if (window._knx_pendingScripts[label]) {
        clearTimeout(window._knx_pendingScripts[label]);
        delete window._knx_pendingScripts[label];
    }
}

var failsafe = {};

failsafe.jsonpc = 0;            // counter

// Return a function name for JSONP use which will call on_success
// with its argument if called; if it isn't called within the timeout
// (45 seconds), it calls on_failure instead.

// Copied, more or less, from jQuery.
failsafe.make_cb = function(on_success, on_failure) {
    var cbname = 'callback' + failsafe.jsonpc++;
    var timeout;

    var cleanup = function() {
        failsafe[cbname] = undefined;
        // cargo-culted from jQuery; I don't know when it can fail:
        try { delete failsafe[cbname] } catch(e) { }
        try { clearTimeout(timeout); } catch(e) { }
    };

    timeout = setTimeout(function() { cleanup(); on_failure(); /* @@ call an eventLog() that warns us a failsafe was used */ }, 45000);
    failsafe[cbname] = function(data) { cleanup(); on_success(data); };
    return 'failsafe.'+cbname;
}

function by_id(id) { return document.getElementById(id) }

pageTrackerPending = [];

function drain_pageTrackerPending() {
    var local_pageTrackerPending = pageTrackerPending;
    pageTrackerPending = [];
    foreach(local_pageTrackerPending, function (e) { pageTracker._trackEvent( e.category, e.action, e.optional_label, e.optional_value ); } );
}

function eventLog(category, action, optional_label, optional_value) {
    if (typeof pageTracker != typeof undefined) {
        pageTracker._trackEvent(category, action, optional_label, optional_value);
    } else {
        pageTrackerPending[pageTrackerPending.length] = {category: category, action: action, optional_label: optional_label, optional_value: optional_value};
    }
    debugLog(category +': '+ action +' ('+ optional_label +') @ '+ optional_value +"ms");
}

function debugLog(e) {
  if (!window.debugLog_string) 
    window.debugLog_string='';
  var time = new Date();
  var msg = time+' (.'+(time.getTime() % 1000)+'): '+ e+'\n';
  window.debugLog_string += msg;
  $('#debug-log').val(debugLog_string);
  if (typeof console != "undefined") console.log(msg);
}
debugLog('debugLog function defined');

function knxassert(bool, msg) {
    if (!bool) {
        var msg = "ASSERTION FAILED: " + msg;
        debugLog(msg);
        window.knxassert_error = new Error(msg); // save backtrace!
        throw window.knxassert_error;
    }
}

// XXX move these above drain_pageTrackerPending

function array_index_of(array, item) {
    for (var ii = 0; ii < array.length; ii++) if (array[ii] === item) return ii;
    return null;
}

function foreach(list, callback) {
    for (var ii = 0; ii < list.length; ii++) callback(list[ii]);
}

function filter(list, callback) {
    var rv = [];
    for (var ii = 0; ii < list.length; ii++) {
        if (callback(list[ii])) rv.push(list[ii]);
    }
    return rv;
}


/* data formats */

function htmlescape(html) {
    if (!html) return html;
    // This replacement stuff turns out not to be a bottleneck of
    // vcard2html.
    return ((''+html).
            replace(/&/g, "&amp;").
            replace(/</g, "&lt;").
            replace(/>/g, "&gt;").
            replace(/\"/g, "&quot;"));
}

function htmlunescape(html) {
    if (!html) return html;
    return ((''+html).
            replace(/&quot;/g, '"').
            replace(/&gt;/g, ">").
            replace(/&lt;/g, "<").
            replace(/&amp;/g, "&amp;"));
}


/* hCard rendering */

function domain_of(url) {
    var m = new RegExp("(http://)?(www\.)?([^\/]*)").exec(url, 'g')
    if (!m) return url
    return m[m.length-1]
}

function autolinkify(text) {
    return (''+text).replace(/http:\/\/[^\s<>"]*[^\s<>".,]/g,
                             '<a href="$&" target="_blank">$&</a>');
}

function random_digit() {
    return Math.floor(Math.random()*10);
}

// Replace last seven digits in phone number with 555RRRR, where the
// Rs are random digits.  For movie mode.
function mangle_telephone_number(tel) {
    tel = tel.replace(/\d([^\d]*)\d([^\d]*)\d([^\d]*)\d([^\d]*)\d([^\d]*)\d([^\d]*)\d([^\d]*)$/,
                      function(str, a, b, c, d, e, f, g) {
            return ('5' + a + 
                    '5' + b +
                    '5' + c + 
                    random_digit() + d +
                    random_digit() + e +
                    random_digit() + f +
                    random_digit() + g);
        }
    )

    return tel;
}

function vcard2html(vcard) {
var html = 
'<div class="vcard'
    +( vcard.photo != undefined? ' vcard_with_photo': '')
    +'" style="display:none">';
if (vcard.photo != undefined) html +=
'  <img class="photo" src="'+vcard['photo']+'">';
if (vcard.favicon) html +=
'  <img class="favicon" src="'+vcard.favicon+'">';
html +=
'  <a class="fn url" href="'+vcard['url']+'"'+(vcard.account_id ? ' title="'+vcard.account_id+'"' : '')+' target="_blank">';
if (vcard.fn && (vcard.fn != (vcard['given-name']+' '+vcard['family-name']))) {
html += vcard['fn'];
    } else {
if (vcard['given-name'] != undefined) html +=
'     <span class="given-name">'+vcard['given-name']+'</span>';
if (vcard['family-name'] != undefined) html +=
'     <span class="family-name">'+vcard['family-name']+'</span>';
    }
html += '</a>';
if (vcard.nickname != undefined) html +=
'  <div class="nickname">'+vcard['nickname']+'</div>';
if (vcard.title != undefined) html +=
'  <div class="title">'+vcard['title']+'</div>';
if (vcard.category != undefined) html +=
'   <span class="category">'+htmlescape(vcard['category'])+',</span>';
if (vcard.locality != undefined) html +=
'   <span class="adr locality">'+htmlescape(vcard['locality'])+'</span>';
if (vcard.bday != undefined) html +=
'  <div class="bday">'+vcard['bday']+'</div>';
if (vcard.rss != undefined) html +=
'  <a href="'+vcard['rss']+'" class="rss">rss</a>';
if (vcard.tel != undefined) {
    var tel = vcard.tel;
    if (knx_movie_mode) tel = mangle_telephone_number(tel);
html +=
'  <a href="tel:'+tel+'" class="tel">'+tel+'</a>';
}
if (vcard.comment != undefined) html +=
'  <span class="comment">'+htmlescape(vcard['comment'])+'</span>';
var websites = vcard.websites || []
  for (var ii = 0; ii < websites.length; ii++) { html +=
'  <a class="website url" href="'+websites[ii]+'" target="_blank">'+domain_of(websites[ii])+'</a>';
  }
if (vcard.lastTweet != undefined && vcard.lastTweet.id) { 
    var tweet = autolinkify(htmlescape(vcard.lastTweet.text));
html +=
'  <div class="tweet" title="'+htmlescape(vcard.lastTweet.id)+'">'+tweet+' @ <span class="tweet_date">'+htmlescape(vcard.lastTweet.created_at.substr(0,16))+'</span></div>';
}
html +=
'</div>';
return html;
}


/* string handling and name matching */

function starts_with(prefix, string) {
  return string.indexOf(prefix) == 0
}

function ends_with(suffix, string) {
  var i = string.lastIndexOf(suffix);
  return ((i>=0) && (i == (string.length - suffix.length)));
}

function words(string) {
  return string.split(/[\_\.\+\,\@\\\/\s]+/)
}

function name_matches_term(term, name, names) {
  if (starts_with(term, name)) return true;
  if (ends_with(term, name)) return true;

  for (var ii = 0; ii < names.length; ii++) {
    if (starts_with(term, names[ii])) return true;
    if (ends_with(term, names[ii])) return true;
  }
  return false;
}

function name_matches(searchstring, name) {
  if (!name) return false;
  searchstring = searchstring.toLowerCase();
  name = name.toLowerCase();

  var terms = words(searchstring);
  var names = words(name);
  for (var ii = 0; ii < terms.length; ii++) {
      if (!name_matches_term(terms[ii], name, names)) return false;
  }
  return true;
}

function matches_website(searchstring, websites) {
  if (!websites) return false
  searchstring = searchstring.toLowerCase()
  var re = new RegExp('^' + searchstring + '|[^a-zA-Z0-9]' + searchstring)
  for (var ii = 0; ii < websites.length; ii++) {
    var websiteNoProto = websites[ii].toLowerCase()
    if (websiteNoProto.indexOf('://') != -1) {
      websiteNoProto = websiteNoProto.substring(websiteNoProto.indexOf('://')+3);
    }
    if (name_matches(searchstring, websiteNoProto))return true
  }
  return false
}

function guess_names_from_string(name) {
    var names = words(name);

    if (names.length == 1) {
        return {
            'given-name': name,
            'family-name': name
        };
    } else {
        // note we assume its length is at least 1!
        var family_name = names.pop();
        return {
            'given-name': names.join(' '),
            'family-name': family_name
        };
    }
}


/* handling of addressbook entries ("friends") */

// Channel, aka Observable. In our system we're using it as a (usually
// public) property, not a base class.
function Channel() {
    this._observers = [];
}
Channel.prototype = {
    on_change: function(observer) {
        this._observers.push(observer);
    },

    remove_observer: function(observer) {
        var index = array_index_of(this._observers, observer);
        if (index === null) return;
        this._observers.splice(index, 1);
    },

    changed: function() {
        for (var ii = 0; ii < this._observers.length; ii++) {
            this._observers[ii]();
        }
    }
}

knx_data_lifetime = 15*60*1000;

// A Socnet is a social networking service. The addr_book is a
// collection of Socnets.
function Socnet(name, addr_book) {
    this._name = name;
    this._state = state.unknown;
    this._vcards = [];
    this._addr_book = addr_book;
    this.state_channel = new Channel();
    this._last_event_time = new Date().getTime();
};
Socnet.prototype = {
    changed: function() { this._addr_book.vcard_channel.changed(); },

    // `clear` deletes all the VCARDs. WARNING: do not do this and
    // leave the socnet in "loaded" state!
    clear: function() { 
        // this `if` saves us three out of 9 "updating hcards" during page
        if (!this._vcards.length) return;
        foreach(this._vcards, remove_hcard_for_vcard); // XXX layer inversion!
        this._vcards = [];
        this.changed();
    },

    log: function(msg) { 
        var now = new Date().getTime();
        eventLog(this._name, this._state.name, msg, now - this._last_event_time); 
        this._last_event_time = now;
    },
    state: function() { return this._state; },
    set_state: function(state) {
        this.log('→ '+state.name);
        state.set(this);
        this._state = state;        // unless an exception was raised
        this.state_channel.changed();
    },

    // make this the current version of this socnet in the addr_book;
    // done immediately in the foreground loading case, after loading
    // finishes otherwise
    install: function() {
        this.log('→ install');  // XXX should not look like set_state log; fix when we revamp event log scheme
        this.state_channel.on_change(render_current_socnets);
        // XXX we should have a method on addr_book to get a socnet
        // without making a new one...
        var old_me = this._addr_book._socnet_objs[this._name];
        knxassert(this !== old_me, "double install!");
        this._addr_book._socnet_objs[this._name] = this;
        if (old_me) old_me.set_state(state.unknown);
        this.changed(); // XXX not really! But the addr_book may have new vcards now
    },

    // who am I logged in as?
    add_me: function(username) { 
        this._me = username;
        $('#'+this._name).attr('title', this._me); // XXX this should not be here
    },

    // Adds zero or more VCARDs to the socnet, which will remain until
    // the socnet transitions to "unknown" or "loading" state.
    add_vcards: function(vcards) {
        this.log('adding '+vcards.length+' vcards');
        this._vcards = this._vcards.concat(vcards);
        this.changed();
    },

    vcards: function() { return this._vcards; },
    toString: function() { return '(socnet '+this._name+')'; },

    // Returns true if it's yet time to refetch this socnet.
    stale: function() {
        if (this.state() !== state.loaded) return false;
        return this.loaded_time + knx_data_lifetime < new Date().getTime();
    },

    // doesn't actually produce a string, but rather a JSONable object
    // containing serializable state.
    serialize: function() {
        if (this.state() !== state.loaded) return null;
        return {
            vcards: this.vcards(),
            me: this._me, 
            loaded_time: this.loaded_time
        };
    },

    // similarly, sets state of socnet from that object.
    deserialize: function(blob) {
        // 24 hours is maximum age of cached data
        if (blob.loaded_time + 24*60*60*1000 < new Date().getTime()) return;
        this.set_state(state.loading);
        this._vcards = blob.vcards;
        this.set_state(state.loaded);
        this.loaded_time = blob.loaded_time; // must come after state transition
        if (blob.me) this.add_me(blob.me);
    }
};

// A socnet can be in states "unknown", "loading", or "loaded".
// These state objects represent those states, verify transitions, and
// conduct side effects associated with transitions.

function SocnetState(name, set) {
    this.name = name;
    this.set = set; // `set` gets called before setting a socnet to this state
};
SocnetState.prototype.toString = function() { return this.name };

state = {
    // The initial state of a socnet is 'unknown'.
    unknown: new SocnetState('unknown', function(socnet) { socnet.clear(); }),

    // A socnet in the "unknown" or "loading" state can take a
    // transition to the "loading" state, which deletes all vcards.
    loading: new SocnetState('loading',function(socnet) {
        knxassert(socnet.state() !== state.loaded);
        socnet.clear();
    }),

    // Once a socnet finishes loading, it can transition to the
    // 'loaded' state to indicate that it has finished loading and is
    // therefore safe to cache.
    loaded: new SocnetState('loaded', function(socnet) {
        knxassert(socnet.state() === state.loading,
                  'invalid state transition '+socnet.state()+
                  ' -> loaded on '+socnet);
        socnet.loaded_time = new Date().getTime();
      var shortcodes = 0;
      if (shortcodes) {
	var urls = [];
        var vcards = socnet.vcards();
        var vcards_len = vcards.length;
        for (var i = 0; i < vcards_len; i++) {
            urls[i] = vcards[i].url;
        }
	$.post('http://mmc.angstro.com/~dl/knx/knxto.php', { 'json_longs': $.toJSON(urls) }, function(r){
            ther = r;
	    shortm = {};
            debugLog(socnet._name + ' post callback: ' + ther.length);
	    foreach(ther, function(id) {
		shortm[id.link] = [id.direct, id.indirect];
            });
	    for (var ii = 0; ii < socnet._vcards.length; ii++) {
		shorturls = shortm[socnet._vcards[ii].url];
                if (shorturls && shorturls.length == 2) {
                    socnet._vcards[ii].dirshort = shorturls[0];
                    socnet._vcards[ii].indshort = shorturls[1];
                }
	    }
	    shortm = {};
	}, 'json');
      }
    })
}

addr_book = {
    vcard_channel: new Channel(),

    _socnet_objs: {},
    _refetch_functions: {},

    tentative_socnet: function(socnet_name) {
        return new Socnet(socnet_name, this);
    },

    socnet: function(socnet_name) {
        var socnet = this._socnet_objs[socnet_name];
        if (!socnet) {
            socnet = this.tentative_socnet(socnet_name);
            socnet.install();
        }
        return socnet;
    },

    refetch: function(socnet_name) {
        var attempt_fetch = this._refetch_functions[socnet_name];
        if (!attempt_fetch) return; // we don't know how to refetch it
        var new_socnet = this.tentative_socnet(socnet_name);
        var installer = function() {
            if (new_socnet.state() === state.loaded) {
                new_socnet.install();
                new_socnet.state_channel.remove_observer(installer);
            }
        }        
        new_socnet.state_channel.on_change(installer);
        attempt_fetch(new_socnet);
    },

    refetch_when_stale: function(socnet_name, attempt_fetch) {
        this._refetch_functions[socnet_name] = attempt_fetch;
    },

    refetch_stale_socnets: function() {
        for (var i in this._socnet_objs) {
            if (this.socnet(i).stale()) this.refetch(i);
        }
    },


    // Returns the number of VCARDs associated with ALL socnets.
    count_all_vcards: function() {
        return this.current_friends().length;
    },

    // Returns the number of known socnets.
    count_socnets: function() {
        var svcs = 0;
        for (var i in this._socnet_objs) {
            if (this.socnet(i).state() !== state.unknown) svcs++;
        }
        return svcs;
    },

    // Returns a list of all VCARDs for all socnets.
    current_friends: function() {
        var rv = [];
        for (var socnet in this._socnet_objs)
            rv = rv.concat(this.socnet(socnet).vcards());
        return rv;
    },

    // Bump this number when you change the schema of the locally
    // cached data.
    current_serialization_version: 13,

    // Returns a JSON string containing the current state.
    serialize: function() {
        var blob = {
          version: this.current_serialization_version, 
          socnets: {}
        };
        for (var net in this._socnet_objs) {
            var socnet = this.socnet(net).serialize();
            if (socnet) blob.socnets[net] = socnet;
        }
        return $.toJSON(blob);
    },

    // Restores the state of the address book from a serialized JSON string.
    deserialize: function(json) {
        var blob = $.evalJSON(json);
        if (blob.version != this.current_serialization_version) return;
        for (var net in blob.socnets) {
            this.socnet(net).deserialize(blob.socnets[net]);
        }
        this.vcard_channel.changed();
    }
};

setInterval(function() { addr_book.refetch_stale_socnets() }, 3*60*1000);

// Construct a filter function that returns true if a friend matches
// the search string
function friend_matches_search(searchstring) {
    return function(friend) {
        return name_matches(searchstring, friend.fn)             || 
               name_matches(searchstring, friend.nickname)       ||
               name_matches(searchstring, friend['given-name'])  ||
               name_matches(searchstring, friend['family-name']) ||
               matches_website(searchstring, friend.websites);
    };
}

// Construct a filter function that returns true if a friend matches
// the partial given and family names.
function friend_matches_name(given_name, family_name) {
    return function(friend) {
        // special case for twitter nicknames like "adambosworth" or
        // "tinnychun"
        if (friend['nickname'] &&
            starts_with((given_name+family_name).toLowerCase(), 
                        friend['nickname'].toLowerCase()))
            return true;

        // otherwise, do a real fielded search
        return (name_matches(given_name, friend['given-name']) &&
                name_matches(family_name, friend['family-name']));
    };
}

// Generate a supposedly unique ID for a VCARD.
// XXX move this to VCARD class?
function vcard_unique_id(vcard) {
    knxassert(vcard.socnet, "socnet name cannot be null");
    knxassert(vcard.socnet.indexOf(' ') === -1,
              "socnet name cannot have space: " + vcard.socnet);
    knxassert(vcard.account_id, "vcard must have account_id");
    return vcard.socnet + ' ' + vcard.account_id;
}

// This indexes hCard DOM nodes by unique ID.
hcard_dom_nodes_by_unique_id = {};
function get_hcard_for_vcard(vcard) {
    var id = vcard_unique_id(vcard);
    var cached_node = hcard_dom_nodes_by_unique_id[id];
    if (cached_node) return cached_node;

    var workspace_node = document.createElement('div');
    var results_node = by_id('results');

    workspace_node.innerHTML = vcard2html(vcard);
    var node = workspace_node.lastChild;
    workspace_node.removeChild(node);
    results_node.appendChild(node);

    hcard_dom_nodes_by_unique_id[id] = node;

    return node;
}

function remove_hcard_for_vcard(vcard) {
    var id = vcard_unique_id(vcard);
    var hcard = hcard_dom_nodes_by_unique_id[id];
    if (!hcard) return;
    delete hcard_dom_nodes_by_unique_id[id];
    hcard.parentNode.removeChild(hcard);
}    

function filtered_friends(criterion) {
    return filter(addr_book.current_friends(), criterion);
}

currently_displayed_vcards = [];
currently_displayed_vcard_channel = new Channel();

function display_vcards(vcards) {
    debugLog('hiding hCards');
    $('.vcard').hide();
    debugLog('showing hCards');
    var results = by_id('results');

    // Here we ensure that (a) all the relevant cards exist in the DOM
    // and (b) they are in the order specified.

    foreach(vcards, function(vcard) {
        var hcard = get_hcard_for_vcard(vcard);
        results.appendChild(hcard);
        hcard.style.display = ''; // normal, i.e. don't hide.
    });
    debugLog('showed '+vcards.length+' hCards');

    currently_displayed_vcards = vcards;
    currently_displayed_vcard_channel.changed();
}

previous_search_description = "";
current_filtered_friends = [];
pagination_start = 0;
page_size = 10;

function draw_friends_page() {
    debugLog('displaying hCards');
    var vcards = current_filtered_friends;
    display_vcards(vcards.slice(pagination_start,
                                pagination_start + page_size));

    debugLog('finished querying hCards');
    setTimeout(function(){debugLog('page idle');}, 0);

    var count = vcards.length;
    var total = addr_book.count_all_vcards();
    var total_socnets = addr_book.count_socnets();
    $('#count_cards').text((count === total) ? '' : ''+count+' out of ');
    $('#count_all_cards').text(total ? total : 'all');
    $('#count_svcs').text(total_socnets ? total_socnets : 'multiple');

    draw_pagination_ui();

    // XXX should these go in display_vcards?
    $('#empty_set_message')[0].style.display = (total === 0) && (previous_search_description == 'search: ') ? '' : 'none';
    $('#helpful_reminder')[0].style.display = (total === 0) && (previous_search_description != 'search: ') ? '' : 'none';
    $('#empty_result_message')[0].style.display = (count === 0) && (total > 0) ? '' : 'none';
}

function clamp(min, val, max) {
    knxassert(min <= max, "clamp values invalid: "+min+", "+val+", "+max);
    if (val < min) return min;
    if (val > max) return max;
    return val;
}

function go_to_position(n) {
    pagination_start = clamp(0, n, current_filtered_friends.length-1);
    draw_friends_page();
}

function page_up() { go_to_position(pagination_start - page_size); }
function page_down() { go_to_position(pagination_start + page_size); }

function jslink(js, text, title) {
    var titleattr = title ? ' title="'+htmlescape(title)+'"' : '';
    var onclickattr = ' onclick="'+htmlescape(js)+';return false"';
    return $('<a href="#"'+onclickattr+titleattr+'>'+htmlescape(text)+'</a>');
}

function draw_pagination_ui() {
    debugLog('drawing pagination UI');
    var npages = Math.ceil(current_filtered_friends.length / page_size);
    var current_page = Math.ceil(pagination_start / page_size);
    var pages_to_show = 9;
    var max_page_list_start = clamp(0, npages - pages_to_show, npages);
    var page_list_start = clamp(0, current_page - 4, max_page_list_start);
    var page_list_sentinel = clamp(0, page_list_start + pages_to_show, npages);

    var dest = $('#pagination');
    dest.empty();
    if (npages < 2) return;

    if (current_page != 0) {
        dest.append(jslink('page_up()', '< prev', 'previous page of results'));
    }
    dest.append(document.createTextNode(' '));
    for (var ii = page_list_start; ii < page_list_sentinel; ii++) {
        if (ii == current_page) {
            dest.append(document.createTextNode((ii+1)+' '));
        } else {
            dest.append(jslink('go_to_position(' + ii*page_size + ')', ii+1, 
                               'go to page '+(ii+1)+' of results'));
            dest.append(document.createTextNode(' '));
        }
    }
    if (current_page != npages-1) {
        dest.append(jslink('page_down()', 'next >', 'next page of results'));
    }
    debugLog('drew pagination UI');
}

// Carry out a new search, but only if it's different from the old search.
function render_filtered_friends_by(predicate, description) {
    if (description === previous_search_description) return;
    
    debugLog('starting search on '+description);

    var vcards = filtered_friends(predicate);

    debugLog('sorting vcards');
    vcards.sort(function(a,b) {
        var priority = {
              'facebook':  1,
              'twitter':   2,
              'linkedin':  3,
              'flickr':    4,
              'yahoo':     5,
              'gmail':     6
            }
	function key(x) { 
            return (
                (x['family-name'] ? x['family-name'] + ' ' + x['given-name'] : x['fn'])+
                (x['nickname'] ? x['nickname'] : x['account_id'])+
                (priority[x['socnet']])
                ).toUpperCase(); 
        }
	var akey = key(a);
	var bkey = key(b);
	return((akey > bkey) - (akey < bkey));
    });
    debugLog('done sorting vcards');

    current_filtered_friends = vcards;
    pagination_start = 0;
    previous_search_description = description;

    draw_friends_page();
}

function render_current_socnets() {
    for(name in addr_book._socnet_objs) {
	socnet = addr_book._socnet_objs[name];
        $('#'+socnet._name).attr('title', socnet._me); 
        $('#'+socnet._name+' img').attr('src', 'i/' + socnet._name + "-" + socnet._state);
    }
}

/* photostream stuff */

knx_max_photos_per_person = 4;  // used in plugin code

// XXX this should probably be a method on the vcard
function photostream_div_for(vcard) {
    return $(get_hcard_for_vcard(vcard)).find('.photostream')[0];
}

function add_to_photostream(vcard, photo) {
    var photostream = photostream_div_for(vcard);
    if (!photostream) {
        var hcard = get_hcard_for_vcard(vcard);
        photostream = document.createElement('div');
        photostream.setAttribute('class', 'photostream');
        hcard.appendChild(photostream);
    }
    photostream.appendChild(photostream_photo(photo));
}

// return a DOM node representing a photostream photo, given {href, src, title}
function photostream_photo(photo) {
    var link = document.createElement('a');
    link.setAttribute('href', photo.href);
    debugLog('photo link is '+photo.href);
    link.setAttribute('title', photo.title);
    link.setAttribute('target', '_blank');
    link.setAttribute('class', 'fbphoto'); // XXX facebook-specific

    var img = document.createElement('img');
    img.setAttribute('src', photo.src);
    link.appendChild(img);

    return link;
}


/* interface to the DHTML user interface inputs */

function render_filtered_friends_by_search_string() {
    var qt = by_id('n').value;
    render_filtered_friends_by(friend_matches_search(qt), "search: '"+qt+"'");
}

function render_filtered_friends_by_card() {
  var given_name = by_id('FirstName').value;
  var family_name = by_id('LastName').value;
  render_filtered_friends_by(friend_matches_name(given_name, family_name),
                             "names: " + family_name + ", " + given_name);
}

function render_indirect_friend(scode) {
    render_filtered_friends_by(function(friend) { return friend.indshort == scode; },
			       "shortcode: " + scode);
}

function render_filtered_friends() {
    if ($('#shortCodeForm').is(':visible')) {
	render_indirect_friend($('#ShortCode').val());
    } else if ($('#contactDetailsForm').is(':hidden')) {
        render_filtered_friends_by_search_string();
    } else {
        render_filtered_friends_by_card();
    }
}

function parseQueryParams() {
  var url = document.location.search;
  var qry = url.indexOf('?');
  if(qry == 0) {
    var paramstr = url.substring(1); /* skip the ? */
    var q = { 'n':'', 'q':'', 'aF':'', 'aL':'', 'sc':''};
    var params = paramstr.split('&');
    for (var i = 0; i < params.length; i++) {
        var sp = params[i].split('=');
        if(sp.length > 1) {
            q[decodeURIComponent(sp[0].replace(/\+/g, " "))] = decodeURIComponent(sp[1].replace(/\+/g, " "));
        } else if (i == 0) {
            q['n'] = sp[0].replace(/\+/g, " ");
        }
    }
    debugLog($.toJSON(q));
    /* should loop over elements in search div? */
    $('#n').val(q['n']);
    if (q['q'] != '') $('#n').val(q['q']);
    $('#FirstName').val(q['aF']);
    $('#LastName').val(q['aL']);
    $('#ShortCode').val(q['sc']);
    if ((q['aF'] || q['aL']) && !q['n']) {
      show_advanced();
    }
    if (q['debug']) {
      $('#devtools').show();
      $('#debug-log').show();
    }
    if (q['sc']) {
      show_shortcode();
    }
  }
}

card_redrawer = undefined;

// Handle changes to the search criteria. Ideally we only want to
// redisplay the user isn't
// still typing. But if it takes too long to respond, it will feel
// unresponsive.
function update_search_criteria() {
    if (card_redrawer) clearTimeout(card_redrawer);
    card_redrawer = setTimeout(function() {
            card_redrawer = undefined;
            render_filtered_friends();
        }, 200);
}

// force rerender when addr_book has changed (even if 
// the search hasn't!)
addr_book.vcard_channel.on_change(function() {
    previous_search_description = ''; // no real search can have this
    update_search_criteria();
});


/* startup, shutdown, local storage */

function LocalStorage(ls) { this._ls = ls; }
LocalStorage.prototype.get = function(name) { return this._ls[name]; }
LocalStorage.prototype.set = function(name, value) { this._ls[name] = value; }
LocalStorage.prototype.remove = function(name) { this._ls.removeItem(name); }

function GlobalStorage(gs) { this._gs = gs[location.hostname]; }
GlobalStorage.prototype.get = function(name) { 
    var entry = this._gs[name];
    if (!entry) return undefined;
    return entry.value; 
}
GlobalStorage.prototype.set = function(name, value) { this._gs[name] = value; }
GlobalStorage.prototype.remove = function(name) { delete this._gs[name]; }

// Find DOM storage object in FF2, FF3, FF3.5, Safari 4, etc.
function get_local_storage() {
    // in Google Chrome Mac Beta 4.0.223.11, localStorage is null
    // instead of undefined
    if (window.localStorage) return new LocalStorage(localStorage);
    if (window.globalStorage) return new GlobalStorage(globalStorage);
    return undefined;
}

function attemptCachingFromLocalStorage() {
    var ls = get_local_storage();
    if (ls && ls.get('addr_book')) addr_book.deserialize(ls.get('addr_book'));
}

function attemptCachingToLocalStorage() {
    var ls = get_local_storage();
    if (ls) ls.set('addr_book', addr_book.serialize());
}

function deleteCacheFromLocalStorage() {
    var ls = get_local_storage();
    if (ls) {
        ls.remove('addr_book');
        $(window).unbind( 'unload', attemptCachingToLocalStorage );
    }
    return false;
}

var knx = {
    _plugins: [],
    register_plugin: function(plugin) {
        this._plugins.push(plugin);
    },
    initialize_plugins: function() {
        for (var ii = 0; ii < this._plugins.length; ii++)
            if (this._plugins[ii].initialize != undefined)
                this._plugins[ii].initialize();
    },
    reset_plugins: function() {
        for (var ii = 0; ii < this._plugins.length; ii++)
            if (this._plugins[ii].reset != undefined)
                this._plugins[ii].reset();
    }
};


$(function() {
  attemptCachingFromLocalStorage();
  knx.initialize_plugins();
});
$(window).unload( attemptCachingToLocalStorage );

function logout_from_knx() {
    $('#signout img').attr('src', 'i/trefoil_icon_bw.png');
    $('.signout').fadeOut();
    deleteCacheFromLocalStorage();
    eraseCookie('knx_windowid');
    eraseCookie('knx_closeme');
    knx.reset_plugins(); /* the actual window reloading is handled by the Facebook plugin, unfortunately */
}

function signout_visible_p() {
    if (addr_book.count_socnets() > 0) {
        $('#signout img').attr('src', 'i/trefoil_icon_8bit.png');
        $('.signout').fadeIn();
    } else {
        $('#signout img').attr('src', 'i/trefoil_icon_bw.png');
        $('.signout').fadeOut();
    }
}
addr_book.vcard_channel.on_change(function () {setTimeout(signout_visible_p, 1)});


/* misc for development */

function timeit(thunk) {
    var start = new Date();
    thunk();
    var duration = (new Date()).getTime() - start.getTime();
    setTimeout(function() {
            var end = new Date();
            var real_duration = end.getTime() - start.getTime();
            console.log('really '+real_duration+'ms');
        }, 0);
    return ''+duration+'ms';
}

// Called as an event handler on a .visibility_toggle button to toggle
// the visibility of some other element.
function toggle_visibility() {
    var button = $(this);
    var toggled = $(button.attr('toggles'));
    toggled.toggle();

    // The (single) status indicator is inside the button, with class
    // "toggle_indicator"; we change its text.
    var indicator = button.find('.toggle_indicator');
    indicator.text(toggled.is(':hidden') 
                   ? indicator.attr('when_hidden') 
                   : indicator.attr('when_shown'));
}

$(function() { $('.visibility_toggle').click(toggle_visibility) });

function validate_no_dup_unique_ids() {
    var by_uid = {};
    foreach(addr_book.current_friends(), function(vcard) {
            var uid = vcard_unique_id(vcard);
            if (!by_uid[uid]) by_uid[uid] = [];
            by_uid[uid].push(vcard);
        });

    dup_uids = [];
    for (var ii in by_uid) {
        if (by_uid.hasOwnProperty(ii)) {
            if (by_uid[ii].length > 1) dup_uids.push(by_uid[ii]);
        }
    }
    knxassert(dup_uids.length == 0, "duplicate unique IDs");
}

function set_movie_mode() {
    createCookie('knx_movie_mode', 'true', 365);
}

function clear_movie_mode() {
    eraseCookie('knx_movie_mode');
}

// XXX set at page load time (and the UI controls to change it reload
// the page)
knx_movie_mode = !!readCookie('knx_movie_mode');
