morpheus.SignalToNoise = function(list1, list2) {
	var m1 = morpheus.Mean(list1);
	var m2 = morpheus.Mean(list2);
	var s1 = Math.sqrt(morpheus.Variance(list1, m1));
	var s2 = Math.sqrt(morpheus.Variance(list2, m2));
	return (m1 - m2) / (s1 + s2);
};
morpheus.SignalToNoise.toString = function() {
	return 'Signal to noise';
};

morpheus.createSignalToNoiseAdjust = function(percent) {
	percent = percent || 0.2;
	var f = function(list1, list2) {
		var m1 = morpheus.Mean(list1);
		var m2 = morpheus.Mean(list2);
		var s1 = Math.sqrt(morpheus.Variance(list1, m1));
		var s2 = Math.sqrt(morpheus.Variance(list2, m2));
		s1 = morpheus.SignalToNoise.thresholdStandardDeviation(m1, s1, percent);
		s2 = morpheus.SignalToNoise.thresholdStandardDeviation(m2, s2, percent);
		// ensure variance is at least 20% of mean
		return (m1 - m2) / (s1 + s2);
	};
	f.toString = function() {
		return 'Signal to noise (adjust standard deviation)';
	};
	return f;
};

morpheus.SignalToNoise.thresholdStandardDeviation = function(mean,
		standardDeviation, percent) {
	var returnValue = standardDeviation;
	var absMean = Math.abs(mean);
	var minStdev = percent * absMean;
	if (minStdev > standardDeviation) {
		returnValue = minStdev;
	}

	if (returnValue < percent) {
		returnValue = percent;
	}
	return returnValue;
};

morpheus.createContingencyTable = function(listOne, listTwo, groupingValue) {
	if (groupingValue == null || isNaN(groupingValue)) {
		groupingValue = 1;
	}
	var aHit = 0;
	var aMiss = 0;
	for (var j = 0, size = listOne.size(); j < size; j++) {
		var val = listOne.getValue(j);
		if (!isNaN(val)) {
			if (val >= groupingValue) {
				aHit++;
			} else {
				aMiss++;
			}
		}

	}
	var bHit = 0;
	var bMiss = 0;
	for (var j = 0, size = listTwo.size(); j < size; j++) {
		var val = listTwo.getValue(j);
		if (!isNaN(val)) {
			if (val >= groupingValue) {
				bHit++;
			} else {
				bMiss++;
			}
		}

	}
	// listOne=drawn, listTwo=not drawn
	// green=1, red=0
	var N = aHit + aMiss + bHit + bMiss;
	var K = aHit + bHit;
	var n = aHit + aMiss;
	var k = aHit;
	var a = k;
	var b = K - k;
	var c = n - k;
	var d = N + k - n - K;
	return [ a, b, c, d ];
};
morpheus.FisherExact = function(listOne, listTwo) {
	var abcd = morpheus.createContingencyTable(listOne, listTwo, 1);
	return morpheus.FisherExact.fisherTest(abcd[0], abcd[1], abcd[2], abcd[3]);
};

morpheus.createFisherExact = function(groupingValue) {
	var f = function(listOne, listTwo) {
		var abcd = morpheus.createContingencyTable(listOne, listTwo,
				groupingValue);
		return morpheus.FisherExact.fisherTest(abcd[0], abcd[1], abcd[2],
				abcd[3]);
	};
	return f;

};

/**
 * Computes the hypergeometric probability.
 */
morpheus.FisherExact.phyper = function(a, b, c, d) {
	return Math
			.exp((morpheus.FisherExact.logFactorial(a + b)
					+ morpheus.FisherExact.logFactorial(c + d)
					+ morpheus.FisherExact.logFactorial(a + c) + morpheus.FisherExact
					.logFactorial(b + d))
					- (morpheus.FisherExact.logFactorial(a)
							+ morpheus.FisherExact.logFactorial(b)
							+ morpheus.FisherExact.logFactorial(c)
							+ morpheus.FisherExact.logFactorial(d) + morpheus.FisherExact
							.logFactorial(a + b + c + d)));

};

