/*  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/>.
 */

use crate::constraints::{Constraints, ConstraintMatrix};
use crate::logger::Logger;
use crate::numbers::Number;
use crate::parser::blt::{BLTParser, ParseError};
use crate::sharandom::SHARandom;
use crate::stv::{QuotaMode, STVOptions};

use std::cmp::max;
use std::collections::HashMap;
use std::iter::Peekable;

#[cfg(not(target_arch = "wasm32"))]
use utf8_chars::BufReadCharsExt;
#[cfg(not(target_arch = "wasm32"))]
use std::fs::File;
#[cfg(not(target_arch = "wasm32"))]
use std::io::BufReader;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;

/// An election to be counted
pub struct Election<N> {
	/// Name of the election
	pub name: String,
	/// Number of candidates to be elected
	pub seats: usize,
	/// [Vec] of [Candidate]s in the election
	pub candidates: Vec<Candidate>,
	/// Indexes of withdrawn candidates
	pub withdrawn_candidates: Vec<usize>,
	/// [Vec] of [Ballot]s cast in the election
	pub ballots: Vec<Ballot<N>>,
	/// Constraints on candidates
	pub constraints: Option<Constraints>,
}

impl<N: Number> Election<N> {
	/// Parse the given BLT file and return an [Election]
	pub fn from_blt<I: Iterator<Item=char>>(input: Peekable<I>) -> Result<Self, ParseError> {
		let mut parser = BLTParser::new(input);
		parser.parse_blt()?;
		return Ok(parser.as_election());
	}
	
	/// Parse the BLT file at the given path and return an [Election]
	#[cfg(not(target_arch = "wasm32"))]
	pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ParseError> {
		let mut reader = BufReader::new(File::open(path).expect("IO Error"));
		let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
		return Ok(Election::from_blt(chars)?);
	}
	
	/// Convert ballots with weight >1 to multiple ballots of weight 1
	pub fn normalise_ballots(&mut self) {
		let mut normalised_ballots = Vec::new();
		for ballot in self.ballots.iter() {
			let mut n = N::new();
			let one = N::one();
			while n < ballot.orig_value {
				let new_ballot = Ballot {
					orig_value: N::one(),
					preferences: ballot.preferences.clone(),
				};
				normalised_ballots.push(new_ballot);
				n += &one;
			}
		}
		self.ballots = normalised_ballots;
	}
}

/// A candidate in an [Election]
#[derive(PartialEq, Eq, Hash)]
pub struct Candidate {
	/// Name of the candidate
	pub name: String,
}

/// The current state of counting an [Election]
//#[derive(Clone)]
pub struct CountState<'a, N: Number> {
	/// Pointer to the [Election] being counted
	pub election: &'a Election<N>,
	
	/// [HashMap] of [CountCard]s for each [Candidate] in the election
	pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
	/// [CountCard] representing the exhausted pile
	pub exhausted: CountCard<'a, N>,
	/// [CountCard] representing loss by fraction
	pub loss_fraction: CountCard<'a, N>,
	
	/// [crate::stv::meek::BallotTree] for Meek STV
	pub ballot_tree: Option<crate::stv::meek::BallotTree<'a, N>>,
	
	/// Values used to break ties, based on forwards tie-breaking
	pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
	/// Values used to break ties, based on backwards tie-breaking
	pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
	/// [SHARandom] for random tie-breaking
	pub random: Option<SHARandom<'a>>,
	
	/// Quota for election
	pub quota: Option<N>,
	/// Vote required for election
	///
	/// Only used in ERS97/ERS76.
	pub vote_required_election: Option<N>,
	
	/// Number of candidates who have been declared elected
	pub num_elected: usize,
	/// Number of candidates who have been declared excluded
	pub num_excluded: usize,
	
	/// [ConstraintMatrix] for constrained elections
	pub constraint_matrix: Option<ConstraintMatrix>,
	
	/// The type of stage being counted
	///
	/// For example, "Surplus of", "Exclusion of"
	pub kind: Option<&'a str>,
	/// The description of the stage being counted, excluding [CountState::kind]
	pub title: String,
	/// [Logger] for this stage of the count
	pub logger: Logger<'a>,
}

