var EXPORTED_SYMBOLS = ["YOONO_BKM"];

Components.utils.import("resource://yoono/yoonoPrefs.js");

var yoono = {};
var log = {info:function() {},debug:function() {},warn:function(){},error:function (){},fatal:function(){}};

// Globals const
var CI = Components.interfaces;
var CL = Components.classes;
const OBS=CL['@mozilla.org/observer-service;1'].getService(CI.nsIObserverService);

const DIRSERVICE = CL['@mozilla.org/file/directory_service;1'].getService(CI.nsIProperties);

const MAX_PATH_LENGTH = 5000;
const LIVETAG = '.LIV';

const TOOLBARNAME = "PERSONAL_BOOKMARK_FOLDER";

const PRIVATEFILE = "yoonoprivatefolders.txt";
const YOONO_DIR   = "yoono";
const BACKUP_DIR  = "yoonobookmarkbackups";
const HISTORYPREF  = "extensions.yoono.bookmarks.maxhistory";

const BACKUPS_NUMBER = 5;

const PREFS  = CL['@mozilla.org/preferences-service;1'].getService(CI.nsIPrefBranch);

/*** Globals var ***/
var gCurrentTree = null;
var gCurrentNode = null;
var nbBkms = 1;
var gBkmById = {};
var gBkmByUrl = {};



/***************************************************/
/*** Browser specific version Javascript loading ***/
var restore={};
try {
	// Load version specific JS from : chrome/$ff-version$/*
	var loader = CL["@mozilla.org/moz/jssubscript-loader;1"].createInstance(CI.mozIJSSubScriptLoader);
	loader.loadSubScript("chrome://yoonospecific/content/bookmarks.js");
	loader.loadSubScript("chrome://yoonospecific/content/bookmarks-restore.js",restore);
} catch(e) {
	var console = Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService);
	var scriptError = CL["@mozilla.org/scripterror;1"].createInstance(CI.nsIScriptError);
	scriptError.init("Error loading specific code >>> "+e+" <<<\n"+e.stack, e.filename, null, e.lineNumber, null, scriptError.errorFlag, "");
	console.logMessage(scriptError);
}



function Bookmarks() {
	this.wrappedJSObject=this;
}

/********************************/
/*** Component initialization ***/
Bookmarks.prototype.start = function (y) {
try {
	yoono=y;
	log=y.log;
	
	log.debug("load bookmarks component");
	
	function launch() {
    privateMgr.init();
    var userCred = yoono.main.getUserCredential();

    // If user created
    if (userCred && userCred.userId) {
      var aIsModified = YOONO_PREFS.storeLastModified("init");
      var aSyncMode = ( aIsModified == true ? "manual-sync" : "auto-sync" );
      if (YOONO_PREFS.get('nosynchro')) aSyncMode = 'no-sync';
      // If user anonymous
      if(userCred.anonymous) {
        aSyncMode = 'no-sync';
      }
      yoono.server.launch('connect',aSyncMode); // on se connecte apres avoir charge les marque-pages
    }
    // Disable backup on each firefox startup
    //yoono.bkm.backup();
	}
	
	this.initBackups();
	
	// Launch version specific loading
	start(launch);
	
} catch(e) {
	log.exception(e);
	this.dump();
}
}

Bookmarks.prototype.uninstall = function () {
  privateMgr.uninstall();
}

/*
Bookmarks.prototype.mergeBookmarksWithServer = function() {
  YOONO_PREFS.set('synchroaction', "merge");
  // The load-all-links command is always processed last, whatever its position in the request
  yoono.server.launch('load-all-links');
}

Bookmarks.prototype.importBookmarksFromServer = function() {
  YOONO_PREFS.set('synchroaction', "import");
  // The load-all-links command is always processed last, whatever its position in the request
  yoono.server.launch('load-all-links');
}

Bookmarks.prototype.exportBookmarksOnServer = function() {
  yoono.server.launch('save-all-links');
}
*/

