package no.valg.valgnatt.backend.mandatberegning.beregning;

import no.valg.valgnatt.backend.mandatberegning.domene.Kvotient;
import no.valg.valgnatt.backend.mandatberegning.domene.PartiFylkeKvotient;
import no.valg.valgnatt.backend.mandatberegning.domene.PartiKvotient;
import no.valg.valgnatt.database.Konstanter;
import no.valg.valgnatt.database.domene.ValgType;
import no.valg.valgnatt.database.geografi.Fylke;
import no.valg.valgnatt.database.geografi.Land;
import no.valg.valgnatt.database.geografi.MandatData;
import no.valg.valgnatt.database.geografi.Mandater;
import no.valg.valgnatt.database.geografi.PartiMandater;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;

import static java.math.BigDecimal.ROUND_HALF_UP;
import static no.valg.valgnatt.database.Konstanter.MAKS_PRESISJON;


@Component
public final class Utjevningsmandat {

	// Bruker samme presisjon i divisjonene som EVA Admin - skal sikre oss samme resultat. Skal også
	// være høy nok presisjon til at vi aldri får et tilfelle hvor avrunding endrer mandatfordeling.
	private static final int EVAADMIN_PRESISJON = 7;

	private Utjevningsmandat() {
	}

	/**
	 * Poenget med denne "wrapper'en" er at den konverterer fra område til datastrukturer som eksplisitt
	 * viser det sett av data som trengs for en utjevningsmandatberegning (lettere å forstå?). I tillegg
	 * gjør dette utjevningsmandatberegningen mer testbar.
	 */
	public static UtjevningsmandatResultat beregnOgOppdater(Land land, Map<String, Integer> distriktsmandaterPrFylke, boolean prognose) {
		// aldri listestemmer i utjevningsmandatberegning, fjern blanke
		Map<String, Integer> stemmerPrParti = land.getValgdistrikt().getStemmerPrPartiTilMandatberegning(false, true, prognose);
		Map<String, Map<String, Integer>> distriktsmandaterPrPartiPrFylke = new TreeMap<>();
		Map<String, Map<String, Integer>> stemmerPrPartiPrFylke = new TreeMap<>();
		Set<String> lokalePartikoder = new TreeSet<>();
		klargjørDataTilBeregning(land, distriktsmandaterPrPartiPrFylke, stemmerPrPartiPrFylke, lokalePartikoder, prognose);
		UtjevningsmandatResultat resultat = beregn(stemmerPrParti, lokalePartikoder, distriktsmandaterPrPartiPrFylke, stemmerPrPartiPrFylke, distriktsmandaterPrFylke);
		Map<String, Integer> utjMandaterPrParti = resultat.getPartiPrFylke().values().stream().collect(Collectors.groupingBy(parti -> parti, Collectors.summingInt(parti -> 1)));
	
		for (PartiMandater pm : land.getValgdistrikt().getPartiMandater()) {
			String partikode = pm.getPartikode();
			Mandater ujmForPartiILandet = pm.getMandatData(prognose).getUtjevningmandater();
			ujmForPartiILandet.setAntall(utjMandaterPrParti.get(partikode) == null ? 0 : utjMandaterPrParti.get(partikode));
			ujmForPartiILandet.setLavesteKvotientMedMandat(resultat.getKvotientPrParti().getOrDefault(partikode, new Kvotient(BigDecimal.ZERO)).getKvotient()); 
			ujmForPartiILandet.setLavesteMandatrangMedMandat(resultat.getMandatnrPrParti().get(partikode));
		}
		for (Fylke fylke : land.getFylker().values()) {
			String partikode = resultat.getPartiPrFylke().get(fylke.getNr());
			for (PartiMandater pm : fylke.getValgdistrikt().getPartiMandater()) {
				pm.getMandatData(prognose).getUtjevningmandater().setAntall(pm.getPartikode().equals(partikode) ? 1 : 0);
			}
		}
		return resultat;
	}

