/*
 Copyright 2005-2008 Foxmarks Inc.

 foxmarks-service.js: component that implements the "service" interface to
 the core synchronization code.

 */

var Cc = Components.classes;
var Ci = Components.interfaces;
var Xmarks;

if(Xmarks === undefined){
    Xmarks = {};
}

function LoadJavascript(filename, id) {
    if (id == "undefined") {
        Cc["@mozilla.org/moz/jssubscript-loader;1"].
        getService(Ci.mozIJSSubScriptLoader).
        loadSubScript(filename, null);
    }
}

function GetTopWin(wintype) {
    var topwin = Cc['@mozilla.org/appshell/window-mediator;1'].
        getService(Ci.nsIWindowMediator).
        getMostRecentWindow(wintype);

    return topwin;
}

function FoxmarksSetCookie() {
    var url = "http://www.xmarks.com";
    var d = new Date();
    d.setTime(d.getTime() + (20 * 365 * 24 * 60 * 60 * 1000));
    var cm = Components.classes["@mozilla.org/cookiemanager;1"].
                    getService(Components.interfaces.nsICookieManager2);

    cm.add(".xmarks.com", "/", "mid", Xmarks.gSettings.machineId, false, false, false,
            d.getTime() / 1000);
}

function FoxmarksLaunchUpgradePage() {
    FoxmarksSetCookie();

    upgradeCallback.timer = Cc["@mozilla.org/timer;1"].
        createInstance(Ci.nsITimer);
    upgradeCallback.timer.initWithCallback(upgradeCallback, 5000,
        Ci.nsITimer.TYPE_ONE_SHOT);
}

function FoxmarksBuildPostData(num_bookmarks){
    return {
        username: Xmarks.gSettings.username,
        version: Xmarks.FoxmarksVersion(),
        types: {
            bookmarks: {
                is_enabled: Xmarks.gSettings.isSyncEnabled("bookmarks"),
                count: num_bookmarks
            },
            passwords: {
                is_enabled: Xmarks.gSettings.isSyncEnabled("passwords")
            }
        }
    };
}
var upgradeCallback = {
    notify: function(timer) {
        FoxmarksSyncService.server.countItems("bookmarks",
            "bookmark", function(status, num_bookmarks){
            var currver = Xmarks.FoxmarksVersion();
            var wm = Cc['@mozilla.org/appshell/window-mediator;1'].
                getService(Ci.nsIWindowMediator);
            var topWindow = wm.getMostRecentWindow('navigator:browser');

            if(topWindow &&
                topWindow.document){ 
                var appcontent =
                    topWindow.document.getElementById("appcontent");
                var listener = function(e){
                    var doc = e.originalTarget;
                    var button = 
                        doc.getElementById(
                        "set_up_password_sync_button"
                    );

                    if(button){
                        button.setAttribute("onclick",  null);
                        button.addEventListener(
                            "click",
                            function(){
                                var os = Cc["@mozilla.org/observer-service;1"].
                                    getService(Ci.nsIObserverService);
                                os.notifyObservers(null, 
                                    "foxmarks-showsettingpane", 
                                    "foxmarks-syncpane"
                                );
                            }, 
                            true
                        );
                        appcontent.removeEventListener(
                            "DOMContentLoaded",
                            listener, 
                            false
                        );
                    }
                };
                if(appcontent){
                    appcontent.addEventListener(
                        "DOMContentLoaded",
                        listener, 
                        false
                    );
                }
            }
            // We used to set the protocol as Xmarks.gSettings.httpProtocol,
            // but that causes warnings when that page references non-HTTPS
            // ads, so we peg the protocol as http.
            Xmarks.OpenInNewTab("http://" + Xmarks.gSettings.webHost + 
                "/firefox/upgrade/" + currver, true, 
                FoxmarksBuildPostData(num_bookmarks));
            Xmarks.gSettings.currVersion = currver;
        });
    }
}
function FoxmarksCheckForServerChanges() {
    if(Xmarks.gSettings.useOwnServer){
        var os = Cc["@mozilla.org/observer-service;1"]
            .getService(Ci.nsIObserverService);
        os.notifyObservers(null, "foxmarks-realstart", "");
    } else {
        var url = Xmarks.gSettings.httpProtocol + Xmarks.gSettings.apiHost +
            "/internal/serverprefs";
        var request = new Request(
            "POST", 
            url,
            {
                mid: Xmarks.gSettings.machineId,
                version: Xmarks.gSettings.serverVersion,
                abgroup: Xmarks.gSettings.abgroup,
                cid: "xmfx"
            },
            {
                isAuthRequest: false,
                headers: null,
                ignoreBody: false,
                ignoreAuthTokens: true
            }
        );
        request.Start(function(response){
            if(response && response.status === 0){

                if(response.version > Xmarks.gSettings.serverVersion){
                    if(response.prefs){ 
                        var prefs = response.prefs;
                        for(var pref in prefs){
                            if(prefs.hasOwnProperty(pref)){
                                Xmarks.gSettings[pref] = prefs[pref];
                            }
                        }
                    }
                    Xmarks.gSettings.serverVersion = response.version;
                }
            }
            var os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
            os.notifyObservers(null, "foxmarks-realstart", "");
        });
    }
}

