// note-sys.js
// Copyright (C) 2006 Notefish. All rights reserved.
// system note scripts

// global functions

// perform global initialization
function InitNotes()
{
	if (EnableDebug) debug.init();

	theApp = new App();
	AttachHtml(); // this is generated by server
	theApp.server.loadNotes(0); // load the new web notes (for the first time)
	theApp.hilitReqNote();
}

// perform global uninitialization
function UninitNotes()
{
	if (theApp) theApp.uninit();
	theApp = null;
}

// application class
// the starting point
function App()
{
	// set an event multiplexer to window.onscroll
	window.onScrollHandler = new MultiEvent();
	window.onscroll = function(e) { window.onScrollHandler.handler(e); };

	// set event multiplexers for document
	document.onOverHandler = new MultiEvent();
	document.onmouseover = function(e) { document.onOverHandler.handler(e); };
	document.onOutHandler  = new MultiEvent();
	document.onmouseout  = function(e) { document.onOutHandler.handler(e); }

	this.server = new Server();
	if (!ReadOnlyMode){
		this.tagEdit   = new TagEdit();
		this.noteUI    = new NoteUI();
		this.secUI     = new SecUI();
		this.noteTools = new NoteTools();
		this.proPortal = new ProPortal();
	}
	this.autoPlace = new AutoPlacement();
	this.mover = new Mover();
	this.noteMover = new NoteMover(this.mover);
	this.noteResizer = new NoteResizer();
	this.sections = new Array();
	this.tempSection = new Section(0, 0, "New Web Notes");
	this.htmlCont = GetElem("content");

	this._initProfiles();

	// extracting the project id and note id (if any)
	var re = /\?p=(\d+)(&id=(\d+))?/;
	var matches = document.location.search.match(re);
	this.projectId = matches[1];
	if (matches[3]) this.reqNoteId = matches[3];

	// init application dialogs
	this.dialogs = new Dialogs();
	this.dialogs.addDialog(new SecDlg(this.dialogs));
	this.dialogs.addDialog(new NoteDlg(this.dialogs));
	this.dialogs.addDialog(new SelProDlg(this.dialogs));

	this.idPend = new IdPend();

	// init new note polling
	if (!ReadOnlyMode)
		this.pollTimerId = window.setInterval("theApp.server.loadNewNotes()", NewNotePollTime);

	this.flare = new NoteFlare();
}

App.prototype.server      = null; // server representation
App.prototype.tagEdit     = null; // tag editor
App.prototype.noteTools   = null; // note toolbar
App.prototype.proProtal   = null; // portal to other projects
App.prototype.noteUI      = null; // active note ui
App.prototype.secUI       = null; // active section ui
App.prototype.autoPlace   = null; // auto placement object
App.prototype.sections    = null; // array of all sections
App.prototype.tempSection = null; // section for new notes
App.prototype.htmlCont    = null; // html content of the application
App.prototype.profiles    = null; // note profiles that control note look
App.prototype.idPend      = null; // storage for objects pending for id assignment

App.prototype.tempSecActive = false; // when true, the temp section is visible

App.prototype.projectId      = 0;  // current project id
App.prototype.lastModified   = ""; // project 'last modified' timestamp
App.prototype.projectVersion = 0;  // project version, used to request new notes
                                   // and verify update operations

// note highlighting
App.prototype.flare       = null;
App.prototype.reqNoteId   = 0;     // id of the note request for highlighting in the page url

// moving support
App.prototype.mover       = null;
App.prototype.noteMover   = null;

App.prototype.pollTimerId    = 0;  // new note polling timer

// unload the application
App.prototype.uninit =
	function()
	{
		if (this.pollTimerId)
			window.clearInterval(this.pollTimerId);

		this.tempSection.uninit();
		var secs = this.sections;
		var len = secs.length;
		for (var i=0; i<len; ++i){
			secs[i].uninit();
			secs[i] = null;
		}
		this.sections = null;

		this.server.uninit();      this.server      = null;
		this.autoPlace.uninit();   this.autoPlace   = null;
		this.noteMover.uninit();   this.noteMover   = null;
		this.mover.uninit();       this.mover       = null;
		this.noteResizer.uninit(); this.noteResizer = null;
		this.dialogs.uninit();     this.dialogs     = null;
		this.flare.uninit();

		if (!ReadOnlyMode){
			this.tagEdit.uninit();   this.tagEdit   = null;
			this.noteTools.uninit(); this.noteTools = null;
			this.proPortal.uninit(); this.proPortal = null;
			this.noteUI.uninit();    this.noteUI    = null;
			this.secUI.uninit();     this.secUI     = null;
		}

		this.idPend   = null;
		this.htmlCont = null;

		window.onscroll        = null;
		window.onScrollHandler = null;

		document.onmouseover   = null;
		document.onOverHandler = null;
		document.onmouseout    = null;
		document.onOutHandler  = null;
	};