	private static void klargjørDataTilBeregning(Land land, Map<String, Map<String, Integer>> distriktsmandaterPrPartiPrFylke, Map<String, Map<String, Integer>> stemmerPrPartiPrFylke, Set<String> lokalePartikoder, boolean prognose) {
		for (Fylke fylke : land.getFylker().values()) {
			Map<String, Integer> distriktsmandaterPrParti = new TreeMap<>();
			Map<String, Integer> stemmerPrPartiIEttFylke = new TreeMap<>();
			stemmerPrPartiPrFylke.put(fylke.getNr(), stemmerPrPartiIEttFylke);
			distriktsmandaterPrPartiPrFylke.put(fylke.getNr(), distriktsmandaterPrParti);
			for (PartiMandater pm : fylke.getValgdistrikt().getPartiMandater()) {
				if (pm.isStillerliste() && !pm.getPartikode().equals(Konstanter.BLANKE)) {
					MandatData mandatData = pm.finnBesteMandatData(prognose);
					
					distriktsmandaterPrParti.put(pm.getPartikode(), land.getValgType() == ValgType.FY ? mandatData.getAntallDistriktsmandaterGittSTValg() : mandatData.getDistriktsmandater().getAntall());
					stemmerPrPartiIEttFylke.put(pm.getPartikode(), mandatData.getStemmer());
				}
				if (pm.getPartikategori().equals(Konstanter.DEFAULT_PARTIKATEGORI)) {
					lokalePartikoder.add(pm.getPartikode());
				}
			}
		}
	}

