/*  OpenTally: Open-source election vote counting
 *  Copyright © 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/>.
 */

#![allow(rustdoc::private_intra_doc_links)]
#![allow(unused_unsafe)] // Confuses cargo check

use crate::constraints::Constraints;
use crate::election::{CandidateState, CountState, Election, StageKind};
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
use crate::parser::blt;
use crate::stv;
use crate::ties;

extern crate console_error_panic_hook;

use itertools::Itertools;
use js_sys::Array;
use wasm_bindgen::prelude::wasm_bindgen;

use std::cmp::max;

// Error handling

#[wasm_bindgen]
extern "C" {
	fn wasm_error(message: String);
}

macro_rules! wasm_error {
	($type:expr, $err:expr) => { {
		unsafe { wasm_error(format!("{}: {}", $type, $err)); }
		panic!("{}: {}", $type, $err);
	} }
}

// Init

/// Wrapper for [Fixed::set_dps]
#[wasm_bindgen]
pub fn fixed_set_dps(dps: usize) {
	Fixed::set_dps(dps);
}

/// Wrapper for [GuardedFixed::set_dps]
#[wasm_bindgen]
pub fn gfixed_set_dps(dps: usize) {
	GuardedFixed::set_dps(dps);
}

// Helper macros for making functions

macro_rules! impl_type {
	($type:ident) => { paste::item! {
		// Counting
		
		/// Wrapper for [blt::parse_iterator]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<election_from_blt_$type>](text: String) -> [<Election$type>] {
			// Install panic! hook
			console_error_panic_hook::set_once();
			
			let election: Election<$type> = match blt::parse_iterator(text.chars().peekable()) {
				Ok(e) => e,
				Err(err) => wasm_error!("Syntax Error", err),
			};
			return [<Election$type>](election);
		}
		
		/// Call [Constraints::from_con] and set [Election::constraints]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<election_load_constraints_$type>](election: &mut [<Election$type>], text: String) {
			election.0.constraints = Some(Constraints::from_con(text.lines().map(|s| s.to_string()).into_iter()));
		}
		
		/// Wrapper for [stv::preprocess_election]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<preprocess_election_$type>](election: &mut [<Election$type>], opts: &STVOptions) {
			stv::preprocess_election(&mut election.0, &opts.0);
		}
		
		/// Wrapper for [stv::count_init]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
			match stv::count_init(&mut state.0, opts.as_static()) {
				Ok(_) => (),
				Err(err) => wasm_error!("Error", err),
			}
		}
		
		/// Wrapper for [stv::count_one_stage]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> bool {
			match stv::count_one_stage::<[<$type>]>(&mut state.0, &opts.0) {
				Ok(v) => v,
				Err(err) => wasm_error!("Error", err),
			}
		}
		
		// Reporting
		
		/// Wrapper for [init_results_table]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
			return init_results_table(&election.0, &opts.0, report_style);
		}
		
		/// Wrapper for [describe_count]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &STVOptions) -> String {
			return describe_count(filename, &election.0, &opts.0);
		}
		
		/// Wrapper for [update_results_table]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions, report_style: &str) -> Array {
			return update_results_table(stage_num, &state.0, &opts.0, report_style);
		}
		
		/// Wrapper for [update_stage_comments]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<update_stage_comments_$type>](state: &[<CountState$type>]) -> String {
			return update_stage_comments(&state.0);
		}
		
		/// Wrapper for [finalise_results_table]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
			return finalise_results_table(&state.0, report_style);
		}
		
		/// Wrapper for [final_result_summary]
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<final_result_summary_$type>](state: &[<CountState$type>], opts: &STVOptions) -> String {
			return final_result_summary(&state.0, &opts.0);
		}
		
		// Wrapper structs
		
		/// Wrapper for [CountState]
		///
		/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
		///
		#[wasm_bindgen]
		pub struct [<CountState$type>](CountState<'static, $type>);
		#[wasm_bindgen]
		impl [<CountState$type>] {
			/// Create a new [CountState] wrapper
			pub fn new(election: &[<Election$type>]) -> Self {
				return [<CountState$type>](CountState::new(election.as_static()));
			}
		}
		
		/// Wrapper for [Election]
		///
		/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
		///
		#[wasm_bindgen]
		pub struct [<Election$type>](Election<$type>);
		#[wasm_bindgen]
		impl [<Election$type>] {
			/// Return [Election::seats]
			pub fn seats(&self) -> usize { self.0.seats }
			
			/// Return the underlying [Election] as a `&'static Election`
			///
			/// # Safety
			/// This assumes that the underlying [Election] is valid for the `'static` lifetime, as it would be if the [Election] were created from Javascript.
			///
			fn as_static(&self) -> &'static Election<$type> {
				unsafe {
					let ptr = &self.0 as *const Election<$type>;
					&*ptr
				}
			}
		}
	}}
}

