// vim:et:sw=4:ts=4
// YourWeb, 2006.
// by Kevin Ko <kevin.s.ko@gmail.com>
// $Id: myfetch.js,v 1.48 2008/07/10 16:58:28 scrabbly Exp $
//
// Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 2.5
//   http://creativecommons.org/licenses/by-nc-sa/2.5/
//
var APP_ID = "ls_myweb";
var YAHOO_BASEURL = "http://search.yahooapis.com/MyWebService/V1/urlSearch";
var YAHOO_FETCHSIZE = 15;   // default fetch size from MyWeb
var DELI_DELAY = 1200;      // delay in ms 
var DELI_BASEURL = "http://del.icio.us/api/posts/add?"; 
var GOOG_DELAY = 100;      // delay in ms 
var GOOG_BASEURL = "http://www.google.com/bookmarks/mark";
var GOOG_PUSHSIZE = 75;     // number of bookmarks to push to google each time
var MSIE_DELAY = 1000;      // MSIE poll delay

var fetchCb = null;         // function for fetch callbacks
var tmplFactories = new Object;
var templates = new Object; 
var gbformCb = null;        // google form callback

function YahooResult(resultSet)
{
    this.firstPos = resultSet['firstResultPosition'];
    this.numReturned = resultSet['totalResultsReturned'];
    this.totalAvailable = resultSet['totalResultsAvailable'];
    this.entries = new Array(); // urlEntry
    var result = resultSet['Result'];
    for (var i = 0; i < result.length; i++) {
        var t = result[i];
        var date = new Date(t['Date']*1000); // to milli-seconds
        var tags = new Array();
        var tagContainer = t['Tags']['Tag'];
        for (var j = 0; j < tagContainer.length; j++) {
            tags.push(tagContainer[j]);
        }
        var ue = new UrlEntry(t['Title'], t['Summary'], t['Url'],
                t['Note'], date, tags);
        this.entries.push(ue);
    }
}

function YahooResultContainer()
{
    this.reset();
}

YahooResultContainer.prototype = {
    // assume sequential insertion; return true on success
    append: function(res) {
        if (this.isComplete)
            return false;
        if (this.firstPos == null) {
            this.firstPos = res.firstPos;
            this.totalAvailable = res.totalAvailable;
        }
        this.tailPos = res.firstPos + res.numReturned - 1;
        if (res.firstPos > this.tailPos)
            return false; // no new data
        if (this.tailPos >= this.totalAvailable) {
            this.isComplete = true;
        }
        this.urls = this.urls.concat(res.entries);
        return true;
    },
    reset: function() {
        this.firstPos = null;       // Yahoo begins from 1
        this.tailPos = null;        // The last position containing data
        this.totalAvailable = null;
        this.isComplete = false;
        this.urls = new Array();    // UrlResults
    },
    getUrlEntries: function() {
        return this.urls;
    }
};

function FetchGateway() 
{
    this.resetCbF = null;
    this.fetchFinalCbF = null;
}

FetchGateway.prototype = {
    setResetCb: function(f) {
        this.resetCbF = f;
    },
    setFetchFinalCb: function(f) {
        this.fetchFinalCbF = f;
    },
    setFetchCb: function(f) {
    }
}

/**
 * This provides the FetchGateway interface.
 * @param {ResultContainer} resultContainer will append fetched results here
 * @param actionElem DOM element for performing the fetch
 * @constructor
 */
function YahooGateway(divAction)
{
    this.resultContainer = new YahooResultContainer();
    this.yahooId = null;
    this.resetCbF = null;
    this.fetchFinalCbF = null;
    this.fetchCbF = null;
    this.fetchSize = YAHOO_FETCHSIZE;
    this.actionElem = divAction; // DOM element for issuing request
    this.fetchCbName = null; // callback function after request
   
    var obj = this;
    this.begin = function() {
        obj.fetch(1, obj.fetchSize);
    };
}