	/**
	 * Denne metoden har ansvaret for å beregne utjevningsmandater (som bare skjer i ST-valg). Som input mottar man følgende:
	 * <p/>
	 * map av parti->stemmer (for hele landet)
	 * map av fylke->(parti->distriktsmandater)
	 * map av fylke->(parti->stemmer)
	 * map av fylke->distriktsmandater
	 * <p/>
	 * Dette betyr at fordelingen av distriktsmandater er foretatt før vi begynner på beregningen av utjevningsmandater.
	 * <p/>
	 * 0. Fjerner BLANKE-partiet - vi beregner alle mandater som om alle BLANKE stemmer er kastet/borte.
	 * 1. Lager en map over partier som er over sperregrensen med tilhørende antall stemmer, når vi ser hele landet under ett.
	 * 2. Beregn stemmetallskvotienter for hele landet under ett, etter Sainte Lagües modifiserte metode, med partier som er over sperregrensen.
	 * Antall kvotienter som beregnes antall plasser*antall-partier, selv om de i praksis bare er de første 2-300 kvotientene som har noen betydning.
	 * 3. Beregner totalt antall distriktsmandater et parti har fått, altså hvor mange av de 150 et parti har fått.
	 * 4. Fjern de høyeste stemmetallskvotienter utifra hvilke partier som har distriktsmandater. Dette er dermed den sorterte listen
	 * over de partier/kvotienter som de høyeste på landsbasis, men som ikke fikk et av de 150 distriktsmandatene. De 19 første
	 * kvotientene/partiene har dermed vunnet utjevningsmandater.
	 * 5. Gir tilbake en map over partier og antall utjevningsmandater vunnet. I tillegg blir sisteUtjevningsmandatRangPrParti oppdatert men en map
	 * over partiet -> siste/laveste mandat. Får et parti flere utjevningsmandater, så vil sisteUtjevningsmandatRangPrParti kun inneholde rangeringen
	 * til det siste mandatet.
	 * 6. Beregn en ny kvotient pr parti med utjevningsmandat pr fylke. Denne kvotienten er det vanlige stemmetallskvotienten for
	 * det neste mandatet partiet kan vinne men i tillegg delt på gjennomsnittlig-antall-stemmer-bak-et-mandat-i-dette-fylket. På
	 * den måten kan man sammenligne kvotientene på tvers av alle fylkene. Både remQuotient og partiFylkeKvotienter inneholder
	 * disse kvotientene.
	 * 7. Fordel utjevningsmandatene, ett pr fylke. Fordelingen gjøres slik at man først gir til det partiet med høyest kvotient (fra 6.) (som også angir fylket).
	 * Deretter bruker man neste kvotient i listen og fordeler et utjevningsmandat til det partiet i det fylket. Dersom fylket allerede har gitt bort
	 * sitt utjevningsmandat, så går man videre til neste kvotient. Dersom partiet allerede har fått alle sine utjevningsmandata så går man videre til
	 * neste kvotient. Til slutt er alle utjevningsmandater utdelt, ett i hvert fylke og et korrekt antall til hvert parti.
	 */
	private static UtjevningsmandatResultat beregn(Map<String, Integer> stemmerPrParti,
												   Set<String> lokalePartikoder,
												   Map<String, Map<String, Integer>> distriktsmandaterVunnetPrPartiPrFylke,
												   Map<String, Map<String, Integer>> stemmerPrPartiPrFylke,
												   Map<String, Integer> distriktsmandaterTilValgPrFylke) {

		Map<String, Integer> stemmerPrPartiUtenBlanke = fjernBlankePartiet(stemmerPrParti); // 0.
		Map<String, Integer> stemmerPrPartiOverSperregrensen = beregnStemmerPrPartiOverSperregrensen(stemmerPrPartiUtenBlanke, lokalePartikoder); // 1.
		int antallMandaterTotalt = distriktsmandaterTilValgPrFylke.values().stream().mapToInt(i -> i).sum() + distriktsmandaterTilValgPrFylke.size(); //
		PartiKvotient[] landsKvotienter = MandatberegningFelles.sorterteKvotienter(antallMandaterTotalt, stemmerPrPartiOverSperregrensen, null); // 2.
		Map<String, Integer> distriktsmandaterPrParti = beregnDistriktsmandaterPrParti(distriktsmandaterVunnetPrPartiPrFylke); // 3.
		// 4.
		for (Entry<String, Integer> distriktsmandaterForEttParti : distriktsmandaterPrParti.entrySet()) {
			landsKvotienter = fjernHøyesteKvotienterTilsvarendeAntallDistriktsmandater(distriktsmandaterForEttParti, landsKvotienter);
		}
		// 5.
		Map<String, Integer> antallUtjevningmandaterPrParti = beregnAntallUtjevningmandaterPrParti(stemmerPrPartiPrFylke.size(), landsKvotienter);
		// 6.
		Map<String, Map<String, Kvotient>> kvotienterPrPartiPrFylke = new TreeMap<>();
		List<PartiFylkeKvotient> partiFylkeKvotientListe = beregnPartiFylkeKvotienter(antallUtjevningmandaterPrParti, distriktsmandaterVunnetPrPartiPrFylke,
				stemmerPrPartiPrFylke, kvotienterPrPartiPrFylke, distriktsmandaterTilValgPrFylke);
		// 7.
		Map<String, Map<String, Integer>> mandatnrPrPartiPrFylke = new TreeMap<>();
		Map<String, String> partiSomFikkUtjevningsmandatPrFylke = fordelUtjevningsmandatenePrFylkeOgParti(antallUtjevningmandaterPrParti, partiFylkeKvotientListe,
				mandatnrPrPartiPrFylke);

		return new UtjevningsmandatResultat(partiSomFikkUtjevningsmandatPrFylke, kvotienterPrPartiPrFylke, mandatnrPrPartiPrFylke);
	}

	private static Map<String, Integer> fjernBlankePartiet(Map<String, Integer> partiStemmeMap) {
		// Vi lager et nytt objekt, slik at vi ikke endrer på noen utenfor denne metoden
		Map<String, Integer> partiStemmeMapUtenBlanke = new TreeMap<>(partiStemmeMap);
		partiStemmeMapUtenBlanke.remove(Konstanter.BLANKE);
		return partiStemmeMapUtenBlanke;
	}