/***********************/
/*** Backups service ***/
Bookmarks.prototype.initBackups = function () {
	// Search backup dir
	this.backupDir = DIRSERVICE.get("ProfD",CI.nsIFile);
	this.backupDir.append(BACKUP_DIR);
	if(!this.backupDir.exists()) 
		this.backupDir.create(CI.nsIFile.DIRECTORY_TYPE, 0755);
}
Bookmarks.prototype.setMaxBackups = function (n) {

	PREFS.setIntPref(HISTORYPREF,n);
	this.flushBackups();
	
}
Bookmarks.prototype.getMaxBackups = function () {
	return PREFS.getIntPref(HISTORYPREF);
}
Bookmarks.prototype.flushBackups = function () {
	// Remove one file, if we reach backups limit
	while(true) {
		var e = this.backupDir.directoryEntries;
		var older=null;
		var nbFiles=0;
		while(e.hasMoreElements()) {
			var file=e.getNext().QueryInterface(Components.interfaces.nsIFile);
			if (!file.isFile()) continue;
			if (!older || file.lastModifiedTime < older.lastModifiedTime) {
				older=file;
			}
			nbFiles++;
		}
		if (nbFiles>this.getMaxBackups())
			older.remove(false);
		else
			break;
	}
}
Bookmarks.prototype.backup = function () {
try {	
	// Create a new backup file
	var d=new Date();
	var file = this.backupDir.clone();
	file.append(d.getFullYear()+"_"+(d.getMonth()+1)+"_"+d.getDate()+"-"+d.getHours()+"h"+d.getMinutes()+"m"+d.getSeconds()+"s.html");
	
	// Build backup!
	restore.backup(file);
	
	this.flushBackups();
} catch(e) {
	log.exception(e);
}
}
Bookmarks.prototype.restore = function (name) {
	// Disable server synch
	Server2Browser.working=true;
	
	// Launch restore
	log.debug("START RESTORE");
	var file=this.backupDir.clone();
	file.append(name);
	restore.restore(file);
	log.debug("END RESTORE");
	
	// Export all bkms
	yoono.server.launch('save-all-links');
	
	// Reenable server synch
	Server2Browser.working=false;
}
Bookmarks.prototype.getBackupList = function (file) {
	
	var e = this.backupDir.directoryEntries;
	var backups=[]
	while(e.hasMoreElements()) {
		var file=e.getNext().QueryInterface(Components.interfaces.nsIFile);
		if (!file.isFile()) continue;
		backups.push(file);
	}
	backups.sort(function (a,b) {return a.lastModifiedTime - b.lastModifiedTime;});
	
	var list=[];
	for(var i=0;i<backups.length;i++) {
		var file=backups[i];
		var m=file.leafName.match(/(\d+)_(\d+)_(\d+)-(\d+)h(\d+)m(\d+)s\.html/);
		if (m) {
			list.push({file:file, name:file.leafName, date:new Date(m[1],parseInt(m[2]) - 1,m[3],m[4],m[5],m[6])});
		} else
			file.remove(false);
	}
	
	return list;
}



/*** Debuging ***/
Bookmarks.prototype.dump = function () {

	log.info("Bookmark tree : \n"+gCurrentTree.toString());

}
Bookmarks.prototype.bookmarksNumber = function () {
	
	var c=0;
	for each(var n in gBkmById) {
		if (!n.isFolder()) {
			c++;
		}
	}
	return c;
	
}

/************************/
/*** Common interface ***/
Bookmarks.prototype.loadAllLinks = function (links) {
try {
	
	return Server2Browser.loadAllLinks(links);
	
} catch(e) {
	log.exception(e);
}
}

Bookmarks.prototype.getSyncId = function () {
	
	return gCurrentTree?gCurrentTree.getSyncid():"";
	
}

Bookmarks.prototype.getCurrentTree = function () {

	return gCurrentTree;

}

Bookmarks.prototype.getNbBkms = function () {
	return nbBkms;
}

Bookmarks.prototype.isPrivate = function (id) {
	
	if (gBkmById[id])
		return gBkmById[id].isPrivate();
	return false;
	
}
Bookmarks.prototype.addToPrivateList = function (id) {
	privateMgr.addToList(id);
}
Bookmarks.prototype.removeFromPrivateList = function (id) {
	privateMgr.removeFromList(id);
}

Bookmarks.prototype.getNode = function (id) {
	return gBkmById[id];
}


/**************************/
/*** Specific interface ***/
// TODO : check utility of each function
// Returns full boookmark list as an array
Bookmarks.prototype.getFullListUrl = function () {
	var urlList = [];
	for each (var node in gBkmById) {
		if (node.isFolder() || !node.getUrl())
			continue;
		urlList.push(node.getUrl());
	}
	return urlList;
}
// Returns array of url contained in the folder whose id is passed
Bookmarks.prototype.getNodeListUrl = function(nodeId) {
	var node = this.getNode(nodeId);
  if (node)
    return(this.getUrlsInSubFolder(node));
  else // we may select places top container "all bookmarks" which are not tracked by this component
    return this.getFullListUrl();
}
Bookmarks.prototype.getUrlsInSubFolder = function(node) {
	var obj = {
		list : [],
		onNode : function (node) {
			if(!node.isFolder()) {
				this.list.push(node.getUrl());
			}
		}
	}
	node.traverseNode(obj, 'force');
	return obj.list;
}

Bookmarks.prototype.isKnownUrl = function (url) {
    if (gBkmByUrl[url])
      return true;
    else
      return false;
}

Bookmarks.prototype.addBkmAtRoot = function (url, title) {
	gBrowserBkm.createBookmark(title, url, gCurrentTree);
}

/******************************/
/*** Old code compatibility ***/
// TODO : remove all call
Bookmarks.prototype.getNodesForTreeView = function (node, level) {

	log.backtrace("Deprecated call!");
	// initialisation
	if (!node)
		var node = gCurrentTree;
	if (!node)
		return {open:false, name:'none', children: []};
	if (!level)
		var level = 0;

	var obj = new Object();
	obj.open = false;
	obj.level = level;
	obj.node = node;
	obj.published = false;
	obj.origPublished = false; // to save original state to detect changes
	obj.private = node.isPrivate();
	obj.parent = null;
	obj.origPrivate = node.isPrivate(); // to save original state to detect changes
	obj.id = BKMSERV.bookmarksMenuFolder;
	obj.children = [];

	return obj;
}
// END of public interface! //
//////////////////////////////