// hide the "loading, please wait..." message
// and show the main project content
App.prototype.showMainView =
	function()
	{
		GetElem("splash").style.display = "none";
		GetElem("main").style.display = "";
	};

// make the project current
App.prototype.makeCurrent =
	function()
	{
		this.server.makeCurrent();
		var e = GetElem("cur-pro");
		e.className = "pro-info";
		e.innerHTML = "- your current page";
	};

// insert a new section, interactively
App.prototype.insertSection =
	function(sec)
	{
		// the section doesn't have an id yet
		// we have to assign an internal id
		var intId = this.idPend.addObject(sec, "section");

		var secs = this.sections;

		// insert the note and show its html
		var idx = sec.idx;
		if (idx>secs.length || idx==-1) idx = secs.length;
		var beforeHtml = null;
		if (idx<secs.length)
			beforeHtml = secs[idx].html;
		else if (this.tempSecActive)
			beforeHtml = this.tempSection.html;

		var sections = new Array();
		for (var i=0; i<idx; ++i) sections.push(secs.shift());
		sections.push(sec);
		while (secs.length>0){
			var s = secs.shift();
			s.idx++;
			sections.push(s);
		}

		this.sections = sections;
		sec.idx = idx;

		if (beforeHtml)
			this.htmlCont.insertBefore(sec.html, beforeHtml);
		else
			this.htmlCont.appendChild(sec.html);

		sec.html.scrollIntoView();

		// tell server about the new note
		this.server.createSection(sec, intId);
	};

// highlight the note requested in the page url
App.prototype.hilitReqNote =
	function()
	{
		if (!this.reqNoteId) return;

		// there is a note to highlight
		var n = this.findNote(this.reqNoteId);
		if (n){
			n.html.scrollIntoView();
			this.flare.flare(n);
		}
	};

// find a note by scanning all sections
// fairly slow, should not be used in loops
App.prototype.findNote =
	function(id)
	{
		var secs = this.sections;
		var len = secs.length;
		for (var i=0; i<len; ++i){
			var n = secs[i].findNote(id);
			if (n) return n;
		}
		if (this.tempSecActive){
			var n = this.tempSection.findNote(id);
			if (n) return n;
		}
		return null;
	};

// edit an existing section
// a section can have its title and position changed
// parameters:
//   sec:     section to edit
//   name:    new section name
//   afterId: id of the section to place after
//            if 0, the section becomes first
App.prototype.editSection =
	function(sec, name, afterId)
	{
		if (sec.id==afterId)
			throw "Can't place section after itself";


		// calculate all indexes
		var afSec = null;
		var afIdx = -1;
		if (afterId){
			afSec = this.findSection(afterId);
			afIdx = afSec.idx;
		}
		var seIdx = sec.idx;
		var newIdx = seIdx>afIdx?afIdx+1:afIdx;
		if (seIdx==newIdx && sec.title==name) return;

		sec.setTitle(name);

		// change the section position
		var i0 = Math.min(afIdx, seIdx);
		var i1 = Math.max(afIdx, seIdx);

		// find which way indices are affected
		var delta = seIdx>afIdx?1:-1;

		// all indices between i0 and i1 will change
		var aff = new Array();
		var secs = this.sections;
		for (var i=i0+1; i<i1; ++i){
			aff.push(secs[i]);
			secs[i].idx += delta;
		}

		// if the target section was located before
		// the afSec, afSec index is also affected
		if (seIdx<afIdx){
			aff.push(afSec);
			afSec.idx += delta;
		}

		// relocate the section in the array of sections
		secs.splice(sec.idx, 1);
		secs.splice(newIdx, 0, sec);
		sec.idx = newIdx;

		// relocate the section html
		this.htmlCont.removeChild(sec.html);
		if (sec.idx==secs.length-1){
			if (!this.tempSecActive)
				this.htmlCont.appendChild(sec.html);
			else
				this.htmlCont.insertBefore(sec.html, this.tempSection.html);
		}
		else
			this.htmlCont.insertBefore(sec.html, secs[sec.idx+1].html);
		if (seIdx!=newIdx) sec.html.scrollIntoView();

		// send the server request
		this.server.editSection(sec, aff);
	};

// attach a section
// the section object does not need HTML, since
// it is provided by the server, and the section object
// needs to attach to it
// the assumption is sections are attached ordered by index
// and no insertSection calls are made before attachSection calls
// attachSection cannot be called for the virtual section
App.prototype.attachSection =
	function(sec)
	{
		var secs = this.sections;
		secs.push(sec);
		if (secs.length==1) sec.minHeight = MinSSectionHeight;
		else secs[0].minHeight = MinSectionHeight;
	};