impl_type!(Fixed);
impl_type!(GuardedFixed);
impl_type!(NativeFloat64);
impl_type!(Rational);

/// Wrapper for [stv::STVOptions]
#[wasm_bindgen]
pub struct STVOptions(stv::STVOptions);

#[wasm_bindgen]
impl STVOptions {
	/// Wrapper for [stv::STVOptions::new]
	pub fn new(
		round_surplus_fractions: Option<usize>,
		round_values: Option<usize>,
		round_votes: Option<usize>,
		round_quota: Option<usize>,
		sum_surplus_transfers: &str,
		meek_surplus_tolerance: String,
		normalise_ballots: bool,
		quota: &str,
		quota_criterion: &str,
		quota_mode: &str,
		ties: Array,
		random_seed: String,
		surplus: &str,
		surplus_order: &str,
		transferable_only: bool,
		exclusion: &str,
		meek_nz_exclusion: bool,
		sample: &str,
		sample_per_ballot: bool,
		early_bulk_elect: bool,
		bulk_exclude: bool,
		defer_surpluses: bool,
		immediate_elect: bool,
		min_threshold: String,
		constraints_path: Option<String>,
		constraint_mode: &str,
		pp_decimals: usize,
	) -> Self {
		Self(stv::STVOptions::new(
			round_surplus_fractions,
			round_values,
			round_votes,
			round_quota,
			sum_surplus_transfers.into(),
			meek_surplus_tolerance,
			normalise_ballots,
			quota.into(),
			quota_criterion.into(),
			quota_mode.into(),
			ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)),
			surplus.into(),
			surplus_order.into(),
			transferable_only,
			exclusion.into(),
			meek_nz_exclusion,
			sample.into(),
			sample_per_ballot,
			early_bulk_elect,
			bulk_exclude,
			defer_surpluses,
			immediate_elect,
			min_threshold,
			constraints_path,
			constraint_mode.into(),
			false,
			false,
			pp_decimals,
		))
	}
	
	/// Wrapper for [stv::STVOptions::validate]
	pub fn validate(&self) {
		match self.0.validate() {
			Ok(_) => {}
			Err(err) => { wasm_error!("Error", err) }
		}
	}
}

impl STVOptions {
	/// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions`
	///
	/// # Safety
	/// This assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript.
	///
	fn as_static(&self) -> &'static stv::STVOptions {
		unsafe {
			let ptr = &self.0 as *const stv::STVOptions;
			&*ptr
		}
	}
}

// Reporting