YahooGateway.prototype = {
    getUrlEntries: function() {
        return this.resultContainer.getUrlEntries();
    },
    reset: function() {
        this.resultContainer.reset();
    },
    setId: function(id) {
        this.yahooId = id;
    },
    setResetCb: function(f) {
        this.resetCbF = f;
    },
    /**
     * Designate the function called when fetch completes.
     * @param {function} f function({@link ResultContainer})
     */
    setFetchFinalCb: function(f) {
        this.fetchFinalCbF = f;
    },
    /**
     * Designate the function called after each fetch completes.
     * @param {function} f function({@link ResultContainer})
     */
    setFetchCb: function(f) {
        this.fetchCbF = f;
    },
    /**
     * Provides current progress.
     * Returned array has two elements: [0] = current index, [1] = total 
     * @return {Array} 
     */
    getStatus: function() {
        return [this.resultContainer.tailPos, this.resultContainer.totalAvailable];
    },
    /**
     * Constructs a MyWeb request url.
     * @private
     */
    buildUrl: function(yid, numRes, sPos, cbStr) {
        var r = YAHOO_BASEURL + "?"
            + "appid=" + APP_ID 
            + "&yahooid=" + yid 
            + "&results=" + numRes 
            + "&start=" + sPos 
            + "&output=json" 
            + "&callback=" + cbStr;
        return r;
    },
    fetch: function(startPos, numRes) {
        if (this.yahooId == null)
            return false;
        var d = this.actionElem;
        var s = document.createElement("script");
        s.src = this.buildUrl(this.yahooId, numRes, startPos, this.fetchCbName);
        Lib.prototype.replaceFirstChild(d, s);
        return true;
    },
    /**
     * Generate function suitable for handling Yahoo's per-fetch callback.
     * The return value should be assigned to the function named by the
     * cbName parameter. 
     * @param {String} cbName name of the callback function; it must lie in the
     * global namespace.
     * @return function
     */
    createFetchCb: function(cbName) {
        // base exists in the closure for the callback
        this.fetchCbName = cbName;
        var base = this; 
        var cbF = function(obj) {
            if (!base.resultContainer.append(new YahooResult(obj['ResultSet']))) {
                base.reset();
                base.resetCbF();
                return false;
            }
            if (base.fetchCbF) {
                base.fetchCbF(base);
            }
            if (base.resultContainer.isComplete == false) {
                // TODO: incorporate a time delay?
                base.fetch(base.resultContainer.tailPos+1, base.fetchSize);
            } else {
                if (base.fetchFinalCbF) {
                    base.fetchFinalCbF(base);
                }
            }
        };
        return cbF;
    }
}

function PushGateway()
{
    this.intervalId = null;
    this.delay = null;
    this.currIndex = 0;
    this.button = null;
    this.actionElem = null;

    this.delayTimeoutF = null;
    // the callback functions; have the form function({PushGateway})
    this.pushCbF = null;
    this.pushFinalCbF = null;
}

