/******************************************************************************
 * pcrsetup.js
 * Copyright 2005, 2008 Attotron Biotechnologies Corporation
 * Distributed under the terms of the GNU General Public License,
 * 	http://www.gnu.org/copyleft/gpl.html
 * Author: Robert M. Horton <rmhorton@attotron.com>
 * version 8, 9/17/2008: 
 *		* template concentrations in pM
 *		* add 'comments' attribute to pcr element
 * version 7, 5/8/2008: 
 *		* template concentrations in fM, primer concentrations entered in uM
 *		* concentrations are converted to M in function buildModel().
 ******************************************************************************/

/******************************************************************************
 * To Do
 * 	 Is DEFAULT_TEMPLATE_CONC still used for template@concentration?
 ******************************************************************************/

var releaseNotesWindow;
function showReleaseNotes(){
	var programName = "PCR Setup";
	var version = '8.0';
	
	var html = '<html><head><title>' + programName;
	html += ' version ' + version + ' Release Notes<\/title><\/head>';
	html += '<body bgcolor="#FFFF55"><h1 align="center">' + programName + '<\/h1>';
	html += '<h2 align="center">Version ' + version + '<\/h2>';
	
	html += 'Both primer concentration and template concentration affect yields in this version (they did not really work before). Note that template concentrations are in picomoles per liter, and primer concentrations are in micromoles per liter.';
	html += '<p>';
	html += 'Taq polymerase is now sensitive to heat, and will slowly be inactivated at temperatures nearing 100 degrees, with a <a href="http://www.invitrogen.com/site/us/en/home/Products-and-Services/Applications/Nucleic-Acid-Amplification-and-Expression-Profiling/PCR/PCR-Misc/Taq-DNA-Polymerase.html">temperature-dependent halflife</a>.';
	html += '<p>';
	html += 'Template denaturation is concentration-dependent; products melt at lower temperature in lower concetrations. In some cases, this may affect product yields in our model. However, in most circumstances, yields in our model are limited by primer concentration or polymerase acivity. This may not be realistic, as the inhibitory effects of double-stranded DNA on DNA polymerases may be more important (see SantaLucia J. Physical Principles and Visual-OMP Software for Optimal PCR Design. Methods in Molecular Biology, vol. 402: PCR Primer Design. Ed: A. Yuryev, Humana Press, Totowa, NJ)';
	html += '<p>';
	html += 'Band intensities on the gel increase linearly with DNA quantity up to a certain point; once the maximum intensity is reached, the bands get thicker with more DNA. This increase in thickness is linear up to a point, above which it is non-linear. The intensity of the marker bands is near the upper limit of the linear range. TIFF images should thus be usable for relative quantitation of bands less intense than those in the marker.';
	html += '<p>';
	html += 'If you plan to use this simulator in a class, please <a href="mailto:rmhorton@attotron.com?subject=pcr_class">let us know</a> when you will need it, so we can coordinate upgrades and maintenance with your schedule. Please send questions, comments, bug reports, or suggestions to <a href="mailto:rmhorton@attotron.com?subject=pcrsetup">rmhorton@attotron.com</a>';
	html += '<hr />';
	html += '<h3>Recent updates</h3>';
	html += '<ul>';
	html += '<li>8 Oct 08: Better marker size input validation';
	html += '</ul>';
	html += '<\/body><\/html>';

	var height = 600;
	var width = 640;
	var Xoffset = window.screenX + window.outerWidth/2 - width/2 ;
	var Yoffset = window.screenY + window.outerHeight/2 - height/2;
	var attr = 'HEIGHT=' + height + ',WIDTH=' + width;
	attr += ',SCROLLBARS,RESIZABLE,screenX=' + Xoffset + ',screenY=' + Yoffset;
	
	if (!releaseNotesWindow || releaseNotesWindow.closed) {
		releaseNotesWindow = window.open("","release_notes_window",attr);
	}
	releaseNotesWindow.document.open();
	releaseNotesWindow.document.write(html);
	releaseNotesWindow.document.close();
	releaseNotesWindow.focus();
}

// for older, brain-dead IE versions ignorant of stack operations
Array.prototype.push = function (item){ this[this.length] = item; }

// 'Constants': These are used in initialize(), so new values can be specified in pcrsetup.cgi
var MAX_PRODUCT_SIZE = 10000;	// primer pairs further apart than this are ignored
var MARKER_MIN = 100;
var MARKER_MAX = 2000;
var MARKER_STEP = 100;
var MARKER_CONC = 1.0;	// will be multiplied by last term in 'bands' attribute of 'wt_marker' XML element; see Marker object
var DEFAULT_TEMPLATE_CONC = '0.5';	// string prevents scientific notation