/// Generate the lead-in description of the count in HTML
fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String {
	let mut result = String::from("<p>Count computed by OpenTally (revision ");
	result.push_str(crate::VERSION);
	let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
	result.push_str(&format!(r#"). Read {:.0} ballots from &lsquo;{}&rsquo; for election &lsquo;{}&rsquo;. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.len(), election.seats));
	
	let opts_str = opts.describe::<N>();
	if opts_str.len() > 0 {
		result.push_str(&format!(r#"Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, opts_str))
	} else {
		result.push_str(r#"Counting using default options.</p>"#);
	}
	
	return result;
}

#[inline]
fn should_show_vre(opts: &stv::STVOptions) -> bool {
	return opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 || opts.early_bulk_elect;
}

/// Generate the first column of the HTML results table
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String {
	let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
	
	if report_style == "ballots_votes" {
		result.push_str(&r#"<tr class="hint-papers-votes"><td></td></tr>"#);
	}
	
	for candidate in election.candidates.iter() {
		if report_style == "votes_transposed" {
			result.push_str(&format!(r#"<tr class="candidate transfers"><td>{}</td></tr>"#, candidate.name));
		} else {
			result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
		}
	}
	
	if report_style == "votes_transposed" {
		result.push_str(r#"<tr class="info transfers"><td>Exhausted</td></tr>"#);
	} else {
		result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr>"#);
	}
	
	if report_style == "votes_transposed" {
		result.push_str(r#"<tr class="info transfers"><td>Loss by fraction</td></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
	} else {
		result.push_str(r#"<tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
	}
	
	if should_show_vre(opts) {
		result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
	}
	
	return result;
}

/// Generate subsequent columns of the HTML results table
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
	let result = Array::new();
	
	// Insert borders to left of new exclusions in Wright STV
	let classes_o; // Outer version
	let classes_i; // Inner version
	if opts.exclusion == stv::ExclusionMethod::Wright && (if let StageKind::ExclusionOf(_) = state.title { true } else { false }) {
		classes_o = r#" class="blw""#;
		classes_i = r#"blw "#;
	} else {
		classes_o = "";
		classes_i = "";
	}
	
	// Hide transfers column for first preferences if transposed
	let hide_xfers_trsp;
	if let StageKind::FirstPreferences = state.title {
		hide_xfers_trsp = true;
	} else if opts.exclusion == stv::ExclusionMethod::Wright && (if let StageKind::ExclusionOf(_) = state.title { true } else { false }) {
		hide_xfers_trsp = true;
	} else {
		hide_xfers_trsp = false;
	}
	
	// Header rows
	let kind_str = state.title.kind_as_string();
	let title_str;
	match &state.title {
		StageKind::FirstPreferences | StageKind::SurplusesDistributed | StageKind::BulkElection => {
			title_str = format!("{}", state.title);
		}
		StageKind::SurplusOf(candidate) => {
			title_str = candidate.name.clone();
		}
		StageKind::ExclusionOf(candidates) => {
			if candidates.len() > 5 {
				let first_4_cands = candidates.iter().map(|c| &c.name).sorted().take(4).join(",<br>");
				title_str = format!("{},<br>and {} others", first_4_cands, candidates.len() - 4);
			} else {
				title_str = candidates.iter().map(|c| &c.name).join(",<br>");
			}
		}
	};
	
	match report_style {
		"votes" => {
			result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
			result.push(&format!(r#"<td{}>{}</td>"#, classes_o, kind_str).into());
			result.push(&format!(r#"<td{}>{}</td>"#, classes_o, title_str).into());
		}
		"votes_transposed" => {
			if hide_xfers_trsp {
				result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
				result.push(&format!(r#"<td{}>{}</td>"#, classes_o, kind_str).into());
				result.push(&format!(r#"<td{}>{}</td>"#, classes_o, title_str).into());
			} else {
				result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
				result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str).into());
				result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str).into());
				//result.push(&format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1).into());
			}
		}
		"ballots_votes" => {
			result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
			result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str).into());
			result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str).into());
			result.push(&format!(r#"<td{}>Ballots</td><td>Votes</td>"#, classes_o).into());
		}
		_ => unreachable!("Invalid report_style")
	}
	
	for candidate in state.election.candidates.iter() {
		let count_card = &state.candidates[candidate];
		
		// TODO: REFACTOR THIS!!
		
		match report_style {
			"votes" => {
				match count_card.state {
					CandidateState::Hopeful | CandidateState::Guarded => {
						result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
						result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
					}
					CandidateState::Elected => {
						result.push(&format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
						result.push(&format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
					}
					CandidateState::Doomed => {
						result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
						result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
					}
					CandidateState::Withdrawn => {
						result.push(&format!(r#"<td class="{}count excluded"></td>"#, classes_i).into());
						result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, classes_i).into());
					}
					CandidateState::Excluded => {
						result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
						if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
							result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, classes_i).into());
						} else {
							result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
						}
					}
				}
			}
			"votes_transposed" => {
				match count_card.state {
					CandidateState::Hopeful | CandidateState::Guarded => {
						if hide_xfers_trsp {
							result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
						} else {
							result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
						}
					}
					CandidateState::Elected => {
						if hide_xfers_trsp {
							result.push(&format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
						} else {
							result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
						}
					}
					CandidateState::Doomed => {
						if hide_xfers_trsp {
							result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
						} else {
							result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
						}
					}
					CandidateState::Withdrawn => {
						if hide_xfers_trsp {
							result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, classes_i).into());
						} else {
							result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i).into());
						}
					}
					CandidateState::Excluded => {
						if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
							if hide_xfers_trsp {
								result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, classes_i).into());
							} else {
								result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">Ex</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
							}
						} else {
							if hide_xfers_trsp {
								result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
							} else {
								result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
							}
						}
					}
				}
			}
			"ballots_votes" => {
				match count_card.state {
					CandidateState::Hopeful | CandidateState::Guarded => {
						result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
						result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
					}
					CandidateState::Elected => {
						result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
						result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
					}
					CandidateState::Doomed => {
						result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
						result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
					}
					CandidateState::Withdrawn => {
						result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, classes_i).into());
						result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i).into());
					}
					CandidateState::Excluded => {
						result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
						if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
							result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, classes_i).into());
						} else {
							result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
						}
					}
				}
			}
			_ => unreachable!("Invalid report_style")
		}
	}
	
	match report_style {
		"votes" => {
			result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
		}
		"votes_transposed" => {
			if hide_xfers_trsp {
				result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into());
				result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
			} else {
				result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)).into());
				result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals), pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
			}
		}
		"ballots_votes" => {
			result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
		}
		_ => unreachable!("Invalid report_style")
	}
	
	// Calculate total votes
	let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
	total_vote += &state.exhausted.votes;
	total_vote += &state.loss_fraction.votes;
	
	match report_style {
		"votes" => {
			result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
		}
		"votes_transposed" => {
			if hide_xfers_trsp {
				result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
			} else {
				result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
			}
		}
		"ballots_votes" => {
			// Calculate total ballots
			let mut total_ballots = state.candidates.values().fold(N::zero(), |acc, cc| { acc + cc.num_ballots() });
			total_ballots += state.exhausted.num_ballots();
			result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)).into());
		}
		_ => unreachable!("Invalid report_style")
	}
	
	if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
		result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
	} else {
		result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
	}
	
	if should_show_vre(opts) {
		if let Some(vre) = &state.vote_required_election {
			if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
				result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)).into());
			} else {
				result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)).into());
			}
		} else {
			if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
				result.push(&format!(r#"<td class="{}count"></td>"#, classes_i).into());
			} else {
				result.push(&format!(r#"<td class="{}count"></td><td class="count"></td>"#, classes_i).into());
			}
		}
	}
	
	return result;
}

/// Get the comment for the current stage
fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
	return state.logger.render().join(" ");
}

