/*  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::election::{CandidateState, CountState, Election};
use crate::numbers::{Fixed, NativeFloat64, Number, Rational};
use crate::stv;

extern crate console_error_panic_hook;

use js_sys::Array;
use wasm_bindgen::prelude::wasm_bindgen;

// Init

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

// Helper macros for making functions

macro_rules! impl_type {
	($type:ident) => { paste::item! {
		// Counting
		
		#[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> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter());
			return [<Election$type>](election);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) {
			stv::count_init(&mut state.0, &opts);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> bool {
			return stv::count_one_stage(&mut state.0, &opts);
		}
		
		// Reporting
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &stv::STVOptions) -> String {
			return init_results_table(&election.0, opts);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &stv::STVOptions) -> String {
			return describe_count(filename, &election.0, opts);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &stv::STVOptions) -> Array {
			return update_results_table(stage_num, &state.0, opts);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<update_stage_comments_$type>](state: &[<CountState$type>]) -> String {
			return update_stage_comments(&state.0);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array {
			return finalise_results_table(&state.0);
		}
		
		#[wasm_bindgen]
		#[allow(non_snake_case)]
		pub fn [<final_result_summary_$type>](state: &[<CountState$type>]) -> String {
			return final_result_summary(&state.0);
		}
		
		// Wrapper structs
		// Required as we cannot specify &'static in wasm-bindgen: issue #1187
		
		#[wasm_bindgen]
		pub struct [<CountState$type>](CountState<'static, $type>);
		#[wasm_bindgen]
		impl [<CountState$type>] {
			pub fn new(election: &[<Election$type>]) -> Self {
				return [<CountState$type>](CountState::new(election.as_static()));
			}
		}
		
		#[wasm_bindgen]
		pub struct [<Election$type>](Election<$type>);
		#[wasm_bindgen]
		impl [<Election$type>] {
			pub fn seats(&self) -> usize { self.0.seats }
			
			fn as_static(&self) -> &'static Election<$type> {
				// Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187
				unsafe {
					let ptr = &self.0 as *const Election<$type>;
					&*ptr
				}
			}
		}
	}}
}

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

// Reporting

fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> 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>"#);
	for candidate in election.candidates.iter() {
		result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
	}
	result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><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 opts.quota_mode == stv::QuotaMode::ERS97 {
		result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
	}
	return result;
}

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. Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, total_ballots, filename, election.name, election.candidates.len(), election.seats, opts.describe::<N>()));
	return result;
}

fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
	let result = Array::new();
	result.push(&format!(r#"<td>{}</td>"#, stage_num).into());
	result.push(&format!(r#"<td>{}</td>"#, state.kind.unwrap_or("")).into());
	result.push(&format!(r#"<td>{}</td>"#, state.title).into());
	for candidate in state.election.candidates.iter() {
		let count_card = state.candidates.get(candidate).unwrap();
		if count_card.state == stv::CandidateState::ELECTED {
			result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
		} else if count_card.state == stv::CandidateState::EXCLUDED {
			result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
			if count_card.votes.is_zero() {
				result.push(&r#"<td class="count excluded">Ex</td>"#.into());
			} else {
				result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
			}
		} else {
			result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
			result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
		}
	}
	result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
	result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.exhausted.votes, opts.pp_decimals)).into());
	result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
	result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
	
	// 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;
	result.push(&format!(r#"<td class="count">{}</td>"#, pp(&total_vote, opts.pp_decimals)).into());
	
	result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
	if opts.quota_mode == stv::QuotaMode::ERS97 {
		result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
	}
	
	return result;
}

fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
	return state.logger.render().join(" ");
}

fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
	let result = Array::new();
	
	// Header rows
	result.push(&r#"<td rowspan="3"></td>"#.into());
	result.push(&"".into());
	result.push(&"".into());
	
	// Candidate states
	for candidate in state.election.candidates.iter() {
		let count_card = state.candidates.get(candidate).unwrap();
		if count_card.state == stv::CandidateState::ELECTED {
			result.push(&format!(r#"<td rowspan="2" class="bb elected">ELECTED {}</td>"#, count_card.order_elected).into());
		} else if count_card.state == stv::CandidateState::EXCLUDED {
			result.push(&format!(r#"<td rowspan="2" class="bb excluded">Excluded {}</td>"#, -count_card.order_elected).into());
		} else {
			result.push(&r#"<td rowspan="2" class="bb"></td>"#.into());
		}
		result.push(&"".into());
	}
	
	return result;
}

fn final_result_summary<N: Number>(state: &CountState<N>) -> 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));
		}
	}
	winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
	
	for (winner, _) in winners.into_iter() {
		result.push_str(&format!("<li>{}</li>", winner.name));
	}
	
	result.push_str("</ol>");
	return result;
}

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;
}
