//
// Meeting Delay Simulation 
//	Raja, Brian, September 16, 2008
// (c) Copyright 2008 Navaraga Corporation
//
var totalHours = 40;
var maxUsers = 5;
var employee;
var meeting;
var order;
var meetingPositions;
var totalMeetings;
var parallelProjects;
var planLength;
var newPlanLength;
var newPlanTime;

var meetingPercentage;
var totalMeetingHours;
var totalNonMeetingHours;

var maxWaitUsed = false;

var empNames = ["Ann", "Bob", "Cindy", "David", "Emily"];

var dbgbuf = "";
function dbg(buf) {
	dbgbuf += buf + "<br>";
}

function showdebug() {
	document.getElementById("worklist").innerHTML = dbgbuf;
	dbgbuf = "";
}

function icon(code, count) {
	var buf = "d";

	switch(code) {
		case "T" : 
			buf = "t";
			break;
		case "O" :
			buf = "o";
			break;
		case "W":
			buf = "b";
			break;
		case "M":
			buf = "m";
			break;
		case "D":
			buf = "d";
			break;
		case "B":
			buf = "b";
			break;
	}
	return "<img height=12 width=" + count + " src=/img/" + buf + ".png>";
}

var lasticon = "";
var iconcount = 0;

function iconflush(newicon) {
	var buf = "";

	if (iconcount) 
		buf= icon(lasticon, iconcount);

	lasticon = newicon;
	iconcount = newicon.length;
	return buf;
}

function iconcache(code) {
	var buf = "";

	if (code) {
		if (code == lasticon)
			iconcount++;
		else 
			buf = iconflush(code);
	}
	else 
		buf = iconflush("");

	return buf;
}

function mtgReduction(i) {
	var last = newPlanTime[ newPlanTime.length - 1 ];

	var result = Math.round( ( (last - newPlanTime[i]) / last ) * 100 );

	return result;
}

function nfill(num) {
	var buf = num.toString();
	if (buf.length == 1)
		return "00" + buf;
	if (buf.length == 2)
		return "0" + buf;
	return buf;
}

function nfraction(num) {
	if (num.toFixed)
		return num.toFixed(1);
	return (Math.floor(num*10) / 10);
}

function nformat(num) {
	num = Math.floor(num);

	if (num < 1000)
		return num.toString();

	return nformat( Math.floor(num/1000) ) + "," + nfill( num%1000 );
}

function writeMeetings() {
	var buf = "<table><thead><tr><td colspan=3>3. Meetings</thead><tr class=rhead><td>Meeting<td>Attendees<td>Timeline";

	var nMtg = 1;

	for (var p = 0; (p < planLength) || (p == 0 && planLength == 0); p++)
	    if (meetingPositions.plan[p])
		for (i = 0; i<totalMeetings; i++) 
			if (meeting[i].position == p) {
				buf += "<tr><td>Meeting " + nMtg++ + " <td> ";
				var sep = " ";
				for (var k=0; k<empNames.length; k++) 
					for (var j=0; j<meeting[i].attendees.length; j++) 
						if (k == meeting[i].attendees[j]) {
							buf += sep + empNames[ k ];
							sep = ", ";
						}
				buf += "<td>" + Math.floor(100* meeting[i].position / (planLength? planLength : 1)) + "%";
			}

	buf += "</table>";

	return buf;
}

function writeSavings() {

	buf = "<table><thead><tr><td colspan=3>4. Savings from fewer meetings</thead><tr class=rhead><td>Meetings<td>Project Duration<td>Savings";

	for (var i=newPlanTime.length - 1; i>=0; i--) 
		buf += "<tr><td>" + i + " meetings <td nowrap>" + showHours( newPlanTime[i], false ) + 
			"<td>" + mtgReduction(i) + "%" ;
	
	buf += "</table>";

	return buf;
}