// delete a section
App.prototype.deleteSection =
	function(sec)
	{
		if (sec==this.tempSection)
			throw "Can't delete the virtual section";
		var secs = this.sections;
		var len  = secs.length;
		if (len==1){
			alert("Sorry. Cannot delete the last section.");
			return;
		}
		if (!confirm("Are you sure you want to delete this section\n"+
		             "and all the notes inside it?")){
			return;
		}

		// remove the section html
		this.htmlCont.removeChild(sec.html);

		// delete the section object
		for (var i=0; i<len; ++i){
			if (i>sec.idx){
				secs[i-1] = secs[i];
				secs[i-1].idx--;
			}
		}
		secs.pop();
		if (secs.length==1 && !this.tempSecActive){
			secs[0].hideEmptyTitle();
			secs[0].setMinHeight(MinSSectionHeight);
		}

		this.server.deleteSection(sec);
	};

// hide the virtual (temp) section
App.prototype.hideTempSection =
	function()
	{
		// remove the section html
		this.htmlCont.removeChild(this.tempSection.html);
		this.tempSecActive = false;
		var secs = this.sections;
		if (secs.length==1) secs[0].hideEmptyTitle();
	};

// create a new note
App.prototype.insertNote =
	function(sec, title, source, content)
	{
		this.server.insertNote(sec, title, source, content);
	};

// if the note has non-zero width, the note is inserted
// into its section; otherwise the autoplacement is performed
App.prototype.placeNote =
	function(note, secid, show)
	{
		var sec = this.findSection(secid);

		// ignore the note if it's there already
		if (sec && sec.findNote(note.id)) return;

		// now this is complicated:
		// if a note is new, but there is only one section
		// the note goes into this section, and the
		// "new web notes" section is not shown
		var len = this.sections.length;
		if (secid==0 && len==1 && this.sections[0].title=="" && note.w==0)
			sec = this.sections[0];

		// now let's see if we need to display
		// the "new web notes" section
		if (sec==this.tempSection){
			if (!sec.html) sec.init();
			this.htmlCont.appendChild(sec.html);
			this.tempSecActive = true;
		}

		if (note.isnew){
			if (!ReadOnlyMode){
				note.isnew = false;
				this.autoPlace.placeNote(note, sec, false, note.w!=0, show);
			}
		}
		else{
			sec.insertNote(note.y, note.x, note);
			if (show) note.html.scrollIntoView();
			sec.minInsPos(note.y + note.h);
		}
	};

// the following three functions are
// all part of note editing

// start editing the note specified
// see applyNoteChanges, confirmNoteChanges
App.prototype.startNoteChanges =
	function(note)
	{
		this.dialogs.cancelCurDialog();
		this.server.loadNote4Edit(note);
	};

// submit the note changes to server
// see startNoteChanges, confirmNoteChanges
App.prototype.applyNoteChanges =
	function(nid, sid, title, source, content)
	{
		var sec = this.findSection(sid);
		var note = sec.findNote(nid);
		this.server.editNote(note, title, source, content);
	};

// apply the confirmed changes to the note
// see startNoteChanges, applyNoteChanges
App.prototype.confirmNoteChanges =
	function(nid, sid, title, source, content, modified)
	{
		var sec = this.findSection(sid);
		var note = sec.findNote(nid);

		sec.removeNote(note);
		note.title = title;
		note.src = source;
		note.content = content;
		note.modified = modified;
		note.refresh(true);
		this.autoPlace.placeNote(note, sec, true, true);
	};

// delete the note specified
App.prototype.deleteNote =
	function(note)
	{
		if (!confirm("Do you really want to delete this note?")) return;

		this.noteUI.deactivate(true);
		if (this.flare.note==note)
			this.flare.stopFlare();
		var sec = note.section;
		sec.removeNote(note);
		sec.delBlanks(note.y-1, note.h+2); // to make sure we grab at least
		                                   // one row from each side of the note
		if (sec==this.tempSection && sec.getNoteCount()==0)
			this.hideTempSection();

		this.server.removeNote(note, sec);
	};

// move a note to a different project
App.prototype.moveNoteToProject = 
	function(nid, sid, dstpid)
	{
		var sec = this.findSection(sid);
		var note = sec.findNote(nid);

		this.dialogs.cancelCurDialog();

		sec.removeNote(note);
		sec.delBlanks(note.y - 1, note.h + 2);
		if (sec.id==0){ // this is the virtual section
			// if no notes left in the virtual section
			// just remove it
			if (sec.getNoteCount()==0)
				this.hideTempSection();
		}

		this.server.moveNoteToProject(note, sec, dstpid);
	};

// return a section that owns the
// absolute y coordinate specified
// when strict is true for the points beyond any
// section, no attempt is made to take the nearest section
App.prototype.sectionFromY =
	function(y, strict)
	{
		var secs = this.sections;
		var len = secs.length;
		var y0 = 0;
		if (len>0){
			y0 = GetAbsTop(secs[0].html);
			if (!strict && y<y0) y = y0;
		}
		for (var i=0; i<len; ++i){
			var html = secs[i].html;
			var secy = GetAbsTop(html);
			var sech = html.offsetHeight;

			if (y>=secy && y<=secy+sech) return secs[i];
		}

		// check if it's a virtual section
		if (this.tempSecActive){
			var html = this.tempSection.html;
			var secy = GetAbsTop(html);
			var sech = html.offsetHeight;
			if (y>=secy && y<=secy+sech) return this.tempSection;
		}

		return null;
	};