/*** Metric prefixes ***
	1.0E+24	yotta-	Y
	1.0E+21	zetta-	Z
	1.0E+18	exa-	E
	1.0E+15	peta-	P
	1.0E+12	tera-	T
	1.0E+9	giga-	G
	1.0E+6	mega-	M
	1.0E+3	kilo-	k
	1.0E+2	hecto-	h
	1.0E+1	deca-	da
	1.0E-1	deci-	d
	1.0E-2	centi-	c
	1.0E-3	milli-	m
	1.0E-6	micro-	µ
	1.0E-9	nano-	n
	1.0E-12	pico-	p
	1.0E-15	femto-	f
	1.0E-18	atto- 	a 
	1.0E-21	zepto-	z
	1.0E-24	yocto-	y
*/

// These units are used in buildModel(). They must agree with the units on the UI (pcrsetup.cgi)
var PRIMER_CONC_UNITS = 1.0E-6;	// &micro;M: 0.1 .. 1 uM
var TEMPLATE_CONC_UNITS = 1.0E-12;	// pico: 	plasmid: 0.1 .. 10 nM; genome: 1 .. 10 fM ???

var NL = "\n";

// Global variables

var TheGel;
var UserName;
var LOG;
	
// Model Classes

function Primer(sequence, concentration){
	this.sequence = sequence;
	this.concentration = concentration;

	this.asXml = function (indent){
		var xml = indent + '<dna';
		xml += ' seq=\"' + this.sequence + '\"';
		xml += ' conc=\"' + this.concentration + '\"';
		xml += ' />' + NL;
		return xml;
	};
}

function Pcr(){
	this.template = 'NONE';
	this.annealTemp = 99999;
	this.denatureTemp = 99999;
	this.numCycles = 99999;

	this.polymeraseSurvivalPerCycle = function(denatSeconds, denatCelsius){
		// half life is a function of temperature
		// half-life vs. temperature data from http://www.invitrogen.com/site/us/en/home/Products-and-Services/Applications/Nucleic-Acid-Amplification-and-Expression-Profiling/PCR/PCR-Misc/Taq-DNA-Polymerase.html
		// fitted at zunzun.com
		
		var a = 3.2144030683560305E+02;
		var b = -4.7912019414064705E-01;
		var c = 4.3337831170786885E+01;
		var hl = 60 * a * Math.exp(b * denatCelsius + c);	// half-life in seconds
		var fractionSurviving = Math.exp(-1 * denatSeconds * Math.log(2) / hl);
		return fractionSurviving.toFixed(4);
	}
	
	this.assembleComments = function(){
		// polymeraseActivity=100.0 nonProcessivityPenalty=0.00005 polymeraseSurvivalPerCycle=0.98
		var comments = new Array();
		// comments.push('polymeraseActivity=100.0');	// should be a function of polymerase quantity; not user specified in this UI
		// comments.push('nonProcessivityPenalty=0.00005');	// function of polymerase
		comments.push('polymeraseSurvivalPerCycle=' + this.polymeraseSurvivalPerCycle(30, this.denatureTemp));
		// survival per cycle = 0.9847 at 96 degrees for 30 seconds
		return comments.join(' ');
	}
	
	this.primers=new Array();

	this.setAttributes = function(template, template_conc, anneal_temp, denature_temp, num_cycles){
		this.template = template;
		this.templateConc = template_conc;
		this.annealTemp = anneal_temp;
		this.denatureTemp = denature_temp;
		this.numCycles = num_cycles;
	}
	
	this.addPrimer = function(sequence, concentration){
		var np = new Primer(sequence, concentration);
		this.primers.push(np);
	}

	this.asXml = function (indent, rxnNum){
		var xml = indent + '<pcr';
		xml += ' label="' + rxnNum + '"';	// 3a; should be lane number
		xml += ' denatureTemp="' + this.denatureTemp + '"';
		xml += ' annealTemp="' + this.annealTemp + '"';
		xml += ' numCycles="' + this.numCycles + '"';
		xml += ' maxProdSize="' + MAX_PRODUCT_SIZE + '"';
		xml += ' comments="' + this.assembleComments() + '"';
		xml += ' >' + NL
		for (var i=0; i<this.primers.length; i++){
			xml += this.primers[i].asXml(indent + "\t");
		}
		// version 3a: Template component concentrations given in ug/ml.
		// User specifies total template concentration in ug/ml.
		// Component concentraions calculated as fractions of total.
		var nameConcList = this.template.split(",");
		var nameConcPairList = new Array();
		var totalTemplateComponentConc = 0;
		for (var i=0; i< nameConcList.length;i++){
			var aNameConc = nameConcList[i];
			var nameConcPair = aNameConc.split('@');	// formerly ':'
			if (nameConcPair.length < 2) nameConcPair[1] = DEFAULT_TEMPLATE_CONC;
			nameConcPairList[i] = nameConcPair;
			totalTemplateComponentConc += parseFloat(nameConcPair[1]);
		}
		for (var i=0; i< nameConcPairList.length;i++){
			var nameConcPair = nameConcPairList[i];
			var templateName = nameConcPair[0];
			var templateConc = this.templateConc * nameConcPair[1]/totalTemplateComponentConc;
			xml += indent + '\t<dna seq="' + templateName;	// formerly added 'org:'
			xml += '" conc="' + templateConc + '" />' + NL;
		}
		xml += indent + '<\/pcr>' + NL;
		return xml;
	}
}

