var EXPORTED_SYMBOLS = ["commandQueueMgr"];

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

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

// Vars
const YOONO_DIR = "yoono";
const SYNC_FILE = "yoono_commands.log";
const SYNC_RUNNING_FILE = "yoono_running_commands.log";


// Globals
const CI = Components.interfaces;
const CL = Components.classes;
const DIRSERVICE = CL['@mozilla.org/file/directory_service;1'].getService(CI.nsIProperties);
const PREFSSERVICE = CL["@mozilla.org/preferences-service;1"].getService(CI.nsIPrefService);
const PREFS = PREFSSERVICE.getBranch("extensions.yoono.");


const REQUEST_DELAY = 1000;  // en ms.


//**************************************************************************/
//
// All request can now pile up but only transactional are saved over sessions
//

function CommandQueue() {
	this.wrappedJSObject=this;
	
	
	this._nextTransactionFile = DIRSERVICE.get("ProfDS", CI.nsIFile);
	this._runningTransactionFile = DIRSERVICE.get("ProfDS", CI.nsIFile);
	
	this._nextTransactionFile.append(YOONO_DIR); this._nextTransactionFile.append(SYNC_FILE);
	this._runningTransactionFile.append(YOONO_DIR); this._runningTransactionFile.append(SYNC_RUNNING_FILE);
	
	this._running=[];
	this._next=[];
	
	this.isRunning=false;
	
	this._observers=[]; 	

}

CommandQueue.prototype.init = function (y) {

	yoono = y;
	log = y.log;
	try {
		// Releases before 3.0.6 wrote the sync file in the user chrome directory
		// Newer releases write the sync 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(SYNC_FILE);
		if (oldFile.exists()) {
			var newFile = DIRSERVICE.get('ProfDS', CI.nsIFile);
			newFile.append(YOONO_DIR);
			oldFile.moveTo(newFile,SYNC_FILE);
		}
		
		
		this.reload();
	} catch(e) {
		log.exception(e);
	}
}

CommandQueue.prototype.uninstall = function() {
  try {
    log.warn("Remove sync-file");
    var file = DIRSERVICE.get("UChrm", CI.nsIFile);
    file.append(SYNC_FILE);
    if( file.exists() ) {
      file.remove(true);
    }
  } catch(e) {}
  try {
    file = DIRSERVICE.get("ProfDS", CI.nsIFile);
    file.append(YOONO_DIR);
    file.append(SYNC_FILE);
    if( file.exists() ) {
      file.remove(true);
    }
  } catch(e) {}
  try {
    file = DIRSERVICE.get("ProfDS", CI.nsIFile);
    file.append(YOONO_DIR);
    file.append(SYNC_RUNNING_FILE);
    if( file.exists() ) {
      file.remove(true);
    }
  } catch(e) {}
}

// Load commands from command file from last session.
CommandQueue.prototype.reload = function() {
	var start = (new Date()).getTime();
	
	var istream = CL["@mozilla.org/network/file-input-stream;1"].createInstance(CI.nsIFileInputStream);

	// Reload next requests
	if( this._nextTransactionFile.exists() ) {
		try {
			istream.init(this._nextTransactionFile, 0x01, 0444, 0);
			var converter = CL["@mozilla.org/intl/converter-input-stream;1"].createInstance( CI.nsIConverterInputStream);
			converter.init(istream, 'UTF-8', 1024, '-');
			var line = {};
			this._next = [];
			var content="";
			while(converter.readString(4096, line)) {
				content+=line.value;
			}
			var requests=content.split('\n');
			for each(var r in requests) {
				if (r.length<=3) continue;
				this._next.push( new XML(r) );
			}
			istream.close();
		} catch(e) {
			log.exception(e);
			var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
			outputStream.init(this._nextTransactionFile, 0x02 | 0x08 | 0x20 , 0644, 0);
			outputStream.write("", 0);
			outputStream.flush();
			outputStream.close();
		}
	}
	// Reload running requests
	if( this._runningTransactionFile.exists() ) {
		try {
			istream.init(this._runningTransactionFile, 0x01, 0444, 0);
			var converter = CL["@mozilla.org/intl/converter-input-stream;1"].createInstance( CI.nsIConverterInputStream);
			converter.init(istream, 'UTF-8', 1024, '-');
			var line = {};
			this._running = [];
			var content="";
			while(converter.readString(4096, line)) {
				content+=line.value+"\n";
			}
			var requests=content.split('\n');
			for each(var r in requests) {
				if (r.length<=3) continue;
				this._running.push( new XML(r) );
			}
			istream.close();
		} catch(e) {
			log.exception(e);
			var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
			outputStream.init(this._runningTransactionFile, 0x02 | 0x08 | 0x20 , 0644, 0);
			outputStream.write("", 0);
			outputStream.flush();
			outputStream.close();
		}
	}
	log.debug("reload elapse-time = "+ ((new Date()).getTime() - start));
	
	this.flush();
}