	private static Map<String, Integer> beregnStemmerPrPartiOverSperregrensen(Map<String, Integer> stemmerPrParti, Set<String> lokalePartikoder) {
		int alleStemmerILandet = stemmerPrParti.values().stream()
				.mapToInt(i -> i)
				.sum();
		Map<String, Integer> stemmerPrPartiSomErOverSperregrensen = new TreeMap<>();
		stemmerPrParti.forEach((partikode, antStemmer) -> {
			if (lokalePartikoder.contains(partikode)) {
				return;
			}
			BigDecimal andel = new BigDecimal(antStemmer).divide(new BigDecimal(alleStemmerILandet), EVAADMIN_PRESISJON, ROUND_HALF_UP);
			if (andel.compareTo(Konstanter.SPERREGRENSE) >= 0) {
				stemmerPrPartiSomErOverSperregrensen.put(partikode, antStemmer);
			}
		});
		return stemmerPrPartiSomErOverSperregrensen;
	}

	/**
	 * Fjerner like mange kvotienter som det er mandater for partiet.
	 * Når dette er gjort for alle partier, så sitter vi igjen med de kvotienter
	 * som *ikke* har vunnet distriktsmandater, i synkende rekkefølge. De 19 (ett pr fylke) første
	 * kvotientene vi har igjen da er de som har vunnet et utjevningsmandat.
	 */
	private static PartiKvotient[] fjernHøyesteKvotienterTilsvarendeAntallDistriktsmandater(Entry<String, Integer> seatsParty, PartiKvotient[] landsKvotienter) {
		List<PartiKvotient> landKvotientListe = new ArrayList<>(Arrays.asList(landsKvotienter));
		int fjernet = 0;
		Iterator<PartiKvotient> iterator = landKvotientListe.iterator();
		while (iterator.hasNext() && (fjernet < seatsParty.getValue())) {
			PartiKvotient partiKvotient = iterator.next();
			if (partiKvotient.getPartikode().equals(seatsParty.getKey())) {
				iterator.remove();
				fjernet++;
			}
		}
		return landKvotientListe.toArray(new PartiKvotient[0]);
	}

	private static Map<String, String> fordelUtjevningsmandatenePrFylkeOgParti(Map<String, Integer> antallUtjevningmandaterPrParti,
																			   List<PartiFylkeKvotient> partiFylkeKvotienter,
																			   Map<String, Map<String, Integer>> mandatnrPrPartiPrFylke) {
		int mandatnr = 0;
		Set<String> fjernedePartier = new HashSet<>();
		Set<String> fjernedeFylker = new HashSet<>();
		Map<String, String> partiPrFylke = new TreeMap<>();
		Collections.sort(partiFylkeKvotienter);
		for (PartiFylkeKvotient partiFylkeKvotient : partiFylkeKvotienter) {
			if (kanFordeles(partiFylkeKvotient, fjernedeFylker, fjernedePartier)) {
				// bygger opp mandatnrPrPartiPrFylke-strukturen slik: fylke -> parti -> utjevningsmandatnummer (1-19)
				// MSI feb 2015: vet ikke hva denne brukes til
				mandatnr++;
				Map<String, Integer> mandatnrPrParti = new TreeMap<>();
				mandatnrPrParti.put(partiFylkeKvotient.getPartikode(), mandatnr);
				mandatnrPrPartiPrFylke.put(partiFylkeKvotient.getFylkeNr(), mandatnrPrParti);
				// legger til utjevningsmandatet (fylke, parti) til retur-resultatet
				partiPrFylke.put(partiFylkeKvotient.getFylkeNr(), partiFylkeKvotient.getPartikode());
				// oppdaterer fjernedeFylker - dette fylket har brukt opp sitt utjevningsmandat!
				fjernedeFylker.add(partiFylkeKvotient.getFylkeNr());
				// oppdaterer removedParties - til slutt har partiet mottatt alle sine utjevningsmandater!
				oppdaterFjernedePartier(antallUtjevningmandaterPrParti, partiPrFylke, fjernedePartier, partiFylkeKvotient.getPartikode());
			}
		}
		return partiPrFylke;
	}