impl<'a, N: Number> CountState<'a, N> {
	/// Construct a new blank [CountState] for the given [Election]
	pub fn new(election: &'a Election<N>) -> Self {
		let mut state = CountState {
			election: &election,
			candidates: HashMap::new(),
			exhausted: CountCard::new(),
			loss_fraction: CountCard::new(),
			ballot_tree: None,
			forwards_tiebreak: None,
			backwards_tiebreak: None,
			random: None,
			quota: None,
			vote_required_election: None,
			num_elected: 0,
			num_excluded: 0,
			constraint_matrix: None,
			kind: None,
			title: String::new(),
			logger: Logger { entries: Vec::new() },
		};
		
		for candidate in election.candidates.iter() {
			state.candidates.insert(candidate, CountCard::new());
		}
		
		for withdrawn_idx in election.withdrawn_candidates.iter() {
			state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn;
		}
		
		if let Some(constraints) = &election.constraints {
			let mut num_groups: Vec<usize> = constraints.0.iter().map(|c| c.groups.len()).collect();
			let mut cm = ConstraintMatrix::new(&mut num_groups[..]);
			
			// Init constraint matrix total cells min/max
			for (i, constraint) in constraints.0.iter().enumerate() {
				for (j, group) in constraint.groups.iter().enumerate() {
					let mut idx = vec![0; constraints.0.len()];
					idx[i] = j + 1;
					let mut cell = &mut cm[&idx];
					cell.min = group.min;
					cell.max = group.max;
				}
			}
			
			// Fill in grand total, etc.
			cm.update_from_state(&state.election, &state.candidates);
			cm.init();
			//println!("{}", cm);
			
			// Require correct number of candidates to be elected
			let idx = vec![0; constraints.0.len()];
			cm[&idx].min = election.seats;
			cm[&idx].max = election.seats;
			
			state.constraint_matrix = Some(cm);
		}
		
		return state;
	}
	
	/// [Step](CountCard::step) every [CountCard] to prepare for the next stage
	pub fn step_all(&mut self) {
		for (_, count_card) in self.candidates.iter_mut() {
			count_card.step();
		}
		self.exhausted.step();
		self.loss_fraction.step();
	}
	
	/// List the candidates, and their current state, votes and transfers
	pub fn describe_candidates(&self, opts: &STVOptions) -> String {
		let mut candidates: Vec<(&Candidate, &CountCard<N>)>;
		
		if opts.sort_votes {
			// Sort by votes if requested
			candidates = self.candidates.iter()
				.map(|(c, cc)| (*c, cc)).collect();
			// First sort by order of election (as a tie-breaker, if votes are equal)
			candidates.sort_unstable_by(|a, b| b.1.order_elected.cmp(&a.1.order_elected));
			// Then sort by votes
			candidates.sort_by(|a, b| a.1.votes.cmp(&b.1.votes));
			candidates.reverse();
		} else {
			candidates = self.election.candidates.iter()
				.map(|c| (c, &self.candidates[c]))
				.collect();
		}
		
		let mut result = String::new();
		
		for (candidate, count_card) in candidates {
			match count_card.state {
				CandidateState::Hopeful => {
					result.push_str(&format!("- {}: {:.dps$} ({:.dps$})\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
				}
				CandidateState::Guarded => {
					result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Guarded\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
				}
				CandidateState::Elected => {
					if let Some(kv) = &count_card.keep_value {
						result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})\n", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
					} else {
						result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}\n", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=opts.pp_decimals));
					}
				}
				CandidateState::Doomed => {
					result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Doomed\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
				}
				CandidateState::Withdrawn => {
					if !opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
						result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Withdrawn\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
					}
				}
				CandidateState::Excluded => {
					// If --hide-excluded, hide unless nonzero votes or nonzero transfers
					if !opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
						result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Excluded {}\n", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=opts.pp_decimals));
					}
				}
			}
		}
		return result;
	}
	
	/// Produce summary rows for the current stage
	pub fn describe_summary(&self, opts: &STVOptions) -> String {
		let mut result = String::new();
		
		result.push_str(&format!("Exhausted: {:.dps$} ({:.dps$})\n", self.exhausted.votes, self.exhausted.transfers, dps=opts.pp_decimals));
		result.push_str(&format!("Loss by fraction: {:.dps$} ({:.dps$})\n", self.loss_fraction.votes, self.loss_fraction.transfers, dps=opts.pp_decimals));
		
		let mut total_vote = self.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
		total_vote += &self.exhausted.votes;
		total_vote += &self.loss_fraction.votes;
		result.push_str(&format!("Total votes: {:.dps$}\n", total_vote, dps=opts.pp_decimals));
		
		result.push_str(&format!("Quota: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals));
		if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
			result.push_str(&format!("Vote required for election: {:.dps$}\n", self.vote_required_election.as_ref().unwrap(), dps=opts.pp_decimals));
		}
		
		return result;
	}
}