function showplan2(buf) {
	var j;

	buf += "<br>";
	if (newPlanTime) {
		buf += "Data = ";
		for (j = 0; j < newPlanTime.length; j++) {
			if (newPlanTime[j])
				buf += newPlanTime[j] + ", ";
		}
		buf += "<br>";
	}
	buf += "<br>";
	for (var i=0; i<employee.length; i++) {
		var pbuf = "";
		buf += "New Plan [ " + i + " ] = ";
		iconcache(null);
		for (j = 0; j < employee[i].newPlan.length; j++) {
		       pbuf += iconcache(employee[i].newPlan[j]);
		}
		pbuf += iconcache(null);
		document.getElementById("nplan"+i).innerHTML = pbuf;
		buf += pbuf + " (" + employee[i].newPlan.length + ")<br>";
	}
	buf += "<br>";
	for (var i=0; i<employee.length; i++) {
		buf += "Old Plan [ " + i + " ] = ";
		for (j = 0; j < employee[i].plan.length; j++) 
			buf += employee[i].plan[j];
		buf += " (" + employee[i].plan.length + ")<br>";
	}
	if (meetingPositions.plan) {
		buf += "Old Plan [ M ] = ";
		for (j = 0; j < meetingPositions.plan.length; j++) 
			buf += meetingPositions.plan[j];
		buf += "<br>";
	}
	dbg(buf);
}

function showplan(buf) {
	var j;

	showMeetings();

	buf += "<br>";
	for (var i=0; i<employee.length; i++) {
		var pbuf = "";
		iconcache(null);
		for (j = 0; j < employee[i].newPlan.length; j++) {
		       pbuf += iconcache(employee[i].newPlan[j]);
		}
		pbuf += iconcache(null);
		document.getElementById("nplan"+i).innerHTML = pbuf;
	}
	dbg(buf);
}

function plural(n) {
	return (n==1) ? "" : "s";
}

function showHours(num, emphasis) {
	var hday = 8;
	var hweek = 5 * hday;
	var hmonth = 4 * hweek;
	var hyear = 52 * hweek;
	var sep = " (";
	var n;
	var buf = "";

	if (emphasis)
		buf += "<em>";
	buf += num;
	if (emphasis)
		buf += "</em>";
	buf += " hours";

	if (num > hyear) {
		n = Math.floor( num / hyear );
		if (n) {
			buf += sep + n + " year" + plural(n);
			sep = ", "
			num = num % hyear;
		}
	}

	if (num > hmonth) {
		n = Math.floor( num / hmonth );
		if (n) {
			buf += sep + n + " month" + plural(n);
			sep = ", ";
			num = num % hmonth;
		}
	}

	if (num > hweek) {
		n = Math.floor( num / hweek );
		if (n) {
			buf += sep + n + " week" + plural(n);
			sep = ", ";
			num = num % hweek;
		}
	}

	if (num > hday) {
		n = Math.floor( num / hday );
		if (n) {
			buf += sep + n + " day" + plural(n);
			sep = ", ";
			num = num % hday;
		}

		if (num) 
			buf += sep + num + " hour" + plural(num);
	}
	if (sep != " (")
		buf += ")";

	return buf;
}

function oneLessMeeting() {
	var buf = "One less meeting will save <em>";

	buf += (newPlanTime.length > 2) ? mtgReduction(newPlanTime.length - 2) : "0";
	buf += '%</em>. <br><a href="javascript:showDetails()"><div style="text-align:center;">show details <br><img src=/img/chart_thumb.png></div></a>';

	return buf;
}

function showAnswer() {

	document.getElementById("answer").innerHTML = "<p>Answer: " 
		+ showHours(newPlanLength, true) + "<br>" 
		+ oneLessMeeting()
		+ "</p>";
}

