/**
 * @typedef {Object}
 *
 * ValueAddedMonitoring class
 * TODO clean up signature?
 * TODO Rename to ClassMonitor?
 * Filters are taken from default or given location inputs, unfilteredResults
 * are then converted to filteredResults (all assignments)
 * VA Calc then uses given assignment+filtered results to get total initial/total actual ucas points
 * and uses formula to calculate VA score. This is then passed to percentile calucaltion code using
 * each courses individual percentile boundaries
 * Course level will strip oiut non Common Assesssment Points [CAPs], if there are none it just shows
 * latest assignment
 */
export class ValueAddedMonitoring {
	constructor(context, code, json, injected, externalFilters, capOnly) {
		// Constructor
		this.code = code;
		this.context = $(context);
		this.chart = null;
		this.filteredResults = null;
		this.gradePointValueInputs = null;
		this.gradeStudentsValueInputs = null;
		this.teachers = null;
		this.assignmentOrder = [];
		this.studentOrder = [];
		this.studentData = [];
		this.totalPoints = 0;
		this.totalStudents = 0;
		this.vaBoxHeight = 120;
		this.containerClass = '.value-added';
		this.usingExternalFilters = false;
		this.externalFilters = null;
		this.useOnlyCommonAssessments = capOnly == true;
		this.className = null;
		this.classReference = null;
		this.finishedCalculating = false;
		this.noResults = false;
		this.courseOverallAverage = null;
		this.hasFinalResults = false;
		this.filterEnabled = false; // Not currently used but may be in future
		this.stats = null;
		this.colors = ['D60A0A', 'D60A0A', 'D60A0A', '5E2727', '000', '000', '110E0E', '0E0EC8', '0E0EC8'];
		this.useSplitCharts = true; // While we cannot use dual axis charts use split charts
		this.multiplier = 100;
		// Modal loading controls
		this.chartsReadyLookup = [];
		this.totalChartsLookup = [];
		this.chartsReadyTimer = null;

		$('.results', this.context).not('course-average').each((idx, elem) => tippy(elem, {content: '---'}));

		$('.results .results-pop-out').hide();
		$('.class-view .class-final-average').hide();
		$('.class-view .final-results-box .class-final-average').hide();
		$('.class-view .final-results-box .no-results-message').show();


		// Look for filter inputs outside of normal context (used when one
		// filter panel controls multiple class filters, courseview etc)
		if (externalFilters != null) {
			this.externalFilters = externalFilters;
			this.usingExternalFilters = true;
		}

		// Get real context if given something other than container
		if (!this.context.is(this.containerClass)) {
			this.context = this.context.closest(this.containerClass);
		}

		// Prefer data from injected object (used with Course level ajax)
		if (injected && injected.results) {
			this.unfilteredResults = injected.results;
		} else {
			// Get data from page
			json = $('.results-json', context).val();
			if (json == undefined) {
				throw "json was undefined";
			}
			this.unfilteredResults = $.parseJSON(json);
		}

		if (injected && injected.gradesLookup) {
			this.gradesLookup = injected.gradesLookup;
		} else {
			this.gradesLookup = $.parseJSON($('input.grades-lookup', this.context).first().val());
		}

		if (injected && injected.assignmentLookup) {
			this.assignmentsLookup = injected.assignmentLookup;
		} else {
			this.assignmentsLookup = $.parseJSON($('input.assignment-lookup', this.context).first().val());
		}

		if (injected && injected.hasOwnProperty('commonAssessmentsLookup')) {
			this.commonAssessmentsLookup = injected.commonAssessmentsLookup;
			this.commonAssessmentIds = injected.commonAssessmentIds;
		}

		this.gradeCount = parseInt($('.num-grade-cols', this.context).first().val());
		this.expectedPoints = $('input.initial-expected-points', this.context).first().val();
		this.expectedLookupData = $.parseJSON($('input.expectations-lookup', this.context).first().val());
		this.classPointsDisplay = $('.class-points', this.context).first();
		this.classValueAddedScoreDisplay = $('.current-class .class-value-added-score', this.context).first();
		this.percentiles = $.parseJSON($('.percentiles').val());

		// Populate local dropdown if there is one
		this.assignmentSelectInput = $('select.assignment-select', this.context).first();

		this.assignmentSelectInput.prepend($('<option>', {value: '-1'}).text('None (No initial values)'));
		for (var assignmentId in this.unfilteredResults) {
			this.assignmentOrder[this.assignmentOrder.length] = assignmentId;
			this.assignmentSelectInput.prepend($('<option>', {value: assignmentId}).text(
				this.getAssignmentNameForId(assignmentId)
			));
		}

		this.assignmentSelectInput.change(function (e) {
			MonitoringObjects.getMonitor(this).filterChange();
		});

		if ($('.class-view').length > 0) {
			this.modalIndex = initiateModal($('.class-view'), 'lock', null, 'ops_page_loading');
		}

		this.resetPercentileToMiddle('current-class');
		this.resetPercentileToMiddle('class-final-average');
		this.resetPercentileToMiddle('class-overall-average');
	} // End constructor
}


ValueAddedMonitoring.prototype.setCourseAverage = function (percentileNumber) {
	this.courseOverallAverage = percentileNumber;
};

/**
 * Add qtips to relevent display boxes
 */
ValueAddedMonitoring.prototype.setupTooltips = function () {
	// Update qtips on va results boxes
	var vaBoxes = ['current-class', 'class-overall-average', 'class-final-average'];
	for (var targetClassIndex in vaBoxes) {
		var targetClass = vaBoxes[targetClassIndex];
		var data = $('.' + targetClass + 'Tooltip.results-pop-out', this.context).first().html();
		$('.results.' + targetClass, this.context).each((idx, target) => {
			if (target.hasOwnProperty('_tippy')) {
				tippy(target, {content: data, allowHTML: true});
			}
		});
	}
};

ValueAddedMonitoring.prototype.getAssignmentNameForId = function (assignmentId) {
	var name = this.assignmentsLookup[assignmentId];
	if (name) {
		return name;
	}
	throw "Name not found for assignment id " + assignmentId;
};

ValueAddedMonitoring.prototype.setStudentMultiplier = function (value) {
	if (value == null) {
		throw "Monitoring Setup Failed: Must have a student Multiplier";
	}

	this.multiplier = parseFloat(value);
};

ValueAddedMonitoring.prototype.setChart = function (chart) {
	var firstChart = this.chart == null;
	this.chart = chart;

	// Don't duplicate columns for responsive recreations of same chart
	if (firstChart) {
		this.chartAssignments(
			this.unfilteredResults,
			'Current VA Scores per Assignment',
			'currentVaScores'
		);
	}
};

ValueAddedMonitoring.prototype.setVaOnlyChart = function (chart) {
	var firstChart = this.vaOnlyChart == null;
	this.vaOnlyChart = chart;

	// Don't duplicate columns for responsive recreations of same chart
	if (firstChart) {
		this.chartAssignments(
			this.unfilteredResults,
			'Current VA Scores per Assignment',
			'currentVaScores'
		);
	}
};

ValueAddedMonitoring.prototype.setStudentIds = function (data) {
	this.studentOrder = data;
};