/*
// TODO : make BkmsObserver specific to FF3 
Browser2Server = {
	notifyServer : function( cmd, arg ) {
		if (Server2Browser.isWorking())	return;
		
		yoono.server.addToNextCommandScript(command, arg);
	},
	itemAdded : function (node) {
		if (node.isAcceptable() && !node.isFolder())
			this.notifyServer('add-link', node);
	},
	itemRemoved : function (node) {
		if (node.isAcceptable())
			this.notifyServer('remove-link', node);
	},
	itemChanged : function(oldNode, newNode) (
		if (newNode.update() && newNode.isAcceptable())
			this.notifyServer('update-link', [oldNode, newNode]);
	},
	itemMoved : function(node) {
	
	}
};
*/

////////////////////////////
// BkmsObserver
// :: observe bookmarks modifications
var BkmsObserver = {
	_working : false,
	
	onBeginUpdateBatch: function() {
		log.debug("Begin update batch");
		this._working = true;
	},
	onEndUpdateBatch: function() {
		log.debug("End update Batch");
		this._working = false;
	},
	notifyServer : function(command, node) {
		if (Server2Browser.isWorking())	return;
		
		if (this._working) {
			yoono.server.addToNextCommandScript(command, node);
		} else {
			yoono.server.launch(command, node);
		}
					
	},
	onItemAdded: function(id, folder, index) {
		
		//log.debug("add item id : "+id);
		
		var parent=gBkmById[folder];
		if (!parent) {
			//log.debug("Ignore bookmarks with parent not in our tree");
			return ;
		}
		
		if (parent.isLive()) {
			//log.debug("we ignore subitem of live bookmarks");
			return;
		}
		
		if (gBkmById[id]) {
			var node=gBkmById[id];
		} else {
			var node=new bkmNode(id, parent);
			parent.addChild(node);
		}
		//log.debug("parent : "+parent.getName()+" name:"+node.getName()+" isFolder:"+node.isFolder());
		
		
		// Delay creation, because livemarks are really livemarks some times later :/ (ie LIVESERV.isLivemark()->true) [FF3]
		// (they are first folder, then livemark)
		// (they are valid livemarks when we receive itemChanged on feedURI property)
		if (!Server2Browser.isWorking()) {
			var _this=this;
			var callback = {
				notify : function(timer) {
					try {
						if (node.isAcceptable() && !node.isFolder()) // we must delay isAcceptable because we check isFolder into...
							_this.notifyServer('add-link', node);
					} catch(e) {
						log.exception(e);
					}
				}
			};
			var timer = CL['@mozilla.org/timer;1'].createInstance(CI.nsITimer);
			timer.initWithCallback(callback, 1000 , timer.TYPE_ONE_SHOT);
		}
		return node;
		
	},
	
  onBeforeItemRemoved: function(id, type) {
  
  },
  
	onItemRemoved: function(id, folder, index) {
	try {
		//log.debug("remove item : "+id);
		var node=gBkmById[id];
		if (!node) {
			//return log.debug("removed node not tracked");
			return;
		}
		if (node.isFolder()) {
			var children = node.getAllChildren();
			for each(var child in children) {
				child.remove();
				if (child.isAcceptable())
					this.notifyServer('remove-link', child);
			}
		} else {
			if (node.isAcceptable())
				this.notifyServer('remove-link', node);
		}
		node.remove();
	} catch(e) {
		log.exception(e);
		yoono.bkm.dump();
	}
	},
	
	onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex) {
	try {
		if (oldParent == newParent)
			return log.debug("order moves are ignored");
		log.debug("move item : "+id+" parentId:"+newParent);
		
		var node=gBkmById[id];
		
		// For unknown bookmark, check if destination is known
		if (!node) {
			if (gBkmById[newParent]) {
				return this.onItemAdded(id, newParent, newIndex);
			} else {
				return log.debug("moved node not tracked (may be in private)");
			}
		}
		
		log.debug("oldparent:"+oldParent+" > newparent:"+newParent);
		
		if (node.isFolder()) {
			// First save a copy of old children
			var childs=[];
			node.traverseNode({onNode:function(child){
					if (!child.isFolder())
						childs.push(child.clone());
				}});
			// Update originals
			node.getParent().removeChild(node);
			var dest=gBkmById[newParent];
			dest.addChild(node);
			node.update(dest);
			// Send requests
			for each(var child in childs) {
				var newChild=gBkmById[child.getId()];
				log.debug("oldpath:"+child.getPath().toServer+" > newpath: "+newChild.getPath().toServer);
				if (newChild.isAcceptable())
					this.notifyServer('update-link', [child, newChild]);
			}
		} else {
			var oldNode=node.clone();
			node.getParent().removeChild(node);
			var dest=gBkmById[newParent];
			dest.addChild(node);
			node.update(dest);
			log.debug("oldpath:"+oldNode.getPath().toServer+" > newpath: "+node.getPath().toServer);
			if (node.isAcceptable())
				this.notifyServer('update-link', [oldNode, node]);
		}
		
	} catch(e) {
		log.exception(e);
		yoono.bkm.dump();
	}
	},
	
	onItemChanged: function(id, property, isAnnotationProperty, value) {
	try {
		if ( ['title', 'uri', 'livemark/feedURI'].indexOf(property) == -1 ) {
			//log.info("only title, uri and feedURI properties are tracked (not "+property+")"+id+" > "+(gBkmById[id]?gBkmById[id].isFolder():"/"));
			return;
		}
		//log.debug("change item : "+id+"/"+property+"/"+isAnnotationProperty+"/"+value);
		var node=gBkmById[id];
		
		
		
		//////
		if (node.isFolder() && property=="title") {
			// First save a copy of old children
			var childs=[];
			node.traverseNode({onNode:function(child){
					if (!child.isFolder())
						childs.push(child.clone());
				}});
			// Update originals
			node.update();
			// Send requests
			for each(var child in childs) {
				var newChild=gBkmById[child.getId()];
				log.debug("oldpath:"+child.getPath().toServer+" > newpath: "+newChild.getPath().toServer);
				if (newChild.isAcceptable())
					this.notifyServer('update-link', [child, newChild]);
			}
		} else {
			var oldNode=node.clone();
			if (node.update() && node.isAcceptable()) {
				// See onItemAdded ...
				if (property=="livemark/feedURI" && !oldNode.getUrl())
					return log.debug("we ignore first livemark url notification");
				this.notifyServer('update-link', [oldNode, node]);
			} else 
				log.warn("not acceptable ? no update ? ");
		}
		
	} catch(e) {
		log.exception(e);
		yoono.bkm.dump();
	}
	},
	onItemVisited: function(id, visitID, time) {
		
	},
	
	QueryInterface: function(iid) {
		if (iid.equals(Ci.nsINavBookmarkObserver) ||
			iid.equals(Ci.nsISupports)) {
			return this;
		}
		throw Components.results.NS_ERROR_NO_INTERFACE;
	}

};
//////////////////////////////