/// Generate the final column of the HTML results table
fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
	let result = Array::new();
	
	// Header rows
	match report_style {
		"votes" | "votes_transposed" => {
			result.push(&r#"<td rowspan="3"></td>"#.into());
			result.push(&"".into());
			result.push(&"".into());
		}
		"ballots_votes" => {
			result.push(&r#"<td rowspan="4"></td>"#.into());
			result.push(&"".into());
			result.push(&"".into());
			result.push(&"".into());
		}
		_ => unreachable!("Invalid report_style")
	}
	
	let rowspan = if report_style == "votes_transposed" { "" } else { r#" rowspan="2""# };
	
	// Candidate states
	for candidate in state.election.candidates.iter() {
		let count_card = &state.candidates[candidate];
		if count_card.state == stv::CandidateState::Elected {
			result.push(&format!(r#"<td{} class="bb elected">ELECTED {}</td>"#, rowspan, count_card.order_elected).into());
		} else if count_card.state == stv::CandidateState::Excluded {
			result.push(&format!(r#"<td{} class="bb excluded">Excluded {}</td>"#, rowspan, -count_card.order_elected).into());
		} else if count_card.state == stv::CandidateState::Withdrawn {
			result.push(&format!(r#"<td{} class="bb excluded">Withdrawn</td>"#, rowspan).into());
		} else {
			result.push(&format!(r#"<td{} class="bb"></td>"#, rowspan).into());
		}
		
		if report_style != "votes_transposed" {
			result.push(&"".into());
		}
	}
	
	return result;
}

/// Generate the final lead-out text summarising the result of the election
fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
	let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
	
	let mut winners = Vec::new();
	for (candidate, count_card) in state.candidates.iter() {
		if count_card.state == CandidateState::Elected {
			winners.push((candidate, count_card.order_elected, &count_card.keep_value));
		}
	}
	winners.sort_unstable_by(|a, b| a.1.cmp(&b.1));
	
	for (winner, _, kv_opt) in winners.into_iter() {
		if let Some(kv) = kv_opt {
			result.push_str(&format!("<li>{} (<i>kv</i> = {:.dps2$})</li>", winner.name, kv, dps2=max(opts.pp_decimals, 2)));
		} else {
			result.push_str(&format!("<li>{}</li>", winner.name));
		}
	}
	
	result.push_str("</ol>");
	return result;
}

/// HTML pretty-print the number to the specified decimal places
fn pp<N: Number>(n: &N, dps: usize) -> String {
	if n.is_zero() {
		return "".to_string();
	}
	
	let mut raw = format!("{:.dps$}", n, dps=dps);
	if raw.contains('.') {
		raw = raw.replacen(".", ".<sup>", 1);
		raw.push_str("</sup>");
	}
	
	if raw.starts_with('-') {
		raw = raw.replacen("-", "&minus;", 1);
	}
	
	return raw;
}

/// Signed version of [pp]
fn pps<N: Number>(n: &N, dps: usize) -> String {
	if n.is_zero() {
		return "".to_string();
	}
	
	let mut raw = format!("{:.dps$}", n, dps=dps);
	if raw.contains('.') {
		raw = raw.replacen(".", ".<sup>", 1);
		raw.push_str("</sup>");
	}
	
	if raw.starts_with('-') {
		raw = raw.replacen("-", "&minus;", 1);
	} else {
		raw.insert(0, '+');
	}
	
	return raw;
}