// find section by its id
App.prototype.findSection =
	function(id)
	{
		if (id==0) return this.tempSection;
		var secs = this.sections;
		var len = secs.length;
		for (var i=0; i<len; ++i)
			if (secs[i].id==id) return secs[i];
		return null;
	};

// enable/disable all notes in the
App.prototype.enableAllNotes =
	function(enable)
	{
		var secs = this.sections;
		var slen = secs.length;
		for (var i=0; i<slen; ++i){
			if (!secs[i]) continue;
			var notes = secs[i].notes;
			var nlen = notes.length;
			for (var j=0; j<nlen; ++j)
				if (notes[j]) notes[j].enableDrag(enable);
		}
	};

App.prototype._initProfiles =
	function()
	{
		var profiles = new Object();

		var profile = new NoteProfile();
		profile.name = "blue";
		profile.foreColor = "#0059b3";
		profile.backColor = "#eaf4ff";
		profile.iconColor = "#66ccff";
		profiles[profile.name] = profile;

		profile = new NoteProfile();
		profile.name = "yellow";
		profile.foreColor = "#db8400";
		profile.backColor = "#ffffd4";
		profile.iconColor = "#ffff99";
		profiles[profile.name] = profile;

		profile = new NoteProfile();
		profile.name = "green";
		profile.foreColor = "#008000";
		profile.backColor = "#ebffe6";
		profile.iconColor = "#66ff66";
		profiles[profile.name] = profile;

		profile = new NoteProfile();
		profile.name = "red";
		profile.foreColor = "#bf2600";
		profile.backColor = "#ffebe8";
		profile.iconColor = "#ff7939";
		profiles[profile.name] = profile;

		this.profiles = profiles;
	};

// the singleton application object
// must be created from body.onload
var theApp = null;

// objects pending id assignment by server
function IdPend()
{
	this.objects = new Array();
}

IdPend.prototype.objects = null;
IdPend.prototype.intId = 1; // next value to use for new internal id

// add object waiting for an id assignment
// the return value is an internal id
IdPend.prototype.addObject =
	function(object, type)
	{
		// find a spare space
		var objs = this.objects;
		var len = objs.length;
		var i = 0;
		for (; i<len; ++i)
			if (objs[i]==null) break;

		// store the object
		objs[i] = new Object();
		objs[i].intId = this.intId++;
		objs[i].obj = object;
		objs[i].type = type;

		return objs[i].intId;
	};

// assign the permanent id to the object
// the object is returned
IdPend.prototype.assignId =
	function(intId, type, id)
	{
		// find the object
		var objs = this.objects;
		var len = objs.length;
		var i = 0;
		for (; i<len; ++i)
			if (objs[i].intId==intId && objs[i].type==type) break;
		if (i==len) throw "Invalid internal id";
		var obj = objs[i].obj;
		objs[i] = null;
		obj.id = id;

		return obj;
	};

// the server class
// responsible for network communications
function Server()
{
	this.reqs = new Array();
	this.qreqs = new Queue();
	var server = this; // to create closure
	this.cleanupTimer = window.setInterval(
		function(){ server._cleanOldReqs(); },
		RequestTimeout
	);
}

Server.prototype.reqs = null;      // pending requests
Server.prototype.cleanupTimer = 0; // cleanup timer id
Server.prototype.qreqs = null;     // queued requests
Server.prototype.qproc = false;    // when true, a request from the queue is being processed

// uninitialize server
Server.prototype.uninit =
	function()
	{
	};

Server.prototype.makeCurrent =
	function()
	{
		var pid = theApp.projectId;
		var url = "x-cur-project.php?p="+pid;
		this._submitRequest("GET", url, null, pid, "make-cur");
	};

Server.prototype.getTags = 
	function()
	{
		var pid = theApp.projectId;
		var url = "x-tags.php?p=" + pid;
		this._submitRequest("GET", url, null, pid, "tags");
	};

Server.prototype.setTags = 
	function(tags)
	{
		var pid = theApp.projectId;
		var url = "x-set-tags.php?p=" + pid + "&tags=" + encodeURIComponent(tags);
		this._submitRequest("GET", url, null, pid, "tags");
	};

// load notes for a section
Server.prototype.loadNotes =
	function(secId)
	{
		var url = "x-notes.php?p="+theApp.projectId+"&v="+theApp.projectVersion+"&sec="+secId;
		this._submitRequest("GET", url, null, secId, "notes");
	};

// load new notes
Server.prototype.loadNewNotes =
	function()
	{
		var url = "x-notes.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&sec=0&new=1";
		this._submitRequest("GET", url, null, 0, "new-notes", false, true);
	};