	// Dersom fylket/partiet ikke har mottatt alle sine utjevningsmandater, så kan man starte fordelingen til dette fylke/partiet
	private static boolean kanFordeles(PartiFylkeKvotient partiFylkeKvotient, Set<String> fjernedeFylker, Set<String> fjernedePartier) {
		for (String fylkeNr : fjernedeFylker) {
			if (fylkeNr.equals(partiFylkeKvotient.getFylkeNr())) {
				return false;
			}
		}
		for (String partikode : fjernedePartier) {
			if (partikode.equals(partiFylkeKvotient.getPartikode())) {
				return false;
			}
		}
		return true;
	}

	private static void oppdaterFjernedePartier(Map<String, Integer> utjevningmandaterPrParti, Map<String, String> partiPrFylke, Set<String> fjernedePartier, String
			partikode) {
		int antallMandater = 0;
		for (String partikodeSomHarMottattMandat : partiPrFylke.values()) {
			if (partikodeSomHarMottattMandat.equals(partikode)) {
				antallMandater++;
			}
		}
		if (utjevningmandaterPrParti.containsKey(partikode) && utjevningmandaterPrParti.get(partikode) == antallMandater) {
			fjernedePartier.add(partikode);
		}

	}

	private static Map<String, Integer> beregnAntallUtjevningmandaterPrParti(Integer antallUtjevningsmandater, PartiKvotient[] landsKvotienter) {
		Map<String, Integer> utjevningmandaterPrParti = new TreeMap<>();
		for (int i = 0; i < antallUtjevningsmandater; i++) {
			String partikode = landsKvotienter[i].getPartikode();
			utjevningmandaterPrParti.put(partikode, utjevningmandaterPrParti.get(partikode) == null ? 1 : utjevningmandaterPrParti.get(partikode) + 1);
		}
		return utjevningmandaterPrParti;
	}

	/**
	 * Lager en liste over alle kvotienter for partier som har vunnet utjevningsmandatene. Denne
	 * kvotientlisten danner grunnlag for å bestemme hvilke fylker partiene skal få utjevningsmandater
	 * i, men altså ikke *om* partiene har vunnet utjevningsmandater - det er allerede bestemt.
	 *
	 * Formelen for kvotienten er slik:
	 * kvot = Partiets stemmer i et fylket / ((2* antall distriktmandater vunnet) + 1) / (antall stemmer bak distriktsmandat i dette fylket)
	 */
	private static List<PartiFylkeKvotient> beregnPartiFylkeKvotienter(Map<String, Integer> antallUtjevningsmandaterPrParti,
																	   Map<String, Map<String, Integer>> distriktsmandatPrPartiPrFylke,
																	   Map<String, Map<String, Integer>> stemmerPrPartiPrFylke,
																	   Map<String, Map<String, Kvotient>> kvotienterPrPartiPrFylke,
																	   Map<String, Integer> mandaterPrFylke) {

		Map<String, BigDecimal> stemmerBakEtMandatPrFylke = beregnStemmerBakEtMandatPrFylke(mandaterPrFylke, stemmerPrPartiPrFylke);

		List<PartiFylkeKvotient> partiFylkeKvotientListe = new ArrayList<>();
		stemmerPrPartiPrFylke.forEach((fylkeNr, stemmerPrParti) -> stemmerPrParti.forEach((partikode, stemmer) -> {
			if (!antallUtjevningsmandaterPrParti.containsKey(partikode)) {
				return;
			}
			int antallDistriktsmandaterVunnet = 0;
			Map<String, Integer> distriktsmandatResulatForEttFylke = distriktsmandatPrPartiPrFylke.get(fylkeNr);
			if (distriktsmandatResulatForEttFylke != null && distriktsmandatResulatForEttFylke.get(partikode) != null) {
				antallDistriktsmandaterVunnet = distriktsmandatResulatForEttFylke.get(partikode);
			}
			Kvotient kvot = getKvot(stemmerBakEtMandatPrFylke, fylkeNr, stemmer, antallDistriktsmandaterVunnet);
			kvotienterPrPartiPrFylke.putIfAbsent(fylkeNr, new TreeMap<>());
			kvotienterPrPartiPrFylke.get(fylkeNr).put(partikode, kvot);

			// oppdaterer også quotients-tabellen med de samme data som remQuotient, bare en annen form
			PartiFylkeKvotient partiFylkeKvotient = new PartiFylkeKvotient(partikode, fylkeNr, kvot);
			partiFylkeKvotient.setStemmetall(stemmer); // Vi trenger stemmetallet, fordi vi skal sortere denne senere
			partiFylkeKvotientListe.add(partiFylkeKvotient);
		}));
		return partiFylkeKvotientListe;
	}

