/*
    pyRCV2: Preferential vote counting
    Copyright © 2020–2021  Lee Yingtong Li (RunasSudo)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

function clickAdvancedOptions() {
	if (document.getElementById('divAdvancedOptions').style.display === 'none') {
		document.getElementById('divAdvancedOptions').style.display = 'grid';
		document.getElementById('btnAdvancedOptions').innerHTML = 'Hide advanced options';
	} else {
		document.getElementById('divAdvancedOptions').style.display = 'none';
		document.getElementById('btnAdvancedOptions').innerHTML = 'Show advanced options';
	}
}

function changePreset() {
	if (document.getElementById('selPreset').value === 'scottish') {
		document.getElementById('selQuotaCriterion').value = 'geq';
		document.getElementById('selQuota').value = 'droop';
		document.getElementById('selQuotaMode').value = 'static';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = false;
		document.getElementById('chkDeferSurpluses').checked = false;
		document.getElementById('selNumbers').value = 'fixed';
		document.getElementById('txtDP').value = '5';
		document.getElementById('txtPPDP').value = '2';
		document.getElementById('chkRoundQuota').checked = true;
		document.getElementById('txtRoundQuota').value = '0';
		document.getElementById('chkRoundVotes').checked = false;
		document.getElementById('chkRoundTVs').checked = false;
		document.getElementById('chkRoundWeights').checked = false;
		document.getElementById('selSurplus').value = 'size';
		document.getElementById('selTransfers').value = 'wig';
		document.getElementById('selPapers').value = 'both';
		document.getElementById('selExclusion').value = 'one_round';
		document.getElementById('selTies').value = 'backwards_random';
	} else if (document.getElementById('selPreset').value === 'stvc') {
		document.getElementById('selQuotaCriterion').value = 'gt';
		document.getElementById('selQuota').value = 'droop_exact';
		document.getElementById('selQuotaMode').value = 'progressive';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = true;
		document.getElementById('chkDeferSurpluses').checked = false;
		document.getElementById('selNumbers').value = 'rational';
		document.getElementById('txtPPDP').value = '2';
		document.getElementById('chkRoundQuota').checked = false;
		document.getElementById('chkRoundVotes').checked = false;
		document.getElementById('chkRoundTVs').checked = false;
		document.getElementById('chkRoundWeights').checked = false;
		document.getElementById('selSurplus').value = 'size';
		document.getElementById('selTransfers').value = 'wig';
		document.getElementById('selPapers').value = 'both';
		document.getElementById('selExclusion').value = 'one_round';
		document.getElementById('selTies').value = 'backwards_random';
	} else if (document.getElementById('selPreset').value === 'senate') {
		document.getElementById('selQuotaCriterion').value = 'geq';
		document.getElementById('selQuota').value = 'droop';
		document.getElementById('selQuotaMode').value = 'static';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = true;
		document.getElementById('chkDeferSurpluses').checked = false;
		document.getElementById('selNumbers').value = 'fixed';
		document.getElementById('txtDP').value = '5';
		document.getElementById('txtPPDP').value = '0';
		document.getElementById('chkRoundQuota').checked = true;
		document.getElementById('txtRoundQuota').value = '0';
		document.getElementById('chkRoundVotes').checked = true;
		document.getElementById('txtRoundVotes').value = '0';
		document.getElementById('chkRoundTVs').checked = false;
		document.getElementById('chkRoundWeights').checked = false;
		document.getElementById('selSurplus').value = 'order';
		document.getElementById('selTransfers').value = 'uig';
		document.getElementById('selPapers').value = 'both';
		document.getElementById('selExclusion').value = 'by_value';
		document.getElementById('selTies').value = 'backwards_random';
	} else if (document.getElementById('selPreset').value === 'meek') {
		document.getElementById('selQuotaCriterion').value = 'gt';
		document.getElementById('selQuota').value = 'droop_exact';
		document.getElementById('selQuotaMode').value = 'progressive';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = false;
		document.getElementById('chkDeferSurpluses').checked = false;
		document.getElementById('selNumbers').value = 'fixed';
		document.getElementById('txtDP').value = '9';
		document.getElementById('txtPPDP').value = '2';
		document.getElementById('chkRoundQuota').checked = false;
		document.getElementById('chkRoundVotes').checked = false;
		document.getElementById('chkRoundTVs').checked = false;
		document.getElementById('chkRoundWeights').checked = false;
		document.getElementById('selSurplus').value = 'size';
		document.getElementById('selTransfers').value = 'meek';
		document.getElementById('selPapers').value = 'both';
		document.getElementById('selExclusion').value = 'one_round';
		document.getElementById('selTies').value = 'random';
	} else if (document.getElementById('selPreset').value === 'wright') {
		document.getElementById('selQuotaCriterion').value = 'geq';
		document.getElementById('selQuota').value = 'droop';
		document.getElementById('selQuotaMode').value = 'static';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = true;
		document.getElementById('chkDeferSurpluses').checked = false;
		document.getElementById('selNumbers').value = 'fixed';
		document.getElementById('txtDP').value = '5';
		document.getElementById('txtPPDP').value = '2';
		document.getElementById('chkRoundQuota').checked = true;
		document.getElementById('txtRoundQuota').value = '0';
		document.getElementById('chkRoundVotes').checked = false;
		document.getElementById('chkRoundTVs').checked = false;
		document.getElementById('chkRoundWeights').checked = false;
		document.getElementById('selSurplus').value = 'size';
		document.getElementById('selTransfers').value = 'wig';
		document.getElementById('selPapers').value = 'both';
		document.getElementById('selExclusion').value = 'wright';
		document.getElementById('selTies').value = 'backwards_random';
	} else if (document.getElementById('selPreset').value === 'prsa77') {
		document.getElementById('selQuotaCriterion').value = 'geq';
		document.getElementById('selQuota').value = 'droop';
		document.getElementById('selQuotaMode').value = 'static';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = false;
		document.getElementById('chkDeferSurpluses').checked = true;
		document.getElementById('selNumbers').value = 'fixed';
		document.getElementById('txtDP').value = '5';
		document.getElementById('txtPPDP').value = '3';
		document.getElementById('chkRoundQuota').checked = true;
		document.getElementById('txtRoundQuota').value = '3';
		document.getElementById('chkRoundVotes').checked = true;
		document.getElementById('txtRoundVotes').value = '3';
		document.getElementById('chkRoundTVs').checked = true;
		document.getElementById('txtRoundTVs').value = '3';
		document.getElementById('chkRoundWeights').checked = true;
		document.getElementById('txtRoundWeights').value = '3';
		document.getElementById('selSurplus').value = 'order';
		document.getElementById('selTransfers').value = 'eg';
		document.getElementById('selPapers').value = 'transferable';
		document.getElementById('selExclusion').value = 'parcels_by_order';
		document.getElementById('selTies').value = 'backwards_random';
	} else if (document.getElementById('selPreset').value === 'ers97') {
		document.getElementById('selQuotaCriterion').value = 'geq';
		document.getElementById('selQuota').value = 'droop_exact';
		document.getElementById('selQuotaMode').value = 'ers97';
		document.getElementById('chkBulkElection').checked = true;
		document.getElementById('chkBulkExclusion').checked = true;
		document.getElementById('chkDeferSurpluses').checked = true;
		document.getElementById('selNumbers').value = 'fixed';
		document.getElementById('txtDP').value = '5';
		document.getElementById('txtPPDP').value = '2';
		document.getElementById('chkRoundQuota').checked = true;
		document.getElementById('txtRoundQuota').value = '2';
		document.getElementById('chkRoundVotes').checked = true;
		document.getElementById('txtRoundVotes').value = '2';
		document.getElementById('chkRoundTVs').checked = true;
		document.getElementById('txtRoundTVs').value = '2';
		document.getElementById('chkRoundWeights').checked = true;
		document.getElementById('txtRoundWeights').value = '2';
		document.getElementById('selSurplus').value = 'size';
		document.getElementById('selTransfers').value = 'eg';
		document.getElementById('selPapers').value = 'transferable';
		document.getElementById('selExclusion').value = 'by_value';
		document.getElementById('selTies').value = 'forwards_random';
	}
}

async function clickCount() {
	// Read BLT file
	let bltFile = document.getElementById('bltFile').files[0];
	let text = await bltFile.text();
	
	// Initialise table rows
	let tblResults = document.getElementById('result');
	tblResults.innerHTML = '';
	let candMap = {}; // candidate name -> rows
	let divResultLogs1 = document.getElementById('resultLogs1');
	let divResultLogs2 = document.getElementById('resultLogs2');
	divResultLogs1.innerHTML = '';
	divResultLogs2.innerHTML = '';
	
	// Step election
	let worker = new Worker('worker.js');
	let election;
	let trComment, trExhausted1, trExhausted2, trLTF1, trLTF2, trTotal, trQuota, trVRE;
	let olLogs;
	
	worker.onmessage = function(evt) {
		if (evt.data.type === 'require_input') {
			let input = window.prompt(evt.data.message);
			if (input !== null) {
				worker.postMessage({'type': 'require_input', 'input': input});
			}
		}
		
		if (evt.data.type === 'stv_exception') {
			window.alert('An error occurred while counting the votes:\n\n' + evt.data.message);
		}
		
		if (evt.data.type === 'init') {
			election = evt.data.election;
			
			// Comment row
			trComment = document.createElement('tr');
			let elTd = document.createElement('td');
			trComment.appendChild(elTd);
			tblResults.appendChild(trComment);
			
			// Candidates
			for (let candidate of election.candidates) {
				let elTr1 = document.createElement('tr');
				let elTr2 = document.createElement('tr');
				
				elTd = document.createElement('td');
				elTd.setAttribute('rowspan', '2');
				elTd.style.borderTop = '1px solid black';
				elTd.innerText = candidate;
				elTr1.appendChild(elTd);
				
				tblResults.appendChild(elTr1);
				tblResults.appendChild(elTr2);
				
				candMap[candidate] = [elTr1, elTr2];
			}
			
			// Exhausted votes row
			trExhausted1 = document.createElement('tr');
			trExhausted1.classList.add('info');
			trExhausted2 = document.createElement('tr');
			trExhausted2.classList.add('info');
			
			elTd = document.createElement('td');
			elTd.setAttribute('rowspan', '2');
			elTd.style.borderTop = '1px solid black';
			elTd.innerText = 'Exhausted';
			trExhausted1.appendChild(elTd);
			
			tblResults.appendChild(trExhausted1);
			tblResults.appendChild(trExhausted2);
			
			// Loss to fraction row
			trLTF1 = document.createElement('tr');
			trLTF1.classList.add('info');
			trLTF2 = document.createElement('tr');
			trLTF2.classList.add('info');
			
			elTd = document.createElement('td');
			elTd.setAttribute('rowspan', '2');
			elTd.style.borderTop = '1px solid black';
			elTd.innerText = 'Loss to fraction';
			trLTF1.appendChild(elTd);
			
			tblResults.appendChild(trLTF1);
			tblResults.appendChild(trLTF2);
			
			// Total row
			trTotal = document.createElement('tr');
			trTotal.classList.add('info');
			elTd = document.createElement('td');
			elTd.style.borderTop = '1px solid black';
			elTd.innerText = 'Total';
			trTotal.appendChild(elTd);
			tblResults.appendChild(trTotal);
			
			// Quota row
			trQuota = document.createElement('tr');
			trQuota.classList.add('info');
			elTd = document.createElement('td');
			elTd.style.borderTop = '1px solid black';
			elTd.style.borderBottom = '1px solid black';
			elTd.innerText = 'Quota';
			trQuota.appendChild(elTd);
			tblResults.appendChild(trQuota);
			
			// Vote required for election row
			if (document.getElementById('selQuotaMode').value === 'ers97') {
				trVRE = document.createElement('tr');
				trVRE.classList.add('info');
				elTd = document.createElement('td');
				elTd.style.borderTop = '1px solid black';
				elTd.style.borderBottom = '1px solid black';
				elTd.innerText = 'Vote required for election';
				trVRE.appendChild(elTd);
				tblResults.appendChild(trVRE);
			}
			
			// Result logs
			let elP = document.createElement('p');
			let filePath = document.getElementById('bltFile').value;
			filePath = filePath.substring(Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')) + 1);
			if (pyRCV2version === 'GITVERSION') {
				elP.innerText = 'Count computed by pyRCV2 (development version).';
			} else {
				elP.innerText = 'Count computed by pyRCV2 (revision ' + pyRCV2version + ').';
			}
			elP.innerText += ' Read ' + evt.data.total_ballots + ' ballots from ‘' + filePath + '’ for election ‘' + election.name + '’.';
			elP.innerText += ' There are ' + election.candidates.length + ' candidates for ' + election.seats + ' vacancies.';
			if (evt.data.options === '') {
				elP.innerText += ' Counting using default options.';
			} else {
				elP.innerHTML += ' Counting using options <span style="font-family:monospace;">' + evt.data.options + '</span>.';
			}
			divResultLogs1.appendChild(elP);
			
			elP = document.createElement('p');
			elP.innerText = 'Stage comments:';
			divResultLogs2.appendChild(elP);
			olLogs = document.createElement('ol');
			divResultLogs2.appendChild(olLogs);
		}
		
		if (evt.data.type === 'result') {
			let result = evt.data.result;
			
			// Display results
			elTd = document.createElement('td');
			elTd.innerText = result.stage + '. ' + result.comment;
			trComment.appendChild(elTd);
			
			for (let [candidate, countCard] of result.candidates) {
				[elTr1, elTr2] = candMap[candidate];
				
				elTd = document.createElement('td');
				elTd.classList.add('count');
				if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
					elTd.classList.add('excluded');
				} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
					elTd.classList.add('elected');
				}
				elTd.style.borderTop = '1px solid black';
				elTd.innerHTML = ppVotes(countCard.transfers);
				elTr1.appendChild(elTd);
				
				elTd = document.createElement('td');
				elTd.classList.add('count');
				
				if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
					elTd.classList.add('elected');
					elTd.innerHTML = ppVotes(countCard.votes);
					elTr1.querySelector('td:first-child').classList.add('elected');
				} else {
					elTr1.querySelector('td:first-child').classList.remove('elected');
					
					if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
						elTd.classList.add('excluded');
						elTd.innerText = 'WD';
					} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
						elTd.classList.add('excluded');
						elTd.innerText = 'Ex';
					} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
						elTd.classList.add('excluded');
						elTd.innerHTML = ppVotes(countCard.votes);
					} else {
						elTd.innerHTML = ppVotes(countCard.votes);
					}
				}
				
				elTr2.appendChild(elTd);
			}
			
			// Display exhausted votes
			elTd = document.createElement('td');
			elTd.classList.add('count');
			elTd.style.borderTop = '1px solid black';
			elTd.innerHTML = ppVotes(result.exhausted.transfers);
			trExhausted1.appendChild(elTd);
			
			elTd = document.createElement('td');
			elTd.classList.add('count');
			elTd.innerHTML = ppVotes(result.exhausted.votes);
			trExhausted2.appendChild(elTd);
			
			// Display loss to fraction
			elTd = document.createElement('td');
			elTd.classList.add('count');
			elTd.style.borderTop = '1px solid black';
			elTd.innerHTML = ppVotes(result.loss_fraction.transfers);
			trLTF1.appendChild(elTd);
			
			elTd = document.createElement('td');
			elTd.classList.add('count');
			elTd.innerHTML = ppVotes(result.loss_fraction.votes);
			trLTF2.appendChild(elTd);
			
			// Display total
			elTd = document.createElement('td');
			elTd.classList.add('count');
			elTd.style.borderTop = '1px solid black';
			elTd.innerHTML = ppVotes(result.total);
			trTotal.appendChild(elTd);
			
			// Display quota
			elTd = document.createElement('td');
			elTd.classList.add('count');
			elTd.style.borderTop = '1px solid black';
			elTd.style.borderBottom = '1px solid black';
			elTd.innerHTML = ppVotes(result.quota);
			trQuota.appendChild(elTd);
			
			// Display vote required for election
			if (result.vote_required_election !== null) {
				elTd = document.createElement('td');
				elTd.classList.add('count');
				elTd.style.borderTop = '1px solid black';
				elTd.style.borderBottom = '1px solid black';
				elTd.innerHTML = ppVotes(result.vote_required_election);
				trVRE.appendChild(elTd);
			}
			
			// Result logs
			let elLi = document.createElement('li');
			elLi.innerText = result.logs.join(' ');
			olLogs.appendChild(elLi);
		}
		
		if (evt.data.type === 'done') {
			let result = evt.data.result;
			let winners = [];
			
			// Display results
			
			for (let [candidate, countCard] of result.candidates) {
				[elTr1, elTr2] = candMap[candidate];
				elTd = document.createElement('td');
				elTd.setAttribute('rowspan', '2');
				if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
					elTd.classList.add('excluded');
					elTd.innerHTML = 'Withdrawn';
				} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
					elTd.classList.add('excluded');
					elTd.innerHTML = 'Excluded&nbsp;' + (-countCard.order_elected);
				} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
					elTd.classList.add('elected');
					elTd.innerHTML = 'ELECTED&nbsp;' + countCard.order_elected;
					winners.append([candidate, countCard]);
				}
				elTd.style.borderTop = '1px solid black';
				elTr1.appendChild(elTd);
			}
			
			elTd.style.borderBottom = '1px solid black';
			
			let elP = document.createElement('p');
			elP.innerText = 'Count complete. The winning candidates are, in order of election:'
			divResultLogs2.appendChild(elP);
			
			winners.sort(function(x1, x2) { return x1[1].order_elected - x2[1].order_elected; });
			let elOl = document.createElement('ol');
			for (let [candidate, countCard] of winners) {
				let elLi = document.createElement('li');
				elLi.innerText = candidate;
				elOl.appendChild(elLi);
			}
			divResultLogs2.appendChild(elOl);
			
			document.getElementById('printPane').style.display = 'block';
		}
	}
	
	worker.onerror = function(evt) {
		window.alert('An unknown error occurred while counting the votes. More details may be available in the browser\'s developer console.');
		throw evt;
	}
	
	worker.postMessage({
		'type': 'init',
		'data': {
			'numbers': document.getElementById('selNumbers').value,
			'fixedDPs': parseInt(document.getElementById('txtDP').value),
			'ppDPs': parseInt(document.getElementById('txtPPDP').value),
			'transfers': document.getElementById('selTransfers').value,
			'options': {
				'quota_criterion': document.getElementById('selQuotaCriterion').value,
				'quota': document.getElementById('selQuota').value,
				'quota_mode': document.getElementById('selQuotaMode').value,
				'bulk_elect': document.getElementById('chkBulkElection').checked,
				'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
				'defer_surpluses': document.getElementById('chkDeferSurpluses').checked,
				'surplus_order': document.getElementById('selSurplus').value,
				'papers': document.getElementById('selPapers').value,
				'exclusion': document.getElementById('selExclusion').value,
				'ties': document.getElementById('selTies').value,
				'round_quota': document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
				'round_votes': document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
				'round_tvs': document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
				'round_weights': document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
			},
			'seed': document.getElementById('txtSeed').value,
			'data': text
		}
	});
	
	// Pretty printing helper functions
	let ppDPs = parseInt(document.getElementById('txtPPDP').value);
	if (document.getElementById('chkRoundVotes').checked) {
		let ppDPs2 = parseInt(document.getElementById('txtRoundVotes').value);
		if (ppDPs2 < ppDPs) {
			ppDPs = ppDPs2;
		}
	}
	if (document.getElementById('selNumbers').value === 'fixed') {
		let ppDPs2 = parseInt(document.getElementById('txtDP').value);
		if (ppDPs2 < ppDPs) {
			ppDPs = ppDPs2;
		}
	}
	function ppVotes(v) {
		let result = parseFloat(v).toFixed(ppDPs);
		if (parseFloat(result) == 0) {
			return '&nbsp;'
		}
		if (result.indexOf('.') >= 0) {
			result = result.substring(0, result.indexOf('.')) + '<sup>' + result.substring(result.indexOf('.')) + '</sup>';
		}
		return result;
	}
}

// Provide a default seed
if (document.getElementById('txtSeed').value === '') {
	function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
	let d = new Date();
	document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
}

// Split across pages when required for printing

function printResult() {
	let printableWidth; // Printable width in CSS pixels
	let paperSize = document.getElementById('selPaperSize').value;
	if (paperSize === 'A4') {
		printableWidth = (29.7 - 2) * 96 / 2.54;
	} else if (paperSize === 'A3') {
		printableWidth = (42.0 - 2) * 96 / 2.54;
	} else if (paperSize === 'letter') {
		printableWidth = (27.9 - 2) * 96 / 2.54;
	}
	printableWidth = Math.round(printableWidth);
	
	let wprint = window.open('');
	wprint.document.title = 'pyRCV2 Report';
	
	// Add stylesheets
	for (let elCSSBase of document.querySelectorAll('head link')) {
		let elCSS = wprint.document.createElement('link');
		elCSS.rel = elCSSBase.rel;
		elCSS.type = elCSSBase.type;
		elCSS.href = elCSSBase.href;
		wprint.document.head.appendChild(elCSS);
	}
	
	// Configure printing
	let elStyle = wprint.document.createElement('style');
	elStyle.innerHTML = '@page { size: ' + paperSize + ' landscape; margin: 1cm; } @media print { body { padding: 0; } }';
	wprint.document.head.appendChild(elStyle);
	
	let elContainer = wprint.document.createElement('div');
	elContainer.id = 'printContainer';
	elContainer.style.width = printableWidth + 'px';
	wprint.document.body.appendChild(elContainer);
	
	// Copy result logs 1
	let divResultLogs1 = document.getElementById('resultLogs1');
	let divResultLogs2 = wprint.document.createElement('div');
	divResultLogs2.innerHTML = divResultLogs1.innerHTML;
	elContainer.appendChild(divResultLogs2);
	
	// Parse table, accounting for rowspan
	let elTrs1 = document.querySelector('#result').rows;
	let rows = [];
	for (let elTr1 of elTrs1) {
		rows.push([]);
	}
	for (let r = 0; r < elTrs1.length; r++) {
		for (let c = 0; c < elTrs1[r].cells.length; c++) {
			let elTd1 = elTrs1[r].cells[c];
			rows[r].push(elTd1);
			
			let rowspan = elTd1.getAttribute('rowspan');
			// NB: Only works for rowspan in first column
			if (rowspan !== null && c == 0) {
				rowspan = parseInt(rowspan);
				// Add ghost cells
				for (let i = 1; i < rowspan; i++) {
					rows[r + i].push(null);
				}
			}
		}
	}
	
	function copyColumn(c, elTrs2) {
		let tdsAdded = [];
		for (let r = 0; r < rows.length; r++) {
			if (c < rows[r].length) {
				let elTd1 = rows[r][c];
				if (elTd1 !== null) {
					let elTd2 = wprint.document.createElement('td');
					elTd2.innerHTML = elTd1.innerHTML;
					elTd2.className = elTd1.className;
					elTd2.setAttribute('rowspan', elTd1.getAttribute('rowspan'));
					elTd2.setAttribute('style', elTd1.getAttribute('style'));
					elTrs2[r].appendChild(elTd2);
					tdsAdded.push(elTd2);
				}
			}
		}
		return tdsAdded;
	}
	
	function copyTableColumns(startCol) {
		// Add table
		let elTable2 = wprint.document.createElement('table');
		elTable2.className = 'result';
		if (startCol > 1) {
			elTable2.style.pageBreakBefore = 'always';
		}
		elContainer.appendChild(elTable2);
		
		// Add rows
		let elTrs2 = [];
		for (let elTr1 of elTrs1) {
			let elTr2 = wprint.document.createElement('tr');
			elTr2.className = elTr1.className;
			elTrs2.push(elTr2);
			elTable2.appendChild(elTr2);
		}
		
		// Copy first column
		copyColumn(0, elTrs2);
		
		// Copy column by column
		for (let c = startCol; c < rows[0].length + 1; c++) { // +1 to account for final status column
			let tdsAdded = copyColumn(c, elTrs2);
			
			// Check if overflowed
			if (elTable2.clientWidth > printableWidth) {
				for (let elTd2 of tdsAdded) {
					elTd2.remove();
				}
				// Start new table
				return copyTableColumns(c);
			}
		}
	}
	
	copyTableColumns(1);
	
	// Copy result logs2
	divResultLogs1 = document.getElementById('resultLogs2');
	divResultLogs2 = wprint.document.createElement('div');
	divResultLogs2.innerHTML = divResultLogs1.innerHTML;
	elContainer.appendChild(divResultLogs2);
	
	wprint.print();
}