function Marker(with_min_band_size, with_max_band_size, with_band_size_inc){
	this.minBandSize=with_min_band_size;
	this.maxBandSize=with_max_band_size;
	this.bandSizeInc=with_band_size_inc;
	
	this.asXml = function(indent){
		var xml = indent + '<wt_marker label="M" conc="' + MARKER_CONC + '" bands="linear:';
		xml += this.minBandSize + ':' + this.maxBandSize + ':' + this.bandSizeInc + ':1.0" />' + NL; // last number -> band intensity
		return xml;
	}
}

function Gel(){
	this.marker = new Marker(MARKER_MIN, MARKER_MAX, MARKER_STEP);	// overwritten in initialize()

	this.lanes = new Array();
	this.outFormat = 'svg';
	this.id = '@invokeCnt@';
	this.debugLvl='off';

	this.addLane = function (number,with_pcr){
		this.lanes[number] = with_pcr;
	}
	
	this.asXml = function(indent){
		if (null == indent ) indent = "\t";
		var xml = '<script userId="' + UserName + '"';
		xml += ' debugLvl="' + this.debugLvl + '"';
		xml += ' outBasepath="@scriptOutBasepath@"';
		xml += '>' + NL;
		xml += indent + '<gel label="PCRs"';
		xml += ' outFormat="' + this.outFormat + '"';
		xml += ' xslParms="minBandSize=' + 0.9 * this.marker.minBandSize;
		xml += ',maxBandSize=' + 1.1 * this.marker.maxBandSize + '"';	// v3
		xml += ' maxBandSize="' + this.marker.maxBandSize + '"';	// v3
		xml += '>' + NL;
		xml += this.marker.asXml(indent + "\t");
		for (var i=1;i<this.lanes.length;i++){	// lanes counted from 1
			xml += this.lanes[i].asXml(indent + "\t",i);
		}
		xml += indent + "<\/gel>" + NL;
		xml += "<\/script>" + NL;
		return xml + NL;
	}
}

// Model building functions

function buildModel(){
	LOG = "LOG:" + NL;
	TheGel.outFormat = document.forms['controls'].outFormat.value;
	TheGel.debugLvl = document.forms['TextDisplay'].debugLvl.value;
	for (var i=0;i<document.forms.length;i++){
		var theForm = document.forms[i];
		LOG += theForm.name + NL;
		if (theForm.name.match(/pcrForm_(\d+)/)){
			var reactionNumber = RegExp.$1;
			LOG += "rxn #=" + reactionNumber + NL;
			var pcr = new Pcr();
			var template = theForm.template[theForm.template.selectedIndex].value;
			var template_conc = theForm.templateConc.value * TEMPLATE_CONC_UNITS;
			var anneal_temp = theForm.annealTemp.value;
			var denature_temp = theForm.denatureTemp.value;
			var num_cycles = theForm.cycles.value;
			pcr.setAttributes(template, template_conc, anneal_temp, denature_temp, num_cycles);
			var seq = '';
			var conc = 0;
			for (var j=0;j<theForm.elements.length;j++) {
				var el = theForm.elements[j];
				LOG += "element=[" + el.id + "]=" + el.value + NL;
				if (el.id.match(/primer([A-Z])_seq/)){
					if ( 'text' == el.type ){
						seq = el.value;
					} else {	// 'select-one' == el.type
						seq = el[el.selectedIndex].value;
					}
				}
				if (el.id.match(/primer([A-Z])_conc/)){
					var letter = RegExp.$1;
					conc = el.value * PRIMER_CONC_UNITS;
					if ('' != seq) {
						LOG += 'primer ' + letter + ", conc=" + conc + ", seq=" + seq + NL;
						pcr.addPrimer(seq,conc);
					}
				}
			}
			TheGel.addLane(reactionNumber,pcr);
		}
	}
	showXml();
}

function refreshMarker(markerForm){
	validateInt(markerForm.minSize,  'Minimum marker size', 10, 1000, 100);
	var minSize = markerForm.minSize.value;
	validateInt(markerForm.maxSize,  'Maximum marker size', parseInt(minSize) + 1, 10000, 1000);
	var maxSize = markerForm.maxSize.value;
	var stepGuess = parseInt(maxSize - minSize) / 10;
	validateInt(markerForm.stepSize, 'Marker step size', 1, maxSize - minSize, stepGuess);
	var stepSize = markerForm.stepSize.value;
	TheGel.marker = new Marker(minSize, maxSize, stepSize);
};