ValueAddedMonitoring.prototype.setStudentData = function (data) {
	this.studentData = data;
};

ValueAddedMonitoring.prototype.setStudentChart = function (chart) {
	var firstChart = this.studentChart == null;
	this.studentChart = chart;
	if (firstChart) {
		this.chartStudents(
			this.unfilteredResults,
			'YTD UCAS Progress',
			'studentVa'
		);
	}
};

// getFilters finds elements with right class
// //TODO Fix for BTEC grades (not D*D*)
ValueAddedMonitoring.prototype.getFilters = function () {
	var filters;
	if (this.usingExternalFilters) {
		filters = this.externalFilters.find('.filter:checked');
	} else {
		var filterContainer = $('.filters', this.context);
		filters = filterContainer.find('.filter:checked');
	}
	var results = {'include': {}, 'custom_groups': {'include': []}, 'flags': {'include': [], 'exclude': []}};
	filters.each(function (i, filter) {
		filter = $(filter);
		var filterId = filter.data('id');
		var filterFieldName = filter.data('field');

		if (filter.is('.flag')) {
			// Flag filter
			if (filter.is('.include-filter')) {
				results.flags.include[results.flags.include.length] = filterId;
			} else {
				results.flags.exclude[results.flags.exclude.length] = filterId;
			}
		} else if (filter.is('.course_custom_group')) {
			// Group filter
			/* if (!results.custom_groups.include.hasOwnProperty(filterFieldName)) {
				 results.custom_groups.include[filterFieldName] = [];
			 }*/
			results.custom_groups.include[results.custom_groups.include.length] = filterId;
		} else {
			// Normal filter
			if (!results.include.hasOwnProperty(filterFieldName)) {
				results.include[filterFieldName] = [];
			}
			results.include[filterFieldName][results.include[filterFieldName].length] = filterId;
		}
	});
	return results;
};

ValueAddedMonitoring.prototype.clearLiveData = function () {
	// Clear current grade inputs
	$('input.live_result', this.context).each(function (a, target) {
		target = $(target);
		target.val(0);
	});

	// Clear current actual grade inputs
	$('input.actual_result', this.context).each(function (a, target) {
		target = $(target);
		target.val(0);
	});

	// Clear initial values numbers and totals
	$('span.initial-value-students-display', this.context).each(function (a, target) {
		target = $(target);
		target.html('0');
		target.parent().find('input.initial-value-students').val(0);
	});

	$('span.initial-value-subtotal-display', this.context).each(function (a, target) {
		target = $(target);
		target.html('0');
	});

	$('span.initial-expected-points-display', this.context).first().html('0');
	$('input.initial-expected-points', this.context).val(0);
	$('span.diff-display', this.context).html('');
};

ValueAddedMonitoring.prototype.resetLiveData = function () {
	// Clear current grade inputs
	$('input.live_result', this.context).each(function (a, target) {
		target = $(target);
		targetReplacer = target.next('input[type=hidden]');
		target.val(targetReplacer.val());
	});

	$('.diff-display', this.context).each(function (a, b) {
		$(b).hide().html('');
	});

	this.assignmentValuesChanged();
};

ValueAddedMonitoring.prototype.updateAssignmentName = function (name) {
	$('.current-assignments-display', this.context).first().html(name);
	$('.results.current-class .results-pop-out .assignment-name', this.context).first().html(name);
};

ValueAddedMonitoring.prototype.setupFullClassExpectations = function () {
	var accountedFor = [];
	var initialValueData = {totalStudents: 0, totalPoints: 0};

	for (var a in this.unfilteredResults) {
		var assignment = this.unfilteredResults[a];
		for (var m in assignment) {
			var mark = assignment[m];
			if (accountedFor.indexOf(mark['person_id']) === -1) {
				accountedFor.push(mark['person_id']);
				initialValueData = this.collateExpectationForGcsePoints(
					mark['gcsePoints'],
					initialValueData
				);
			}
		}
	}

	// Extract totals
	this.totalStudents = initialValueData.totalStudents;
	this.expectedPoints = initialValueData.totalPoints;
	this.noResults = this.totalStudents == 0;
	this.populateExpectations(initialValueData);

	// Upgrade grand totals
	$('.total-students-display', this.context).first().html(this.totalStudents);
	$('.initial-expected-points-display', this.context).first().html(this.expectedPoints.toFixed(0));

	// Update results popout box
	$('.results.current-class .results-pop-out .expected-points', this.context).html(
		this.expectedPoints.toFixed(0)
	);
	$('.results.current-class .results-pop-out .students-included', this.context).html(
		this.totalStudents
	);

	$('input.initial-expected-points', this.context).first().val(this.expectedPoints.toFixed(2));
	this.recalculateLiveValueAddedScore();
	this.displayOverallAveragePercentile(this.getClassOverallAverage());
	this.displayCourseAveragePercentile(this.courseOverallAverage);
	this.setupTooltips();
	this.assignmentValuesChanged();
};

/**
 * As this is first point where we are sure there are filtered results available
 * should consider getting this to call a post-filter method perhaps
 */
ValueAddedMonitoring.prototype.filterChange = function () {
	// Use filtered results to rebuild stats live on page
	this.finishedCalculating = false;
	this.noResults = false;
	var context = this.context;
	var resultObj = this.getFilteredResults(this.getFilters());
	var results = resultObj.results;
	var assignmentToUse = MonitoringObjects.getSelectedAssignment(this);
	var assignment = null;
	this.tinkerCheck(); // Reset message
	// this.resetPercentileToMiddle('current-class');
	var noAssignment = false;

	// If they chose modelling mode
	if (assignmentToUse == -1) {
		this.clearLiveData();
		this.setupFullClassExpectations();
		this.hidePercentile('current-class');
		noAssignment = true;
	} else {
		assignment = results[assignmentToUse];
	}

	if (!noAssignment) {
		// Warning message
		//context.addClass('filtered');

		this.clearLiveData();
		this.totalStudents = 0;
		this.totalPoints = 0;

		// Add filtered data back in
		var initialValueData = {totalStudents: 0, totalPoints: 0};
		// Currently one one in this array after multi-assignment was removed
		for (var j in assignment) {
			var grade = assignment[j];
			var gradeId = grade['grade_id'];
			var gradeInput = $('input.live_result.grade-' + gradeId, context).first();
			var gradeActualInput = $('input.actual_result.grade-' + gradeId, context).first();
			var updatedGradeCount = parseInt(gradeInput.val()) + 1;

			// Update grade column
			gradeInput.val(updatedGradeCount);
			gradeActualInput.val(updatedGradeCount);

			initialValueData = this.collateExpectationForGcsePoints(grade['gcsePoints'], initialValueData);
		}
		// Extract totals
		this.totalStudents = initialValueData.totalStudents;
		this.expectedPoints = initialValueData.totalPoints;
		this.noResults = this.totalStudents == 0;

		this.populateExpectations(initialValueData);

		// Upgrade grand totals
		$('.total-students-display', context).first().html(this.totalStudents);
		$('.initial-expected-points-display', context).first().html(this.expectedPoints.toFixed(0));

		// Update results popout box
		$('.results.current-class .results-pop-out .expected-points').html(this.expectedPoints.toFixed(0));
		$('.results.current-class .results-pop-out .students-included').html(this.totalStudents);

		$('input.initial-expected-points', context).first().val(this.expectedPoints.toFixed(2));
		this.recalculateLiveValueAddedScore();

		this.displayOverallAveragePercentile(this.getClassOverallAverage());
		this.displayCourseAveragePercentile(this.courseOverallAverage);
		this.setupTooltips();
	}

	if (this.hasFinalResults) {
		var finalVa = this.calculateFinalGradeValueAdded();
		var finalVaPercentileData = this.calculatePercentile(finalVa.score, this.percentiles);
		this.finalVaPercentile = finalVaPercentileData.percentileNumber;
		this.displayFinalGradePercentile(finalVaPercentileData);
		this.setVATooltipValues(
			'class-final-average',
			finalVa.score,
			finalVaPercentileData,
			finalVa.points,
			finalVa.expected,
			finalVa.students
		);
		this.updateTooltips();
	} else {
		$('.final-results-box .class-final-average').hide();
		$('.final-results-box .no-results-message').show();
	}
	this.updateGradeValues();
	this.updateChart();
};

