/*  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(mutable_borrow_reservation_conflict)]

/// Gregory method of surplus distributions
pub mod gregory;
/// Meek method of surplus distributions, etc.
pub mod meek;

/// WebAssembly wrappers
//#[cfg(target_arch = "wasm32")]
pub mod wasm;

use crate::constraints;
use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote};
use crate::sharandom::SHARandom;
use crate::ties::{self, TieStrategy};

use itertools::Itertools;
use wasm_bindgen::prelude::wasm_bindgen;

use std::collections::HashMap;
use std::ops;

/// Options for conducting an STV count
pub struct STVOptions {
	/// Round transfer values to specified decimal places
	pub round_tvs: Option<usize>,
	/// Round ballot weights to specified decimal places
	pub round_weights: Option<usize>,
	/// Round votes to specified decimal places
	pub round_votes: Option<usize>,
	/// Round quota to specified decimal places
	pub round_quota: Option<usize>,
	/// How to calculate votes to credit to candidates in surplus transfers
	pub sum_surplus_transfers: SumSurplusTransfersMode,
	/// (Meek STV) Limit for stopping iteration of surplus distribution
	pub meek_surplus_tolerance: String,
	/// Convert ballots with value >1 to multiple ballots of value 1 (used only for [STVOptions::describe])
	pub normalise_ballots: bool,
	/// Quota type
	pub quota: QuotaType,
	/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
	pub quota_criterion: QuotaCriterion,
	/// Whether to apply a form of progressive quota
	pub quota_mode: QuotaMode,
	/// Tie-breaking method
	pub ties: Vec<TieStrategy>,
	/// Method of surplus distributions
	pub surplus: SurplusMethod,
	/// Order to distribute surpluses
	pub surplus_order: SurplusOrder,
	/// Examine only transferable papers during surplus distributions
	pub transferable_only: bool,
	/// Method of exclusions
	pub exclusion: ExclusionMethod,
	/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
	pub meek_nz_exclusion: bool,
	/// Bulk elect as soon as continuing candidates fill all remaining vacancies
	pub early_bulk_elect: bool,
	/// Use bulk exclusion
	pub bulk_exclude: bool,
	/// Defer surplus distributions if possible
	pub defer_surpluses: bool,
	/// (Meek STV) Immediately elect candidates even if keep values have not converged
	pub meek_immediate_elect: bool,
	/// Path to constraints file (used only for [STVOptions::describe])
	pub constraints_path: Option<String>,
	/// Mode of handling constraints
	pub constraint_mode: ConstraintMode,
	/// Print votes to specified decimal places in results report
	pub pp_decimals: usize,
}

impl STVOptions {
	/// Returns a new [STVOptions] based on arguments given as strings
	pub fn new(
		round_tvs: Option<usize>,
		round_weights: Option<usize>,
		round_votes: Option<usize>,
		round_quota: Option<usize>,
		sum_surplus_transfers: &str,
		meek_surplus_tolerance: &str,
		normalise_ballots: bool,
		quota: &str,
		quota_criterion: &str,
		quota_mode: &str,
		ties: &Vec<String>,
		random_seed: &Option<String>,
		surplus: &str,
		surplus_order: &str,
		transferable_only: bool,
		exclusion: &str,
		meek_nz_exclusion: bool,
		early_bulk_elect: bool,
		bulk_exclude: bool,
		defer_surpluses: bool,
		meek_immediate_elect: bool,
		constraints_path: Option<&str>,
		constraint_mode: &str,
		pp_decimals: usize,
	) -> Self {
		return STVOptions {
			round_tvs,
			round_weights,
			round_votes,
			round_quota,
			sum_surplus_transfers: match sum_surplus_transfers {
				"single_step" => SumSurplusTransfersMode::SingleStep,
				"by_value" => SumSurplusTransfersMode::ByValue,
				"per_ballot" => SumSurplusTransfersMode::PerBallot,
				_ => panic!("Invalid --sum-transfers"),
			},
			meek_surplus_tolerance: meek_surplus_tolerance.to_string(),
			normalise_ballots,
			quota: match quota {
				"droop" => QuotaType::Droop,
				"hare" => QuotaType::Hare,
				"droop_exact" => QuotaType::DroopExact,
				"hare_exact" => QuotaType::HareExact,
				_ => panic!("Invalid --quota"),
			},
			quota_criterion: match quota_criterion {
				"geq" => QuotaCriterion::GreaterOrEqual,
				"gt" => QuotaCriterion::Greater,
				_ => panic!("Invalid --quota-criterion"),
			},
			quota_mode: match quota_mode {
				"static" => QuotaMode::Static,
				"ers97" => QuotaMode::ERS97,
				"ers76" => QuotaMode::ERS76,
				_ => panic!("Invalid --quota-mode"),
			},
			ties: ties.into_iter().map(|t| match t.as_str() {
				"forwards" => TieStrategy::Forwards,
				"backwards" => TieStrategy::Backwards,
				"random" => TieStrategy::Random(random_seed.as_ref().expect("Must provide a --random-seed if using --ties random").clone()),
				"prompt" => TieStrategy::Prompt,
				_ => panic!("Invalid --ties"),
			}).collect(),
			surplus: match surplus {
				"wig" => SurplusMethod::WIG,
				"uig" => SurplusMethod::UIG,
				"eg" => SurplusMethod::EG,
				"meek" => SurplusMethod::Meek,
				_ => panic!("Invalid --surplus"),
			},
			surplus_order: match surplus_order {
				"by_size" => SurplusOrder::BySize,
				"by_order" => SurplusOrder::ByOrder,
				_ => panic!("Invalid --surplus-order"),
			},
			transferable_only,
			exclusion: match exclusion {
				"single_stage" => ExclusionMethod::SingleStage,
				"by_value" => ExclusionMethod::ByValue,
				"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
				"wright" => ExclusionMethod::Wright,
				_ => panic!("Invalid --exclusion"),
			},
			meek_nz_exclusion,
			early_bulk_elect,
			bulk_exclude,
			defer_surpluses,
			meek_immediate_elect,
			constraints_path: match constraints_path {
				Some(p) => Some(p.to_string()),
				None => None,
			},
			constraint_mode: match constraint_mode {
				"guard_doom" => ConstraintMode::GuardDoom,
				"rollback" => ConstraintMode::Rollback,
				_ => panic!("Invalid --constraint-mode"),
			},
			pp_decimals,
		};
	}
	
	/// Converts the [STVOptions] into CLI argument representation
	pub fn describe<N: Number>(&self) -> String {
		let mut flags = Vec::new();
		let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
		if let Some(dps) = self.round_tvs { flags.push(format!("--round-tvs {}", dps)); }
		if let Some(dps) = self.round_weights { flags.push(format!("--round-weights {}", dps)); }
		if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
		if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
		if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); }
		if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
		if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
		if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); }
		if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); }
		if self.surplus != SurplusMethod::Meek && self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
		let ties_str = self.ties.iter().map(|t| t.describe()).join(" ");
		if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); }
		for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } }
		if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
		if self.surplus != SurplusMethod::Meek && self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
		if self.surplus != SurplusMethod::Meek && self.transferable_only { flags.push("--transferable-only".to_string()); }
		if self.surplus != SurplusMethod::Meek && self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
		if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); }
		if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); }
		if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); }
		if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); }
		if self.surplus == SurplusMethod::Meek && self.meek_immediate_elect { flags.push("--meek-immediate-elect".to_string()); }
		if let Some(path) = &self.constraints_path {
			flags.push(format!("--constraints {}", path));
			if self.constraint_mode != ConstraintMode::GuardDoom { flags.push(self.constraint_mode.describe()); }
		}
		if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
		return flags.join(" ");
	}
	
	/// Validate the combination of [STVOptions] and panic if invalid
	pub fn validate(&self) {
		if self.surplus == SurplusMethod::Meek {
			if self.transferable_only { panic!("--surplus meek is incompatible with --transferable-only"); }
			if self.exclusion != ExclusionMethod::SingleStage { panic!("--surplus meek requires --exclusion single_stage"); }
		}
	}
}

/// Enum of options for [STVOptions::sum_surplus_transfers]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SumSurplusTransfersMode {
	/// Sum and round all surplus transfers for a candidate in a single step
	SingleStep,
	/// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value
	ByValue,
	/// Sum and round a candidate's surplus transfers individually for each ballot paper
	PerBallot,
}

impl SumSurplusTransfersMode {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			SumSurplusTransfersMode::SingleStep => "--sum-surplus-transfers single_step",
			SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value",
			SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::quota]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaType {
	/// Droop quota
	Droop,
	/// Hare quota
	Hare,
	/// Exact Droop quota (Newland–Britton/Hagenbach-Bischoff quota)
	DroopExact,
	/// Exact Hare quota
	HareExact,
}

impl QuotaType {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			QuotaType::Droop => "--quota droop",
			QuotaType::Hare => "--quota hare",
			QuotaType::DroopExact => "--quota droop_exact",
			QuotaType::HareExact => "--quota hare_exact",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::quota_criterion]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaCriterion {
	/// Elect candidates on equalling or exceeding the quota
	GreaterOrEqual,
	/// Elect candidates on strictly exceeding the quota
	Greater,
}

impl QuotaCriterion {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
			QuotaCriterion::Greater => "--quota-criterion gt",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::quota_mode]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaMode {
	/// Static quota
	Static,
	/// Static quota with ERS97 rules
	ERS97,
	/// Static quota with ERS76 rules
	ERS76,
}

impl QuotaMode {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			QuotaMode::Static => "--quota-mode static",
			QuotaMode::ERS97 => "--quota-mode ers97",
			QuotaMode::ERS76 => "--quota-mode ers76",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::surplus]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SurplusMethod {
	/// Weighted inclusive Gregory method
	WIG,
	/// Unweighted inclusive Gregory method
	UIG,
	/// Exclusive Gregory method (last bundle)
	EG,
	/// Meek method
	Meek,
}

impl SurplusMethod {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			SurplusMethod::WIG => "--surplus wig",
			SurplusMethod::UIG => "--surplus uig",
			SurplusMethod::EG => "--surplus eg",
			SurplusMethod::Meek => "--surplus meek",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::surplus_order]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SurplusOrder {
	/// Transfer the largest surplus first, even if it arose at a later stage of the count
	BySize,
	/// Transfer the surplus of the candidate elected first, even if it is smaller than another
	ByOrder,
}

impl SurplusOrder {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			SurplusOrder::BySize => "--surplus-order by_size",
			SurplusOrder::ByOrder => "--surplus-order by_order",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::exclusion]
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum ExclusionMethod {
	/// Transfer all ballot papers of an excluded candidate in one stage
	SingleStage,
	/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
	ByValue,
	/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
	ParcelsByOrder,
	/// Wright method (re-iterate)
	Wright,
}

impl ExclusionMethod {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			ExclusionMethod::SingleStage => "--exclusion single_stage",
			ExclusionMethod::ByValue => "--exclusion by_value",
			ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
			ExclusionMethod::Wright => "--exclusion wright",
		}.to_string()
	}
}

/// Enum of options for [STVOptions::constraint_mode]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum ConstraintMode {
	/// Guard or doom candidates as soon as required to secure a conformant result
	GuardDoom,
	/// TODO: NYI
	Rollback,
}

impl ConstraintMode {
	/// Convert to CLI argument representation
	fn describe(self) -> String {
		match self {
			ConstraintMode::GuardDoom => "--constraint-mode guard_doom",
			ConstraintMode::Rollback => "--constraint-mode rollback",
		}.to_string()
	}
}

/// An error during the STV count
#[wasm_bindgen]
#[derive(Debug)]
pub enum STVError {
	/// User input is required
	RequireInput,
	/// Tie could not be resolved
	UnresolvedTie,
}

impl STVError {
	/// Return the name of the error as a string
	pub fn name(&self) -> &'static str {
		match self {
			STVError::RequireInput => "RequireInput",
			STVError::UnresolvedTie => "UnresolvedTie",
		}
	}
}

/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
pub fn count_init<'a, N: Number>(state: &mut CountState<'a, N>, opts: &'a STVOptions) -> Result<bool, STVError>
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
	// Initialise RNG
	for t in opts.ties.iter() {
		if let TieStrategy::Random(seed) = t {
			state.random = Some(SHARandom::new(seed));
		}
	}
	
	constraints::update_constraints(state, opts);
	
	distribute_first_preferences(state, opts);
	calculate_quota(state, opts);
	elect_meeting_quota(state, opts)?;
	init_tiebreaks(state, opts);
	
	return Ok(true);
}

/// Perform a single stage of the STV count
pub fn count_one_stage<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
	for<'r> &'r N: ops::Div<&'r N, Output=N>,
	for<'r> &'r N: ops::Neg<Output=N>,
{
	state.logger.entries.clear();
	state.step_all();
	
	// Finish count
	if finished_before_stage(&state) {
		return Ok(true);
	}
	
	// Attempt early bulk election
	if opts.early_bulk_elect {
		if bulk_elect(state, &opts)? {
			return Ok(false);
		}
	}
	
	// Continue exclusions
	if continue_exclusion(state, &opts) {
		calculate_quota(state, opts);
		elect_meeting_quota(state, opts)?;
		update_tiebreaks(state, opts);
		return Ok(false);
	}
	
	// Exclude doomed candidates
	if exclude_doomed(state, &opts)? {
		calculate_quota(state, opts);
		elect_meeting_quota(state, opts)?;
		update_tiebreaks(state, opts);
		return Ok(false);
	}
	
	// Distribute surpluses
	if distribute_surpluses(state, &opts)? {
		calculate_quota(state, opts);
		elect_meeting_quota(state, opts)?;
		update_tiebreaks(state, opts);
		return Ok(false);
	}
	
	// Attempt late bulk election
	if bulk_elect(state, &opts)? {
		return Ok(false);
	}
	
	// Exclude lowest hopeful
	if exclude_hopefuls(state, &opts)? {
		calculate_quota(state, opts);
		elect_meeting_quota(state, opts)?;
		update_tiebreaks(state, opts);
		return Ok(false);
	}
	
	panic!("Count incomplete but unable to proceed");
}

/// See [next_preferences]
struct NextPreferencesResult<'a, N> {
	candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
	exhausted: NextPreferencesEntry<'a, N>,
	total_ballots: N,
	total_votes: N,
}

/// See [next_preferences]
struct NextPreferencesEntry<'a, N> {
	//count_card: Option<&'a CountCard<'a, N>>,
	votes: Vec<Vote<'a, N>>,
	num_ballots: N,
	num_votes: N,
}

/// Count the given votes, grouping according to next available preference
fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> NextPreferencesResult<'a, N> {
	let mut result = NextPreferencesResult {
		candidates: HashMap::new(),
		exhausted: NextPreferencesEntry {
			votes: Vec::new(),
			num_ballots: N::new(),
			num_votes: N::new(),
		},
		total_ballots: N::new(),
		total_votes: N::new(),
	};
	
	for mut vote in votes.into_iter() {
		result.total_ballots += &vote.ballot.orig_value;
		result.total_votes += &vote.value;
		
		let mut next_candidate = None;
		
		for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) {
			let candidate = &state.election.candidates[*preference];
			let count_card = &state.candidates[candidate];
			
			if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
				next_candidate = Some(candidate);
				vote.up_to_pref = i + 1;
				break;
			}
		}
		
		// Have to structure like this to satisfy Rust's borrow checker
		if let Some(candidate) = next_candidate {
			if result.candidates.contains_key(candidate) {
				let entry = result.candidates.get_mut(candidate).unwrap();
				entry.num_ballots += &vote.ballot.orig_value;
				entry.num_votes += &vote.value;
				entry.votes.push(vote);
			} else {
				let entry = NextPreferencesEntry {
					num_ballots: vote.ballot.orig_value.clone(),
					num_votes: vote.value.clone(),
					votes: vec![vote],
				};
				result.candidates.insert(candidate, entry);
			}
		} else {
			result.exhausted.num_ballots += &vote.ballot.orig_value;
			result.exhausted.num_votes += &vote.value;
			result.exhausted.votes.push(vote);
		}
	}
	
	return result;
}

/// Distribute first preference votes
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
	match opts.surplus {
		SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
			gregory::distribute_first_preferences(state);
		}
		SurplusMethod::Meek => {
			meek::distribute_first_preferences(state, opts);
		}
	}
}

/// Calculate the quota, given the total vote, according to [STVOptions::quota]
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
	match opts.quota {
		QuotaType::Droop | QuotaType::DroopExact => {
			total /= N::from(seats + 1);
		}
		QuotaType::Hare | QuotaType::HareExact => {
			total /= N::from(seats);
		}
	}
	
	if let Some(dps) = opts.round_quota {
		match opts.quota {
			QuotaType::Droop | QuotaType::Hare => {
				// Increment to next available increment
				let mut factor = N::from(10);
				factor.pow_assign(dps as i32);
				total *= &factor;
				total.floor_mut(0);
				total += N::one();
				total /= factor;
			}
			QuotaType::DroopExact | QuotaType::HareExact => {
				// Round up to next available increment if necessary
				total.ceil_mut(dps);
			}
		}
	}
	
	return total;
}

/// Calculate the quota according to [STVOptions::quota]
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
	// Calculate quota
	if state.quota.is_none() || opts.surplus == SurplusMethod::Meek {
		let mut log = String::new();
		
		// Calculate the total vote
		let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
		log.push_str(format!("{:.dps$} usable votes, so the quota is ", total_vote, dps=opts.pp_decimals).as_str());
		
		let quota = total_to_quota(total_vote, state.election.seats, opts);
		
		log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
		state.quota = Some(quota);
		state.logger.log_literal(log);
	}
	
	if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
		// ERS97/ERS76 rules
		
		// -------------------------
		// Reduce quota if allowable
		
		if state.num_elected == 0 {
			let mut log = String::new();
			
			// Calculate the total vote
			let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
			log.push_str(format!("{:.dps$} usable votes, so the quota is reduced to ", total_vote, dps=opts.pp_decimals).as_str());
			
			let quota = total_to_quota(total_vote, state.election.seats, opts);
			
			if &quota < state.quota.as_ref().unwrap() {
				log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
				state.quota = Some(quota);
				state.logger.log_literal(log);
			}
		}
		
		// ------------------------------------
		// Calculate vote required for election
		
		if state.num_elected < state.election.seats {
			let mut log = String::new();
			
			// Calculate total active vote
			let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
				match cc.state {
					CandidateState::Elected => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
					_ => { acc + &cc.votes }
				}
			});
			log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str());
			
			let vote_req = total_to_quota(total_active_vote, state.election.seats - state.num_elected, opts);
			
			if &vote_req < state.quota.as_ref().unwrap() {
				// VRE is less than the quota
				if let Some(v) = &state.vote_required_election {
					if &vote_req != v {
						log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
						state.vote_required_election = Some(vote_req);
						state.logger.log_literal(log);
					}
				} else {
					log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
					state.vote_required_election = Some(vote_req);
					state.logger.log_literal(log);
				}
			} else {
				// VRE is not less than the quota, so use the quota
				state.vote_required_election = state.quota.clone();
			}
		}
	} else {
		// No ERS97/ERS76 rules
		if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek {
			state.vote_required_election = state.quota.clone();
		}
	}
}

/// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion]
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
	match opts.quota_criterion {
		QuotaCriterion::GreaterOrEqual => {
			return count_card.votes >= *quota;
		}
		QuotaCriterion::Greater => {
			return count_card.votes > *quota;
		}
	}
}

/// Declare elected all candidates meeting the quota
fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
	let vote_req = state.vote_required_election.as_ref().unwrap().clone(); // Have to do this or else the borrow checker gets confused
	
	let mut cands_meeting_quota: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
		.map(|c| (c, &state.candidates[c]))
		.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts) })
		.collect();
	
	// Sort by votes
	cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
	let mut cands_meeting_quota: Vec<&Candidate> = cands_meeting_quota.iter().map(|(c, _)| *c).collect();
	
	let elected = !cands_meeting_quota.is_empty();
	
	while !cands_meeting_quota.is_empty() {
		// Declare elected in descending order of votes
		let max_cands = ties::multiple_max_by(&cands_meeting_quota, |c| &state.candidates[c].votes);
		let candidate = if max_cands.len() > 1 {
			choose_highest(state, opts, max_cands)?
		} else {
			max_cands[0]
		};
		
		let count_card = state.candidates.get_mut(candidate).unwrap();
		count_card.state = CandidateState::Elected;
		state.num_elected += 1;
		count_card.order_elected = state.num_elected as isize;
		state.logger.log_smart(
			"{} meets the quota and is elected.",
			"{} meet the quota and are elected.",
			vec![&candidate.name]
		);
		
		if constraints::update_constraints(state, opts) {
			// Recheck as some candidates may have been doomed
			let mut cmq: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
				.map(|c| (c, &state.candidates[c]))
				.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts) })
				.collect();
			cmq.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
			cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect();
		} else {
			cands_meeting_quota.remove(cands_meeting_quota.iter().position(|c| *c == candidate).unwrap());
		}
		
		if opts.quota_mode == QuotaMode::ERS97 {
			// Vote required for election may have changed
			// This is not performed in ERS76 - TODO: Check this
			calculate_quota(state, opts);
			
			// Repeat in case vote required for election has changed
			match elect_meeting_quota(state, opts) {
				Ok(_) => {}
				Err(e) => { return Err(e); }
			}
		}
	}
	
	return Ok(elected);
}

/// Determine whether the transfer of all surpluses can be deferred
///
/// The value of [STVOptions::defer_surpluses] is not taken into account and must be handled by the caller
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, total_surpluses: &N) -> bool
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>
{
	// Do not defer if this could change the last 2 candidates
	let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
		.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
		.collect();
	hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes));
	if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) {
		return false;
	}
	
	// Do not defer if this could affect a bulk exclusion
	if opts.bulk_exclude {
		let to_exclude = hopefuls_to_bulk_exclude(state, opts);
		let num_to_exclude = to_exclude.len();
		if num_to_exclude > 0 {
			let total_excluded = to_exclude.into_iter()
				.fold(N::new(), |acc, c| acc + &state.candidates[c].votes);
			if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) {
				return false;
			}
		}
	}
	return true;
}

/// Distribute surpluses according to [STVOptions::surplus]
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
	for<'r> &'r N: ops::Div<&'r N, Output=N>,
	for<'r> &'r N: ops::Neg<Output=N>,
{
	match opts.surplus {
		SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
			return gregory::distribute_surpluses(state, opts);
		}
		SurplusMethod::Meek => {
			return meek::distribute_surpluses(state, opts);
		}
	}
}

/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
	let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
		.filter(|c| {
			let cc = &state.candidates[c];
			return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
		})
		.collect();
	
	if state.num_elected + hopefuls.len() <= state.election.seats {
		state.kind = None;
		state.title = "Bulk election".to_string();
		
		// Bulk elect all remaining candidates
		while !hopefuls.is_empty() {
			let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes);
			let candidate = if max_cands.len() > 1 {
				choose_highest(state, opts, max_cands)?
			} else {
				max_cands[0]
			};
			
			let count_card = state.candidates.get_mut(candidate).unwrap();
			count_card.state = CandidateState::Elected;
			state.num_elected += 1;
			count_card.order_elected = state.num_elected as isize;
			
			state.logger.log_smart(
				"{} is elected to fill the remaining vacancy.",
				"{} are elected to fill the remaining vacancies.",
				vec![&candidate.name]
			);
			
			if constraints::update_constraints(state, opts) {
				// Recheck as some candidates may have been doomed
				hopefuls = state.election.candidates.iter()
					.filter(|c| {
						let cc = &state.candidates[c];
						return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
					})
					.collect();
			} else {
				hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
			}
		}
		
		return Ok(true);
	}
	return Ok(false);
}

fn exclude_doomed<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
	for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
	let doomed: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
		.filter(|c| state.candidates[c].state == CandidateState::Doomed)
		.collect();
	
	if !doomed.is_empty() {
		let excluded_candidates;
		if opts.bulk_exclude {
			excluded_candidates = doomed;
		} else {
			// Exclude only the lowest-ranked doomed candidate
			let min_cands = ties::multiple_min_by(&doomed, |c| &state.candidates[c].votes);
			excluded_candidates = if min_cands.len() > 1 {
				vec![choose_lowest(state, opts, min_cands)?]
			} else {
				vec![min_cands[0]]
			};
		}
		
		let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
		state.kind = Some("Exclusion of");
		state.title = names.join(", ");
		state.logger.log_smart(
			"Doomed candidate, {}, is excluded.",
			"Doomed candidates, {}, are excluded.",
			names
		);
		
		exclude_candidates(state, opts, excluded_candidates);
		return Ok(true);
	}
	
	return Ok(false);
}

/// Determine which continuing candidates could be excluded in a bulk exclusion
///
/// The value of [STVOptions::bulk_exclude] is not taken into account and must be handled by the caller
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
	let mut excluded_candidates = Vec::new();
	
	let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
		.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
		.collect();
	
	// Sort by votes
	// NB: Unnecessary to handle ties, as ties will be rejected at "Do not exclude if this could change the order of exclusion"
	hopefuls.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
	
	let total_surpluses = state.candidates.iter()
		.filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap())
		.fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap());
	
	// Attempt to exclude as many candidates as possible
	for i in 0..hopefuls.len() {
		let try_exclude = &hopefuls[0..hopefuls.len()-i];
		
		// Do not exclude if this leaves insufficient candidates
		if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
			continue;
		}
		
		// Do not exclude if this could change the order of exclusion
		let total_votes = try_exclude.into_iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes);
		if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes {
			continue;
		}
		
		for (c, _) in try_exclude.into_iter() {
			excluded_candidates.push(**c);
		}
		break;
	}
	
	return excluded_candidates;
}

/// Exclude the lowest-ranked hopeful candidate(s)
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
	for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
	let mut excluded_candidates: Vec<&Candidate> = Vec::new();
	
	// Attempt a bulk exclusion
	if opts.bulk_exclude {
		excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
	}
	
	// Exclude lowest ranked candidate
	if excluded_candidates.is_empty() {
		let hopefuls: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
			.filter(|c| state.candidates[c].state == CandidateState::Hopeful)
			.collect();
		
		let min_cands = ties::multiple_min_by(&hopefuls, |c| &state.candidates[c].votes);
		excluded_candidates = if min_cands.len() > 1 {
			vec![choose_lowest(state, opts, min_cands)?]
		} else {
			vec![min_cands[0]]
		};
	}
	
	let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
	state.kind = Some("Exclusion of");
	state.title = names.join(", ");
	state.logger.log_smart(
		"No surpluses to distribute, so {} is excluded.",
		"No surpluses to distribute, so {} are excluded.",
		names
	);
	
	exclude_candidates(state, opts, excluded_candidates);
	
	return Ok(true);
}

/// Continue the exclusion of a candidate who is being excluded
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
	for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
	// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
	let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
		.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.is_empty()))
		.collect();
	
	if !excluded_with_votes.is_empty() {
		excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
		
		let order_excluded = excluded_with_votes[0].1.order_elected;
		let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter()
			.filter(|(_, cc)| cc.order_elected == order_excluded)
			.map(|(c, _)| *c)
			.collect();
		
		let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
		state.kind = Some("Exclusion of");
		state.title = names.join(", ");
		state.logger.log_smart(
			"Continuing exclusion of {}.",
			"Continuing exclusion of {}.",
			names
		);
		
		exclude_candidates(state, opts, excluded_candidates);
		return true;
	}
	
	return false;
}

/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
where
	for<'r> &'r N: ops::Sub<&'r N, Output=N>,
	for<'r> &'r N: ops::Mul<&'r N, Output=N>,
	for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
	match opts.exclusion {
		ExclusionMethod::SingleStage => {
			match opts.surplus {
				SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
					gregory::exclude_candidates(state, opts, excluded_candidates);
				}
				SurplusMethod::Meek => {
					meek::exclude_candidates(state, opts, excluded_candidates);
				}
			}
		}
		ExclusionMethod::ByValue | ExclusionMethod::ParcelsByOrder => {
			// Exclusion in parts compatible only with Gregory method
			gregory::exclude_candidates(state, opts, excluded_candidates);
		}
		ExclusionMethod::Wright => {
			gregory::wright_exclude_candidates(state, opts, excluded_candidates);
		}
	}
}

/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
	if state.num_elected >= state.election.seats {
		return true;
	}
	return false;
}

/// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate
///
/// The given candidates are assumed to be tied in this round
fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
	for strategy in opts.ties.iter() {
		match strategy.choose_highest(state, &candidates) {
			Ok(c) => {
				return Ok(c);
			}
			Err(e) => {
				if let STVError::UnresolvedTie = e {
					continue;
				} else {
					return Err(e);
				}
			}
		}
	}
	panic!("Unable to resolve tie");
}

/// Break a tie between the given candidates according to [STVOptions::ties], selecting the lowest candidate
///
/// The given candidates are assumed to be tied in this round
fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
	for strategy in opts.ties.iter() {
		match strategy.choose_lowest(state, &candidates) {
			Ok(c) => {
				return Ok(c);
			}
			Err(e) => {
				if let STVError::UnresolvedTie = e {
					continue;
				} else {
					return Err(e);
				}
			}
		}
	}
	panic!("Unable to resolve tie");
}

/// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
fn init_tiebreaks<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
	if !opts.ties.iter().any(|t| t == &TieStrategy::Forwards) && !opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
		return;
	}
	
	// Sort candidates in this stage by votes, grouping by ties
	let mut sorted_candidates: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
	sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
	let sorted_candidates: Vec<Vec<(&&Candidate, &CountCard<N>)>> = sorted_candidates.into_iter()
		.group_by(|(_, cc)| &cc.votes)
		.into_iter()
		.map(|(_, candidates)| candidates.collect())
		.collect();
	
	// Update forwards tie-breaking order
	if opts.ties.iter().any(|t| t == &TieStrategy::Forwards) {
		let mut hm: HashMap<&Candidate, usize> = HashMap::new();
		for (i, group) in sorted_candidates.iter().enumerate() {
			for (candidate, _) in group.iter() {
				hm.insert(candidate, i);
			}
		}
		state.forwards_tiebreak = Some(hm);
	}
	
	// Update backwards tie-breaking order
	if opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
		let mut hm: HashMap<&Candidate, usize> = HashMap::new();
		for (i, group) in sorted_candidates.iter().enumerate() {
			for (candidate, _) in group.iter() {
				hm.insert(candidate, i);
			}
		}
		state.backwards_tiebreak = Some(hm);
	}
}

/// If required, update the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
fn update_tiebreaks<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
	if let None = state.forwards_tiebreak {
		if let None = state.backwards_tiebreak {
			return;
		}
	}
	
	// Sort candidates in this stage by votes, grouping by ties
	let mut sorted_candidates: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
	sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
	let sorted_candidates: Vec<Vec<&Candidate>> = sorted_candidates.into_iter()
		.group_by(|(_, cc)| &cc.votes)
		.into_iter()
		.map(|(_, candidates)| candidates.map(|(c, _)| *c).collect())
		.collect();
	
	// Update forwards tie-breaking order
	if let Some(hm) = state.forwards_tiebreak.as_mut() {
		// TODO: Check if already completely sorted
		let mut sorted_last_round: Vec<(&&Candidate, &usize)> = hm.iter().collect();
		sorted_last_round.sort_unstable_by(|a, b| a.1.cmp(b.1));
		let sorted_last_round: Vec<Vec<&Candidate>> = sorted_last_round.into_iter()
			.group_by(|(_, v)| **v)
			.into_iter()
			.map(|(_, group)| group.map(|(c, _)| *c).collect())
			.collect();
		
		let mut i: usize = 0;
		for mut group in sorted_last_round.into_iter() {
			if group.len() == 1 {
				hm.insert(group[0], i);
				i += 1;
				continue;
			} else {
				// Tied in last round - refer to this round
				group.sort_unstable_by(|a, b|
					sorted_candidates.iter().position(|x| x.contains(a)).unwrap()
					.cmp(&sorted_candidates.iter().position(|x| x.contains(b)).unwrap())
				);
				let tied_last_round = group.into_iter()
					.group_by(|c| sorted_candidates.iter().position(|x| x.contains(c)).unwrap());
				
				for (_, group2) in tied_last_round.into_iter() {
					for candidate in group2 {
						hm.insert(candidate, i);
					}
					i += 1;
				}
			}
		}
	}
	
	// Update backwards tie-breaking order
	if let Some(hm) = state.backwards_tiebreak.as_mut() {
		let hm_orig = hm.clone();
		let mut i: usize = 0;
		for group in sorted_candidates.iter() {
			if group.len() == 1 {
				hm.insert(group[0], i);
				i += 1;
				continue;
			} else {
				// Tied in this round - refer to last round
				let mut tied_this_round: Vec<&Candidate> = group.into_iter().map(|c| *c).collect();
				tied_this_round.sort_unstable_by(|a, b| hm_orig[a].cmp(&hm_orig[b]));
				let tied_this_round = tied_this_round.into_iter()
					.group_by(|c| hm_orig[c]);
				
				for (_, group2) in tied_this_round.into_iter() {
					for candidate in group2 {
						hm.insert(candidate, i);
					}
					i += 1;
				}
			}
		}
	}
}