//////////////////////////////
// Server2Browser
// :: handle info from server and sync with rdf view
// :: initial loading of all bookmarks from server
var Server2Browser = {
	isWorking : function() {return this.working;},
	// la liste des marque-pages pr�sents dans l'ordi
	notOnServer : {},
	// une liste des chemins deja calcules
	foldersList : {},
	// list of incoming private folders
	privateIncomingFoldersList : {},
	// List of folders that will have to be added to private folder file
	newPrivateFolders : {},
	// List of links that where in a browser private folder that the synchro sets public
	linksBecomingPublic : {},

	/*
	* List all private folders marked as such in incoming folder list
	*/
	listPrivateIncomingFolders : function (linkList) {
		var key = '';
		for each (var addLink in linkList) {
			var privateFolder = addLink.@['private'].toString();
			if('CLIENT' == privateFolder) {
				key = addLink.@path.toString();
				log.debug('Private incoming folder found : ' + key);
				this.privateIncomingFoldersList[key] = {exists: false} ;
			}
		}
	},

	/*
	* manages the list of folders that where in a browser private folder that the synchro
	* now sets to public
	* @param node: folder that becomes public
	*/
	addLinksBecomingPublic : function(node) {
		var add = {
			onNode : function() {}
		};
		add.onNode = function(node) {
			if (node.isFolder() )
				return;
			var key = node.getPath().toServer + node.getUrl();
			// log.debug("XXX BECOMING PUBLIC : " + key);
			Server2Browser.linksBecomingPublic[key] = {
				'node' : node,
				'mustCreate' : false
			};
		};
		node.traverseNode(add);
	},

	/*
	* @param linkList : liste des elements a ajouter a l'arborescence (object xmlList)
	*/
	loadAllLinks : function (linkList) {
		
		log.debug('begin loadAllLinks');
		this.foldersList = {};
		this.privateIncomingFoldersList = {};
		this.newPrivateFolders = {};
		this.linksBecomingPublic = {};
		this.notOnServer = {};
		this.working = true;
		
		var _this=this;
		gBrowserBkm.runInBatchMode(function () {
				_this.loadAllInBatched(linkList);
			});
		
		return this.resp;
	},
	loadAllInBatched : function(linkList) {
		// on renvoie l'action effectivement effectu�e, et le nombre de marque-pages import�s
		this.resp = { 
			action : YOONO_PREFS.get('synchroaction'),
			// nombre reellement crees
			added : 0,
			// importes depuis le serveur
			imported : 0,
			clearCommands : true
		};
		
		// List can contain (empty) incoming private folders just to be able not to erase them from the browser
		// in case they are not marked private yet (sync...)
		// They have the private="CLIENT" attribute. Parse the list to find them and set them as private
		this.listPrivateIncomingFolders(linkList);

		// on pousse tous les noeuds existants dans notOnServer pour detecter les suppressions a faire
		// (except private folders, either existing or incoming)
		var aNotOnServerCount = 0;
		for each (var node in gBkmById) {
			if (node.isFolder() ) {
				var path = node.getPath().toServer;
				// If private folder, not in the incoming private folders list, set it to public
				if(!this.privateIncomingFoldersList[path] && node.isPrivate()) {
					log.debug('Path ' + path + ' is now public on server');
					var id = node.getId();
					// Remove it from list of private folders
					privateMgr.removeFolderFromList(id);
					node.setPrivate(false);
					// This folder may have content that is not yet on the server (added when it was private)
					// and that must be added, excluding what is already on the server... what a mess :-)
					// Store its content in a special list that will be used with the notOnServer list to build
					// the add-link orders (and to leave them on the computer !)
					this.addLinksBecomingPublic(node);
				}
			}

			// skip private nodes, and other stuff
			if (!node.isAcceptable()) {
				continue;
			}
			if (node.isFolder() ) {
				this.foldersList[node.getPath().toServer] = node;
			} else {
				var key = node.getPath().toServer + node.getUrl();
				if (!this.notOnServer[key]) {
					this.notOnServer[key] = new Array(); // array to manage duplicates
				}
				this.notOnServer[key].push(node);
				aNotOnServerCount++;
			}
		}

		// Send a global notifications that we are starting a synchro
		OBS.notifyObservers (this,"yoono-synchro-start",YOONO_PREFS.get('synchroaction'));
		yoono.bkm.backup();

		log.debug('notOnServer.length : '+aNotOnServerCount);

		this.saveLinks(linkList);
		log.debug('imported='+this.resp.imported+', added='+this.resp.added+', synchroaction='+YOONO_PREFS.get('synchroaction'));

		if (YOONO_PREFS.get('synchroaction') == 'import') {
			if(this.resp.imported > 0) {
				var length = 0;
				// Must delete from server links that remain in notOnServer
				for each (var nodeList in this.notOnServer) {
					length += nodeList.length;
				}
				log.debug('length='+length);
				if (length) {
					for (var i in this.notOnServer) {
						log.debug('i='+i);
						// Remove bkms that were not on server
						// except if they used to be in a private folder that became public during this sync
						if(!this.linksBecomingPublic[i]) {
							for (var j = this.notOnServer[i].length ; j-- > 0; ) {
								this.removeBkm(this.notOnServer[i][j]);
							}
						} else {
							// Mark link for addition to server
							this.linksBecomingPublic[i].mustCreate = true;
						}
						delete (this.notOnServer[i]);
					}
					
				}
				// Create the links that were added to private folders that became public because of the sync
				log.debug('Adding links that became public :');
				var cpt = 0;
				for (var newNode in this.linksBecomingPublic) {
					if(this.linksBecomingPublic[newNode].mustCreate) {
						log.debug('Adding ' + newNode);
						yoono.server.launch('add-link', this.linksBecomingPublic[newNode].node);
					}
				}
				log.debug(cpt + ' links that became public added.');

			} else {
				log.warn("abnormal situation : invalid import, server returns 0 link. Local bookmarks aren't modified");
				this.resp.clearCommands = false;
			}
		}

		// Add to private folder list file the new ones just loaded
		privateMgr.appendToList(this.newPrivateFolders);
		
		aNotOnServerCount=0;
		for(var key in this.notOnServer)
			aNotOnServerCount++;
		
		if (aNotOnServerCount>0)
			yoono.server.launch('save-all-links-add', this.notOnServer);
		
		this.working = false;
		this.notOnServer = {};
		this.foldersList = {};
		
		// Send a global notifications that we are ending a synchro
		// Needed by sidebar Wizard to know when to send bkm sharing information
		OBS.notifyObservers (this,"yoono-synchro-end",YOONO_PREFS.get('synchroaction'));

		log.debug('end loadAllLinks');
		return this.resp;

	},

	/*
	@param linkList : liste d'element de la forme <link title="aTitle" url="aUrl"/>
	cree les liens correspondants
	*/
	saveLinks : function(linkList) {
		for each (var addLink in linkList) {
			try {
				var pathAsStr = addLink.@path.toString();
				var url = addLink.@url.toString();
				var title = addLink.@title.toString();
				var pos = title.lastIndexOf(LIVETAG);
				if (pos != -1 && pos == title.length - LIVETAG.length) {
					title = title.slice(0, -LIVETAG.length);
					var type = 'l';
				} else {
					var type = 'b';
				}
				if (title.match(/^NOTITLE-/))
					title = '';

				var key = pathAsStr + url;
				if (this.notOnServer[key]) {
					// on prend le premier
					var node = this.notOnServer[key][0];
					// si on recoit une url deja presente avec un titre different,
					// on ecrase le titre existant au lieu de dupliquer le mp
					
					if (node.getName() != title) {
						gBrowserBkm.rename(node,title);
					}
					// on supprime les doublons
					for (var i = this.notOnServer[key].length ; i-- > 1; ) {
						this.removeBkm(this.notOnServer[key][i]);
					}
					// Since the bkm exists on the server, remove it from the list
					delete (this.notOnServer[key]);
				} else {
					// new or modified bkm
					this.checkOrCreateBkm(addLink, pathAsStr, title, url, type);
				}
				
				this.resp.imported++;
			} catch(e) {
				log.exception(e);
			}
		}

	},

	checkOrCreateBkm : function(addLink, pathAsStr, title, url, type) {
		var folder;
		var privateFolder = addLink.@['private'].toString();
		
		var path = yoono.utils.splitPath(pathAsStr);
		
		if ('/' != pathAsStr) {
			if(!(folder = this.foldersList[pathAsStr])) {
				folder = this.getOrCreateFolder(path);
				this.foldersList[pathAsStr] = folder;
			}
		} else {
			folder = gCurrentTree;
		}

		// If private folder received from server already exists as not private
		if(("CLIENT" == privateFolder) && Server2Browser.privateIncomingFoldersList[pathAsStr] && Server2Browser.privateIncomingFoldersList[pathAsStr].exists) {
			// Record it as 'to be added to private folders'
			var id = folder.getId();
			if('/' == pathAsStr) id = gCurrentTree.id;
			log.debug('New private folder: Path:' + pathAsStr + ' id: ' + id + ' title: ' + title + ' url: ' + url);
			Server2Browser.newPrivateFolders[id] = true;
			// delete it from list so that later 'isAcceptable' calls won't be disturbed...
			// setting it public again would be impossible otherwise
			delete (Server2Browser.privateIncomingFoldersList[pathAsStr]);
		}

		//log.info('Checking Path:' + pathAsStr + ' title: ' + title + ' url: <' + url + '> folder : \n'+folder.getName());
		if (!url)
			return false;
		
		for each(var c in folder.getChildren()) {
			if (c.getUrl() == url) {
				return false;
			}
		}
		
		if (type == 'b') {
			gBrowserBkm.createBookmark( title, url, folder);
		} else if (type == 'l') {
			gBrowserBkm.createLivemark( title, url, folder);
		}
		this.resp.added++;
		return true;
	},
	
	getOrCreateFolder : function(path) {
		if (path.length == 1) {
			return gCurrentTree;
		}
		
		if (path[(path.length - 1)] == TOOLBARNAME && path.length == 2) {
			return gToolbarNode;
		}
		
		var parent = this.getOrCreateFolder(path.slice(0, -1));
		
		var lastFolderName = path[path.length - 1];
		
		for each(var c in parent.getChildren()) {
			if (c.getName() == lastFolderName && c.isFolder()) {
				return c;
			}
		}
		
		return gBrowserBkm.createFolder(lastFolderName, parent);
	},
	
	removeBkm : function (node) {
		//log.debug('remove node (name='+node.getName()+',path='+node.getPath().toServer + ')');
		var parent = node.getParent();
		
		gBrowserBkm.remove(node);
		
		if (parent.isFolder() && parent.getChildren().length==0)
			gBrowserBkm.remove(parent);
		
	}
	
}
///////////////////////////////