morpheus.FisherExact.logFactorials = [ 0.00000000000000000,
		0.00000000000000000, 0.69314718055994531, 1.79175946922805500,
		3.17805383034794562, 4.78749174278204599, 6.57925121201010100,
		8.52516136106541430, 10.60460290274525023, 12.80182748008146961,
		15.10441257307551530, 17.50230784587388584, 19.98721449566188615,
		22.55216385312342289, 25.19122118273868150, 27.89927138384089157,
		30.67186010608067280, 33.50507345013688888, 36.39544520803305358,
		39.33988418719949404, 42.33561646075348503, 45.38013889847690803,
		48.47118135183522388, 51.60667556776437357, 54.78472939811231919,
		58.00360522298051994, 61.26170176100200198, 64.55753862700633106,
		67.88974313718153498, 71.25703896716800901 ];
morpheus.FisherExact.logFactorial = function(k) {
	if (k >= 30) { // stirlings approximation
		var C0 = 9.18938533204672742e-01;
		var C1 = 8.33333333333333333e-02;
		var C3 = -2.77777777777777778e-03;
		var C5 = 7.93650793650793651e-04;
		var C7 = -5.95238095238095238e-04;
		var r = 1.0 / k;
		var rr = r * r;
		return (k + 0.5) * Math.log(k) - k + C0 + r
				* (C1 + rr * (C3 + rr * (C5 + rr * C7)));
		// log k! = (k + 1/2)log(k) - k + (1/2)log(2Pi) + stirlingCorrection(k)
	}
	return morpheus.FisherExact.logFactorials[k];
};

morpheus.FisherExact.fisherTest = function(a, b, c, d) {
	// match R 2-sided fisher.test
	var p = morpheus.FisherExact.phyper(a, b, c, d);
	var sum = p;
	for (var _a = 0, n = a + b + c + d; _a <= n; _a++) {
		var _b = a + b - _a;
		var _c = a + c - _a;
		var _d = b + d - _b;
		if (_a !== a && _b >= 0 && _c >= 0 && _d >= 0) {
			var _p = morpheus.FisherExact.phyper(_a, _b, _c, _d);
			if (_p <= p) {
				sum += _p;
			}
		}
	}
	return Math.min(1, sum);
	// var lt = jStat.hypgeom.cdf(a, a + b + c + d, a + b, a + c);
	// var gt = jStat.hypgeom.cdf(b, a + b + c + d, a + b, b + d);
	// return Math.min(1, 2 * Math.min(lt, gt));
};
morpheus.FisherExact.toString = function() {
	return 'Fisher Exact Test';
};

morpheus.FoldChange = function(list1, list2) {
	var m1 = morpheus.Mean(list1);
	var m2 = morpheus.Mean(list2);
	return (m1 / m2);
};
morpheus.FoldChange.toString = function() {
	return 'Fold Change';
};
morpheus.TTest = function(list1, list2) {
	var m1 = morpheus.Mean(list1);
	var m2 = morpheus.Mean(list2);
	var s1 = Math.sqrt(morpheus.Variance(list1, m1));
	var s2 = Math.sqrt(morpheus.Variance(list2, m2));
	var n1 = morpheus.CountNonNaN(list1);
	var n2 = morpheus.CountNonNaN(list2);
	return ((m1 - m2) / Math.sqrt((s1 * s1 / n1) + (s2 * s2 / n2)));
};
morpheus.TTest.toString = function() {
	return 'T-Test';
};
morpheus.Spearman = function(list1, list2) {
	var flist1 = [];
	var flist2 = [];
	for (var i = 0, n = list1.size(); i < n; i++) {
		var v1 = list1.getValue(i);
		var v2 = list2.getValue(i);
		if (isNaN(v1) || isNaN(v2)) {
			continue;
		}
		flist1.push(v1);
		flist2.push(v2);
	}
	var rank1 = morpheus.Ranking(flist1);
	var rank2 = morpheus.Ranking(flist2);
	return morpheus.Pearson(new morpheus.Vector('', rank1.length)
			.setArray(rank1), new morpheus.Vector('', rank2.length)
			.setArray(rank2));
};
morpheus.Spearman.toString = function() {
	return 'Spearman rank correlation';
};
morpheus.WeightedMean = function(weights, values) {
	var numerator = 0;
	var denom = 0;
	for (var i = 0, size = values.size(); i < size; i++) {
		var value = values.getValue(i);
		if (!isNaN(value)) {
			var weight = weights.getValue(i);
			if (!isNaN(weight)) {
				numerator += (weight * value);
				denom += weight;
			}
		}
	}
	return denom == 0 ? NaN : numerator / denom;
};
morpheus.WeightedMean.toString = function() {
	return 'Weighted average';
};