// notify the server about new position for a note
// auto specifies if being positioned is a new
// note sent from a source web page and being positioned
// for the first time
Server.prototype.positionNote =
	function(note, dstsec, srcsec, auto)
	{
		//var url = "x-pos-note.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		var url = "x-pos-note.php?p="+theApp.projectId+
		          "&dst="+dstsec.id+(srcsec?("&src="+srcsec.id):"")+
		          "&id="+note.id+
		          "&x="+note.x+"&y="+note.y+
		          "&w="+note.w+"&h="+note.h;
		if (auto) url += "&auto";

		var dstaff = "dstaff="+this._getDirtyYs(dstsec);
		var srcaff = srcsec?("&srcaff="+this._getDirtyYs(srcsec)):"";
		var data = dstaff+srcaff;

		this._queueRequest("POST", url, data, note.id, "pos-note");
	};

// notify the server that a new section has been created
// the server will return new section id
Server.prototype.createSection =
	function(sec, intId)
	{
		var url = "x-new-section.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&idx="+sec.idx+"&intid="+intId+
		          "&title="+encodeURIComponent(sec.title);
		this._submitRequest("GET", url, null, intId, "new-section");
	};

// remove the section specified on server
Server.prototype.deleteSection =
	function(sec)
	{
		var url = "x-del-section.php?p="+theApp.projectId+"&v="+theApp.projectVersion+"&sec="+sec.id;
		this._submitRequest("GET", url, null, sec.id, "del-section");
	};

// edit the section: rename and reposition it
Server.prototype.editSection =
	function(sec, aff)
	{
		// build a map of id->idx for affected section
		var saff = "";
		var len = aff.length;
		for (var i=0; i<len; ++i){
			var s = aff[i];
			saff += (s.id+":"+s.idx);
			if (i!=len-1) saff += ",";
		}

		var url = "x-edit-section.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&sec="+sec.id+"&title="+encodeURIComponent(sec.title)+
		          "&idx="+sec.idx+"&aff="+saff;
		this._submitRequest("GET", url, null, sec.id, "edit-section");
	};

// set note profile
Server.prototype.profileNote =
	function(note, profile)
	{
		var url = "x-profile.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&id="+note.id+"&profile="+profile;
		this._submitRequest("GET", url, null, note.id, "profile-note");
	};

// insert a new note
Server.prototype.insertNote =
	function(sec, title, source, content)
	{
		var url = "x-new-note.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&sec="+sec.id+"&reply=1";
		var data = "title="+encodeURIComponent(title)+
		           "&source="+encodeURIComponent(source)+
		           "&content="+encodeURIComponent(content);
		this._submitRequest("POST", url, data, 0, "new-note");
	};

// remove the note
Server.prototype.removeNote =
	function(note, sec)
	{
		//var url = "x-del-note.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		var url = "x-del-note.php?p="+theApp.projectId+
		          "&sec="+sec.id+"&id="+note.id;
		var data = "aff="+this._getDirtyYs(sec);

		this._queueRequest("POST", url, data, note.id, "delete-note");
		//this._submitRequest("POST", url, data, note.id, "delete-note");
	};

// move note to a different project
Server.prototype.moveNoteToProject =
	function(note, sec, dstpid)
	{
		var url = "x-move-note.php?p=" + theApp.projectId +
		          "&dstp=" + dstpid +
			  "&sec="  + sec.id +
		          "&id="   + note.id;
		var data = "&aff="+this._getDirtyYs(sec);

		this._queueRequest("POST", url, data, note.id, "pos-note");
	};

// set new note title, source and content
Server.prototype.editNote =
	function(note, title, source, content)
	{
		var url = "x-edit-note.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&sec="+note.section.id+"&id="+note.id;
		var data = "title="+encodeURIComponent(title)+
		           "&source="+encodeURIComponent(source)+
		           "&content="+encodeURIComponent(content);
		this._submitRequest("POST", url, data, note.id, "edit-note");
	};

// load note for editing
Server.prototype.loadNote4Edit =
	function(note)
	{
		var url = "x-notes.php?p="+theApp.projectId+"&v="+theApp.projectVersion+
		          "&sec="+note.section.id+"&id="+note.id+"&raw=1";
		this._submitRequest("GET", url, null, note.id, "load-note");
	};

// load the project list for the project selection dialog
Server.prototype.loadProjects =
	function(page, size)
	{
		var url = "x-projects.php?page=" + page + "&size=" + size +  "&sort=modified&order=desc&noupdate";
		this._submitRequest("GET", url, null, 0, "load-projects");
	};

// put a request to the queue to make sure the execution is serial
Server.prototype._queueRequest =
	function(method, url, data, reqId, reqType)
	{
		var qr = new Object();
		qr.method = method;
		qr.url = url;
		qr.data = data;
		qr.reqId = reqId;
		qr.reqType = reqType;

		debug.log("Putting request '"+url+"' to the queue");
		
		this.qreqs.push(qr);
		this._submitQRequest();
	};