// Simulate a browser shutdown for unit testing
CommandQueue.prototype.simulateShutdown = function() {
	this._running=[];
	this._next=[];
}

// Reset all list : in-memory and files
// used mainly by unit testing
CommandQueue.prototype.fullReset = function() {
	this._running=[];
	var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
	outputStream.init(this._runningTransactionFile, 0x02 | 0x08 | 0x20 , 0644, 0);
	outputStream.write("", 0);
	outputStream.flush();
	outputStream.close();
	this._next=[];
	var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
	outputStream.init(this._nextTransactionFile, 0x02 | 0x08 | 0x20 , 0644, 0);
	outputStream.write("", 0);
	outputStream.flush();
	outputStream.close();
	
}

// Register a new observer to handle responses and errors
//   observer = {onSendingRequest:function(request){}, onRequestSuccess:function(request, response){return newRequest;}, onRequestError:function(request){}}
//   if onRequestSuccess return a request (string), it is added to the current request and resent
CommandQueue.prototype.addObserver = function(o) {
	this._observers.push(o);
}
CommandQueue.prototype.removeObserver = function(o) {
	for(i=0;i<this._observers.length;i++){
		if(o==this._observers[i]) this._observers.splice(i, 1);
	}
}

// Request which must absolutely be sent
// (equivalent of persistant next in old API)
CommandQueue.prototype.addTransactionalRequest = function (s) {
	
	log.debug("Adding transactional request : "+s.toXMLString());
	// Add to in-memory list
		this._next.push(s)
	
	// Add to transaction file
		var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
		var converter = CL['@mozilla.org/intl/converter-output-stream;1'].createInstance(CI.nsIConverterOutputStream);
		converter.init(outputStream, 'UTF-8', 1024, '-');
		// WRONLY, CREATE_FILE, TRUNCATE
		// from http://lxr.mozilla.org/aviary101branch/source/nsprpub/pr/include/prio.h
		outputStream.init(this._nextTransactionFile, 0x02 | 0x08 | 0x10 , 0644, 0);
		converter.writeString(s.toXMLString().replace(/\n/g,"")+"\n");
		converter.flush();
		outputStream.flush();
		outputStream.close();
	
	// Flush requests
		this.flush();
	
}

// All other requests which doesn't need to be keept over session
// (equivalent of transcient next)
CommandQueue.prototype.addTranscientRequest = function (s) {
  var strCmd = s.toXMLString();
	log.debug("Adding transcient request : " + strCmd);
  // if connect command, remove any previous connect commands...
  if(0 == strCmd.indexOf('<connect')) {
    var nb = this._next.length;
    for(var idx=nb-1; idx >= 0; idx --) {
      var tr = this._next[idx];
      strCmd = tr.toXMLString();
      if(0 == strCmd.indexOf('<connect')) {
        this._next.splice(idx, 1);
      }
    }
  }
	this._next.push(s);
	this.flush();
	
}

// Replace current running requests with this one in order to run before next commands
// (equivalent of transcient running with clearRunninCommands)
CommandQueue.prototype._replaceRunningRequest = function (s) {
	
	log.debug("Replace request to the running one : "+s.toXMLString());
	this._running=[s];
	
}