function FoxmarksLaunchSetupWizard() {
    FoxmarksSetCookie();
    FoxmarksWizardCallback.timer = Cc["@mozilla.org/timer;1"].
        createInstance(Ci.nsITimer);
    FoxmarksWizardCallback.timer.initWithCallback(FoxmarksWizardCallback, 5000,
        Ci.nsITimer.TYPE_ONE_SHOT);
}

var FoxmarksWizardCallback = {
    notify: function(timer) {
        var url = "http://" + Xmarks.gSettings.staticHost + Xmarks.gSettings.wizardPrefs;
        var request = new Request(
            "GET", 
            url,
            null,
            {
                isAuthRequest: false,
                headers: null,
                ignoreBody: false,
                ignoreAuthTokens: true
            }
        );
        request.Start(function(response){
            var os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
            var JSON = Cc["@mozilla.org/dom/json;1"].
                createInstance(Ci.nsIJSON);
            os.notifyObservers(null, "foxmarks-newpopup", JSON.encode(response));
        });
    }
}

function HandleShutdown(cancel) {
    var retval = { helpurl: null };
    var sb = Xmarks.Bundle().GetStringFromName;
    var dontask = {value: false};
    var rv = 0;

    if (!Xmarks.gSettings.haveSynced || GetState() != "dirty") {
        return;
    }

    var topwin = GetTopWin();

    if (!topwin) {
        Xmarks.LogWrite("HandleShutdown: Couldn't find a topwin!");
        return;
    }

    if (Xmarks.gSettings.syncOnShutdown && Xmarks.gSettings.syncOnShutdownAsk) {

        rv = Cc["@mozilla.org/embedcomp/prompt-service;1"].
        getService(Ci.nsIPromptService).
        confirmEx(topwin, sb("appname.long"), sb("msg.unsynced"),
            Ci.nsIPromptService.STD_YES_NO_BUTTONS, null, null, null,
            sb("msg.dontask"), dontask);
        // Reverse sense: confirmEx returns 0 - yes, 1 - no
        rv = !rv;

        // If user says "don't ask me again", set syncOnShutdown to whatever
        // they have chosen in this instance.
        if (dontask.value) {
            Xmarks.gSettings.syncOnShutdown = rv;
        }
        Xmarks.gSettings.syncOnShutdownAsk = !dontask.value;

    } else {                           // don't ask
        rv = Xmarks.gSettings.syncOnShutdown;
    }

    if (rv) {
        var win = topwin.openDialog(
            "chrome://foxmarks/content/foxmarks-progress.xul", "_blank",
            "chrome,dialog,modal,centerscreen", "synch", retval, null);
        if (retval.helpurl) { // we hit an error and user pressed help button
            if (cancel instanceof Ci.nsISupportsPRBoolean) {
                cancel.value = true;
                topwin.openDialog("chrome://browser/content/browser.xul",
                    "_blank", "chrome,all,dialog=no", retval.helpurl);
            }
        }
    }
}

function LoadFiles() {
    LoadJavascript("chrome://foxmarks/content/foxmarks-log.js",
        typeof(Xmarks.LogWrite));
    LoadJavascript("chrome://foxmarks/content/foxmarks-settings.js",
        typeof(Xmarks.gSettings));
    LoadJavascript("chrome://foxmarks/content/foxmarks-update.js",
        typeof(ForceUpdate));
    LoadJavascript("chrome://foxmarks/content/foxmarks-clobber.js",
        typeof(onClobberCancel));
    LoadJavascript("chrome://foxmarks/content/foxmarks-bookmark.js",
        typeof(loadDatasourceSet));
    if ("@mozilla.org/browser/nav-bookmarks-service;1" in Cc)
        LoadJavascript("chrome://foxmarks/content/foxmarks-places.js",
            typeof(BookmarkDatasource));
    else
        LoadJavascript("chrome://foxmarks/content/foxmarks-rdf.js",
            typeof(BookmarkDatasource));
    LoadJavascript("chrome://foxmarks/content/foxmarks-nodes.js",
        typeof(Node));
    LoadJavascript("chrome://foxmarks/content/foxmarks-command.js",
        typeof(Command));
    LoadJavascript("chrome://foxmarks/content/foxmarks-core.js",
        typeof(Synchronize));
    LoadJavascript("chrome://foxmarks/content/foxmarks-network.js",
        typeof(Request));
    LoadJavascript("chrome://foxmarks/content/foxmarks-json.js",
        "undefined");

    LoadJavascript("chrome://foxmarks/content/shared/Base64.js",
        typeof(Base64));
    LoadJavascript("chrome://foxmarks/content/shared/CreateAESManager.js",
        typeof(CreateAESManager));
    LoadJavascript("chrome://foxmarks/content/foxmarks-utils.js",
        typeof(forEach));
    LoadJavascript("chrome://foxmarks/content/foxmarks-unittest.js",
        typeof(gFoxmarksUT));
    LoadJavascript("chrome://foxmarks/content/foxmarks-uitools.js",
        typeof(Xmarks.OpenWindowByType));
    if("@mozilla.org/login-manager;1" in Cc){
        LoadJavascript("chrome://foxmarks/content/foxmarks-password.js",
            typeof(PasswordDatasource));
    }
    LoadJavascript("chrome://foxmarks/content/foxmarks-server.js",
        typeof(SyncServer));
}