// Swap items in the property "plan" for this object, randomly
function shuffle(obj) {
	var i,j,k,t;
	for (k=0; k< (4*obj.plan.length); k++) {
		i = Math.floor(Math.random()*obj.plan.length);
		j = Math.floor(Math.random()*obj.plan.length);
		t = obj.plan[i];
		obj.plan[i] = obj.plan[j];
		obj.plan[j] = t;
	}

	var buf = "Order: ";
	for (i=0; i<obj.plan.length; i++)
		buf += obj.plan[i] + " ";
	
	buf += " (" + obj.plan.length + ")";
	// dbg(buf);
}

function activateMeeting() {
	var list = new Array();
	var ilist = 0;

	for (var i=0; i<totalMeetings; i++)
		if (! meeting[i].active) 
			list[ilist++] = i;
	i = Math.floor(Math.random() * list.length);
	i = list[i];
	meeting[i].active = 1;
	// dbg("activated " + i);
}

function empObject(i) {
	// this.numprojects = parseInt(document.getElementById("np" + i).value);
	this.numprojects = parallelProjects;
	if (maxWaitUsed)
		this.maxwait = parseInt(document.getElementById("mw" + i).value);
	this.effortfraction = 1.0 / this.numprojects;
	this.hours = 0;
	this.meetingHours = 0;
	this.cursor = 0;
	this.plan = new Array(); // each element: T=this project, O=other projects, M=meeting, B=blocked, W=waiting
	this.newPlan = new Array(); 
	this.newcursor = 0;
}

function errMsg(buf) {
	if (buf.length)
		document.getElementById("answer").innerHTML = buf;

	return buf.length == 0;
}

function validateInput() {
	var value = 0;

	buf = document.getElementById("nProjectsInput").value;
	var match = buf.match(/^\d*$/);

	if (match) {
		val = parseInt(buf);
		if (val <= 0 || val > 10) 
			return errMsg("ERROR: " + buf + ". This needs to be between 1 and 10");
	}
	else 
		return errMsg("ERROR: " + buf + ". This needs to be between 1 and 10");

	var buf = document.getElementById("meetingPercentage").value;
	match = buf.match(/^\d*$/);

	if (match) {
		val = parseInt(buf);
		if (val < 0 || val > 100) 
			return errMsg("ERROR: " + buf + ". This needs to be between 0 and 100");
	}
	else 
		return errMsg("ERROR: " + buf + ". This needs to be between 0 and 100");

	return errMsg("");
}

function initGlobals() {
	employee = new Array(maxUsers);
	meeting = new Array(totalHours);
	meetingPositions = new Object;
	newPlanTime = new Array();
	totalMeetings = 0;	// total meetings	
	planLength = 0;

	meetingPercentage = parseInt(document.getElementById("meetingPercentage").value)/100;
	parallelProjects = parseInt(document.getElementById("nProjectsInput").value);
	// totalHours = parseInt(document.getElementById("totalHours").value);
	totalHours = 40;
	totalMeetingHours = Math.floor(totalHours * meetingPercentage);
	totalNonMeetingHours = totalHours - totalMeetingHours;

	order = new Object;
	order.plan = new Array(maxUsers);
	
	// Initialize order for shuffling
	for (i=0; i<maxUsers; i++)
		order.plan[i] = i;
}

function initEmployees() {
	var sumEffort = 0;
	var hourCount = 0;
	var i, j;

	// what % of their time goes for this project
	for (i=0; i<maxUsers; i++) {
		employee[i] = new empObject( i );
		// % of employees time allocated for this project
		sumEffort += employee[i].effortfraction; 
	}
	
	// Compute how much each employee contributes to THIS project
	// Employees share for project visavis others.
	for (i=0; i<maxUsers; i++)
		with (employee[i]) {
			sharefraction = effortfraction / sumEffort; 
			hours = Math.round(sharefraction * 
						totalNonMeetingHours);
			hourCount += hours;
		}

	if (hourCount < totalNonMeetingHours) {
		// Rounding error, did not distrbute ALL the hours among employees.
		// Give them to the employee with the most hours, since this could be + or -
		// dbg("Hours difference: " + (totalNonMeetingHours - hourCount) );
		for (i=j=0; i<maxUsers; i++) 
			j = (employee[i].hours > employee[j].hours) ? i : j;
		employee[j].hours += (totalNonMeetingHours - hourCount);
	}
}