PushGateway.prototype = {
    /**
     * create a hidden iframe inside the given element
     * @param divElem container div for the iframe
     */
    createActionElem: function(divElem) {
        var f = document.createElement("iframe");
        f.style.border = "0px";
        f.style.width = "0px";
        f.style.height = "0px";
        Lib.prototype.replaceFirstChild(divElem, f);
        return f;
    },
    /**
     * Associated with onload event to enforce a time delay between requests.
     */
    createDelayEnableHndlr: function() {
        var obj = this;
        return function(evt) {
            // del.icio.us wants at least a 1s delay
            obj.intervalId = window.setInterval(obj.delayTimeoutF, obj.delay);
        };
    },
    /**
     * Associated with timer expiration to perform the actual operation.
     */
    createDelayTimeoutHndlr: function() {
        var obj = this;
        return function(evt) {
            window.clearInterval(obj.intervalId);
            // callback should refer to the last one sent
            if (obj.pushCbF) {
                obj.pushCbF(obj);
            }
            if (obj.currIndex >= 0 &&
                    obj.currIndex < obj.urlEntries.length) {
                obj.sendOne(obj.currIndex);
                obj.currIndex++;
                if (Lib.prototype.isMSIE()) {
                    // special case for MSIE's lack of iframe onload
                    obj.intervalId = 
                        window.setInterval(obj.createMSIEPoll(), MSIE_DELAY);
                }
            } else {
                obj.button.disabled = false;
                if (obj.pushFinalCbF) {
                    obj.pushFinalCbF(obj);
                }
            }
        };
    },
    createMSIEPoll: function() {
        var obj = this;
        return function(evt) {
            if (Lib.prototype.isMSIE()) {
                // check that the iframe load has completed 
                if (obj.actionElem.readyState == "complete") {
                    window.clearInterval(obj.intervalId);
                    // establish the normal delay
                    obj.intervalId = 
                        window.setInterval(obj.delayTimeoutF, obj.delay);
                }
            }
        };
    },
    /**
     * Associated with some initial event to begin the time-delayed operation.
     */
    createBeginHndlr: function() {
        var obj = this;
        return function(evt) {
            obj.button = evt.target;
            evt.target.disabled = true;
            obj.currIndex = 0;
            obj.sendOne(obj.currIndex);
            if (obj.pushCbF) {
                obj.pushCbF(obj);
            }
            obj.currIndex++;
            // enforce time delay between loads
            if (Lib.prototype.isMSIE()) {
                // MSIE does not have an onload and onreadystatechange does not
                // behave correctly, so use a timer.
                obj.intervalId = 
                    window.setInterval(obj.createMSIEPoll(), MSIE_DELAY);
            } else {
                obj.actionElem["onload"] = obj.delayEnableF;
            }
        };
    },
    setPushCb: function(f) {
        this.pushCbF = f;
    },
    setPushFinalCb: function(f) {
        this.pushFinalCbF = f;
    },
    setDelay: function(t) {
        this.delay = t;
    },
    /**
     * Estimate remaining time based on delay.
     * Returned array has two elements: [0] = minutes, [1] = seconds
     * @return {Array} 
     */
    getTimeRemaining: function() {
        var di = (this.urlEntries.length - this.currIndex);
        var t = di*(this.delay/1000);
        var min = Math.floor(t/60);
        var sec = Math.floor(t-60*min);
        return [min, sec];
    },
    /**
     * Provides current progress.
     * Returned array has two elements: [0] = current index, [1] = total 
     * @return {Array} 
     */
    getStatus: function() {
        return [this.currIndex, this.urlEntries.length];
    },
    setActionLocation: function(val) {
        this.actionElem.src = val;
    },
    sendOne: function(i) {
        var url = this.urlEntries[i];
        this.setActionLocation(this.buildUrl(url));
    }
}

/**
 * @param {String} serviceName (eg. "del.icio.us")
 * @param {String} baseUrl main URL for the API
 * @param {Array of UrlEntry} urlEntries
 * @param actionElem IFRAME element for hosting the requests
 * @constructor
 */
function DeliGateway(serviceName, baseUrl, urlEntries, divAction)
{
    this.serviceName = serviceName;
    this.baseUrl = baseUrl;
    this.urlEntries = urlEntries;
    this.delay = DELI_DELAY;
    this.actionElem = this.createActionElem(divAction);

    // handlers
    this.delayEnableF = this.createDelayEnableHndlr();
    this.delayTimeoutF = this.createDelayTimeoutHndlr();
    this.begin = this.createBeginHndlr();
}

DeliGateway.prototype = new PushGateway();
DeliGateway.prototype.constructor = DeliGateway;

// return two digit string representation of x
DeliGateway.prototype.twoFixed = function(x) {
        return ((x >= 10 || x < 0) ? "" : "0") + x; 
};

DeliGateway.prototype.deliTimeStr = function(date) {
        var r = date.getFullYear() + "-"
            this.twoFixed(date.getMonth()+1) + "-"
            this.twoFixed(date.getDate()) + "T"
            this.twoFixed(date.getHours()) + ":"
            this.twoFixed(date.getMinutes()) + ":"
            this.twoFixed(date.getSeconds()) + "Z";
        return r;
};