var logStream = null;
 
function removeTempLogFile(){
    var fileremoved = Cc['@mozilla.org/file/directory_service;1']
        .getService(Ci.nsIProperties)
        .get('ProfD', Ci.nsIFile);
    fileremoved.append("xmarks.temp.log");
    try {
        fileremoved.remove(false);
    } catch(e){}
}
function logMoveFile(){
    try {
        var file = Cc['@mozilla.org/file/directory_service;1']
            .getService(Ci.nsIProperties)
            .get('ProfD', Ci.nsIFile);
        file.append("xmarks.log");

        var dir = Cc['@mozilla.org/file/directory_service;1']
            .getService(Ci.nsIProperties)
            .get('ProfD', Ci.nsIFile);
        removeTempLogFile();
        file.moveTo(dir, "xmarks.temp.log");


        var fromstream = Cc["@mozilla.org/network/file-input-stream;1"]
            .createInstance(Ci.nsIFileInputStream);
        var tostream = Cc["@mozilla.org/network/file-output-stream;1"]
            .createInstance(Ci.nsIFileOutputStream);

        fromstream.init(file, -1, 0x01, 0);
        var logSeek = fromstream.QueryInterface(Ci.nsISeekableStream);
        var lread = fromstream.QueryInterface(Ci.nsILineInputStream);
        var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
            .createInstance(Ci.nsIScriptableUnicodeConverter);
        converter.charset = "UTF-8";
        var i =  100 * 1024;
        if(i > file.fileSize){
            i = file.fileSize;
        }

        var filenew = Cc['@mozilla.org/file/directory_service;1']
            .getService(Ci.nsIProperties)
            .get('ProfD', Ci.nsIFile);
        filenew.append("xmarks.log");
        tostream.init(filenew, 0x02 | 0x08 | 0x10, 0664, 0);

        logSeek.seek(logSeek.NS_SEEK_END, -i);

        var buf;
        var cont = true;
        var lineData = {};
        var ctr = 0;
        // throw out the first one; could be mid line
        cont = lread.readLine(lineData);
        while(cont){
            lineData = {};
            cont = lread.readLine(lineData);
            if(cont){
                buf = converter.ConvertToUnicode(lineData.value) + "\n";
                tostream.write(buf, buf.length);
            }
        }

     } catch(e){
        Components.utils.reportError(e);
     } finally {
        if(fromstream !== undefined)
            fromstream.close();
        if(tostream !== undefined)
            tostream.close();
     }
    removeTempLogFile();
}
function logFileOpen() {
    var file = Cc['@mozilla.org/file/directory_service;1']
    .getService(Ci.nsIProperties)
    .get('ProfD', Ci.nsIFile);

    var needsTruncate = false;
    var filesize = 0;
    file.append("xmarks.log");

    // check the file size
    try {
       if(file.isFile()){
            filesize = file.fileSize;
       }

       if(filesize > 500 * 1024 && Xmarks.gSettings.truncateLog ){
            logMoveFile();
       }
    } catch(e) {
   //     Components.utils.reportError(e);
    }
    
    try {
        logStream = Cc["@mozilla.org/network/file-output-stream;1"]
        .createInstance(Ci.nsIFileOutputStream);

        // use write, append, create
        logStream.init(file, 0x02 | 0x08 | 0x10, 0664, 0);
    } catch (e) {
        // We failed to open. Close and try again next time.
        logFileClose();
    }
}

function logFileClose() {
    try {
        logStream.close();
    } catch (e) {}
    logStream = null;
}

var gFailureCount = 0;// number of consecutive times we've failed
var gBackoffUntil = 0;// if we've been failing, our backoff time (ms since 1970)
var gServerBackoff = 
    { 'bookmarks': 0,
      'passwords': 0,
      min: function(){
        return Math.min(this['bookmarks'], this['passwords']);
      },
      max: function(){
        return Math.max(this['bookmarks'], this['passwords']);
      },
      clear: function(){
        this['bookmarks'] = 0;
        this['passwords'] = 0;
      }
    };  // seconds server wants us to back off