ValueAddedMonitoring.prototype.getGradeData = function (gradeId) {
	if (this.gradesLookup[gradeId] != undefined) {
		return this.gradesLookup[gradeId];
	} else {
		throw "No grade data found for id:" + gradeId;
	}
};

ValueAddedMonitoring.prototype.getGradeDataFromGrade = function (gradeValue) {
	for (var i in this.gradesLookup) {
		if (this.gradesLookup.hasOwnProperty(i)) {
			var grade = this.gradesLookup[i];
			if (grade['grade'].toLowerCase() == gradeValue.toLowerCase()) {
				return grade;
			}
		}
	}
	throw "No grade data found for grade value:" + gradeValue;
};

ValueAddedMonitoring.prototype.getExpectationData = function (gcseScore) {
	// Treat no GCSE Score as 0.00:
	if (gcseScore === null || gcseScore === undefined) {
		gcseScore = 0.00;
	}
	
	gcseScore = parseFloat(gcseScore);
	
	var reversedData = this.expectedLookupData;
	for (var e = 0; e < reversedData.length; e++) {
		var expectedBand = reversedData[e];

		if (
			gcseScore >= parseFloat(expectedBand.lower)
		) {
			return expectedBand;
		}
	}

	throw "No band found for gcse score of " + gcseScore + ", perhaps there is no band with zero set?";
};

/**
 * lineName = Display
 * lineId = string reference
 */
ValueAddedMonitoring.prototype.chartAssignments = function (assignments, lineName, lineId) {
	var chartToUse = this.chart;
	if (this.useSplitCharts) {
		chartToUse = this.vaOnlyChart;
	}

	if (!chartToUse) {
		return;
	}
	var colId = chartToUse.addNewChartColumn('number', lineName, lineId, null, {
		vaTooltip: {type: 'string', role: 'tooltip', 'p': {'html': true}},
		vaColouration: {type: 'string', role: 'style'},
		vaAnnotation: {type: 'string', role: 'annotation'}
	});
	chartToUse.clearChartRows(lineId);

	// Loop over given assignments
	for (var n in assignments) {
		var foundResult = false;
		var assignment = assignments[n];
		var assignmentData = {
			valueAddedScore: 0,
			totalPoints: 0,
			totalStudents: 0,
			expectedPoints: 0,
			date: null,
			name: null
		};

		// Loop over results in assignment
		if (assignment.length > 0) {
			for (var x = 0; x < assignment.length; x++) {
				var result = assignment[x];
				if (!result) {
					continue;
				}
				foundResult = true;
				var gradeData = this.getGradeData(result.grade_id);
				var expectationData = this.getExpectationData(result.gcsePoints);

				assignmentData.totalPoints += parseFloat(gradeData.ucasValue);
				assignmentData.totalStudents++;
				assignmentData.expectedPoints += parseFloat(expectationData.ucasValue);
				if (assignmentData.date == null) {
					assignmentData.date = result.assignment_date;
					assignmentData.name = result.assignment_name;
				}
			}
		}

		if (foundResult) {
			// Calculate value added score
			var vaScore = this.calculateValueAdded(
				assignmentData.totalStudents,
				assignmentData.totalPoints,
				assignmentData.expectedPoints
			);
			var percentile = this.calculatePercentile(vaScore, this.percentiles);

			// Update tooltip
			this.setVATooltipValues(
				'current-class',
				vaScore,
				percentile,
				assignmentData.totalPoints,
				assignmentData.expectedPoints,
				assignmentData.students
			);

			this.finishedCalculating = true;
			// Add to datatable
			var rowId = chartToUse.addNewDatapoint(lineId, assignmentData.date.date);
			chartToUse.setCell(rowId, 0, new Date(assignmentData.date.date));
			chartToUse.setCell(rowId, colId, percentile.percentileNumber);
			chartToUse.setCell(
				rowId,
				colId + 1,
				'<div class="va-tooltip-chart" style="padding: 0px;">' +
				'<h3>' + assignmentData.name + '</h3><div class="tooltip-body">' +
				'<b>Value Added Score:</b> ' +
				'<span class="va-tooltip va-score-' + percentile.percentileNumber + '">' +
				percentile.percentileNumber + '</span></b>' +
				'<br><b>Expected: </b>' + assignmentData.expectedPoints.toFixed(0) +
				'<br><b>Actual points</b>: ' + assignmentData.totalPoints.toFixed(0) +
				'<br><b>Students</b>: ' + assignmentData.totalStudents.toFixed(0) +
				'</div></div>'
			);
			chartToUse.setCell(rowId, colId + 2, 'point{ fill-color: #' + this.colors[percentile.percentileIndex] + '}');
			// Log student/assignment completion rate to second graph
			if (this.chart) {
				var rateColId = this.chart.addNewChartColumn('number', 'Assignment Hand-in Rate', 'rate', null, {
					rateTooltip: {type: 'string', role: 'tooltip', 'p': {'html': true}},
				});
				var rateRowId = this.chart.addNewDatapoint('rate', assignmentData.date.date);
				var rate = (assignmentData.totalStudents / this.studentOrder.length) * 100;
				this.chart.setCell(
					rateRowId,
					rateColId,
					rate.toFixed(2)
				);
				var markDate = new Date(assignmentData.date.date);
				this.chart.setCell(rateRowId, 0, markDate);
				this.chart.setCell(
					rateRowId,
					rateColId + 1,
					'<div class="va-tooltip-chart" style="padding: 0px;">' +
					'<h3>' +
					assignmentData.name + ' ' +
					('0' + markDate.getDate()).slice(-2) + '/' +
					('0' + (markDate.getMonth() + 1)).slice(-2) + '/' +
					markDate.getFullYear() +
					'</h3>' +
					'<div class="tooltip-body">' +
					'<b>Hand-in rate:</b> <span class="rate-percent">' + rate.toFixed(0) + '%</span>' +
					'<br/><b>Marks:</b> ' + assignmentData.totalStudents + '/' + this.studentOrder.length + '</span>' +
					'</div></div>'
				);
			}
		}
		chartToUse.sort([0]);
		chartToUse.redraw();
		if (this.chart) {
			this.chart.sort([0]);
			this.chart.redraw();
		}
	}
	this.updateTooltips();
};