///////////////////////////////
// bkmNode
// :: each bookmark or folder are mapped to one bkmNode
// :: generate Unique ID

// Public common interface
bkmNode.prototype.getId = function () {
	return this.id;
}
bkmNode.prototype.getUrl = function() {
	return this.url;
}
bkmNode.prototype.getParent = function () {
	return this.parent;
}
bkmNode.prototype.getName = function () {
	return this.name;
}
bkmNode.prototype.getChildren = function () {
	return this.childsList;
}
bkmNode.prototype.isPrivate = function () {
	return this.private;
}
bkmNode.prototype.isChildOfPrivate = function () {
	return this.childOfPrivate;
}
// ""Protected"" interface (used only by bookmark service)
bkmNode.prototype.setPrivate = function (newStatus) {
	if (newStatus!=this.private) {
		this.private=newStatus;
    if(this.private) {
      nbBkms --;
    } else {
      nbBkms ++;
    }
		this.resetSyncid();
		function traverse(node) {
			for each(var child in node.childsList) {
				if (!child.isPrivate()) {
					child.childOfPrivate=newStatus;
					traverse(child);
				}
			}
		}
		traverse(this);
	}
}

bkmNode.prototype.addChild = function (child) {
  nbBkms ++ ;	
	this.childsList.push(child);
	this.resetSyncid();
	
}