function ReturnErrorMsg(code, msg, restoreState, noBackoff) {
    Xmarks.gSettings.lastError  = code;
    Xmarks.gSettings.lastError  = code;
    Xmarks.Notify({status: code, msg: msg });
    ClearBusy();
    SetState(restoreState ? restoreState : ((code == 503 || code == 2)
            ? "dirty" : "error"));
    Xmarks.LogWrite("Returned error: " + msg + "(" + code + ")");
    if(code != 2 && !noBackoff){
        var d = new Date();
        // Initial back-off is 15 minutes, doubling with each error
        gBackoffUntil = d.getTime() + 1000 * (Math.max(
            15 * 60 * Math.pow(2, gFailureCount++),
            gServerBackoff.max()) + Math.floor(Math.random() * 15));
        gServerBackoff.clear();;
        var retry = new Date();
        retry.setTime(gBackoffUntil);
        Xmarks.LogWrite("Will retry at " + retry);
    }
    FoxmarksSyncService.lastError = code;
}

function ReturnErrorCode(code) {
    ReturnErrorMsg(code, Xmarks.MapError(code));
}

function ReturnSuccess(msgname, args, restoreState) {
    if (args == null) {
        var args = {};
    }

    args.status = 0;
    args.msg = Xmarks.Bundle().GetStringFromName(msgname);

    SetState(restoreState ? restoreState : "ready");
    ClearBusy();
    gFailureCount = 0;
    FoxmarksSyncService.lastError = 0;
    Xmarks.LogWrite("Success: " + Xmarks.Bundle().GetStringFromName(msgname));
    Xmarks.Notify(args);
}

function SetBusy() {
    if (IsBusy._busy) {
        return false;
    } else {
        IsBusy._busy = true;
        SetState("working");
        return true;
    }
}

function ClearBusy() {
    IsBusy._busy = false;
}

function IsBusy() {
    return IsBusy._busy;
}
IsBusy._busy = false;

function GetState() {
    return GetState._state;
}
GetState._state = "ready";

function SetState(newstate) {
    if (newstate == GetState()) {
        return;
    }

    var os = Cc["@mozilla.org/observer-service;1"]
    .getService(Ci.nsIObserverService);

    os.notifyObservers(null, "foxmarks-statechange", newstate);

    GetState._state = newstate;
}

// Internal callbacks

function LogStart(op, dest) {
    Xmarks.LogWrite("------ Xmarks/" + Xmarks.FoxmarksVersion() + " (" + 
            FoxmarksSyncService.getStorageEngine("bookmarks") + ") starting " + op +
            " with " + dest + " ------");
}

///////////////////////////////////////////////////////////////////////////
//
// nsFoxmarksService
//

var FoxmarksSyncService = null;  // set during initialization to instance object

function nsFoxmarksService() {
    LoadFiles();
}