function genPlan() {
	var i, j;

	for (i=0; i<maxUsers; i++)  {
		employee[i].plan = new Array(Math.round(employee[i].hours 
						/ employee[i].effortfraction));

		for (j=0; j<employee[i].plan.length; j++) 
			employee[i].plan[j] = (j < employee[i].hours) ? "T" :
							"O";
		// dbg("Employee " + i);
		shuffle(employee[i]);
		if (employee[i].plan.length > planLength)
			planLength = employee[i].plan.length;
	}

	// Extend everyones plan to planLength, filling rest with Ds.
	// Correct it at the end, replacing them with Bs, if these people have to attend a meeting.
	for (i=0; i<maxUsers; i++) 
		for (j=employee[i].plan.length; j<planLength; j++)
			employee[i].plan[j] = "D";
}
	
function initMeetingPositions() {
	meetingPositions.plan = new Array(planLength);
	for (var i=0; i<planLength; i++)
		meetingPositions.plan[i] = 0;

	if (planLength == 0)
		meetingPositions.plan[0] = 0;
}

function mtgObject(mtgSize, mtgPos) {
	this.size = mtgSize;
	this.active = 0;
	this.slot = Math.floor(Math.random() * planLength);

	// Fill the meeting participants randomly
	// dbg("Order");
	shuffle(order);
	this.attendees = new Array(mtgSize);
	for (i=0; i<mtgSize; i++) {
		this.attendees[i] = order.plan[i];
		employee[ order.plan[i] ].meetingHours++;
	}
	this.position = mtgPos;
}

function genMeetings() {
	var hourCount;
	var mtbuf = "Meeting distribution: ";
	var meetingSize;
	var meetingPosition;

	// Create meetings of random number of participants as long as it doesnt exceed total meeting time
	for (hourCount = 0; hourCount < totalMeetingHours ; ) {
		meetingSize = Math.round(Math.random()*(maxUsers-2)) + 2;

		// cover edge cases. Last meeting too big? cap it.
		if ( (hourCount + meetingSize) > totalMeetingHours)
			meetingSize = totalMeetingHours - hourCount;
		// another edge. Last meeting 1 hour? add it to previous
		// if it is already max, remove one to make room for another meeting of 2
		else if ( (hourCount + meetingSize + 1) == totalMeetingHours)
			meetingSize = (meetingSize == maxUsers) ? meetingSize - 1 : meetingSize + 1;

		hourCount += meetingSize;

		meetingPosition = Math.floor(Math.random()*planLength);

		meeting[totalMeetings]  = new mtgObject(meetingSize, meetingPosition);

		meetingPositions.plan[meetingPosition] += 1;
		
		totalMeetings++;
	}

	//dbg("planLength = " + planLength);
	//dbg("Meeting Positions");
	//shuffle(meetingPositions);
	
}

function initNewPlan() {
	newPlanLength = 0;

	for (var i=0; i<maxUsers; i++) {
		employee[i].newPlan = new Array();
		employee[i].newcursor = 0;
		employee[i].cursor = 0;
	}
}

function syncAttendees(mtg) {
	// Normalize new cursors for all participants.
	var i, iemp, maxnewcursor = 0;

	for (i=0; i<meeting[mtg].attendees.length; i++) {
		iemp = meeting[mtg].attendees[i];
		if (employee[ iemp ].newcursor > maxnewcursor)
			  maxnewcursor = employee[iemp].newcursor;
	}

	for (i=0; i<meeting[mtg].attendees.length; i++) {
		iemp = meeting[mtg].attendees[i];
		while (employee[iemp].newcursor < maxnewcursor)
			employee[iemp].newPlan[ employee[iemp].newcursor++ ] = "B";
	}
	// dbg("maxnewcursor " + maxnewcursor);
}