ValueAddedMonitoring.prototype.chartStudents = function (assignments, lineName, lineId) {
	var additionalColumns = {
		vaStudentColouration: {type: 'string', role: 'style'},
		vaTooltip: {type: 'string', role: 'tooltip', 'p': {'html': true}},
		vaAnnotation: {type: 'string', role: 'annotation'},
	};
	// Show final results VA per student, currently disabled
	if (false && this.hasFinalResults) {
		additionalColumns.finalResultColumn = {type: 'number', label: 'Final Grade Value Added'};
		additionalColumns.vaStudentColourationFinal = {type: 'string', role: 'style'};
		additionalColumns.vaTooltipFinal = {type: 'string', role: 'tooltip', 'p': {'html': true}};
		additionalColumns.vaAnnotationFinal = {type: 'string', role: 'annotation'};
	}

	var colId = this.studentChart.addNewChartColumn('number', lineName, lineId, null, additionalColumns);
	this.studentChart.clearChartRows(lineId);
	var students = [];
	var studentsFinal = [];
	var assignmentTotal = this.assignmentOrder.length;

	for (var n in assignments) {
		var assignment = assignments[n];
		if (!assignment) {
			continue;
		}
		for (var j = 0; j < assignment.length; j++) {
			var result = assignment[j];
			if (result) {
				var expectationData = this.getExpectationData(result.gcsePoints);
				// Increment existing values (average VA)
				if (students.hasOwnProperty(result.person_id)) {
					students[result.person_id].actualPoints += this.getGradeData(result.grade_id).ucasValue;
					students[result.person_id].gradesFound++;
					//TODO Fix for BTEC grades (not D*D*)
					students[result.person_id].grades.push(result.grade);
					students[result.person_id].handInTotal++;
				} else {
					// Create new object for student with starting values
					students[result.person_id] = {
						gcsePoints: result.gcsePoints,
						expectedPoints: expectationData.ucasValue,
						gradesFound: 1,
						grades: [],
						actualPoints: this.getGradeData(result.grade_id).ucasValue,
						name: result.student_name,
						targetGrade: result.student_mtg,
						handInTotal: 0,
						reference: result.p_reference,
					};
				}

				// Add final results
				if (this.hasFinalResults) {
					studentsFinal[result.person_id] = this.getGradeDataFromGrade(result.final_grade).ucasValue;
				}
			}
		}
	}

	// Add to chart
	for (var i = 0; i < this.studentOrder.length; i++) {
		// Use ordered reference
		var studentId = this.studentOrder[i];
		var student = students[studentId];
		var studentFinal = studentsFinal[studentId];
		if (!student) {
			continue;
		}
		var vaScore = this.calculateValueAdded(
			1,
			student.actualPoints / student.gradesFound,
			student.expectedPoints
		);
		var additionalStyle = '';
		if (this.hasFinalResults) {
			//additionalStyle = ' opacity: 0.25; ';
		}
		var percentile = this.calculatePercentile(vaScore, this.percentiles);
		var rowId = this.studentChart.addNewDatapoint(lineId);
		var studentAveragePoints = student.actualPoints / student.gradesFound;
		var adjusted = ((studentAveragePoints / student.expectedPoints) * 100).toFixed();
		this.studentChart.setCell(rowId, 0, student.name);
		this.studentChart.setCell(rowId, colId, adjusted);
		var extraData = this.studentData[studentId];

		var attendance = extraData['student_attend_YTD_actual'] / extraData['student_attend_YTD_possible'];
		attendance *= 100;
		var attendanceAlert = '';
		if (attendance <= 93) {
			attendanceAlert = 'attendance-yellow-alert'
			if (attendance <= 80) {
				attendanceAlert = 'attendance-red-alert';
			}
		}
		var handInRate = (student.handInTotal / assignmentTotal) * 100;
		var handInRateAlert = '';
		handInRate = handInRate.toFixed(0);
		if (handInRate <= 93) {
			handInRateAlert = 'attendance-yellow-alert'
			if (handInRate <= 80) {
				handInRateAlert = 'attendance-red-alert';
			}
		}

		this.studentChart.setCell(rowId, colId + 1, '{ color:#' + this.colors[percentile.percentileIndex] + ';' + additionalStyle + '}');
		this.studentChart.setCell(rowId, colId + 2, '<div class="va-tooltip-chart" style="padding: 0px;">' +
			'<h3>' + student.name + '</h3><div class="tooltip-body">' +
			//'<div><b>Average VA:</b> '+percentile.percentileNumber+'</div>'+
			'<div><b>T Number:</b> ' + student.reference + '</div>' +
			'<div><b>UCAS Progress:</b> ' + adjusted + '%</div>' +
			'<div>' +
			'<b>Current UCAS:</b> ' + studentAveragePoints.toFixed() +
			' <b>Target</b>:' + student.expectedPoints +
			'</div>' +
			'<div><b>Attendance (YTD):</b> <span class="' + attendanceAlert + '">' + attendance.toFixed(0) + '%</span></div>' +
			'<div><b>GCSE Points:</b> ' + student.gcsePoints + '</div>' +
			'<div><b>Target Grade:</b> ' + student.targetGrade + '</div>' +
			'<div><b>Grade history:</b> ' + student.grades.join(', ') + '</div>' +
			'<div><b>Hand in rate:</b> <span class="' + handInRateAlert + '">' + handInRate + '%</span></div>' +
			'<span style="display:none">_-_-' + student.reference + '_-_-</span>' +
			'</div></div>'
		);
		this.studentChart.setCell(rowId, colId + 3, '' + adjusted + '%');

		// Currently disabled final va score on vertical chart
		if (false && this.hasFinalResults) {
			var vaFinal = this.calculateValueAdded(1, student.expectedPoints, studentFinal);
			var vaFinalPercentileData = this.calculatePercentile(vaFinal, this.percentiles);
			//adjustedFinal = (9 - vaFinalPercentileData.percentileNumber) - 4;
			//adjustedFinal = 9 - vaFinalPercentileData.percentileNumber;
			adjusted = (studentFinal / student.expectedPoints).toFixed(1);
			this.studentChart.setCell(rowId, colId + 4, adjustedFinal);
			this.studentChart.setCell(rowId, colId + 5,
				'color:#' + this.colors[vaFinalPercentileData.percentileIndex] + ';' +
				'border-width:3'
			);
			this.studentChart.setCell(rowId, colId + 6, '<div class="va-tooltip-chart">Final: ' + student.name + ' Score:' + vaFinalPercentileData.percentileNumber + '</div>');
			this.studentChart.setCell(rowId, colId + 6, 'Target: ' + adjusted + '%');
		}
	}
	var options = this.studentChart.getOptions();
	options.vAxis.slantedText = true;
	options.chartArea.width = '70%';
	this.studentChart.draw(null, options);
	this.finishedCalculating = true;
};