DeliGateway.prototype.buildUrl = function(u) {
    var r = this.baseUrl
        + "url=" + encodeURIComponent(u.url)
        + "&description=" + encodeURIComponent(u.title);
        + "&dt=" + this.deliTimeStr(u.date)
        + "&replace=no";
    if (u.note != "") {
        r += "&extended=" + encodeURIComponent(u.note);
    }
    var tags = u.tags.join(" ");
    if (tags != "") {
        r += "&tags=" + encodeURIComponent(tags);
    }
    return r;
};

/*
 * This is a newer interface to Google Bookmarks.
 * @param actionElem is a div element that can be freely populated by
 * the function.
 * @param bms array of UrlEntries
 * @param start starting position
 * @param count number of entries to export
 */
function GoogleXMLExport(actionElem, bms, start, count) {
    var xmlEscape = function(str) {
        return str.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
    };
    var data = "";
    var end = Math.min(start + count, bms.length);
    for (var i = start; i < end; i++) {
        var b = bms[i];
        data += "<bookmark>";
        data += "<url>" + xmlEscape(b.url) + "</url>";
        data += "<title>" + xmlEscape(b.title) + "</title>";
        data += "<date>" + Math.round(b.date.getTime()/1000) + "</date>";
        if (b.tags.length) {
            data += "<labels><label>" + b.tags.join(",") + "</label></labels>";
        }
        data += "</bookmark>";
    }                                                                           
    var dform = document.createElement("form");
    dform.action = "http://www.google.com/bookmarks/mark?op=upload" +
        "&zx=" + Math.round(65535*Math.random());
    dform.method = "POST";
    var idata = document.createElement("input");
    idata.type = "hidden";
    idata.name = "<?xml version";
    idata.value = "\"1.0\" encoding=\"utf-8\"?>" +                              
        "<bookmarks>" + data + "</bookmarks>";
    dform.appendChild(idata);
    actionElem.appendChild(dform);
    dform.submit();
};

/*
 * This is the old push-based interface into Google Bookmarks.  It's
 * deprecated for the most part.
 * @param baseUrl should not have the trailing ?
 */
function GoogGateway(serviceName, baseUrl, urlEntries, actionFrame)
{
    this.serviceName = serviceName;
    this.baseUrl = baseUrl;
    this.urlEntries = urlEntries;
    this.delay = GOOG_DELAY;
    this.actionElem = null;
    this.actionFrame = actionFrame;

    // handlers
    var obj = this;
    this.begin = function(evt) {
        obj.button = evt.target;
        evt.target.disabled = true;
        obj.currIndex = 0;
        if (obj.pushCbF) {
            obj.pushCbF(obj);
        }
        obj.currIndex++;
        obj.sendOne(obj.currIndex - 1); // avoid race
    };
}
GoogGateway.prototype = new PushGateway();
GoogGateway.prototype.constructor = GoogGateway;

GoogGateway.prototype.sendFin = function() {
    // callback should refer to the last one sent
    if (this.pushCbF) {
        this.pushCbF(this);
    }
    if (this.currIndex >= 0 && this.currIndex < this.urlEntries.length) {
        this.currIndex++;
        this.sendOne(this.currIndex-1); // avoid race
    } else {
        // finished
        // google specific; close the window
        this.actionElem.close();
        this.button.disabled = false;
        if (this.pushFinalCbF) {
            this.pushFinalCbF(this);
        }
    }
};