morpheus.createOneMinusMatrixValues = function(dataset) {
	var f = function(listOne, listTwo) {
		return 1 - dataset.getValue(listOne.getIndex(), listTwo.getIndex());
	};
	f.toString = function() {
		return 'One minus matrix values (for a precomputed similarity matrix)';
	};
	return f;
};

morpheus.Pearson = function(listOne, listTwo) {
	var sumx = 0;
	var sumxx = 0;
	var sumy = 0;
	var sumyy = 0;
	var sumxy = 0;
	var N = 0;
	for (var i = 0, size = listOne.size(); i < size; i++) {
		var x = listOne.getValue(i);
		var y = listTwo.getValue(i);
		if (isNaN(x) || isNaN(y)) {
			continue;
		}
		sumx += x;
		sumxx += x * x;
		sumy += y;
		sumyy += y * y;
		sumxy += x * y;
		N++;
	}
	var numr = sumxy - (sumx * sumy / N);
	var denr = Math.sqrt((sumxx - (sumx * sumx / N))
			* (sumyy - (sumy * sumy / N)));
	return denr == 0 ? 1 : numr / denr;
};
morpheus.Pearson.toString = function() {
	return 'Pearson correlation';
};

morpheus.Jaccard = function(listOne, listTwo) {

	var orCount = 0;
	var andCount = 0;
	for (var i = 0, size = listOne.size(); i < size; i++) {
		var xval = listOne.getValue(i);
		var yval = listTwo.getValue(i);
		if (isNaN(xval) || isNaN(yval)) {
			continue;
		}
		var x = xval > 0;
		var y = yval > 0;
		if (x && y) {
			andCount++;
		} else if (x || y) {
			orCount++;
		}
	}
	if (orCount === 0) {
		return 1;
	}
	return 1 - (andCount / orCount);
};

morpheus.Jaccard.toString = function() {
	return 'Jaccard distance';
};

morpheus.Cosine = function(listOne, listTwo) {
	var sumX2 = 0;
	var sumY2 = 0;
	var sumXY = 0;
	for (var i = 0, size = listOne.size(); i < size; i++) {
		var x = listOne.getValue(i);
		var y = listTwo.getValue(i);
		if (isNaN(x) || isNaN(y)) {
			continue;
		}
		sumX2 += x * x;
		sumY2 += y * y;
		sumXY += x * y;
	}
	return (sumXY / Math.sqrt(sumX2 * sumY2));
};

morpheus.Cosine.toString = function() {
	return 'Cosine similarity';
};

morpheus.Euclidean = function(x, y) {
	var dist = 0;
	for (var i = 0, size = x.size(); i < size; ++i) {
		var x_i = x.getValue(i);
		var y_i = y.getValue(i);
		if (isNaN(x_i) || isNaN(y_i)) {
			continue;
		}
		dist += (x_i - y_i) * (x_i - y_i);
	}
	return Math.sqrt(dist);
};
morpheus.Euclidean.toString = function() {
	return 'Euclidean distance';
};
morpheus.OneMinusFunction = function(f) {
	var dist = function(x, y) {
		return 1 - f(x, y);
	};
	dist.toString = function() {
		var s = f.toString();
		return 'One minus ' + s[0].toLowerCase() + s.substring(1);
	};
	return dist;
};