/// Current state of a [Candidate] during an election count
#[derive(Clone)]
pub struct CountCard<'a, N> {
	/// State of the candidate
	pub state: CandidateState,
	/// Order of election or exclusion
	///
	/// Positive integers represent order of election; negative integers represent order of exclusion
	pub order_elected: isize,
	
	//pub orig_votes: N,
	/// Net votes transferred to this candidate in this stage
	pub transfers: N,
	/// Votes of the candidate at the end of this stage
	pub votes: N,
	
	/// Parcels of ballots assigned to this candidate
	pub parcels: Vec<Parcel<'a, N>>,
	
	/// Candidate's keep value (Meek STV)
	pub keep_value: Option<N>,
}

impl<'a, N: Number> CountCard<'a, N> {
	/// Returns a new blank [CountCard]
	pub fn new() -> Self {
		return CountCard {
			state: CandidateState::Hopeful,
			order_elected: 0,
			//orig_votes: N::new(),
			transfers: N::new(),
			votes: N::new(),
			parcels: Vec::new(),
			keep_value: None,
		};
	}
	
	/// Transfer the given number of votes to this [CountCard], incrementing [transfers](CountCard::transfers) and [votes](CountCard::votes)
	pub fn transfer(&mut self, transfer: &'_ N) {
		self.transfers += transfer;
		self.votes += transfer;
	}
	
	/// Set [transfers](CountCard::transfers) to 0
	pub fn step(&mut self) {
		//self.orig_votes = self.votes.clone();
		self.transfers = N::new();
	}
	
	/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
	pub fn concat_parcels(&mut self) -> Vec<Vote<'a, N>> {
		let mut result = Vec::new();
		for parcel in self.parcels.iter_mut() {
			result.append(&mut parcel.votes);
		}
		return result;
	}
}

/// Parcel of [Vote]s during a count
#[derive(Clone)]
pub struct Parcel<'a, N> {
	/// [Vote]s in this parcel
	pub votes: Vec<Vote<'a, N>>,
	/// Order for sorting with [crate::stv::ExclusionMethod::BySource]
	pub source_order: usize,
}

/// Represents a [Ballot] with an associated value
#[derive(Clone)]
pub struct Vote<'a, N> {
	/// Ballot from which the vote is derived
	pub ballot: &'a Ballot<N>,
	/// Current value of the ballot
	pub value: N,
	/// Index of the next preference to examine
	pub up_to_pref: usize,
}

/// A record of a voter's preferences
pub struct Ballot<N> {
	/// Original value/weight of the ballot
	pub orig_value: N,
	/// Indexes of candidates preferenced on the ballot
	pub preferences: Vec<usize>,
}

/// State of a [Candidate] during a count
#[allow(dead_code)]
#[derive(PartialEq)]
#[derive(Clone)]
#[derive(Debug)]
pub enum CandidateState {
	/// Hopeful (continuing candidate)
	Hopeful,
	/// Required by constraints to be guarded from exclusion
	Guarded,
	/// Declared elected
	Elected,
	/// Required by constraints to be doomed to be excluded
	Doomed,
	/// Withdrawn candidate
	Withdrawn,
	/// Declared excluded
	Excluded,
}