GoogGateway.prototype.setActionLocation = function(u) {
    if (!this.actionElem) {
        this.actionElem = window.open("", "", "height=100,width=100");
        this.childUrl = "gbframe.html?callback=gbformCb";
        /*
            this.actionElem = frames[this.actionFrame];
            this.childUrl = "gbframe.html?callback=gbformCb&frame=1";
         */
    } 
    /*
     * @param f the form passed from the frame/window
     */
    var obj = this;
    gbformCb = function(f) {
        f.bkmk.value = u.url;
        f.title.value = u.title;
        if (u.note != "") {
            f.annotation.value = u.note;
        }
        var tags = u.tags.join(",");
        if (tags != "") {
            f.labels.value = tags;
        }
        f.sig.value = Math.round(65535*Math.random());

        // the following determines load completion indirectly
        obj.oldHref = obj.actionElem.location.href;
        obj.step = 0; // grace period
        var func = function() {
            try {
                if (obj.actionElem.location.href != obj.oldHref) {
                    // dummy; load is presumed complete on exception
                    var x = 1;
                }
            } catch(e) {
                // load is complete; apply a grace period for good measure
                if (obj.step++ > 1) {
                    window.clearInterval(obj.intervalId);
                    obj.sendFin();
                }
            }
        };

        f.submit();
        obj.intervalId = window.setInterval(func, obj.delay);
    };
    this.actionElem.location.href = this.childUrl;
};

GoogGateway.prototype.buildUrl = function(u) {
    return u;
};

// 
// Customized sections follow
//

function fetchStatusUpdate(obj)
{
    if (!templates["fetch_status"]) {
        templates["fetch_status"] = 
            tmplFactories["fetch_status"].create("fetch_status", true);
    }
    var t = templates["fetch_status"];
    t.title.set("Status");
    var s = obj.getStatus();
    t.currCount.set(s[0]);
    t.totalCount.set(s[1]);
    var parent = document.getElementById("status");
    Lib.prototype.replaceFirstChild(parent, t.elem);
}

function pushStatusUpdate(obj)
{
    if (!templates["push_status"]) {
        templates["push_status"] = 
            tmplFactories["push_status"].create("push_status", true);
    }
    var t = templates["push_status"];
    var s = obj.getStatus();
    var tr = obj.getTimeRemaining();
    t.title.set("Status");
    t.currCount.set(s[0]);
    t.totalCount.set(s[1]);
    t.dest.set(obj.serviceName);
    t.min.set(tr[0]);
    t.sec.set(tr[1]);
    var parent = document.getElementById("status");
    Lib.prototype.replaceFirstChild(parent, t.elem);
}

/**
 * @param content DOM element whose innerHTML contains the bookmark content
 */
function bookmarkImport(content, useTagCommas, useFoldersAsTags)
{
    var p;
    if (Lib.prototype.isMSIE()) {
        p = new BookmarkParseIE(useTagCommas, useFoldersAsTags);
    } else {
        p = new BookmarkParse(useTagCommas, useFoldersAsTags);
    }
    var bf = p.parse(content.firstChild);
    return bf.toArray();
}

function bookmarkExport(urlEntries)
{
    var w = window.open();
    w.document.writeln('<!DOCTYPE NETSCAPE-Bookmark-file-1>');
    w.document.writeln('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">');
    w.document.writeln('<TITLE>Bookmarks</TITLE>');
    w.document.writeln('<H1>Bookmarks</H1>');
    w.document.writeln('<DL><p>');
    for (var i = 0; i < urlEntries.length; i++) {
        var url = urlEntries[i];
        var tags = url.tags.join(", ");
        w.document.writeln('<DT><A HREF="' + url.url + 
                '" ADD_DATE="' + url.date.getTime()/1000 +
                '" LAST_MODIFIED="' + url.date.getTime()/1000 +
                '" TAGS="' + tags + '">' +
                url.title + '</A>');
        if (/^\s*$/.exec(url.note) == null) {
            // non-empty note
            w.document.write('<DD>' + url.note);
            if (url.tags.length) {
                w.document.write(' ...\n(tags:  ' + tags + ')');
            }
            w.document.write('\n');
        } else if (url.tags.length) {
            w.document.writeln('<DD> (tags:  ' + tags + ')');
        }
    }
    w.document.write('</DL><p>');
    w.document.close();
}

function State(divAction)
{
    this.divAction = divAction;
    this.currGateway = null;
    this.urlEntries = null;

    this.funcs = new Array(); // subsequent functions
    this.index = 0;
    this.step = 1;
    step1(this);
}