// check if any queued requests is started
// if not, start one
Server.prototype._submitQRequest = 
	function()
	{
		if (this.qreqs.isEmpty()) return;
		if (this.qproc) return;

		this.qproc = true;
		var qr = this.qreqs.pop();
		var url = qr.url+"&v="+theApp.projectVersion;
		
		debug.log("Taking request '"+url+"' from the queue, "+this.qreqs.q.length+" item(s) left");

		this._submitRequest(qr.method, url, qr.data, qr.reqId, qr.reqType, true);
	};

// submit a request to the server
Server.prototype._submitRequest =
	function(method, url, data, reqId, reqType, queued, nolog)
	{
		// check if we're waiting for this request already
		// and also find a free spot in the request array
		var reqs = this.reqs;
		var cnt = reqs.length;
		var idx = cnt;
		for (var i=0; i<cnt; ++i){
			if (reqs[i]==null) idx = i;
			else if (reqs[i].id==reqId && reqs[i].type==reqType)
				return; // it's pending already
		}

		var req = new HttpReq();
		req.idx = idx;
		req.type = reqType;
		req.id = reqId;
		req.started = new Date();
		this.reqs[idx] = req;
		//this._execRequest(req);
		var server = this; // to create closure
		reqs[idx].load(method, url, data,
			function(request)
			{
				server._httpCallback(request);
			}, true, queued, nolog
		);
	};

Server.prototype._httpCallback =
	function(req)
	{
		if (!req.nolog) debug.log("HTTP response, code: "+req.req.status+", for url: "+req.url);

		this.reqs[req.idx] = null;

		//try{ // the xml may potentially have problems in it; just ignore if that's the case
			var xml = req.req.responseXML.documentElement;
			if (xml){
				// update the project version if it was supplied
				var ver = xml.getAttribute("project-version");
				if (ver){
					ver = Number(ver);
					if (ver>theApp.projectVersion) theApp.projectVersion = ver;
				}
			}

			switch (req.type){
			case "tags":
				this._procTagResponse(req);
				break;
			case "notes":
				this._procNoteResponse(req);
				break;
			case "new-notes":
				this._procNewNotesResponse(req);
				break;
			case "new-section":
				this._procNewSecResponse(req);
				break;
			case "load-note":
				this._procNote4EditResponse(req);
				break;
			case "edit-note":
				this._procEditNoteResponse(req);
				break;
			case "new-note":
				this._procNewNoteResponse(req);
				break;
			case "load-projects":
				this._procLoadProjects(req);
				break;
			}

			// if that was a queued request, 
			// we start the next one
			if (req.queued) this.qproc = false;
			this._submitQRequest();
		//}
		//catch (e){
		//	debug.log("Exception: "+e.name+", message: "+e.message);
		//}
	};

Server.prototype._procTagResponse =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		if (!xml) return;

		if (!this._checkProject(xml)) return;

		var tags = GetXmlElemValue(xml).split(",");
		if (tags.length == 1 && tags[0] == "") tags = new Array();

		theApp.tagEdit.onTagsReady(tags);
	};

Server.prototype._procNoteResponse =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		if (!xml) return;
		if (!this._checkProject(xml)) return;

		var secId = Number(xml.getAttribute("section-id"));
		if (!this._checkSection(secId)) return;

		var xmlNotes = xml.childNodes;
		if (!xmlNotes) return;

		var noteCnt = xmlNotes.length;
		for (var i=0; i<noteCnt; ++i){
			var xmlNote = xmlNotes[i];
			if (xmlNote.nodeType!=1) continue; // mozilla may have blank nodes

			var note = this._xml2Note(xmlNote);
			theApp.placeNote(note, secId);
		}
	};

Server.prototype._procNewNotesResponse =
	function(req)
	{
		// new notes need to be added,
		// which is identical to simply loading them
		this._procNoteResponse(req);
	};

Server.prototype._procNote4EditResponse =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		if (!xml) return;
		if (!this._checkProject(xml)) return;

		var secId = Number(xml.getAttribute("section-id"));
		if (!this._checkSection(secId)) return;

		var xmlNote = xml.firstChild;
		while (xmlNote && xmlNote.nodeType!=1) xmlNote = xmlNote.nextSibling;

		// display the dialog
		var noteId   = Number(xmlNote.getAttribute("id"));
		var title    = xmlNote.getAttribute("title");
		var src      = xmlNote.getAttribute("source");
		var content  = xmlNote.firstChild.nodeValue;

		var dlg = theApp.dialogs.getDialog("note-dlg");
		dlg.setParams(noteId, secId, title, src, content);
		theApp.dialogs.openDialog("note-dlg");
	};