ValueAddedMonitoring.prototype.updateChart = function () {
	// Not used until class-level filtering is reenabled
	// Need in chronological order to add to chart
	return;
	
	// if (this.chart) {
	// 	var assignmentOrder = this.assignmentOrder;
	// 	this.chartAssignments(this.filteredResults, 'Filtered VA Scores', 'filteredVAScores');
	// 	var options = this.chart.getOptions();
	// 	options.series[3] = {
	// 		color: '#EEEEEE',
	// 		visibleInLegend: this.filterEnabled,
	// 		pointSize: 3,
	// 		lineWidth: 2
	// 	};
	// 	options.interpolateNulls = true;
	// 	options.vAxis.maxValue = 100;
	// 	options.vAxis.textStyle.color = '#777';
	// 	this.chart.sort([0]);
	// 	//var options = this.chart.getOptions();
	// 	this.chart.draw(null, options);
	// }
	//
	// if (this.studentChart) {
	// 	// TODO enable this if class-level filters are enabled
	// 	// and fix issue with duplicate values appearing
	// 	//this.chartStudents(this.filteredResults, 'VA', 'studentVa');
	// }
};

ValueAddedMonitoring.prototype.getFilteredResults = function (filters) {
	var context = this.context;
	var results = this.unfilteredResults;
	var filtered = [];

	// Used for filter debugging
	var records = 0;
	var recordsTotal = 0;
	var lastAssignmentRecordsTotal = 0;
	var lastAssignmentRecords = 0;

	// Assignments
	// TODO split into functions for ease of maintainance
	for (var assignmentId in results) {
		if (results.hasOwnProperty(assignmentId)) {
			var assignment = results[assignmentId];
			// Individual Grades within Assignment
			for (var resultId in assignment) {
				if (assignment.hasOwnProperty(resultId)) {
					var result = assignment[resultId];
					var passedBasicFilters = true;
					var passedFlagFilters = (filters.flags.include.length === 0);
					var passedCustomGroupFilters = (filters.custom_groups.include.length === 0);

					if (this.className == null) {
						this.className = result.class_name;
						this.classReference = result.class_reference;
						this.classId = result.class_id;
					}

					// If this is first time detecting final results, correct display and set flag
					if (!this.hasFinalResults && result.final_grade != null) {
						this.hasFinalResults = true;
						$('.class-view .class-final-average').show();
						$('.final-results-row').show();
						$('.no-results-message').hide();
					}

					// Check if there are basic filters enabled and if this record passes
					var foundBasicFilters = 0;
					var foundBasicTypes = [];
					var totalBasicFilterTypes = 0;
					for (var filterFieldName in filters.include) {
						if (filters.include.hasOwnProperty(filterFieldName)) {
							totalBasicFilterTypes++;
							if (foundBasicTypes.indexOf(filterFieldName) > -1) {
								// Already found this type
								continue;
							}

							var basicFilterIds = filters.include[filterFieldName];
							if (
								basicFilterIds.indexOf(result[filterFieldName]) > -1 ||
								basicFilterIds.indexOf(parseInt(result[filterFieldName])) > -1
							) {
								foundBasicTypes[foundBasicTypes.length] = filterFieldName;
								foundBasicFilters++;
							} else {
								//console.log(parseInt(result[filterFieldName]), 'not in', basicFilterIds);
								//console.log("DIDNT match with ", result[filterFieldName], 'for', filterFieldName, 'in', basicFilterIds);
							}
						}
					}
					//console.log("Found "+foundBasicFilters+" of "+totalBasicFilterTypes, foundBasicTypes);
					passedBasicFilters = (foundBasicFilters == totalBasicFilterTypes);

					recordsTotal++;
					if (this.assignmentOrder[0] == assignmentId) {
						lastAssignmentRecordsTotal++;
					}

					// If custom groups are in use and we've selected any, limit to the selected module group
					if (!passedCustomGroupFilters && result && result.customGroupsJson != null) {

						var customgroups = $.parseJSON(result.customGroupsJson);

						//var matchedGroups = 0 ;
						for (var customgroupId in customgroups) {
							var customgroup = customgroups[customgroupId];

							if (filters.custom_groups.include.indexOf(parseInt(customgroup.id)) > -1) {
								//console.log( 'Located ' + customgroup.name + ' in ' ) ;
								//console.log( customgroups ) ;
								passedCustomGroupFilters = true;
							}
						}
					}
					if (result && result.flagsJson != null) {
						// Looping over each result, then looping over
						// each filter and each flag to see 
						// if we should keep them in results
						var flags = $.parseJSON(result.flagsJson);
						var excluding = false;
						var including = false;
						if (filters.flags.include.length == 0) {
							including = true;
						}

						// Loop over ticked flag filters
						var foundFlags = 0;

						// Exclude filters
						for (var excludedFlagId in flags) {
							var excludedFlag = flags[excludedFlagId];
							if (filters.flags.exclude.indexOf(parseInt(excludedFlag.id)) > -1) {
								excluding = true;
							}
						}

						// Include filters
						for (var includedFlagId in flags) {
							var includedFlag = flags[includedFlagId];
							if (filters.flags.include.indexOf(parseInt(includedFlag.id)) > -1) {
								foundFlags++;
							}
						}

						passedFlagFilters = !excluding && foundFlags == filters.flags.include.length;
					}

					if (passedFlagFilters && passedBasicFilters && passedCustomGroupFilters) {
						// Rebuild array in same format as it arrived
						records++;
						if (this.assignmentOrder[0] == assignmentId) {
							lastAssignmentRecords++;
						}
						var filterRow;
						if (filtered[assignmentId] == undefined) {
							filterRow = filtered[assignmentId] = [];
						} else {
							filterRow = filtered[assignmentId];
						}
						//console.log( resultId ) ;
						filterRow[resultId] = result;
					}

				}
			}
		}
	}
	this.filteredResults = filtered;
	return {
		'results': filtered,
	};
};

// flagToggle, when a flag is changed
ValueAddedMonitoring.prototype.flagToggle = function (source) {
	source = $(source);
	flagActive = source.is(':checked');
	var radios = source.closest('li.flag').find('input.filter.flag');
	radios.prop('disabled', !flagActive);
	if (!flagActive) {
		radios.removeAttr('checked');
		//clearFilterEffect(source);
	}
	this.filterChange();
};

// resetFilters Clear filters
ValueAddedMonitoring.prototype.resetFilters = function () {
	var context = this.context;
	var basicFilters = $('.filters input[type="checkbox"].basic-filter:not(:checked)', context);
	var flagFilterToggles = $('.filters input[type="checkbox"].flag:checked', context);
	basicFilters.each(function (n, element) {
		element.click();
	});
	flagFilterToggles.each(function (n, element) {
		element.click();
	});
	context.removeClass('filtered');
};