State.prototype = {
    next: function() {
        if (this.index < this.funcs.length) {
            this.funcs[this.index++](this);
        }
    },
    appendStep: function(f) {
        this.funcs.push(f);
    }
};

function step1(state)
{
    var s = document.getElementById("steps");
    templates["step1"].title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step1"].elem);

    var yahooHndlr = function(evt) {
        templates["help"].shade();
        state.appendStep(step_fetch_yahoo);
        state.next();
        return false; // prevent form action
    }
    var browserHndlr = function(evt) {
        templates["help"].shade();
        state.appendStep(step_fetch_browser);
        state.next();
        return false; // prevent form action
    }
    Lib.prototype.addEvent(document.step1.yahoo, 'click', yahooHndlr, true);
    Lib.prototype.addEvent(document.step1.browser, 'click', browserHndlr, true);
}

function step_fetch_yahoo(state)
{
    var s = document.getElementById("steps");
    templates["step_fetch_yahoo"].title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step_fetch_yahoo"].elem);
    state.currGateway = new YahooGateway(state.divAction);

    var submit = function(evt) {
        var id = document.step_fetch_yahoo.yahoo_id.value;
        if (!id) {
            alert("Please complete field.");
            return false;
        }
        document.step_fetch_yahoo.yahoo_id.disabled = true;
        document.step_fetch_yahoo.submit.disabled = true;
        state.currGateway.setId(id);
        state.currGateway.begin();
        return false; // prevent form action
    }
    var reset = function(evt) {
        document.step_fetch_yahoo.yahoo_id.disabled = false;
        document.step_fetch_yahoo.submit.disabled = false;
        alert("Error!  Please verify that you input a valid id.");
    }
    state.currGateway.setResetCb(reset);
    state.currGateway.setFetchCb(fetchStatusUpdate);
    state.currGateway.setFetchFinalCb(function() { 
            state.urlEntries = state.currGateway.getUrlEntries();
            state.currGateway = null;
            state.appendStep(step2);
            state.next(); 
    });
    fetchCb = state.currGateway.createFetchCb("fetchCb");

    Lib.prototype.addEvent(document.step_fetch_yahoo.submit, 'click', submit, true);
}

function step_fetch_browser(state)
{
    var s = document.getElementById("steps");
    templates["step_fetch_browser"].title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step_fetch_browser"].elem);

    var clear = function(evt) {
        document.step_fetch_browser.bookmarks.value = "";
        return false;
    }
    var submit = function(evt) {
        var reset = true;
        var v = document.step_fetch_browser.bookmarks.value;
        if (!v) {
            alert("Please complete field.");
            return false;
        }
        document.step_fetch_browser.bookmarks.disabled = true;
        document.step_fetch_browser.submit.disabled = true;
        document.step_fetch_browser.clear.disabled = true;
        state.divAction.innerHTML = v;
        try {
            state.urlEntries = bookmarkImport(state.divAction,
                document.step_fetch_browser.useTagCommas.checked,
                document.step_fetch_browser.useFoldersAsTags.checked);
            if (!state.urlEntries.length) {
                alert("No valid URLs found.");
            } else {
                reset = false;
                var obj = new Object();
                obj.getStatus = function() {
                    return [state.urlEntries.length, state.urlEntries.length];
                }
                fetchStatusUpdate(obj);
                state.divAction.innerHTML = "";
                state.appendStep(step2);
                state.next();
            }
        } catch (e) {
            alert("Unable to parse the data; please check the contents.");
        }
        if (reset) {
            document.step_fetch_browser.bookmarks.disabled = false;
            document.step_fetch_browser.submit.disabled = false;
            document.step_fetch_browser.clear.disabled = false;
        }
        return false; // prevent form action
    }
    Lib.prototype.addEvent(document.step_fetch_browser.submit, 
            'click', submit, true);
    Lib.prototype.addEvent(document.step_fetch_browser.clear, 
            'click', clear, true);
}

