function InkblotConfig()
{
	this.blotImageBase = "http://static.inkblotpassword.com/BlotImages/";
	return this;
}
inkblotConfig = new InkblotConfig();

function SendAsyncRequest(url)
{
	scriptTag = document.createElement("script");
	scriptTag.src = url;
	scriptTag.type = "text/javascript";
	document.body.appendChild(scriptTag);
}

function Panel(containerDiv)
{
	this.div = document.createElement("div");
	containerDiv.appendChild(this.div);
	this.div.style.position = "absolute";

	this.img = document.createElement("img");
	this.div.appendChild(this.img);
	this.img.style.position = "absolute";
	this.img.style.top = "0px";
	this.img.style.left = "0px";

	this.labelDiv = document.createElement("div");
	this.labelDiv.style.position = "absolute";
	this.labelDiv.style.background = "#ffc0e0";
	this.div.appendChild(this.labelDiv);

	this.SetLabel = function SetLabel(s)
	{
		if (false)
		{
			this.labelDiv.innerHTML = s;
			this.labelDiv.style.display = s=="" ? "none" : "block";
		}
		else
		{
			this.labelDiv.innerHTML = "";
			this.labelDiv.style.display = "none";
		}
	}

	this.Update = function Update(left, top, width, height, z, opacity)
	{
		this.div.style.width = width+"px";
		this.div.style.height = height+"px";
		this.div.style.left = left+"px";
		this.div.style.top = top+"px";
		this.div.style.background = "white";
		this.div.style.border = "1px solid gray";
		this.img.style.width = width+"px";
		this.img.style.height = height+"px";
		this.div.style.opacity = opacity;
		this.z = z;
	}

	this.SetLabel("");
	return this;
}