ValueAddedMonitoring.prototype.gatherGradeInputs = function () {
	this.gradePointValueInputs = [];
	this.gradeStudentsValueInputs = [];

	for (var gradeIndex in this.gradesLookup) {
		var grade = this.gradesLookup[gradeIndex];
		var j = grade['grade_id'];
		// Add to array
		var studentInput = $('.grade-' + j + '.live_result', this.context);
		var pointInput = $('.grade-' + j + '.ucas_value', this.context);
		if (studentInput.length > 0) {
			this.gradeStudentsValueInputs.push(studentInput.first());
		}
		if (pointInput.length > 0) {
			this.gradePointValueInputs.push(pointInput);
		}
	}

	if (this.gradePointValueInputs.length != this.gradeStudentsValueInputs.length) {
		throw "Error: Grade Point and Grade Student input number mismatch [" +
		this.gradePointValueInputs.length + "/" +
		this.gradeStudentsValueInputs.length + "]";
	}

	// Causing monitoring issues: page will not to load VA scores for each class if these errors are thrown.
	// The grade and student values do exist in the markup, and even with after commenting out, data is loading fine.

	// if (this.gradePointValueInputs.length == 0) {
	//     throw "No Grade Point inputs found. [.grade-n-ucas_value]";
	// }

	// if (this.gradeStudentsValueInputs.length == 0) {
	//     throw "No Student Value Inputs found. [.grade-n-live_result]";
	// }
};

ValueAddedMonitoring.prototype.updateGradeValues = function () {
	var context = this.context;
	this.totalStudents = 0;
	this.totalPoints = 0;
	this.gatherGradeInputs();
	for (var j in this.gradePointValueInputs) {
		var points = parseFloat(this.gradePointValueInputs[j].val());
		var students = parseInt(this.gradeStudentsValueInputs[j].val());
		var gradePoints = students * points;
		this.totalPoints += gradePoints;
		this.totalStudents += students;

		// Set total
		$('.grade-total-col-' + j, context).first().html(gradePoints);
	}
};

ValueAddedMonitoring.prototype.recalculateLiveValueAddedScore = function () {
	this.updateGradeValues();
	var valueAddedScore = this.calculateValueAdded(
		this.totalStudents,
		this.totalPoints,
		this.expectedPoints,
		true
	);
	//console.log('actual', this.totalPoints);
	var percentileData = this.calculatePercentile(valueAddedScore, this.percentiles, true);
	this.displayValueAddedPercentile(valueAddedScore, this.totalPoints, percentileData);
	return {
		va: valueAddedScore,
		percentileData: percentileData,
		totalStudents: this.totalStudents,
		totalPoints: this.totalPoints,
		expectedPoints: this.expectedPoints
	};
};

ValueAddedMonitoring.prototype.getStudentsAllocated = function () {
	var total = 0;
	for (var inputId in this.gradeStudentsValueInputs) {
		var input = this.gradeStudentsValueInputs[inputId];
		total += parseInt(input.val());
	}
	return total;
};

ValueAddedMonitoring.prototype.assignmentValuesChanged = function () {
	var targetClass = 'current-class';
	this.updateGradeValues();
	if (this.tinkerCheck()) {
		this.hidePercentile(targetClass);
		this.resetPercentileToMiddle(targetClass);
	} else {
		this.hidePercentile(targetClass);
		var vaData = this.recalculateLiveValueAddedScore();
		this.setVATooltipValues(
			targetClass,
			vaData.va,
			vaData.percentileData,
			vaData.totalPoints,
			vaData.expectedPoints,
			vaData.totalStudents
		);
		this.updateTooltips();
	}
};

/**
 * Calculates VA score (not percentile)
 * Returns null if student count is mismatched
 * Returns float if successful
 * Throws a generic exception if something goes wrong (check console)
 */
ValueAddedMonitoring.prototype.calculateValueAdded = function (totalStudents, totalPoints, expectedPoints) {
	totalStudents = parseInt(totalStudents);
	totalPoints = parseFloat(totalPoints);
	expectedPoints = parseFloat(expectedPoints);

	if (totalStudents == 0 || totalPoints == 0) {
		this.finishedCalculating = true;
		this.noResults = true;
		return 0;
	}

	if (isNaN(totalStudents) || isNaN(totalPoints) || isNaN(expectedPoints)) {
		throw "some values were not valid to calculate Value Added Score - " +
		"students: " + totalStudents +
		", points:" + totalPoints +
		", expected points: " + expectedPoints;
	}

	// Value-Added calculation
	// ((Actual - Expected) / (NumStudents * 100)) +1
	var diff = totalPoints - expectedPoints;
	var valueAddedResult = parseFloat(((diff / (totalStudents * this.multiplier)) + 1).toFixed(2));
	this.finishedCalculating = true;
	this.noResults = false;
	return valueAddedResult;
};

ValueAddedMonitoring.prototype.convertPercentileNumberToData = function (percentileNumber) {
	var percentileKeys = Object.keys(this.percentiles);
	var percentileData = {
		'next': 'N/A',
		'last': 'N/A',
		'percentileIndex': percentileNumber - 1,
		'percentileNumber': percentileNumber
	};
	var nextBoundary = 'N/A';
	var lastBoundary = 'N/A';

	for (var n = 0; n < percentileKeys.length; n++) {
		lastBoundary = parseFloat(this.percentiles[percentileKeys[n]]);
		if (!this.percentiles.hasOwnProperty(percentileKeys[n])) {
			continue;
		}

		if (this.percentiles[n] == percentileNumber) {
			percentileData.next = nextBoundary;
		}
		// Used in next iteration of loop
		nextBoundary = parseFloat(this.percentiles[percentileKeys[n]]);
	}
	return percentileData;
};

ValueAddedMonitoring.prototype.calculatePercentile = function (valueAddedScore, percentiles) {
	var percentileKeys = Object.keys(percentiles);
	var nextBoundary = 'N/A';
	var lastBoundary = 'N/A';

	var percentileData = {
		'next': 'N/A',
		'last': 'N/A',
		'percentileIndex': null
	};

	// Figure out percentile and slide scale along
	for (var n = 0; n < percentileKeys.length; n++) {
		lastBoundary = parseFloat(percentiles[percentileKeys[n]]);
		if (!percentiles.hasOwnProperty(percentileKeys[n])) {
			continue;
		}

		// isLast - We got to the end and still hadn't found percentile so show last one
		var isLast = valueAddedScore < 0 && n == percentileKeys.length - 1;

		if (valueAddedScore >= percentiles[percentileKeys[n]] || isLast) {
			if ($.isNumeric(nextBoundary)) {
				nextBoundary.toFixed(2);
			}

			if ($.isNumeric(lastBoundary)) {
				lastBoundary.toFixed(2);
			}

			percentileData.next = nextBoundary;
			percentileData.last = lastBoundary;
			percentileData.percentileIndex = n;
			percentileData.percentileNumber = n + 1;
			return percentileData;
		}
		// Used in next iteration of loop
		nextBoundary = parseFloat(percentiles[percentileKeys[n]]);
	}
	throw "Percentile not found for value score of " + valueAddedScore;
};

ValueAddedMonitoring.prototype.displayValueAddedPercentile = function (valueAddedScore, points, percentileData) {
	var targetClass = 'current-class';
	$('.' + targetClass + 'Tooltip .percentile-next-boundary span', this.context).first().html(percentileData.next);
	$('.' + targetClass + 'Tooltip .percentile-last-boundary span', this.context).first().html(percentileData.last);
	$('.' + targetClass + 'Tooltip .class-points', this.context).first().html(points.toFixed(0));
	$('.' + targetClass + 'Tooltip .class-value-added-score', this.context).first().html(valueAddedScore);

	if (this.noResults) {
		this.hidePercentile(targetClass);
		return;
	}
	this.displayPercentile(targetClass, percentileData, this.vaBoxHeight);
};