bkmNode.prototype.clone = function () {
	var clone={};
	for(var i in this) {
		clone[i]=this[i];
	}
	return clone;
}

bkmNode.prototype.remove = function () {
	
	this.parent.removeChild(this);
	delete gBkmById[this.id];
  nbBkms --;
	if (this.isBookmark() || this.isLive())
		delete gBkmByUrl[this.getUrl()];
	if (this.isFolder()) {
		for each(c in this.childsList)
			c.remove();
	}
	delete this.childsList;
	
}

bkmNode.prototype.toString = function (indent) {
	
	if (!indent)
		indent="";
	if (this.isFolder()) {
		var str=indent+"[ #"+this.id+" sync:"+this.syncid+" t:"+this.type+", "+(this.private?"private":"public")+" , name:"+this.getName()+"\n";
		for(var i in this.childsList) {
			str+=this.childsList[i].toString(indent+"  ");
		}
		str+=indent+"]\n";
		return str;
	} else {
		var name=this.getName();
		return indent+"( #"+this.id+" sync:"+this.syncid+" t:"+this.type+" , name:"+(name?name.substr(0,50):"")+" )\n";
	}
	
}

bkmNode.prototype.getAllChildren = function () {
	if (!this.isFolder()) 
		return [];
	var children = this.childsList;
	for(var i in this.childsList) {
		children=children.concat(this.childsList[i].getAllChildren());
	}
	return children;
}

	/*
	* @ return : un objet path comprenant :
	* .toSync : une chaine qui sera utilisee pour calculer le sync-id
	* .toServer : une chaine qui sera envoyee au serveur
	*/
bkmNode.prototype.getPath = function() {
	if (!this.path) {
		if (!this.parent) {
			// fin de la recursion
			this.path = {
					toSync : '/',
					toServer : '/'
				};
		} else {
			var pPath=this.parent.getPath();
			this.path = {toSync:pPath.toSync, toServer:pPath.toServer};
			if (this.isFolder()) {
				if (this.parent.parent) { // If the parent is not the root node
					this.path.toSync += '/';
					this.path.toServer += '/';
				}
				this.path.toSync += yoono.utils.normalize(this.getName()).toLowerCase();
				this.path.toServer += yoono.utils.escapePath(this.getName());
			}
		}
	}
	return this.path;
}

