morpheus.PermutationPValues = function(dataset, aIndices, bIndices,
		numPermutations, f) {
	var numRows = dataset.getRowCount();
	/** unpermuted scores */
	var scores = new Float32Array(numRows);
	/** Whether to smooth p values */
	var smoothPValues = true;
	var list1 = new morpheus.DatasetRowView(new morpheus.SlicedDatasetView(
			dataset, null, aIndices));
	var list2 = new morpheus.DatasetRowView(new morpheus.SlicedDatasetView(
			dataset, null, bIndices));

	for (var i = 0; i < numRows; i++) {
		scores[i] = f(list1.setIndex(i), list2.setIndex(i));
	}
	dataset = new morpheus.SlicedDatasetView(dataset, null, aIndices
			.concat(bIndices));
	var rowSpecificPValues = new Float32Array(numRows);
	var permuter = new morpheus.UnbalancedPermuter(aIndices.length,
			bIndices.length);
	var permutationScore = new morpheus.TwoClassPermutationScore();
	permutationScore.init(dataset, f);
	for (var permutationIndex = 0; permutationIndex < numPermutations; permutationIndex++) {
		permutationScore.setPermutation(permuter.next());
		for (var i = 0; i < numRows; i++) {
			var permutedScore = permutationScore.getScore(i);
			var score = scores[i];
			if (permutedScore >= score) {
				rowSpecificPValues[i]++;
			}
		}
	}
	var N = numPermutations;
	var kArray = new Uint32Array(numRows);
	for (var i = 0; i < numRows; i++) {
		var k = rowSpecificPValues[i];
		kArray[i] = k;
		var p;
		if (smoothPValues) {
			p = (k + 1) / (N + 2);
		} else {
			p = k / N;

		}
		// 2-sided p-value
		var oneMinusP = 1 - p;
		if (oneMinusP < p) {
			p = oneMinusP;
		}
		p *= 2;
		if (p === 0) {
			// ensure not degenerate case where profile is
			// completely
			// flat
			// TODO handle cases where profile is flat (but not
			// completely)

			var val = dataset.getValue(i, 0);
			var flat = true;
			for (var j = 1, cols = dataset.getColumnCount(); j < cols && flat; j++) {
				if (dataset.getValue(i, j) != val) {
					flat = false;
				}
			}
			if (flat) {
				p = 1;
			}
		}
		rowSpecificPValues[i] = p;

	}
	this.rowSpecificPValues = rowSpecificPValues;
	this.k = kArray;
	this.fdr = morpheus.FDR_BH(rowSpecificPValues);
	this.scores = scores;
};
morpheus.PermutationPValues.prototype = {
	getBonferroni : function(index) {
		return Math.min(this.rowSpecificPValues[index] * this.numRows, 1);
	}
};

morpheus.UnbalancedPermuter = function(numClassZero, numClassOne) {
	var assignments = new Uint32Array(numClassZero + numClassOne);
	var indices = new Uint32Array(numClassZero + numClassOne);
	for (var i = 0; i < indices.length; i++) {
		indices[i] = i;
	}
	var n = indices.length;
	// Returns a random integer between min (included) and max (included)
	// Using Math.round() will give you a non-uniform distribution!
	function getRandomIntInclusive(min, max) {
		return Math.floor(Math.random() * (max - min + 1)) + min;
	}
	this.next = function() {
		// shuffle indices array
		for (var i = n - 1; i >= 1; i--) {
			var j = getRandomIntInclusive(0, i); // random integer such that
			// 0 ≤ j ≤ i
			// exchange a[j] and a[i]
			var tmp = indices[j];
			indices[j] = indices[i];
			indices[i] = tmp;
		}

		// pick 1st numClassOne indices to be class one
		for (var i = 0; i < n; i++) {
			assignments[i] = 0;
		}
		for (var i = 0; i < numClassOne; i++) {
			assignments[indices[i]] = 1;
		}

		return assignments;
	};
};

morpheus.TwoClassPermutationScore = function() {
	this.classZeroView = null;
	this.classOneView = null;

};
morpheus.TwoClassPermutationScore.prototype = {
	getScore : function(index) {
		this.classZeroView.setIndex(index);
		this.classOneView.setIndex(index);
		return this.f(this.classZeroView, this.classOneView);
	},
	init : function(dataset, f) {
		this.dataset = dataset;
		this.classZeroView = new morpheus.DatasetRowView(dataset);
		this.classOneView = new morpheus.DatasetRowView(dataset);
		this.f = f;
	},
	setPermutation : function(permutedAssignments) {
		var zeroIndices = [];
		var oneIndices = [];
		for (var i = 0, length = permutedAssignments.length; i < length; i++) {
			if (permutedAssignments[i] === 0) {
				zeroIndices.push(i);
			} else {
				oneIndices.push(i);
			}
		}

		this.classZeroView.setDataset(new morpheus.SlicedDatasetView(
				this.dataset, null, zeroIndices));
		this.classOneView.setDataset(new morpheus.SlicedDatasetView(
				this.dataset, null, oneIndices));

	}

};