function step2(state)
{
    var s = document.getElementById("steps");
    templates["step2"].title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step2"].elem);

    var exportHndlr = function(evt) {
        bookmarkExport(state.urlEntries);
        return false; // prevent form action
    }
    var transferHndlr = function(evt) {
        if (!document.step2.delicious.checked &&
            !document.step2.google.checked) {
            alert("Please select a service.");
        } else {
            if (document.step2.delicious.checked) {
                state.appendStep(step_deli);
            }
            if (document.step2.google.checked) {
                state.appendStep(step_goog);
            }
            state.appendStep(step_done);
            state.next();
        }
        return false;
    }
    Lib.prototype.addEvent(document.step2.exportFile, 'click', exportHndlr, true);
    Lib.prototype.addEvent(document.step2.transfer, 'click', transferHndlr, true);
}

function step_goog(state)
{
    var s = document.getElementById("steps");
    var t = templates["step_goog"];
    t.title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step_goog"].elem);
    state.currGateway = new GoogGateway("Google Bookmarks", GOOG_BASEURL,
            state.urlEntries, "action_frame");
    state.currGateway.setPushCb(pushStatusUpdate);

    // Another approach: pushing in blocks of 75 to the XML
    // importing interface.
    var total_count = state.urlEntries.length;
    var curr_pos = 0;

    var window_create = function() {
        var w = window.open("", "", "height=" + window.height +
                ",width=" + window.width +
                ",scrollbars=1,resizable=1,toolbar=1,status=1,location=1");
        return w;
    }
    var w = null;

    var xmlexport = function() {
        if (total_count <= 0)
            return;
        if (w == null || w.closed) {
            w = window_create();
        }
        w.document.open();
        w.document.write("<html><div id='action'></div></html>");
        w.document.close();
        var actionElem = w.document.getElementById("action");
        GoogleXMLExport(actionElem, state.urlEntries,
                curr_pos, GOOG_PUSHSIZE);
        total_count -= GOOG_PUSHSIZE;
        curr_pos += GOOG_PUSHSIZE;
        if (total_count > 0) {
            alert("You have " + total_count + " bookmarks remaining.");
        }
    };

    var handler = function(evt) {
        xmlexport();
        if (total_count > 0) {
            document.step_goog.begin.innerHTML = "Continue";
        } else {
            document.step_goog.begin.innerHTML = "Done";
            document.step_goog.begin.disabled = true;
        }
        return false;
    };

    /*
    var begin = function(evt) {
        // The old method (which seems broken):
        state.currGateway.setPushFinalCb(function() {
                state.currGateway = null;
                state.next();
        });
        state.currGateway.begin(evt);
        return false;
    }
    Lib.prototype.addEvent(document.step_goog.begin, 'click', begin, true);
    */
    Lib.prototype.addEvent(document.step_goog.begin, 'click', handler, true);
}

function step_deli(state)
{
    var s = document.getElementById("steps");
    var t = templates["step_deli"];
    t.title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step_deli"].elem);
    state.currGateway = new DeliGateway("del.icio.us", 
            DELI_BASEURL, state.urlEntries, state.divAction);
    state.currGateway.setPushCb(pushStatusUpdate);

    var begin = function(evt) {
        state.currGateway.setPushFinalCb(function() { 
                state.currGateway = null;
                state.next(); 
        });
        state.currGateway.begin(evt);
        return false;
    }
    Lib.prototype.addEvent(document.step_deli.begin, 'click', begin, true);
}

function step_done(state)
{
    var s = document.getElementById("steps");
    var t = templates["step_done"];
    t.title.set("Step " + state.step++);
    Lib.prototype.replaceFirstChild(s, templates["step_done"].elem);
    var exportHndlr = function(evt) {
        bookmarkExport(state.urlEntries);
        return false; // prevent form action
    }
    Lib.prototype.addEvent(document.step_done.exportFile, 'click', exportHndlr, true);
}

function main() 
{
    var divAction = document.getElementById("action");
    var s = new State(divAction);
}