function FlyingBlots(containerDiv, passwordElement)
{
	this.nBlotSpaces = BlotConfig.maxBlotLen+6;
	this.nBlots = BlotConfig.maxBlotLen;	// Number of available blots; the max the user can select.
	this.minBlots = BlotConfig.minBlotLen;
	this.blotCount = this.nBlots;
	this.minTheta = 0;
	this.maxTheta = 2*3.1415926;
	this.maxOpacity = 1.0;
	this.minOpacity = 0.25;
	this.blotPosition = 0;
	this.panels = new Array();
	this.zPhase = -3.141592*0.05;
	this.username = "";
	this.whichBlotList = "existing";
	this.blotIndices = new Array();
	this.letterCount = 0;
	this.passwordElement = null;
	this.permuted = false;

	this.Setup = function Setup(containerDiv, passwordElement)
	{
		this.containerDiv = containerDiv;
		this.passwordElement = passwordElement;
		this.containerDiv.style.position = "relative";
		this.containerDiv.style.top = "0px";
		this.containerDiv.style.left = "0px";
		this.containerDiv = containerDiv;
		for (var k=0; k<this.nBlots; k+=1)
		{
			this.panels[k] = new Panel(this.containerDiv);
			this.panels[k].k = k;
		}
		this.permutation = this._IdentityPermutation();
		this.AdjustSize(true);
		this.ClearBlotImages();
	}

	this.SetUser = function SetUser(username)
	{
		if (username != this.username)
		{
			this.username = username;
			if (this.username=="")
			{
				this.ClearBlotImages();
			}
			else
			{
				this.RequestBlotIndices();
			}
		}
	}

	this.RequestBlotIndices = function RequestBlotIndices()
	{
		SendAsyncRequest("/blotList?userId="+this.username+"&whichBlotList="+this.whichBlotList);
	}

	// User is changing password; display the proposed new blot set,
	// not the active blots.
	this.UseProposedBlotList = function UseProposedBlotList(whichBlotList, permuted)
	{
		this.whichBlotList = whichBlotList;
		this.permuted = permuted;
	}

	this._IdentityPermutation = function _IdentityPermutation()
	{
		var permutation = new Array();
		for (var i=0; i<this.nBlots; i++)
		{
			permutation[i] = i;
		}
		return permutation;
	}

	this._RandomPermutation = function _RandomPermutation()
	{
		// Note that we only shuffle the first blotCount blots,
		// so that the permuted list contains the same blots as the
		// truncated original.
		var deck = this._IdentityPermutation().slice(0, this.blotCount);
		var origDeck = new Array(deck);
		var permutation = new Array();
		for (var i=0; i<this.blotCount; i++)
		{
			for (var pathologicalLoopLimit=0; pathologicalLoopLimit<100; pathologicalLoopLimit++)
			{
				deckIndex = Math.round(Math.random()*deck.length);
				if (deckIndex==deck.length)
				{
					// if I understand Math.random() correctly, this is a
					// vanishingly unlikely event.
					deckIndex = deck.length - 1;
				}
				// First three blots should not line up with original
				// list; if they do, loop back up and try a different
				// random number.
				if (i>=3 || deck[deckIndex]!=i)
				{
					break;
				}
				//msg("looping with deck["+deckIndex+"]="+deck[deckIndex]+" vs "+i);
			}
			permutation[i] = deck[deckIndex];
			//msg("deck = "+deck+"; removing deckIndex "+deckIndex);
			deck = deck.slice(0,deckIndex).concat(deck.slice(deckIndex+1));
		}
		return permutation;
	}

	this._InvertPermutation = function _InvertPermutation(p)
	{
		var inverse = new Array();
		for (var i=0; i<this.nBlots; i++)
		{
			inverse[p[i]] = i;
		}
		//msg("inverse("+p+") = "+inverse);
		return inverse;
	}

	this.ReplyBlotIndices = function ReplyBlotIndices(blotIndices)
	{
		//msg("reply received: "+blotIndices);
		for (var i=0; i<this.nBlots; i++)
		{
			if (i<blotIndices.length)
			{
				this.blotIndices[i] = blotIndices[i];
			}
			else
			{
				this.blotIndices[i] = -1;
			}
		}
		this.ReloadBlots();
		this.InitialAnimate();
	}

	this.ReloadBlots = function ReloadBlots()
	{
		// Reset permutation as configured
		if (this.permuted)
		{
			this.permutation = this._RandomPermutation();
		}
		else
		{
			this.permutation = this._IdentityPermutation();
		}

		this.UpdateBlotImages();
		this.PasswordKeyUp();
	}

	this.ClearBlotImages = function ClearBlotImages()
	{
		for (var i=0; i<this.nBlots; i++)
		{
			this.blotIndices[i] = -1;
		}
		this.UpdateBlotImages();
		this.UpdatePasswordPosition();
	}

	this.UpdateBlotImages = function UpdateBlotImages()
	{
		for (var k=0; k<this.nBlots; k+=1)
		{
			if (k<this.blotCount)
			{
				var blotIndex = this.blotIndices[this.permutation[k]];
				if (blotIndex==-1)
				{
					src = "/static/blankBlot.png";
				}
				else
				{
					imageName = "blot"+LeftPaddedString(blotIndex, 4, "0")+".png";
					src = inkblotConfig.blotImageBase+imageName;
				}
				//msg(""+k+" => "+src);
				this.panels[k].img.src = src;
				this.panels[k].div.style.visibility = "visible";
			}
			else
			{
				this.panels[k].div.style.visibility = "hidden";
			}
		}
	}

	this.SetPasswordPosition = function SetPasswordPosition(letterCount)
	{
		this.letterCount = letterCount;
		this.UpdatePasswordPosition();
	}
	
	this.UpdatePasswordPosition = function UpdatePasswordPosition()
	{
		if (this.username == "")
		{
			this.blotAnimator.FlyTo(-4);
		}
		else
		{
			this.blotAnimator.FlyTo(parseInt(this.letterCount/2));
		}
	}

	this.InitialAnimate = function InitialAnimate()
	{
		this.blotPosition = -4;
		this.UpdatePasswordPosition();
	}

	this.AdjustSize = function AdjustSize(big)
	{
		if (big)
		{
			this.maxWidth = 256;
			this.minWidth = 64;
		}
		else
		{
			this.maxWidth = 128;
			this.minWidth = 32;
		}
		this.Update(this.blotPosition);
	}

	this.Update = function Update(bp)
	{
		this.blotPosition = bp;
		// Where does the extra 20 come from?
		var containerWidth = parseInt(this.containerDiv.offsetWidth)-20;
		var displayXCtr = containerWidth/2;
		var xRadius = displayXCtr - this.maxWidth/2;
		var containerHeight = parseInt(this.containerDiv.offsetHeight)-20;
		var yRadius = (containerHeight - this.maxWidth)/2;
		//msg("xctr " + displayXCtr + "radius "+radius)
		var zArray = new Array();
		for (var k=0; k<this.nBlots; k+=1)
		{
			var p = (k-this.blotPosition)/this.nBlotSpaces;
			var theta = (this.maxTheta-this.minTheta)*p+this.minTheta;
			var xctr = xRadius*Math.sin(theta);
			var yp = (Math.cos(theta)+1)/2;
			var size = (this.maxWidth-this.minWidth)*yp+this.minWidth;
			var opacity = (this.maxOpacity-this.minOpacity)*yp+this.minOpacity;
			var yctr = (Math.cos(theta)+1)*yRadius;
			var z = Math.cos(theta+this.zPhase);
			//msg("z"+z);
			xctr = xctr;//xctr *= 1.0/(0.5*(z+1)+1)
			this.panels[k].Update(displayXCtr+(xctr-size/2), yctr, size, size, z, opacity);
			//msg("yctr "+yctr+" size "+size+"; this.mw"+this.maxWidth+"; yradius "+yRadius+"; cdoh = "+parseInt(this.containerDiv.offsetHeight));
			zArray.push(this.panels[k]);
		}
		//msg(dump(this.containerDiv));
		zArray.sort(function (a,b) {
			return a.z - b.z;
			});
		for (var k=0; k<this.nBlots; k+=1)
		{
			//msg("ok "+zArray[k].k+" z "+zArray[k].z+" sz "+zArray[k].div.style.width);
			zArray[k].div.style.zIndex = k;
		}
	}

	this.PasswordKeyUp = function PasswordKeyUp()
	{
		this.SetPasswordPosition(this.passwordElement.value.length);

		// Update both password states
		if (this.whichBlotList!="existing")
		{
			permutedBlots.DisplayReadyCode();
			proposedBlots.DisplayReadyCode();
			this.EnableChangePasswordButton();
		}
		else
		{
			this.DisplayReadyCode();
		}

		this.DebugDisplayMnemonics();
	}

	this.EnableChangePasswordButton = function EnableChangePasswordButton()
	{
		// (Only valid if this is a password-change screen)
		var enabled = this.PermutedEntryAgreesCompletely();
		document.getElementById("ChangePasswordSubmitButton").disabled = !enabled;
		//msg("Enabled = "+enabled);
	}

	this.PermutedEntryDisagrees = function PermutedEntryDisagrees()
	{
		for (var permutedCharIndex=0; permutedCharIndex<this.passwordElement.value.length; permutedCharIndex++)
		{
			var permutedBlotNum = parseInt(permutedCharIndex/2);
			var originalBlotNum = permutedBlots.permutation[permutedBlotNum];
			var originalCharIndex = originalBlotNum*2 + (permutedCharIndex&1);
			if (originalCharIndex >= proposedBlots.passwordElement.value.length)
			{
				continue;
			}
			var origChar = proposedBlots.passwordElement.value[originalCharIndex];
			var permutedChar = this.passwordElement.value[permutedCharIndex];
			if (origChar != permutedChar)
			{
//				msg("orig["+originalCharIndex+"]=="+origChar+" but "+
//					"permuted["+permutedCharIndex+"]=="+permutedChar);
				return true;
			}
		}
		return false;
	}

	this.PermutedEntryAgreesCompletely = function PermutedEntryAgreesCompletely()
	{
		return proposedBlots.passwordElement.value.length==this.blotCount*2
			&& permutedBlots.passwordElement.value.length==this.blotCount*2
			&& !permutedBlots.PermutedEntryDisagrees();
	}

	this.DisplayReadyCode = function DisplayReadyCode()
	{
		if (this.whichBlotList == "permuted" && this.PermutedEntryDisagrees())
		{
			this.passwordElement.style.background = "#ffb0b0";
		}
		else if (this.passwordElement.value.length != 2*this.blotCount)
		{
			//msg(this.whichBlotList+" length "+this.passwordElement.value.length);
			this.passwordElement.style.background = "yellow";
		}
		else if ((this.whichBlotList == "proposed"
					|| this.whichBlotList == "permuted")
			&& this.PermutedEntryAgreesCompletely())
		{
			this.passwordElement.style.background = "#b0ffb0";
		}
		else
		{
			this.passwordElement.style.background = "white";
		}
	}

	this.DebugDisplayMnemonics = function DebugDisplayMnemonics()
	{
		if (this.whichBlotList == "proposed")
		{
			invertedPermutation = this._InvertPermutation(permutedBlots.permutation);
			for (var k=0; k<this.blotCount; k+=1)
			{
				var labelText = this.passwordElement.value.slice(k*2, k*2+2);
				this.panels[k].SetLabel(labelText);

				// (test because we can get called before permutation
				// is as long as blotCount.)
				if (k < invertedPermutation.length)
				{
					permutedBlots.panels[invertedPermutation[k]].SetLabel(labelText);
				}
			}
		}
	}

	this.GetBlotCount = function GetBlotCount()
	{
		return this.blotCount;
	}

	this.SetBlotCount = function SetBlotCount(newCount)
	{
		if (newCount>this.nBlots)
		{
			newCount = this.nBlots;
		}
		if (newCount<this.minBlots)
		{
			newCount = this.minBlots;
		}

		if (this.blotCount==newCount)
		{
			// No change -- don't shuffle blots.
			return;
		}
		else
		{
			this.blotCount = newCount;
			this.passwordElement.maxLength = this.blotCount * 2;
			// clear password, since we reorder at least one field.
			this.passwordElement.value = ""
			this.ReloadBlots();
		}
	}

	this.blotAnimator = new BlotAnimator(this);
	this.Setup(containerDiv, passwordElement);

	return this;
}