// Miscellaneous functions

function bounded_chr(num){
	if (num < 'A'.charCodeAt(0)) num = 'A'.charCodeAt(0);
	if (num > 'Z'.charCodeAt(0)) num = 'Z'.charCodeAt(0);
	return String.fromCharCode(num);
}

function bounded_ord(ch){
	ch = ch.charAt(0);
	if (ch < 'A') ch = 'A';
	if (ch > 'Z') ch = 'Z';
	return ch.charCodeAt(0);
}


// Validation functions

function validateFloat(field, name, min, max, guess){
    var value = parseFloat(field.value);
    guess = parseFloat(guess);
    if (isNaN(value)){
		alert( name + " is invalid: using " + guess);
		value = guess;
    }
    if (value < min){
		alert( name + " must be at least " + min + ": using " + min);
		value = min;
    }
    if (value > max){
		alert(name + " must be less than " + max + ": using " + max);
		value = max;
    }
    field.value = value;
    return true;
}

function validateInt(field, name, min, max, guess){
    var value = parseInt(field.value);
    guess = parseInt(guess);
    if (isNaN(value)){
		alert( name + " is invalid: using " + guess);
		value = guess;
    }
    if (value < min){
		alert( name + " must be at least " + min + ": using " + min);
		value = min;
    }
    if (value > max){
		alert(name + " must be less than " + max + ": using " + max);
		value = max;
    }
    field.value = value;
    return true;
}

function validatePrimer(field,minLength,maxLength,guess){
    var primer = field.value;
    var validPrimer = primer.toUpperCase().replace(/[^ACGT~]/g,''); // XXX tilde should only be allowed at the beginning
    if (validPrimer != primer.toUpperCase()) alert("Invalid bases removed.");
    if ( (validPrimer.length > 0) && (validPrimer.length < minLength) ) {
	    alert("If you enter a primer sequence, it must be at least "+minLength+" bases long.");
	    validPrimer = guess;
    }
    if ( validPrimer.length > maxLength ) {
	    alert("Primers can be at most "+maxLength+" bases long.");
	    validPrimer = guess;
    }
    field.value = validPrimer;
    return true;
}

function validateUserName(field,minLength,maxLength,guess){
    var userName = field.value;
    var validUserName = userName.replace(/\s+/g,'_').replace(/[^A-Za-z0-9_]/g,'');
    if (validUserName != userName) alert("Invalid bases removed.");
    if (  validUserName.length < minLength ) {
	    alert("User name must be at least "+minLength+" characters long.");
	    validUserName = guess;
    }
    if ( validUserName.length > maxLength ) {
	    alert("User name  can be at most "+maxLength+" bases long.");
	    validUserName = guess;
    }
    field.value = validUserName;
    return true;
}

// Data copying functions 

function setTextFields (field_id, field_value){
	for (var i=0;i<document.forms.length;i++){
		var frm = document.forms[i];
		if (frm.name.match(/pcrForm_(\d+)/)){
			for (var j=0;j<frm.elements.length;j++) {
				var el = frm.elements[j];
				if (el.id.match(new RegExp(field_id))){
					el.value = field_value;
				}
			}
		}
	}
}

function setPullDowns(selectElement){
	var selectedIndexes = new Array();
	for (var op =0; op<selectElement.options.length; op++){
		if (selectElement.options[op].selected)
			selectedIndexes.push(op);
	}
	for (var i=0;i<document.forms.length;i++){
		if (document.forms[i].name.match(/pcrForm_(\d+)/)){
			var rxn = RegExp.$1;
			var idx = ((rxn - 1) % selectedIndexes.length);
			document.forms[i][selectElement.id].selectedIndex = selectedIndexes[idx];
		}
	}
}

// Button Functions

function showLog(){
	document.forms["TextDisplay"].script.value = LOG;
}

function showXml(){
	document.forms["TextDisplay"].script.value = TheGel.asXml();
}

function clear_text(){
	document.forms["TextDisplay"].script.value = ' ';
}

function submitScript(){
	buildModel();
	document.forms["TextDisplay"].submit();	
}

function initialize(){
	TheGel = new Gel();
	document.forms["markerForm"].minSize.value = MARKER_MIN;
	document.forms["markerForm"].maxSize.value = MARKER_MAX;
	document.forms["markerForm"].stepSize.value = MARKER_STEP;
	refreshMarker(document.forms["markerForm"]);
	
	document.forms["defaultsForm"].template.selectedIndex = 0;
}