bkmNode.prototype.getSyncid = function () {
	//log.debug(" get syncid : "+this.id+" "+this.syncid+" "+this.name);
	if (!this.syncid) {
		var aPath = this.getPath().toSync;

		if (this.isFolder()) {
			var str = aPath + 'F';
			var sum = this.stringToSum(aPath);

			var urlExist = {};

			for (var i = this.childsList.length; i--> 0;) {
				var item= this.childsList[i];
				var url = item.getUrl();
				if (item.isAcceptable() && !urlExist[url]) {
					sum += item.getSyncid();
					if (url) {
						urlExist[url] = true;
					}
				}
			}
		} else {
			var aName = this.getName().substring(0,124);
			var aHref = this.getUrl();
			aName = yoono.utils.normalize(aName);
			if( aHref ) {  // patch pour Firefox 2.0 (microsummaries url=null)
				var aUrl = aHref.substring(0,255);
				var str = aPath.concat( aName.toLowerCase() , aUrl , 'B');
				//var str=[aPath,aName.toLowerCase(),aUrl,'B'].join("");
				sum = this.stringToSum(str);
			}
		}
		this.syncid = sum;
	}
	return this.syncid;
}



var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter);

// we use UTF-8 here, you can choose other encodings.
converter.charset = "UTF-8";

// Inspiration : http://www.webtoolkit.info/javascript-utf8.html
bkmNode.prototype.stringToSum = function (str) {
	var a = 1;
	var b = 0;
	var l = 0;
	
	var result = {};
	var data = converter.convertToByteArray(str, result);
	l=result.value;
	for (var i = 0/*, j=0*/ ; i < l; /*j++*/) {
		// Convert from unicode array to "charCodeAt" values
		var code = -1;
		if (data[i]<128) {
			code=data[i];
			i++;
		} else if ((data[i] > 191) && (data[i] < 224)) {
			code=(((data[i] & 31) << 6) | (data[i+1] & 63));
			i+=2;
		} else {
			code=((data[i] & 15) << 12) | ((data[i+1] & 63) << 6) | (data[i+2] & 63);
			i+=3;
		}
		//if (code!=str.charCodeAt(j))
		//	log.debug(this.id+"@"+i+" : "+str[i]+" -> "+code+"/"+str.charCodeAt(j)+"  ["+str.length+"/"+l+"]");
		a += code;
		b += a + code;
	}
	return (a % 65521) + 65536 * (b % 65521);
}

/*
bkmNode.prototype.stringToSum= function(buf)
{
	var adler = 1;
	var len = buf.length;

	var NMAX = 3854;
	var BASE = 65521;

	var s1 = adler & 0xffff;
	var s2 = (adler & 0xffff0000) / 65536;
	var k;
	var bpos = 0;

	while (len > 0) {
		k = len < NMAX ? len : NMAX;
		len -= k;
		while (k > 0) {
			s1 = (s1 + buf.charCodeAt(bpos)) & 0xffffffff;
			s2 = (s2 + s1) & 0xffffffff;
			bpos += 1;
			k -= 1;
		}
		s1 = s1 % BASE;
		s2 = s2 % BASE;
	}
	return 2;
	return ((s2 & 0x0000ffff)*65536)+s1;
}
*/
bkmNode.prototype.removeChild = function(node) {
	if (!this.isFolder() || !this.childsList)
		return;
	for(i=0;i<this.childsList.length;i++){
		if(node==this.childsList[i]) {
      this.childsList.splice(i, 1);
      nbBkms --;
      break;
    }
	}
	this.resetSyncid();
}

bkmNode.prototype.resetSyncid = function () {
	var pNode = this;
	while (pNode) {
		pNode.syncid = 0;
		pNode = pNode.getParent();
	}
}

bkmNode.prototype.isAcceptable = function () {
	if (this.getPath().toServer.length > MAX_PATH_LENGTH)
		return false;
	if (this.isChildOfPrivate())
		return false;
	return true;
  /*
	// if folder, check if it exists in incoming private folder list so that it won't
	// be erased even if wasn't private before import
	if (this.isFolder()) {
		var key = this.getPath().toServer;
		if(Server2Browser.privateIncomingFoldersList[key] && !this.private) {
			log.debug('Found existing public folder ' + key);
			Server2Browser.privateIncomingFoldersList[key].exists = true;
			this.private = true;
			return false;
		}
	}
	*/

}

/*
	* traverse recursivement le noeud
	* l'objet peut contenir une fonction onNode
	* a etre appliquee a chaque noeud
	* par d�faut, on n'applique pas cette m�thode
	* � un noeud non acceptable, mais on peut forcer
*/
bkmNode.prototype.traverseNode = function (obj, force) {
	if (!this.isAcceptable() && !force)
		return;
	obj.onNode(this);
	if (this.isFolder()) {
		for (var i = this.childsList.length ; i-- > 0; ) {
			this.childsList[i].traverseNode(obj, force);
		}
	}
}

/*
	@ param node      : un noeud de l'arborescence
	@ param xml       : un objet xml
	@ return          : l'objet xml qui contient les attributs title, path, et url correspondants au noeud
	xml est modifie dans cette fonction
*/
bkmNode.prototype.setAllAttributes = function(xml) {
	this.setAttribute('title' , xml);
	this.setAttribute('url', xml);
	this.setAttribute('path', xml);
	return xml;
}