function capWaitTimes(imtg, wtime) {
	if (maxWaitUsed) {
		var wmax = 0;

		for (var i=0; i<meeting[imtg].attendees.length; i++) {
			var iemp = meeting[imtg].attendees[i];
			if (wmax < employee[iemp].maxwait) 
				wmax = employee[iemp].maxwait;
		}

		return (wtime < wmax) ? wtime : wmax;
	}
	return wtime;
}

function placeMeeting(imtg) {
	var i, iemp, waittime;

	// Figure out wait time
	var totalwaittime = 1;

	syncAttendees(imtg); 

	// Total wait time depends on PRODUCT of individual availability
	for (i=0; i<meeting[imtg].attendees.length; i++) {
		iemp = meeting[imtg].attendees[i];
		var personalwaittime = Math.round(1.0 / employee[iemp].effortfraction);
		totalwaittime = totalwaittime * personalwaittime;
	}

	totalwaittime = capWaitTimes(imtg, totalwaittime);

	// Mark wait times in new plan followed by the meeting
	for (i=0; i<meeting[imtg].attendees.length; i++) {
		iemp = meeting[imtg].attendees[i];

		for (j = 0; j < totalwaittime; j++)
			employee[iemp].newPlan [ employee[iemp].newcursor++ ] = "W";

		employee[iemp].newPlan [ employee[iemp].newcursor++ ] = "M";
	}
	// showplan("Meeting " + imtg);
}

function syncNewPlanTails() {
	var i, j;

	for (i=0; i<maxUsers; i++)
		for (j=employee[i].newcursor; j < newPlanLength; j++)
			employee[i].newPlan[j] = "D";

	// Fix DDDs before BBBs, due to equlizing "plan" for everyone, as noted in "correct below" above
	for (i=0; i<maxUsers; i++) {
		var allds = true;
		for (j=(newPlanLength-1); j>=0; j--)
			if (allds) {
				if (employee[i].newPlan[j] != "D")
					allds = false;
			}
			else if (employee[i].newPlan[j] == "D")
				employee[i].newPlan[j] = "B";
	}

	// Trim all O or D items at the end of the new plan.
	while (true) {
		var idle = true;
		for (i = 0; i < maxUsers; i++) {
			var activity = employee[i].newPlan[newPlanLength];
			if (activity != "D" && activity != "O") {
				idle = false;
				break;
			}
		}
		if (! idle)
			break;
		newPlanLength--;
		//dbg("Trim");
	}
}

function transferItem() {
	// Copy work items from old plan to new plan
	for (var i=0; i<maxUsers; i++) {
		employee[i].newPlan[employee[i].newcursor++] = 
			employee[i].plan[ employee[i].cursor++ ];
		if (newPlanLength < employee[i].newcursor)
			newPlanLength = employee[i].newcursor;
	}
}

function allMeetingPlan() {
	for (var iMeeting=0; iMeeting<totalMeetings; iMeeting++) {
		if (meeting[iMeeting].slot == 0 && meeting[iMeeting].active) {
			placeMeeting(iMeeting);
		}
	}
	for (var i=0; i<maxUsers; i++) 
		if (newPlanLength < employee[i].newcursor)
			newPlanLength = employee[i].newcursor;
	syncNewPlanTails();
}

function genNewPlan() {
	initNewPlan();

	if (planLength == 0)
		return allMeetingPlan();
	
	for (var iPlan = 0; iPlan < planLength; iPlan++) {
		for (var iMeeting=0; iMeeting<totalMeetings; iMeeting++) {
			if (meeting[iMeeting].slot == iPlan && meeting[iMeeting].active) {
				placeMeeting(iMeeting);
			}
		}

		transferItem();
	}

	syncNewPlanTails();
	
	//showplan("");
}