function BlotAnimator(flyingBlots)
{
	this.flyingBlots = flyingBlots;
	this.animationRunning = false;

	this.FlyTo = function FlyTo(blotPosition)
	{
		this.startTimeMS = new Date().getTime();
		this.endTimeMS = this.startTimeMS + 250;
		this.startPosition = this.flyingBlots.blotPosition;
		this.endPosition = blotPosition;
		this.AnimateCore(true);
		//msg("FlyTo("+this.startPosition+","+this.endPosition+")");
	}

	this.AnimateNow = function AnimateNow()
	{
		this.AnimateCore(false);
	}

	this.AnimateCore = function AnimateCore(initiating)
	{
		var time = new Date().getTime();
		var scheduleNext = false;
		if (time > this.endTimeMS)
		{
			this.flyingBlots.Update(this.endPosition);
			// no more animation required
			scheduleNext = false;
		}
		else
		{
			var p = (time-this.startTimeMS)/(this.endTimeMS-this.startTimeMS);
			var pos = p*(this.endPosition-this.startPosition)+this.startPosition;
			this.flyingBlots.Update(pos);
			scheduleNext = true;
		}
		//msg("time to go "+(this.endTimeMS-time)+"; scheduleNext = "+scheduleNext);

		if (!scheduleNext)
		{
			//msg("stopped, animation complete");
			this.animationRunning = false;
		}
		else if (!initiating)
		{
			// this.animationRunning should be true, since someone scheduled
			// us for us to get here; but we'll just make sure.
			//msg("Continuing");
			this.animationRunning = true;
			setTimeout(MakeClosure(this, this.AnimateNow), 50);
		}
		else if (!this.animationRunning)
		{
			//msg("Starting");
			this.animationRunning = true;
			setTimeout(MakeClosure(this, this.AnimateNow), 50);
		}
	}
}

function MakeClosure(object, method)
{
	var staticArgs = new Array();
	for (var i=2; i<arguments.length; i++)
	{
		staticArgs.push(arguments[i]);
	}
	var closureFunc = function() {
		var finalArgs = staticArgs.concat(arguments);
		return method.apply(object, finalArgs);
	}
	return closureFunc;
}

function LeftPaddedString(string, length, paddingChar)
{
	string = ""+string;
	//msg("string"+string+" length"+length);
	//msg("stringlen = "+string.length);
	var padAmount = length - string.length+1;
	var padding = new Array(padAmount > 0 ? padAmount : 0).join(paddingChar);
	return padding+string;
}

function ShowHelp(enable)
{
	var div = document.getElementById("HelpGuts");
	div.style.display = enable ? "block" : "none";
}
