/*
    This file is part of LibSCORM 2004.

    LibSCORM 2004 is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    LibSCORM 2004 is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with LibSCORM 2004; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

*/

function SSP(lms) {
	this._lms = lms;
	try {
		this.Load("/");
	} catch(e) {
		if(e.code == 401 || e.code == 301) { // root filesystem not yet created
			this._saveObject(new Array(), this._neuter("/"), "course");
		} else {
			throw e;
		}
	}
}

SSP.prototype.IsDirectory = function(path) {
	return path.match(/\/$/);
}

// Saves a file, and creates any missing directory structure.
// Persistence must be one of
// "session": persists until the current learner attempt on the SCO ends
// "course":  persists until the activity tree containing the SCO is removed from the LMS
// "learner": persists until the learner is removed from the LMS
SSP.prototype.Save = function(obj, path, persistence) {
	if(path == "" || this.IsDirectory(path)) {
		throw new LibScormException(
			-1,
			"SSP::Save",
			"'" + path + "' is not a proper file name.",
			path == "" ? "An empty string, though unique, names nothing."
			           : "It is a directory (its name ends in '/')."
		);
	}
	var dirs = path.match(/\w*\//g);
	// Loop 1-based since the initial "/" never varies
	var prevpath="/", prev;
	for(var i = 1; i < dirs.length; i++) {
		prev = this.Load(prevpath);
		if(typeof prev[dirs[i]] == 'undefined') {
			prev[dirs[i]] = prevpath + dirs[i];
			this._saveObject(new Array(), this._neuter(prev[dirs[i]]), "course");
			this._saveObject(prev, this._neuter(prevpath), "course");
		}
		prevpath = prev[dirs[i]];
	}
	var file   = path.match(/\w+$/);
	prev       = this.Load(prevpath);
	prev[file] = path;
	this._saveObject(prev, this._neuter(prevpath), "course");
	this._saveObject(obj, this._neuter(path), persistence);
}

SSP.prototype.Load = function(path) {
	return this._loadObject(this._neuter(path));
}

// Given a directory, say "/x/", the function will return an array of
// absolute paths for all its contained elements.
// Actually, one can accomplish the same thing using plain old Load,
// since a directory is just an SSP file containing such an array.
SSP.prototype.DirectoryList = function(dir) {
	if(!this.IsDirectory(dir)) {
		throw new LibScormException (
			-1,
			"SSP::DirectoryList",
			"'" + dir + "' is not a directory.",
			"Directory names end in '/'."
		);
	}
	return this.Load(dir);
}

//////////////////////////////////////////////////////////////////////////////
//////////////////////////////// PRIVATE STUFF ///////////////////////////////
//////////////////////////////////////////////////////////////////////////////

SSP.prototype._lms;

// ssp has problems saving buckets with multiple '/' in their names
SSP.prototype._neuter = function(path) {
	return path.replace(/\//g, "-");
}

// Unlike the public SSP::Save, this method cannot distinguish a directory
// from a file. It works low-level, serializing whatever goes into it, under
// any name (SSP::_neuter'ed names only).
SSP.prototype._saveObject = function(obj, guid, persistence) {
	var bytesPerChar = 2;

	this._lms.SetValue("ssp.allocate", "{bucketID="+guid+"}"+
	                   "{requested=10}"+
	                   "{persistence="+persistence+"}");
	var n = Number(this._lms.GetValue("ssp.data.{bucketID="+guid+"}"));
	var s = new JSSerializer();
	s.Serialize(obj);
	var str = s.GetJSString('obj');
	// This while loop can only *add* new buckets if needed.
	// If obj has shrunk since loaded we need to disregard
	// some of the already-allocated buckets, which this
	// loop has nothing to do with. The for loop later on
	// will take care of that.
	while( str.length*bytesPerChar > 1024*(Math.pow(2,n)-1) ) {
		this._lms.SetValue("ssp.allocate", "{bucketID="+guid+"___"+n+"}"+
		                   "{requested="+(1024*Math.pow(2,n))+"}"+
		                   "{persistence="+persistence+"}");
		n++;
	}
	this._lms.SetValue("ssp.data", "{bucketID="+guid+"}"+n);
	var start=0, sz;
	var toSave;
	for(var i = 0; i < n; i++) {
		sz = 1024*Math.pow(2,i)/bytesPerChar;
		toSave = str.substring(start, Math.min(str.length, start+sz));
		this._lms.SetValue("ssp.data",
		            "{bucketID="+guid+"___"+i+"}"+
		            toSave);
		if(str.length < start+sz && i+1<n) { // more buckets than needed
		                                     // (obj must have shrunk since loaded)
			this._lms.SetValue("ssp.data", "{bucketID="+guid+"}"+Number(i+1));
			break;
		}
		start += sz;		
	}
}

SSP.prototype._loadObject = function(guid) {
	var n = this._lms.GetValue("ssp.data.{bucketID="+guid+"}");
	var str = "";
	for(var i = 0; i < n; i++) {
		str += this._lms.GetValue("ssp.data.{bucketID="+guid+"___"+i+"}");
	}
	eval(str);  // str is javascript code which initializes an object obj
	return obj;
}