nsFoxmarksService.prototype = {
    /////////////////////////////////////////////////////////////////////////
    // nsIFoxmarksService

    timer: null,
    _server: null,
    lastmodified: null,

    status: function(syncType) {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }
        LogStart("status", Xmarks.gSettings.host);
        if(syncType === undefined)
            syncType = "bookmarks";

        this.server.status(syncType, function(status, response){
            if (status) {
                ReturnErrorCode(status);
            }
            else {
                ReturnSuccess("msg.accountverified", response);
            }
        });
        return true;
    },
    extstatus: function(syncType) {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }
        LogStart("extstatus", Xmarks.gSettings.host);
        if(syncType === undefined)
            syncType = "bookmarks";

        this.server.extstatus(syncType, function(status, response){
            if (status) {
                ReturnErrorCode(status);
            }
            else {
                ReturnSuccess("msg.accountverified", response);
            }
        });
        return true;
    },
    purgepasswords: function(){
        LogStart("purgepasswords", Xmarks.gSettings.host);
        this.server.purgepasswords(function(status){
            if (!status) {
                ReturnSuccess("msg.synccompleted");
            }
            else {
                ReturnErrorCode(status);
            }
        });
        return true;

    },

    restore: function(rev) {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }
        LogStart("Restoring to Revision: " + rev, Xmarks.gSettings.host);

        this.server.restore(rev, function(status, response){
            if (status) {
                ReturnErrorCode(status);
            }
            else {
                ReturnSuccess("msg.restore", response);
            }
        });
        return true;
    },
    getrevision: function(rev) {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }
        LogStart("Getting Revision: " + rev, Xmarks.gSettings.host);

        this.server.getrevision(rev, function(status, response){
            if (status) {
                ReturnErrorCode(status);
            }
            else {
                ReturnSuccess("msg.getrevision", response);
            }
        });
        return true;
    },

    getrevisions: function() {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }
        LogStart("getrevisions", Xmarks.gSettings.host);

        this.server.getrevisions(function(status, response){
            if (status) {
                ReturnErrorCode(status);
            }
            else {
                ReturnSuccess("msg.getrevisions", response);
            }
        });
        return true;
    },
    verifypin: function(pin) {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }
        LogStart("verify pin", Xmarks.gSettings.host);

        this.server.verifypin(pin, function(status, response){
            if (status) {
                ReturnErrorCode(status);
            }
            else {
                ReturnSuccess("msg.pinverified", response);
            }
        });
        return true;
    },

    synchronize: function(automatic) {
        var prevState = GetState();

        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }

        LogStart("sync", Xmarks.gSettings.host);
        this.server.manual = automatic === true ? false : true;
        this.server.sync(prevState, function(status){
            if (!status) {
                ReturnSuccess("msg.synccompleted");
            }
            else {
                ReturnErrorCode(status);
            }
        });
        return true;
    },

    synchronizeInitial: function (remoteIsMaster, doMerge) {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }

        LogStart("initial sync", Xmarks.gSettings.host);
        this.server.manual = true;

        // We need to self correct for password sync.  If there is an
        // uploadReq == true, we know it doesn't have data on the server.
        // if it's false, we should force a merge per our PRD.
        if(Xmarks.gSettings.isSyncEnabled("passwords")){
            // Set default higher if we're doing password sync
            Xmarks.gSettings.securityLevel = 1;
            if(!Xmarks.gSettings.mustUpload("passwords")){
                Xmarks.gSettings.setMustMerge("passwords", true);
            }
        }

        if (doMerge) {
            this.server.merge(!remoteIsMaster, Finished);
        } else {
            if (remoteIsMaster) {
                this.server.download(Finished);
            } else {
                this.server.upload(Finished);
            }
        }

        return true;

        function Finished(status) {
            if (!status) {
                ReturnSuccess("msg.synccompleted");
            } else {
                ReturnErrorCode(status);
            }
        }
    },

    upload: function () {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }

        LogStart("upload", Xmarks.gSettings.host);

        this.server.manual = true;
        this.server.upload(function(status){
            if (!status) {
                ReturnSuccess("msg.uploadcompleted");
            }
            else {
                ReturnErrorCode(status);
            }
        });
        return true;
    },

    download: function () {
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }

        LogStart("download", Xmarks.gSettings.host);

        this.server.manual = true;
        this.server.download(function(status){
            if (!status) {
                ReturnSuccess("msg.remotefilecopied");
            }
            else {
                ReturnErrorCode(status);
            }
        });
        return true;
    },

    _handleDataResponse: function(response){
        var os = Cc["@mozilla.org/observer-service;1"]
            .getService(Ci.nsIObserverService);

        os.notifyObservers(null, "foxmarks-dataservice", response.toSource());
    },
    getSimilarSites: function(url) {
        LogStart("getSimilarSites", Xmarks.gSettings.apiHost);
        this.server.getSimilarSites(url, this._handleDataResponse);
        return true;
    },
    getTurboTags: function(url) {
        LogStart("getTurboTags", Xmarks.gSettings.apiHost);
        this.server.getTurboTags(url, this._handleDataResponse);
        return true;
    },
    updateReview: function(url, url_id, title, description, rating, review, 
            post_to_fb) {
        LogStart("updateReview", Xmarks.gSettings.apiHost);
        this.server.updateReview(url, url_id, title, description, rating, 
            review, post_to_fb, this._handleDataResponse);
        return true;
    },
    getProfileNames: function() {
        var prevState = GetState();

        var funcFinished = function(status, response) {
            if (!status) {
                Xmarks.LogWrite("GetProfileNames succeeded; response is " +
                        response.toSource());
                ReturnSuccess("error.0", response, prevState);
            } else {
                ReturnErrorMsg(status, Xmarks.MapError(status), prevState);
            }
        };
        // return if we're currently processing another request
        if (!SetBusy()) {
            return false;
        }

        LogStart("getProfileNames", Xmarks.gSettings.acctMgrHost);

        if (this.server.getProfileNames) {
            this.server.getProfileNames(funcFinished);
            return true;
        } else {
            return false;
        }

    },

    launchSuccessPage: function(){
        this.server.countItems("bookmarks",
            "bookmark", function(status, num_bookmarks){
            var currver = Xmarks.FoxmarksVersion();
            // We used to set the protocol from Xmarks.gSettings.httpProtocol
            // but that causes warnings when the dest page contains non-HTTPS
            // content, so we peg the protocol as http. 
            Xmarks.OpenInNewTab("http://" + Xmarks.gSettings.webHost + 
                "/firefox/success/" + currver, true, 
                FoxmarksBuildPostData(num_bookmarks));
            Xmarks.gSettings.currVersion = currver;
        });
    },
    cancel: function() {
        this.server.cancel();
    },
    datacancel: function() {
        this.server.datacancel();
    },

    logWrite: function (msg) {
        if (!logStream)
            logFileOpen();

        var d = new Date();
        var year = d.getFullYear();
        var month = d.getMonth() + 1; if (month < 10) month = "0" + month;
        var day = d.getDate(); if (day < 10) day = "0" + day;
        var hour = d.getHours(); if (hour < 10) hour = "0" + hour;
        var minute = d.getMinutes(); if (minute < 10) minute = "0" + minute;
        var sec = d.getSeconds(); if (sec < 10) sec = "0" + sec;

        // format is [YYYY-MM-DD HH:MM:SS] msg\n
        var string = "[" + year + "-" + month + "-" + day + " " + hour + ":" +
            minute + ":" + sec + "] " + msg + "\n";

        if(Xmarks.gSettings.getDebugOption("dumplog"))
            dump("Xmarks: " + string + "\n");
        logStream.write(string, string.length)
    },

    getState: function() {
        return GetState();
    },

    _password: null,
    _pin: null,
    _auth: null,

    getAuth: function() {
        return this._auth;
    },

    setAuth: function(auth) {
        this._auth = auth;
    },
    getPassword: function() {
        return this._password;
    },

    setPassword: function(password) {
        this._password = password;
    },
    getPin: function() {
        return this._pin;
    },

    setPin: function(password) {
        this._pin = password;
    },

    getStorageEngine: function(synctype) {
        return getDatasourceAttribute(synctype, "engine");
    },

    getLastError: function() {
        return this.lastError;
    },

    lastError: 0,
    _uninstall: false,
    _channel: null,
    _pingListener: {
         onStartRequest: function () {},
         onDataAvailable: function () {},
         onStopRequest: function () {},
         onChannelRedirect: function (aOldChannel, aNewChannel, aFlags) {
            this._newchannel = aNewChannel;
         },
        getInterface: function (aIID) {
            try {
                return this.QueryInterface(aIID);
            } catch (e) {
                throw Components.results.NS_NOINTERFACE;
            }
        },
        onProgress : function (aRequest, aContext, aProgress, aProgressMax) { },
        onStatus : function (aRequest, aContext, aStatus, aStatusArg) { },
        onRedirect : function (aOldChannel, aNewChannel) { },
        QueryInterface : function(aIID) {
            if (aIID.equals(Components.interfaces.nsISupports) ||
                aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
                aIID.equals(Components.interfaces.nsIChannelEventSink) || 
                aIID.equals(Components.interfaces.nsIProgressEventSink) ||
                aIID.equals(Components.interfaces.nsIHttpEventSink) ||
                aIID.equals(Components.interfaces.nsIStreamListener))
            return this;
            throw Components.results.NS_NOINTERFACE;
         } 
     },
     pingServer: function(topic){
        var ioService = Components.classes["@mozilla.org/network/io-service;1"]
            .getService(Components.interfaces.nsIIOService);

        // create an nsIURI
        var attrs = [];
        var session = Date.now().toString(36);
        attrs.push("app="       + "jezebel");
        attrs.push("mid="       + Xmarks.gSettings.machineId);
        attrs.push("abgroup="       + Xmarks.gSettings.abgroup);
        attrs.push("sess="      + session);
        attrs.push("page="      + topic);
        attrs.push("username="  + Xmarks.gSettings.username);
        attrs.push("no_cache="  + Date.now().toString(36));

        var query = attrs.join("&");
        var url = Xmarks.gSettings.httpProtocol + "tr.xmarks.com/tracking/impressions.gif?" + query;
        this._pingRequest = new Request(
            "GET",
            url, 
            null, 
            {
                isAuthRequest: false,
                headers: null,
                ignoreBody: true,
                ignoreAuthTokens: true
            }
        );
        this._pingRequest.Start(function(){});
    },
    getLastModified: function(synctype) {
        return FoxmarksSyncService.lastmodified;
    },

    get server() {
        if (!this._server) {
            this._server = new SyncServer();
        }
        return this._server;
    },

    /////////////////////////////////////////////////////////////////////////
    //
    // nsIObserver

    observe: function(subject, topic, data)  { // called at startup
        var timerCallback = {

            notify: function(timer) {

                var now = new Date().getTime();

                // scan entire bookmark set to find
                // last modified date for entire set
                // XXX: Implement


                // Do automatic sync if necessary
                if (!IsBusy() && (!gFailureCount || now > gBackoffUntil) &&
                    Xmarks.gSettings.synchOnTimer && Xmarks.gSettings.haveSynced) {
                    if (Xmarks.gSettings.minutesSinceLastSync > 
                            Xmarks.gSettings.autoSynchFreq) {
                        FoxmarksSyncService.synchronize(true);
                    } else {
                        if (FoxmarksSyncService.lastmodified && Xmarks.gSettings.haveSynced && 
                            FoxmarksSyncService.lastmodified >
                            Date.parse(Xmarks.gSettings.lastSynchDate) &&
                            now - FoxmarksSyncService.lastmodified > 5 * 60 * 1000) {
                            FoxmarksSyncService.synchronize(true);
                        }
                    }
                }
            }
        }

        if (topic == "app-startup") {
            // Pre-initialization here.
            var os = Cc["@mozilla.org/observer-service;1"].
                getService(Ci.nsIObserverService);
            os.addObserver(this, "quit-application-requested", false);
            os.addObserver(this, "foxmarks-datasourcechanged", false);
            os.addObserver(this, "foxmarks-rununittest", false);
            os.addObserver(this, "foxmarks-unittesterror", false);
            os.addObserver(this, "foxmarks-tr", false);
            os.addObserver(this, "earlyformsubmit", false);
            os.addObserver(this, "final-ui-startup", false);
            os.addObserver(this, "em-action-requested", false);
            os.addObserver(this, "quit-application-granted", false);
            os.addObserver(this, "foxmarks-showsettingpane", false);
            os.addObserver(this, "foxmarks-realstart", false);
        } else if (topic == "final-ui-startup") {
            // check to see if crypto hash is installed (bug #7397)
            // if we error, wait a while and try again
            var counter = 0;
            var cryptoCallback = {
                notify: function() {
                    // give it another shot
                    try {
                        var hash = Cc["@mozilla.org/security/hash;1"].
                                    createInstance(Ci.nsICryptoHash);
                    // otherwise, wait again if we have time
                    } catch(e){
                        counter++;

                        Xmarks.LogWrite("Error: Cryptohash not initialized");
                        if(counter < 10){
                            var timer = Cc["@mozilla.org/timer;1"].
                                createInstance(Ci.nsITimer);
                            timer.initWithCallback(cryptoCallback, 500,
                                Ci.nsITimer.TYPE_ONE_SHOT);
                        }
                        return;
                    }
                    FoxmarksCheckForServerChanges();
                }
            };

            var timer = Cc["@mozilla.org/timer;1"].
                createInstance(Ci.nsITimer);
            timer.initWithCallback(cryptoCallback, 0,
                Ci.nsITimer.TYPE_ONE_SHOT);
        } else if (topic == "foxmarks-realstart") {
            // Real initialization starts here.
            FoxmarksSyncService = this;
            Xmarks.UpdateToXMarks();
            var dsList = loadDatasourceSet(true); 

            FoxmarksSyncService.nat = {}; 
            for(var x = 0; x < dsList.length; x++)
                FoxmarksSyncService.nat[dsList[x].syncType] = dsList[x].WatchForChanges(this.server);

            if (!Xmarks.gSettings.wizardSuppress && !Xmarks.gSettings.useOwnServer &&
                    !Xmarks.gSettings.haveSynced) {
                Xmarks.LogWrite("Starting Wizard: Never Synched");
                FoxmarksLaunchSetupWizard();
            } else if (Xmarks.gSettings.majorVersion < 2) {
                Xmarks.LogWrite("Starting Wizard: Major Upgrade");
                Xmarks.gSettings.majorVersion = 3;
                FoxmarksLaunchSetupWizard();
            } else {
                // need to check for upgrades here
                Xmarks.gSettings.majorVersion = 3;
                var currver = Xmarks.FoxmarksVersion();
                var lastver = Xmarks.gSettings.currVersion;
                var ca = currver.split(".");
                var la = lastver.split(".");
                var newver =false;
                
                if(ca.length != la.length){
                    newver = true;
                } else {
                    for(var x=0; x < ca.length-1; x++){
                        if(parseInt(ca[x]) != parseInt(la[x])){
                            newver = true;
                            break;
                        }
                    }
                }
                if(newver){
                    FoxmarksLaunchUpgradePage();
                }
            }
            
            this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
            this.timer.initWithCallback(timerCallback, 1000*60,
                Ci.nsITimer.TYPE_REPEATING_SLACK);
        } else if (topic == "foxmarks-tr") {
            this.pingServer(data);

        } else if (topic == "quit-application-requested") {
            HandleShutdown();
        } else if(topic == "em-action-requested"){
            subject.QueryInterface(Components.interfaces.nsIUpdateItem);
            if(subject.id == "foxmarks@kei.com"){
                switch(data){
                    case 'item-uninstalled':
                        this._uninstall = true;
                        this.pingServer("uninstall");
                        break;
                    case 'item-disabled':
                        this.pingServer("disable");
                        break;
                    case 'item-cancel-action':
                        if(this._uninstall == false){
                            this.pingServer("cancel-disable");
                        } else {
                            this.pingServer("cancel-uninstall");
                        }
                        this._uninstall = false;
                        break;
                }

            }

        } else if(topic == "quit-application-granted"){
            if(this._uninstall){
                Xmarks.gSettings.clearAllPrefs();
            }
        } else if(topic == "foxmarks-showsettingpane"){
            if(data.length > 0){
                Xmarks.OpenFoxmarksSettingsDialog(data);
            }
        } else if (topic == "foxmarks-unittesterror"){
            try {
                ReturnErrorCode(parseInt(data));
            }
            catch(e){
                Components.utils.reportError(e);
            }
        } else if (topic == "foxmarks-rununittest"){
            this.server.runUnitTest();
        } else if (topic == "foxmarks-datasourcechanged") {
            var a = data.split(';');
            this.lastmodified = parseInt(a[0]);
            var okState = (GetState() == "ready" || GetState() == "unknown");
            if (okState && Xmarks.gSettings.haveSynced &&
                Xmarks.gSettings.isSyncEnabled(a[1]) && 
                this.lastmodified > Date.parse(Xmarks.gSettings.lastSynchDate)) {
                SetState("dirty");
            }
        } else {
            Xmarks.LogWrite("Yikes unknown topic " + topic);
        }
    },

    /////////////////////////////////////////////////////////////////////////
    // nsIFormSubmitObserver
    notify : function (formElement, aWindow, actionURI) {
        if(FoxmarksSyncService && FoxmarksSyncService.nat["passwords"]
                && Xmarks && Xmarks.gSettings.isSyncEnabled("passwords")
                ){
            Xmarks.LogWrite("Before Early Form Submit");
            FoxmarksSyncService.nat["passwords"].formsubmit(formElement);
            Xmarks.LogWrite("After Early Form Submit");
        }
        return true;
    },

    /////////////////////////////////////////////////////////////////////////
    // nsIClassInfo
    getInterfaces: function (aCount) {
        var interfaces = [Ci.nsIFoxmarksService,
        Ci.nsIObserver,
        Ci.nsIFormSubmitObserver,
        Ci.nsiRDFObserver];
        aCount.value = interfaces.length;
        return interfaces;
    },

    getHelperForLanguage: function (aLanguage) {
        return null;
    },

    get contractID() {
        return "@foxcloud.com/extensions/foxmarks;1";
    },

    get classDescription() {
        return "Foxmarks Service";
    },

    get classID() {
        return Components.ID("{49ace257-6111-48b2-a988-f9eb38b0fa58}");
    },

    get implementationLanguage() {
        return Ci.nsIProgrammingLanguage.JAVASCRIPT;
    },

    get flags() {
        return Ci.nsIClassInfo.SINGLETON;
    },

    /////////////////////////////////////////////////////////////////////////
    // nsISupports
    QueryInterface: function (aIID) {
        if (!aIID.equals(Ci.nsIFoxmarksService) &&
            !aIID.equals(Ci.nsISupports) &&
            !aIID.equals(Ci.nsIRDFObserver) &&
            !aIID.equals(Ci.nsIFormSubmitObserver) &&
            !aIID.equals(Ci.nsIObserver))
        throw Components.results.NS_ERROR_NO_INTERFACE;
        return this;
    }
};