ValueAddedMonitoring.prototype.hidePercentile = function (targetClass) {
	var noDataElement = $('.' + targetClass + ' .no-data', this.context).first();
	var percentileStripElement = $('.' + targetClass + ' .percentile-strip', this.context).first();
	percentileStripElement.hide();
	noDataElement.show();
};

/**
 * Start animating percentile strip fruit-machine style
 */
ValueAddedMonitoring.prototype.displayPercentile = function (targetClass, percentileData, vaBoxHeight) {
	var noDataElement = $('.' + targetClass + ' .no-data', this.context).first();
	var percentileStripElement = $('.' + targetClass + ' .percentile-strip', this.context).first();
	percentileStripElement.stop();
	percentileStripElement.show();
	noDataElement.hide();
	var pixelTarget = this.calculatePercentilePixelTarget(percentileData.percentileIndex, vaBoxHeight);

	$('.' + targetClass + ' .percentile-strip', this.context).first().animate(
		{'top': pixelTarget + 'px'},
		1500,
		'easeInOutBack'
	);
};

ValueAddedMonitoring.prototype.displayCourseAveragePercentile = function (percentileNumber) {
	var targetClass = 'course-average';
	if (!this.courseOverallAverage) {
		this.hidePercentile(targetClass);
		return;
	}
	var percentileData = this.convertPercentileNumberToData(percentileNumber);
	this.displayPercentile(targetClass, percentileData, this.vaBoxHeight);
};

ValueAddedMonitoring.prototype.displayOverallAveragePercentile = function (percentileData) {
	var targetClass = 'class-overall-average';
	if (this.noResults) {
		this.hidePercentile(targetClass);
		return;
	}
	this.displayPercentile(targetClass, percentileData, this.vaBoxHeight);
};

ValueAddedMonitoring.prototype.displayFinalGradePercentile = function (percentileData) {
	var targetClass = 'class-final-average';
	var pixelHeight = $('.' + targetClass + ' .percentile-window').outerHeight();
	if (this.noResults) {
		this.hidePercentile(targetClass);
		return;
	}
	this.displayPercentile(targetClass, percentileData, pixelHeight);
};

/**
 * check if values have been altered for modelling purposes
 */
ValueAddedMonitoring.prototype.tinkerCheck = function () {
	var context = this.context; // For out of scope context
	var totalDiff = 0;
	var totalFormer = 0;
	var totalNow = 0;
	var modelMode = MonitoringObjects.getSelectedAssignment(this) === -1;
	context.removeClass('altered');
	$('.altered-student-mismatch-message', context).hide(); /*css('visibility', 'hidden');*/

	$('input.live_result', context).each(function (i, target) {
		target = $(target);
		var monitor = MonitoringObjects.getMonitor(target);
		var context = monitor.context;
		var currentValue = parseInt(target.val());
		var formerValue = parseInt(target.siblings('.actual_result').first().val());
		var diff = currentValue - formerValue;
		totalDiff += diff;
		if (modelMode) {
			formerValue = monitor.studentOrder.length;
			diff = currentValue;
		}
		var diffDisplays = $('.grade-altered-display-col-' + (i + 1) + ' .diff-display', context);
		var diffDisplay;
		diffDisplays.hide();

		totalNow += currentValue;
		totalFormer += formerValue;

		// Update diff display
		if (diff != 0) {
			if (diff > 0) {
				diffDisplay = diffDisplays.filter('.diff-above').first();
				diffDisplay.html('+' + diff);
				diffDisplay.show();
			} else if (diff < 0) {
				diffDisplay = diffDisplays.filter('.diff-below').first();
				diffDisplay.html(diff);
				diffDisplay.show();
			}
			context.addClass('altered');
		}
	});
	var isDifference = totalDiff != 0 && !modelMode || modelMode && this.studentOrder.length != totalDiff;
	if (isDifference) {
		var mismatchMessage = $('.altered-student-mismatch-message', context).first();
		var sign = totalDiff > 0 ? '+' : '';
		mismatchMessage.find('.altered-student-current-count').html(totalNow);
		if (modelMode) {
			// Show max students number as it starts blank
			mismatchMessage.find('.altered-student-start-count').html(this.studentOrder.length);
		} else {
			mismatchMessage.find('.altered-student-start-count').html(totalFormer);
		}
		mismatchMessage.find('.altered-student-diff-total').html(sign + totalDiff);
		mismatchMessage.show(); /*css('visibility','visible');*/
		return true;
	} else {
		return false;
	}
};

ValueAddedMonitoring.prototype.setTeachers = function (teachers) {
	this.teachers = teachers;
};

ValueAddedMonitoring.prototype.setStats = function (stats) {
	this.stats = stats;
};

ValueAddedMonitoring.prototype.getClassOverallAverageVaData = function () {
	var expectedPoints = 0;
	var actualPoints = 0;
	var students = 0;
	// Loop over assignments
	for (var assignmentId in this.filteredResults) {
		var assignment = this.filteredResults[assignmentId];
		// Loop over results for each assignment
		for (var resultIndex in assignment) {
			if (assignment.hasOwnProperty(resultIndex)) {
				var result = assignment[resultIndex];
				if (!result) {
					continue;
				}
				//console.log(result);
				var expectedPointsData = this.getExpectationData(result.gcsePoints);
				var gradeData = this.getGradeData(result.grade_id);
				expectedPoints += parseFloat(expectedPointsData.ucasValue);
				actualPoints += parseFloat(gradeData.ucasValue);
				students++;
			}
		}
	}
	return {
		expectedPoints: expectedPoints,
		actualPoints: actualPoints,
		students: students
	};
};

ValueAddedMonitoring.prototype.getClassOverallAverage = function () {
	var vaData = this.getClassOverallAverageVaData();
	var expectedPoints = vaData.expectedPoints;
	var actualPoints = vaData.actualPoints;
	var students = vaData.students;

	var vaScore = this.calculateValueAdded(
		students,
		actualPoints,
		expectedPoints
	);
	var vaPercentile = this.calculatePercentile(vaScore, this.percentiles);
	this.setVATooltipValues(
		'class-overall-average',
		vaScore,
		vaPercentile,
		actualPoints,
		expectedPoints,
		students,
		'Total Marks:'
	);
	this.updateTooltips();
	this.overallAverage = vaPercentile.percentileNumber;
	return vaPercentile;
};

ValueAddedMonitoring.prototype.setVATooltipValues = function (
	target,
	va,
	percentileData,
	points,
	expPoints,
	students,
	studentLabel
) {
	studentLabel = typeof studentLabel !== 'undefined' ? studentLabel : null;

	var tooltipSelector = '.' + target + 'Tooltip';
	$(tooltipSelector + ' .percentile-next-boundary span', this.context).html(percentileData.next);
	$(tooltipSelector + ' .percentile-last-boundary span', this.context).html(percentileData.last);
	$(tooltipSelector + ' .class-points', this.context).html(points.toFixed(0));
	$(tooltipSelector + ' .class-value-added-score', this.context).html(va);
	$(tooltipSelector + ' .expected-points', this.context).html(expPoints.toFixed(0));
	$(tooltipSelector + ' .students-included', this.context).html(students);
	if (studentLabel) {
		$(tooltipSelector + ' .students-label', this.context).html(studentLabel);
	}
};