// Return the next request to send
CommandQueue.prototype._getRequest = function () {
	if (this._running.length==0) { // switch next requests to running ones
		log.debug("Switch requests");
		// 1) write next into running
			// Switch in-memory lists
				this._running=this._next;
			// Write next into running transaction file
				var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
				var converter = CL['@mozilla.org/intl/converter-output-stream;1'].createInstance(CI.nsIConverterOutputStream);
				converter.init(outputStream, 'UTF-8', 1024, '-');
				// WRONLY, CREATE_FILE, TRUNCATE
				// from http://lxr.mozilla.org/aviary101branch/source/nsprpub/pr/include/prio.h
				outputStream.init(this._runningTransactionFile, -1, 0644, 0);
				converter.writeString(this._next.map(function (r){return r.toXMLString().replace(/\n/g,"");}).join('\n')+"\n");
				converter.flush();
				outputStream.flush();
				outputStream.close();
		// 2) clean up next
			// Clean in-memory
				this._next=[];
			// Clean next transaction file
				var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
				outputStream.init(this._nextTransactionFile, 0x02 | 0x08 | 0x20 , 0644, 0);
				outputStream.write("", 0);
				outputStream.flush();
				outputStream.close();
	} else {
		log.debug("Redo current request");
	}
	return this._running;
}

CommandQueue.prototype._switchToNextRequests = function () {
	this._running=[];
	var outputStream = CL["@mozilla.org/network/file-output-stream;1"].createInstance( CI.nsIFileOutputStream);
	outputStream.init(this._runningTransactionFile, 0x02 | 0x08 | 0x20 , 0644, 0);
	outputStream.write("", 0);
	outputStream.flush();
	outputStream.close();
}

CommandQueue.prototype.isEmpty = function() {
	return (this._next.length == 0 && this._running.length == 0);
}

CommandQueue.prototype._timer = CL['@mozilla.org/timer;1'].createInstance(CI.nsITimer);

// Wait in order to aggregate some requests and try to send 
CommandQueue.prototype.flush = function () {
	if (this.isRunning)
		return log.debug("no flush because a request is running");
	this._timer.initWithCallback(this, REQUEST_DELAY, this._timer.TYPE_ONE_SHOT);
}

CommandQueue.prototype.notify = function () {
	try {
		this._timer.cancel();
		this.send();
	} catch(e) {
		log.exception(e);
	}
}

CommandQueue.prototype.setSyncId = function() {
	
	if( !this.serverSyncId || this.serverSyncId.length == 0 ) {
		YOONO_PREFS.set('prevsyncid',YOONO_PREFS.get('syncid') || '', PREFS.PREF_STRING);
		YOONO_PREFS.set('syncid', yoono.bkm.getSyncId(), PREFS.PREF_STRING);
	} else {
		YOONO_PREFS.set('prevsyncid',this.serverSyncId, PREFS.PREF_STRING);
		YOONO_PREFS.set('syncid', this.serverSyncId, PREFS.PREF_STRING);
		this.serverSyncId = "";
	}
	//yoono.bkm.dump();
	
}

CommandQueue.prototype._replaceSyncId = function (syncid) {
	this.serverSyncId=syncid;
}

CommandQueue.prototype.nextPersistentCommandIsEmpty = function () {
	
	return ( !this._nextTransactionFile.exists() || this._nextTransactionFile.fileSize<=4);
	
}