function recalc(node) {
	// locals
	var i, j, k, r;

	if (! validateInput() )
		return;

	initGlobals();
	initEmployees();
	genPlan();

	initMeetingPositions();
	genMeetings();

	newPlanLength = planLength;

	genNewPlan();
	newPlanTime[0] = newPlanLength;

	for (var round = 0; round < totalMeetings; round++) {
		activateMeeting();
		genNewPlan();
		newPlanTime[round + 1] = newPlanLength;
	}

	showAnswer();

	// showplan("");
	
	// showdebug();
}

function legend() {
	var buf = ": ";

	buf += "<img width=4 height=12 src=/img/t.png> : this project, ";
	buf += "<img width=4 height=12 src=/img/o.png> : other project, ";
	buf += "<img width=4 height=12 src=/img/m.png> : meeting, ";
	buf += "<img width=4 height=12 src=/img/b.png> : waiting<br>";
	
	return buf;
}

function writePlanGraph() {
	var buf = "<table><thead><tr><td colspan=4>2. Schedule</thead><tr class=rhead><td>Name<td>Project<br>Hours<td>Meeting<br>Hours<td>Plan";

	buf += legend();

	for (var i=0; i<employee.length; i++) {
		buf += "\n<tr><td>" + empNames[i] + 
			"<td class=num>" + employee[i].hours + 
			"<td class=num>" + employee[i].meetingHours +
			"<td nowrap>";

		iconcache(null);
		for (j = 0; j < employee[i].newPlan.length; j++) {
		       buf += iconcache(employee[i].newPlan[j]);
		}
		buf += iconcache(null);
	}

	buf += '\n<tr><td><b>Total</b><td class="num total">' + totalNonMeetingHours + '<td class="num total">' + totalMeetingHours + '<td class="total">' + newPlanLength + " hours";

	buf += "\n</table>";

	return buf;
}

function writeInputs() {
	var buf = "<table><thead><td colspan=2>1. Input</thead><tr class=rhead><td>Input<td> description";

	buf += "<tr><td class=num2>" + maxUsers + "<td>people, each doing";
	buf += "<tr><td class=num2>" + parallelProjects + "<td>project(s) in parallel, spending";
	buf += "<tr><td class=num2>" + 40 + "<td>hours on this project with";
	buf += "<tr><td class=num2>" + meetingPercentage * 100 + "<td>% of that time in meetings";

	buf += "</table>";

	return buf;
}


function writeFooter() {
	var buf = '<div id=footer><img id="flogo" src=img/logo_small.png ><div id="privacy"> &copy;2008 navaraga corporation<br> <a href=/about/privacy.html>privacy</a></div><div id=fend></div></div>';

	return buf;
}

function writeToWindow() {
	var newContent = "<h1>Simulation Chart</h1>";
	newContent += writeInputs();
	newContent += writePlanGraph();
	newContent += "<div style='clear:both'></div>";
	newContent += writeMeetings();
	newContent += writeSavings();
	newContent += "<div style='clear:both'></div>";

	return newContent;
}

var opacity = 0;

function blend(id, delta) {
	if ((delta > 0  && opacity < 1) || (delta < 0 && opacity >0) ) {
		var ele = document.getElementById(id);
		opacity += delta;
		ele.style.opacity = opacity;
		ele.style.filter = "alpha(opacity=" + opacity*100 + ")";
		setTimeout("blend('" + id + "', " + delta + ")", 5);
	}
	else if (delta < 0)
		document.getElementById("subwin").style.display = "none";
}

function hideSubwin() {
	setTimeout("blend('subwin', -0.1)", 5);
}


function showDetails() {
	document.getElementById("subwindata").innerHTML = writeToWindow();
	document.getElementById("subwin").style.display = "block";
	document.getElementById("subwin").onclick = hideSubwin;
	setTimeout("blend('subwin', 0.1)", 5);
}