/*
	@ param node      : un noeud de l'arborescence
	@ param xml       : un objet xml
	@ param attribute : une chaine representant l'attribut a traiter (path, url ou title)
	@ return          : l'objet xml qui contient l'attribut correspondant au noeud
	xml est modifie dans cette fonction
*/
bkmNode.prototype.setAttribute = function (attribute, xml) {
	switch (attribute) {
	case ('title') :
		if (this.getName()) {
			// on met un marqueur sur les live-marks pour pouvoir les reconnaitre lors d'un import
			xml.@title = this.getName() + ((this.isLive()) ? LIVETAG : '');
		}
		break;
	case ('path') :
		xml.@path = this.getPath().toServer;
		break;
	case ('url') :
		if (this.getUrl()) {
			xml.@url = this.getUrl();
		}
		break;
	}
	return xml;
}

////////////////////////////////////





//////////////////////////////////////
// privateMgr
// :: manage private bookmarks list in %profile%/$YOONO_DIR/$PRIVATEFILE
// :: Display locks in front of private folders
// :: Call server to synchronise
var privateMgr = {

	file : DIRSERVICE.get("ProfDS", CI.nsIFile),

	init : function () {
	try {
		// Releases before 3.0.6 wrote the locked folders file in the user chrome directory
		// Newer releases write the locked file in the yoono directory
		// If the file exists at the old location, move it first
		var oldFile = DIRSERVICE.get("UChrm", CI.nsIFile);
		oldFile.append(PRIVATEFILE);
		if (oldFile.exists()) {
			var newFile = DIRSERVICE.get('ProfDS', CI.nsIFile);
			newFile.append(YOONO_DIR);
			oldFile.moveTo(newFile,PRIVATEFILE);
		}
		this.file.append(YOONO_DIR);
		this.file.append(PRIVATEFILE);
		this.readList();

	} catch(e) {
		log.exception(e);
	}
	},
  uninstall : function () {
    try {
      var file = DIRSERVICE.get("ProfDS", CI.nsIFile);
      file.append(YOONO_DIR);
      file.append(PRIVATEFILE);
      if( file.exists() ) {
        file.remove(true);
      }
    } catch(e) {}
  },

	appendToList : function (newPrivateFolders) {
	try {
		var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance(CI.nsIFileOutputStream);
		// 0x02 | 0x10 : WRONLY, APPEND or CREATE
		outputStream.init(this.file, 0x02 | 0x08 | 0x10 , 0644, 0);
		for (var id in newPrivateFolders) {
			outputStream.write(id + '\n' , id.length + 1);
		}
		outputStream.close();
	} catch(e) {
		log.exception(e);
	}
	},

	addToList : function (id) {
		var node = gBkmById[id];
		if(!node)
			return log.error('NULL : ' + id);
		
		if (node.isPrivate())
			return log.debug("node is already private");
		
		
		if (node.isFolder()) {
			node.traverseNode({onNode:function(child) {
					if (!child.isFolder())
						yoono.server.launch('remove-link', child);
				}});
			node.setPrivate(true);
			yoono.server.launch('add-private-folder', node);
		} else {
			log.error("add bkm to private list ? (must be called only on folders)");
		}
		this.saveList();
		log.info(node.getName() + ' becomes private');
	},

	removeFromList : function (id) {
		var node = gBkmById[id];
		if(!node)
			return log.error('NULL : ' + id) ; 
		
		if (!node.isPrivate())
			return log.error("node is not private : "+id);
		
		node.setPrivate(false);
		log.info(node.getName() + ' set public by user');
		
		if (node.isFolder()) {
			yoono.server.launch('remove-private-folder', node);
			node.traverseNode({onNode:function(child) {
					if (!child.isFolder()) {
						yoono.server.launch('add-link', child);
          }
				}});
		} else {
			log.error("remove bkm from private list ? (must be called only on folders)");
		}
		this.saveList();
		
	},

	/* Called on a private folder that the server says is now public
	* folder must be removed from private folders list
	*/
	removeFolderFromList : function (id) {
		var node = gBkmById[id];
		node.setPrivate(false);
		log.info(node.getName() + ' set public by server');
		this.saveList();
		node.resetSyncid();
	},

	// cette fonction doit etre appelee une fois que les marque-pages ont ete charges
	readList : function() {
		if (!this.file.exists()) {
			return;
		}
		var istream = CL["@mozilla.org/network/file-input-stream;1"].createInstance(CI.nsIFileInputStream);
		istream.init(this.file, 0x01, 0444, 0);
		istream.QueryInterface(CI.nsILineInputStream);
		var _uptodate = true;
		var line = {};
		var hasmorelines;
		do {
			hasmorelines = istream.readLine(line);
			// on elimine les lignes vides, sources d'erreurs
			if (line.value) {
				// on ne garde que si la ressource existe reelement
				if (gBkmById[line.value]) {
					gBkmById[line.value].setPrivate(true);
					// sinon, on met le fichier a jour
				} else {
					_uptodate = false;
				}
			}
		} while(hasmorelines);
		istream.close();
		if (!_uptodate) {
			this.saveList();
		}
	},

	clearList : function() {
		log.debug('Clearing private folders file');
		var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance(CI.nsIFileOutputStream);
		outputStream.init(this.file, 0x02 | 0x08 | 0x20 , 0644, 0);
		outputStream.close();
	},

	saveList : function () {
		var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance(CI.nsIFileOutputStream);
		outputStream.init(this.file, 0x02 | 0x08 | 0x20 , 0644, 0);

		for (var id in gBkmById) {
			if (gBkmById[id].isPrivate()) {
				outputStream.write(id + '\n' , id.length + 1);
			}
		}
		outputStream.close();
	}

}
////////////////////////////////////



var YOONO_BKM = new Bookmarks();