var gModule = {
    _firstTime: true,

    registerSelf: function (aComponentManager, aFileSpec, aLocation, aType) {
        if (this._firstTime) {
            this._firstTime = false;
            throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
        }


        aComponentManager = aComponentManager.
        QueryInterface(Ci.nsIComponentRegistrar);

        for (var key in this._objects) {
            var obj = this._objects[key];
            aComponentManager.registerFactoryLocation(obj.CID, obj.className,
                obj.contractID, aFileSpec, aLocation, aType);
        }

        // Make the Foxmarks Service a startup observer
        var cm = Cc["@mozilla.org/categorymanager;1"].
        getService(Ci.nsICategoryManager);
        cm.addCategoryEntry("app-startup", this._objects.service.className,
            "service," + this._objects.service.contractID, true, true, null);
    },


    getClassObject: function (aComponentManager, aCID, aIID) {
        if (!aIID.equals(Ci.nsIFactory))
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

        for (var key in this._objects) {
            if (aCID.equals(this._objects[key].CID))
                return this._objects[key].factory;
        }

        throw Components.results.NS_ERROR_NO_INTERFACE;
    },

    _makeFactory: #1= function(ctor) {
        return {
            createInstance: function (outer, iid) {
                if (outer != null)
                    throw Components.results.NS_ERROR_NO_AGGREGATION;
                return (new ctor()).QueryInterface(iid);
            }
        };
    },

    _objects: {
        service: { CID : nsFoxmarksService.prototype.classID,
            contractID : nsFoxmarksService.prototype.contractID,
            className  : nsFoxmarksService.prototype.classDescription,
            factory    : #1#(nsFoxmarksService)
        }
    },

    canUnload: function (aComponentManager)
    {
        return true;
    }
};

function NSGetModule(compMgr, fileSpec)
{
    return gModule;
}
