/*  OpenTally: Open-source election vote counting
 *  Copyright © 2021–2023 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::election::{CandidateState, Election, StageKind};
use crate::numbers::Number;
use crate::stv::{self, CountState};

use itertools::Itertools;

/// Generate the lead-in description of the count in HTML
pub fn describe_count<N: Number>(filename: &str, 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::new(), |mut acc, b| { acc += &b.orig_value; acc });
	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.iter().filter(|c| !c.is_dummy).count(), election.seats));
	
	let opts_str = opts.describe::<N>();
	if !opts_str.is_empty() {
		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;
}

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

/// Generate subsequent columns of the HTML results table
pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Vec<String> {
	let mut result = Vec::new();
	
	// Insert borders to left of new exclusions if reset-and-reiterate method applied
	let classes_o; // Outer version
	let classes_i; // Inner version
	if (opts.exclusion == stv::ExclusionMethod::ResetAndReiterate && matches!(state.title, StageKind::ExclusionOf(_))) || matches!(state.title, StageKind::Rollback) {
		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::ResetAndReiterate && matches!(state.title, StageKind::ExclusionOf(_)) {
		hide_xfers_trsp = true;
	} else if let StageKind::Rollback = state.title {
		hide_xfers_trsp = true;
	} else if let StageKind::BulkElection = state.title {
		hide_xfers_trsp = true;
	} else if state.candidates.values().all(|cc| cc.transfers.is_zero()) && state.exhausted.transfers.is_zero() && state.loss_fraction.transfers.is_zero() {
		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::Rollback | StageKind::RollbackExhausted | 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>");
			}
		}
		StageKind::BallotsOf(candidate) => {
			title_str = candidate.name.clone();
		}
	};
	
	match report_style {
		"votes" => {
			result.push(format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
			result.push(format!(r#"<td{}>{}</td>"#, classes_o, kind_str));
			result.push(format!(r#"<td{}>{}</td>"#, classes_o, title_str));
		}
		"votes_transposed" => {
			if hide_xfers_trsp {
				result.push(format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
				result.push(format!(r#"<td{}>{}</td>"#, classes_o, kind_str));
				result.push(format!(r#"<td{}>{}</td>"#, classes_o, title_str));
			} else {
				result.push(format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
				result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str));
				result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str));
				//result.push(format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1));
			}
		}
		"ballots_votes" => {
			result.push(format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
			result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str));
			result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str));
			result.push(format!(r#"<td{}>Ballots</td><td>Votes</td>"#, classes_o));
		}
		_ => unreachable!("Invalid report_style")
	}
	
	for candidate in state.election.candidates.iter() {
		if candidate.is_dummy {
			continue;
		}
		
		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)));
						result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
					}
					CandidateState::Elected => {
						result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
						result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
					}
					CandidateState::Doomed => {
						result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
						result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
					}
					CandidateState::Withdrawn => {
						result.push(format!(r#"<td class="{}count excluded"></td>"#, classes_i));
						result.push(format!(r#"<td class="{}count excluded">WD</td>"#, classes_i));
					}
					CandidateState::Excluded => {
						result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
						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));
						} else {
							result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
						}
					}
				}
			}
			"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)));
						} 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)));
						}
					}
					CandidateState::Elected => {
						if hide_xfers_trsp {
							result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
						} 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)));
						}
					}
					CandidateState::Doomed => {
						if hide_xfers_trsp {
							result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
						} 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)));
						}
					}
					CandidateState::Withdrawn => {
						if hide_xfers_trsp {
							result.push(format!(r#"<td class="{}count excluded">WD</td>"#, classes_i));
						} else {
							result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i));
						}
					}
					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));
							} 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)));
							}
						} else {
							if hide_xfers_trsp {
								result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
							} 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)));
							}
						}
					}
				}
			}
			"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)));
						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)));
					}
					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)));
						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)));
					}
					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)));
						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)));
					}
					CandidateState::Withdrawn => {
						result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, classes_i));
						result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i));
					}
					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)));
						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));
						} 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)));
						}
					}
				}
			}
			_ => 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)));
			result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
			result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
			result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
		}
		"votes_transposed" => {
			if hide_xfers_trsp {
				result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
				result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
			} 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)));
				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)));
			}
		}
		"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)));
			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)));
			result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
			result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
		}
		_ => unreachable!("Invalid report_style")
	}
	
	// Calculate total votes
	let mut total_vote = state.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
	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)));
		}
		"votes_transposed" => {
			if hide_xfers_trsp {
				result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
			} else {
				result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
			}
		}
		"ballots_votes" => {
			// Calculate total ballots
			let mut total_ballots = state.candidates.values().fold(N::new(), |mut acc, cc| { acc += cc.num_ballots(); acc });
			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)));
		}
		_ => 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)));
	} else {
		result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)));
	}
	
	if stv::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)));
			} else {
				result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)));
			}
		} else {
			if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
				result.push(format!(r#"<td class="{}count"></td>"#, classes_i));
			} else {
				result.push(format!(r#"<td class="{}count"></td><td class="count"></td>"#, classes_i));
			}
		}
	}
	
	return result;
}

/// Generate the final column of the HTML results table
pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Vec<String> {
	let mut result = Vec::new();
	
	// Header rows
	match report_style {
		"votes" | "votes_transposed" => {
			result.push(String::from(r#"<td rowspan="3"></td>"#));
			result.push(String::from(""));
			result.push(String::from(""));
		}
		"ballots_votes" => {
			result.push(String::from(r#"<td rowspan="4"></td>"#));
			result.push(String::from(""));
			result.push(String::from(""));
			result.push(String::from(""));
		}
		_ => unreachable!("Invalid report_style")
	}
	
	let rowspan = if report_style == "votes_transposed" { "" } else { r#" rowspan="2""# };
	
	// Candidate states
	for candidate in state.election.candidates.iter() {
		if candidate.is_dummy {
			continue;
		}
		
		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));
		} else if count_card.state == stv::CandidateState::Excluded {
			result.push(format!(r#"<td{} class="bb excluded">Excluded {}</td>"#, rowspan, -count_card.order_elected));
		} else if count_card.state == stv::CandidateState::Withdrawn {
			result.push(format!(r#"<td{} class="bb excluded">Withdrawn</td>"#, rowspan));
		} else {
			result.push(format!(r#"<td{} class="bb"></td>"#, rowspan));
		}
		
		if report_style != "votes_transposed" {
			result.push(String::from(""));
		}
	}
	
	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;
}