CommandQueue.prototype.send = function () {
	if (this.isRunning)
		return log.debug("Not sending, because a request is running");
	
	var _this=this;
	
	var requests=this._getRequest();
	if (requests.length==0)
		return log.debug("No more requests to send!");
	this.isRunning=true;
	
	// Encapsulate the request
	if (!this._retryOnError)
		this.setSyncId();

  var userCred = yoono.main.getUserCredential();
	var xml = <server-script version="1.0"/>;
		xml.appendChild( <context>
				{(userCred.userId ? <user-id> {userCred.userId}  </user-id> : '')}
				{(YOONO_PREFS.get('syncid')      ? <sync-id> {YOONO_PREFS.get('syncid')} </sync-id> : '')}
				{(YOONO_PREFS.get('prevsyncid')  ? <prev-sync-id> {YOONO_PREFS.get('prevsyncid')} </prev-sync-id> : '')}
			<locale>{yoono.utils.getLocale()}</locale>
			<version>{yoono.main.getExtensionVersion()}</version>
			<client>xpi</client>
		</context>);
	for each(var r in requests) {
		xml.appendChild(r);
	}
	log.info('Sending async POST on ' + YOONO_PREFS.get('serverurl') + 'linkserver \n' + xml.toXMLString().replace(/<user-id>(.*)<\/user-id>/gi,"<user-id>*************</user-id>") + "\n>>>>>>>>>>>> ");
	
	// Warm up ajax request
	var data= 'script=' + encodeURIComponent( xml.toXMLString() );
	
	var xmlhttp = CL["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(CI.nsIXMLHttpRequest);
	
	xmlhttp.open('POST', YOONO_PREFS.get('serverurl') + 'linkserver', true);
	
	xmlhttp.setRequestHeader("Cache-Control", "no-cache");
	xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	
	// Called on any type of error
	function onError() {
		try { xmlhttp.abort(); } catch(e) {}
		
		for each(var o in _this._observers) {
			try {
				o.onRequestError(requests);
			} catch(e) {
				log.exception(e);
			}
		}
		
		_this._retryOnError=true;
		
		// Then try to resend the current request some times after
		var delayFunction={
			notify : function (t) {
				_this.flush();
			}
		};
		var timer = CL['@mozilla.org/timer;1'].createInstance(CI.nsITimer);
		timer.initWithCallback(delayFunction, 6000, timer.TYPE_ONE_SHOT);
		
	}
	
	// Initialize a timer to timeout the request if necessary
	var timeoutObserver = {
		timer : CL['@mozilla.org/timer;1'].createInstance(CI.nsITimer),
		delay : YOONO_PREFS.get('request.timeout') * 1000, // la pref est en secondes
		start : function () {
			this.timer.initWithCallback(this, this.delay, this.timer.TYPE_ONE_SHOT);
		},
		stop : function () {
			this.timer.cancel();
		},
		notify : function (t) {
			this.stop();
			_this.isRunning=false;
			
			onError();
		}
	};
	
	// Called on success
	xmlhttp.onload = function (event) {
		try {
			timeoutObserver.stop();
			_this.isRunning=false;
			
			if((4 != event.target.readyState) || (200 != event.target.status)) {
				log.error(event.target.readyState+"/"+event.target.status);
				return onError();
			}
			
			var data = new XML(xmlhttp.responseText.replace(/<\?xml.*?\?>\n?/g, ""));
			
			log.info('Receiving response \n' + data.toXMLString() + "\n<<<<<<<<<<<<< ");
			
			// Notify all observers and collect new requests 
			// all new requests are added to the running requests list
			var redo=false;
			for each(var o in _this._observers) {
				var replace=false;
				try {
					replace = o.onRequestSuccess(requests,data);
				} catch(e) {
					log.exception(e);
				}
				if (replace) {
					if (replace.overrideSyncId)
						_this._replaceSyncId(replace.overrideSyncId);
					_this._replaceRunningRequest(replace.xml);
					redo=true;
					break;
				}
			}
			
			if (!redo)
				_this._switchToNextRequests();
			
			_this._retryOnError=false;
			_this.flush();
			
		} catch(e) {
			log.exception(e);
		}
	};
	
	// Called on network error
	xmlhttp.onerror = function (e) { // Error
		try {
			timeoutObserver.stop();
			_this.isRunning=false;
			
			onError();
			
		} catch(e) {
			log.exception(e);
		}
	};
	
	// Notify observers
	for each(var o in this._observers) {
		try {
			o.onSendingRequest(requests);
		} catch(e) {
			log.exception(e);
		}
	}
	
	// Go go go!
	xmlhttp.send(data);
	
	timeoutObserver.start();
	
}
	

	

var commandQueueMgr = new CommandQueue();