Server.prototype._procNewNoteResponse =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		if (!xml) return;
		if (!this._checkProject(xml)) return;

		var sid = Number(xml.getAttribute("section-id"));
		if (!this._checkSection(sid)) return;

		var note = this._xml2Note(xml);
		theApp.placeNote(note, sid, true);
	};

Server.prototype._procEditNoteResponse =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		if (!xml) return;
		if (!this._checkProject(xml)) return;

		var sid = Number(xml.getAttribute("section-id"));
		if (!this._checkSection(sid)) return;

		var note = this._xml2Note(xml);
		theApp.confirmNoteChanges(note.id, sid, note.title, note.src, note.content, note.modified);
	};

Server.prototype._procNewSecResponse =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		if (!xml) return;
		if (!this._checkProject(xml)) return;

		var id = Number(xml.getAttribute("id"));
		var iid = Number(xml.getAttribute("int-id"));

		var sec = theApp.idPend.assignId(iid, "section", id);
		sec.initCtrls();
	};

Server.prototype._procLoadProjects =
	function(req)
	{
		var xml = req.req.responseXML.documentElement;
		theApp.dialogs.getDialog("pro-dlg").setProjects(xml);
	};

Server.prototype._checkProject =
	function(xml)
	{
		var projectId = Number(xml.getAttribute("project-id"));
		if (projectId!=theApp.projectId){
			debug.log("Invalid page id: "+projectId);
			return false;
		}
		return true;
	};

Server.prototype._checkSection =
	function(sid)
	{
		var sec = theApp.findSection(sid);
		if (!sec){
			debug.log("Section not found: "+sid);
			return false;
		}
		return true;
	}

Server.prototype._xml2Note =
	function(xml)
	{
		var note      = new Note();
		note.id       = Number(xml.getAttribute("id"));
		note.profile  = xml.getAttribute("profile");
		note.title    = SafeHtml(xml.getAttribute("title"));
		note.src      = xml.getAttribute("source");
		note.content  = xml.firstChild.nodeValue;
		note.modified = xml.getAttribute("modified");

		note.isnew = Number(xml.getAttribute("new"));
		if (!note.isnew){
			// the note is already positioned
			note.x = Number(xml.getAttribute("x"));
			note.y = Number(xml.getAttribute("y"));
		}
		var attr = xml.getAttribute("w");
		if (attr) note.w = Number(attr);
		attr     = xml.getAttribute("h");
		if (attr) note.h = Number(attr);

		note.init();

		return note;
	};

// scheduled request cleanup
// there is no way to detect network errors below http level
// so callbacks for "host not found" type of thing is never called
// the following function cleans requests that sit pending for too long
Server.prototype._cleanOldReqs =
	function()
	{
		var reqs = this.reqs;
		var len = reqs.length;
		var now = new Date();
		for (var i=0; i<len; ++i){
			if (reqs[i]){
				if (now.valueOf()-reqs[i].started.valueOf()>RequestTimeout){
					reqs[i].cancel();
					if (reqs[i].queued){
						// a queued request timed out
						// this means the susequent ones don't make sense
						// the page should be reloaded
						this.qreqs.clear();
						ReloadPage();
					}
					reqs[i] = null;
				}
			}
		}
	};

// for the section's dirty notes
// return the string in format "id1:y1,..,idN:yN"
Server.prototype._getDirtyYs =
	function(sec)
	{
		var res = "";
		var dirty = sec.getDirtyNotes();
		var len = dirty.length;
		for (var i=0; i<len; ++i){
			var n = dirty[i];
			res += (n.id+":"+n.y);
			if (i!=len-1) res += ",";
		}
		return res;
	};

// auto placement provides sizing and
// positioning services for new notes added externally
// one note is processed at a time, others wait in line
function AutoPlacement()
{
	this.notes = new Array();
	this.box = GetElem("auto-place");

	var ap = this; // for a closure
	this.timerId = window.setInterval
		( function(){ ap._onTimer(); }, 50);
}

// this is an element in HTML where
// notes are placed to determine their sizes
// the box is called "autoplace box"
AutoPlacement.prototype.box = null;

// indicates the stage of size calculation
AutoPlacement.prototype.widthReady = false;

// timer is used for delayed size evaluation
// when a note size is being determined, the size
// is not always immmediately available in mozilla
AutoPlacement.prototype.timerId = -1;

// grid cell size, note size is calculated in grid cells
AutoPlacement.prototype.cellWidth  = 0; // in px
AutoPlacement.prototype.cellHeight = 0; // in px

AutoPlacement.prototype.curNote    = null;  // current note
AutoPlacement.prototype.curSection = null;  // current section
AutoPlacement.prototype.curPrepos  = false; // current note preposition (see placeNote)
AutoPlacement.prototype.curPresize = false; // current note has suggested dimensions (see placeNote)
AutoPlacement.prototype.curShow    = false; // when true the note is scrolled into view
                                            // once positioned

// note queue
AutoPlacement.prototype.notes = null;