/* Required filtered results therefore filterChange() called before */
ValueAddedMonitoring.prototype.calculateFinalGradeValueAdded = function () {
	var studentIds = [];
	var grades = $.extend({}, this.gradesLookup);
	var expectedPoints = 0;

	// Prep object with new count field
	for (var gradeIndex in grades) {
		if (grades.hasOwnProperty(gradeIndex)) {
			var gradeCounter = grades[gradeIndex];
			gradeCounter.count = 0;
		}
	}
	// Loop over assignments
	for (var assignmentIndex in this.filteredResults) {
		if (this.filteredResults.hasOwnProperty(assignmentIndex)) {
			var assignment = this.filteredResults[assignmentIndex];
			// Loop over results
			for (var resultIndex in assignment) {
				if (assignment.hasOwnProperty(resultIndex)) {
					var result = assignment[resultIndex];
					// If we haven't already counted this student
					if (studentIds.indexOf(result.person_id) == -1) {
						if (result.final_grade != null) {
							studentIds.push(result.person_id);
							var expectedData = this.getExpectationData(result.gcsePoints);
							expectedPoints += parseFloat(expectedData.ucasValue);
							// Add to grade count
							for (var gradeIndex in grades) {
								if (grades.hasOwnProperty(gradeIndex)) {
									var gradeCounter = grades[gradeIndex];
									if (gradeCounter.grade == result.final_grade) {
										gradeCounter.count++;
										break;
									}
								}
							}
						}
					}
				}
			}
		}
	}
	// get ucas points
	var totalFinalPoints = 0;
	for (var gradeIndex in grades) {
		var grade = grades[gradeIndex];
		totalFinalPoints += grade.ucasValue * grade.count;
	}
	var va = this.calculateValueAdded(studentIds.length, totalFinalPoints, expectedPoints);
	return {
		score: va,
		students: studentIds.length,
		points: totalFinalPoints,
		expected: expectedPoints
	};
};

ValueAddedMonitoring.prototype.addChartName = function (chartName) {
	if (this.totalChartsLookup.indexOf(chartName) === -1) {
		this.totalChartsLookup.push(chartName);
	}
};

ValueAddedMonitoring.prototype.addReadyChart = function (chartName) {
	// Check name is not already counted
	if (this.chartsReadyLookup.indexOf(chartName) === -1) {
		this.chartsReadyLookup.push(chartName);
		if (this.chartsReadyTimer == null) {
			var me = this;
			this.chartsReadyTimer = setTimeout(function () {
				me.chartCheck()
			}, 500);
		}
	}
};

ValueAddedMonitoring.prototype.chartCheck = function () {
	if (this.chartsReadyLookup.length == this.totalChartsLookup.length) {
		this.chartsAllReady = true;
		closeModal(this.modalIndex);
		this.filterChange();
	} else {
		var me = this;
		this.chartsReadyTimer = setTimeout(function () {
			me.chartCheck();
		}, 500);
	}
};

ValueAddedMonitoring.prototype.collateExpectationForGcsePoints = function (gcsePoints, initialValueData) {
	var ucas = $('input.initial-value-ucas', this.context);
	$(ucas).each(function (i, target) {
		target = $(target).first();
		var lower = target.data('lower');
		// Is this grade in this gcse bracket?
		if (parseFloat(gcsePoints) >= parseFloat(lower)) {
			// Store in temp data structure
			var gcseUcasValue = parseFloat(target.val());
			var ucasId = target.closest('tr').find('input.initial-ucas-id').first().val();
			if (!initialValueData.hasOwnProperty(ucasId)) {
				// New value object
				initialValueData[ucasId] = {'students': 0, 'value': gcseUcasValue, 'total': 0};
			}
			initialValueData[ucasId].students++;
			initialValueData[ucasId].total += gcseUcasValue;
			initialValueData.totalStudents++;
			initialValueData.totalPoints += gcseUcasValue;
			return false; // Break jQuery "each" loop 
		}
	});
	return initialValueData;
};

ValueAddedMonitoring.prototype.populateExpectations = function (initialValueData) {
	// Don't want to iterate over them they have served purpose
	delete initialValueData.totalStudents;
	delete initialValueData.totalPoints;
	// Re-enter the filtered Predicted UCAS values into table
	for (var ucasId in initialValueData) {
		if (initialValueData.hasOwnProperty(ucasId)) {
			var rowData = initialValueData[ucasId];
			var row = $('tr.initial-ucas-row-' + ucasId, this.context);
			// Add GCSE student count
			$('span.initial-value-students-display', row).first().html(rowData.students);
			$('input.initial-value-students', row).first().val(rowData.students);

			// Add GCSE subtotal
			$('span.initial-value-subtotal-display', row).first().html(rowData.total.toFixed(2));
			$('input.initial-value-subtotal', row).first().val(rowData.total.toFixed(2));
		}
	}
};

ValueAddedMonitoring.prototype.updateTooltips = function () {
	// Update qtips on va results boxes
	var vaBoxes = ['current-class', 'class-overall-average', 'class-final-average'];
	for (var targetClassIndex in vaBoxes) {
		var targetClass = vaBoxes[targetClassIndex];
		var data = $('.' + targetClass + 'Tooltip.results-pop-out', this.context).first().html();
		$('.results.' + targetClass, this.context).each((idx, target) => {
			if (target.hasOwnProperty('_tippy')) {
				tippy(target, {content: data, allowHTML: true});
			}
		});
	}
};

ValueAddedMonitoring.prototype.getSelectedAssignment = function () {
	var selected = null;
	// Check for dropdown value
	selected = parseInt(this.assignmentSelectInput.val());

	// Still nothing found? Use first one from array
	if (!selected) {
		selected = this.assignmentOrder[this.assignmentOrder.length - 1];
	}
	return selected;
};

// TODO move to superclass
ValueAddedMonitoring.prototype.resetPercentileToMiddle = function (targetClass) {
	var percentileStripElement = $('.' + targetClass + ' .percentile-strip', this.context).first();
	var percentileIndex = Math.ceil(Object.keys(this.percentiles).length / 2);
	percentileStripElement.css('top', this.calculatePercentilePixelTarget(percentileIndex, this.vaBoxHeight));
};

// TODO move to superclass
ValueAddedMonitoring.prototype.calculatePercentilePixelTarget = function (percentileIndex, vaBoxHeight) {
	// If you change the height of animated strip also change the CSS and vaboxheight number
	var pixelStart = vaBoxHeight * -1;
	// Uses percentileIndex not actual Number
	var pixelTarget = (percentileIndex * vaBoxHeight) - pixelStart;
	if (pixelTarget > 0) {
		pixelTarget = pixelTarget * -1;
	}
	return pixelTarget;
};