	private static Kvotient getKvot(Map<String, BigDecimal> stemmerBakEtMandatPrFylke, String fylkeNr, Integer stemmer, int antallDistriktsmandaterVunnet) {
		if (stemmerBakEtMandatPrFylke.get(fylkeNr) == null) {
			return new Kvotient(0);
		}
		return new Kvotient(stemmer, (2 * antallDistriktsmandaterVunnet) + 1).divide(stemmerBakEtMandatPrFylke.get(fylkeNr));
	}

	private static Map<String, BigDecimal> beregnStemmerBakEtMandatPrFylke(Map<String, Integer> mandaterPrFylke, Map<String, Map<String, Integer>>
			stemmerPrPartiPrFylke) {

		Map<String, Integer> stemmerPrFylke = new TreeMap<>();
		stemmerPrPartiPrFylke.forEach((fylke, stemmerPrParti) -> stemmerPrFylke.put(fylke, stemmerPrParti.values().stream().mapToInt(i -> i).sum()));

		Map<String, BigDecimal> stemmerBakEtMandatPrFylke = new TreeMap<>();
		for (String fylkeNr : mandaterPrFylke.keySet()) {
			stemmerBakEtMandatPrFylke.put(fylkeNr, stemmerBakEtMandat(mandaterPrFylke, stemmerPrFylke, fylkeNr));
		}
		return stemmerBakEtMandatPrFylke;
	}

	private static BigDecimal stemmerBakEtMandat(Map<String, Integer> mandaterPrFylke, Map<String, Integer> stemmerPrFylke, String fylkeNr) {
		Integer stemmer = stemmerPrFylke.get(fylkeNr);
		Integer mandater = mandaterPrFylke.get(fylkeNr);
		if (stemmer == null || mandater == null) {
			return null;
		}
		return new BigDecimal(stemmer).divide(new BigDecimal(mandater), MAKS_PRESISJON, ROUND_HALF_UP);
	}

	private static Map<String, Integer> beregnDistriktsmandaterPrParti(Map<String, Map<String, Integer>> distriktsmandaterPrPartiPrFylke) {
		Map<String, Integer> distriktsmandaterPrParti = new TreeMap<>();
		for (Map<String, Integer> fylke : distriktsmandaterPrPartiPrFylke.values()) {
			fylke.forEach((partikode, antallMandater) -> {
				if (distriktsmandaterPrParti.containsKey(partikode)) {
					distriktsmandaterPrParti.put(partikode, distriktsmandaterPrParti.get(partikode) + antallMandater);
				} else {
					distriktsmandaterPrParti.put(partikode, antallMandater);
				}
			});
		}
		return distriktsmandaterPrParti;
	}
}