// uninit
AutoPlacement.prototype.uninit =
	function()
	{
		window.clearInterval(this.timerId);
	};

// calculate size and position for a new note
// note is assumed to be initialized
// when prepos is true, this means the note x and y
// contain the desired coordinates for placing the note to
// when presize is true, the note contains suggested dimensions
// which AutoPlacement uses as a guideline to determine the final dimensions
// when show is true, the note is scrolled into
// view once its positioning complete
AutoPlacement.prototype.placeNote =
	function (note, section, prepos, presize, show)
	{
		// just put the note in the queue
		var item = new Object();
		item.note = note;
		item.section = section;
		item.prepos  = prepos?true:false;   // get rid of the "undefined" case
		item.presize = presize?true:false;  // get rid of undefined
		item.show    = show?true:false;     // get rid of the "undefined" case

		this.notes.push(item);
	};

// a timer driven routine that processes pending notes
AutoPlacement.prototype._onTimer =
	function()
	{
		// check if there is a pending note
		var note = this.curNote;
		if (!note){
			this._queue2Box();
			return;
		}

		// init single cell dimensions
		// not done in constructor since the data
		// maybe invalid at that point
		if (!this.cellHeight){
			this.cellHeight = GetElem("single-cell").offsetHeight;
			this.cellWidth = theApp.sections[0].getSingleCellWidth();
		}

		// generally, size is determined in two steps:
		// first, the width is determined with limited 
		// regard to the width of the note title
		// and then, height is calculated;
		// if the note has size suggestions (presize is true)
		// the suggested width is used as a guidance

		var clone = this.box.firstChild;
		var pxw = clone.offsetWidth;
		var pxh = clone.offsetHeight;
		var w = Math.ceil(pxw/this.cellWidth);

		if (!this.widthReady){
			if (!this.curPresize){
				var halfSection = Math.floor(this.curSection.width/2);
				if (w>halfSection){
					// the note is too wide
					// try forcing it to be narrower
					w = halfSection;
				}
			}
			else{
				w = note.w;
			}
			clone.style.width = w*this.cellWidth;
			clone.rows[0].cells[0].innerHTML
				= this.curNote.html.rows[0].cells[0].innerHTML;
			this.widthReady = true;
			return;
		}

		// at this point, the note can still be ultrawide
		// even though the consequences are not pretty
		// let's simply limit the note width
		if (w > this.curSection.width)
			w = this.curSection.width;

		var h = Math.ceil(pxh/this.cellHeight);
		if (h*this.cellHeight-pxh<2) ++h; // factoring in cell borders

		this.box.removeChild(clone);

		note.h = h;
		note.w = w;

		if (this.curPrepos){
			var sec = this.curSection;
			// sometimes, there is no choice and we have to move notes
			if (note.x + note.w > sec.width)
				note.x = sec.width - note.w;
			if (note.x < 0)	
				note.x = 0;

			// now insert
			sec.insertNote(note.y, note.x, note);
			sec.minInsPos(note.y+note.h);
			sec.delBlanks(note.y-1, sec.height);
			theApp.server.positionNote(note, sec, null);
			if (this.curShow) note.html.scrollIntoView();
		}
		else
			this._posCurNote();

		theApp.noteUI.refresh();

		this._queue2Box();
	};

// find a location for a note
// note dimensiosn should be valid already
AutoPlacement.prototype._posCurNote =
	function()
	{
		var note = this.curNote;
		var sec = this.curSection;

		sec.ensureInsPos(note.w);
		sec.insertNote(sec.insY, sec.insX, note);
		theApp.server.positionNote(note, sec, null, true);
		if (this.curShow){
			var abst = GetAbsTop(note.html);
			if (abst>document.body.scrollTop+window.getCurHeight())
				note.html.scrollIntoView();
			theApp.flare.flare(note);
		}
	};

// extract a note from the queue and put in the autoplace box
AutoPlacement.prototype._queue2Box =
	function()
	{
		this.widthReady = false;
		this.curNote    = null;
		this.curSection = null;
		this.curPrepos  = false;
		this.curPresize = false;
		this.curShow    = false;

		// extract a pending note
		var notes = this.notes;
		var len = notes.length;
		if (!len) return;

		var item = notes.shift();
		this.curNote    = item.note;
		this.curSection = item.section;
		this.curPrepos  = item.prepos;
		this.curPresize = item.presize;
		this.curShow    = item.show;
		var clone = item.note.html.cloneNode(true);
		clone.style.width = "auto";

		// leave only first TitleSizeWords words
		// in the title to curb title's effect on
		// the resulting note width
		var ht = clone.rows[0].cells[0];
		var title = ht.innerHTML;
		var re = "(\\w+\\W*){0,"+TitleSizeWords+"}";
		var matches = title.match(re);
		if (matches && matches.length>0)
			ht.innerHTML = matches[0];

		this.box.appendChild(clone);
	};

