package no.valg.valgnatt.database.geografi;

import lombok.Getter;
import lombok.Setter;
import no.valg.valgnatt.common.ValgnattException;
import no.valg.valgnatt.database.Konstanter;
import no.valg.valgnatt.database.domene.ForeløpigEndeligOppgjør;
import no.valg.valgnatt.database.domene.OpptaltStatus;
import no.valg.valgnatt.database.domene.Valg;
import no.valg.valgnatt.database.domene.ValgType;
import no.valg.valgnatt.database.domene.ValgtingForhand;
import no.valg.valgnatt.database.hibernate.Stemmekrets;
import no.valg.valgnatt.database.hibernate.Valgdistrikt;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.stream.Stream;

/**
 * MSI des 2015:
 * Område tilbyr metoder som kan brukes på subklasse-nivåene, f.eks. det å hente ut antall stemmekretser.
 * I tillegg tilbyr den praktiske metoder for å summere sammen partimandater eller partistemmer. Pluss en del annet
 * som også er felles. Merk at dette ikke er en klasse som et slags toppnivå over Land, men fellesfunksjonalitet som
 * gjelder klasser som implementerer denne abstract-klassen. I noen grad må sub-klassene overstyre
 * implementasjoner som er i denne klassen. Disse overstyringene representerer på mange måter alle særegenhetene
 * og variasjonene mellom områdene.
 */
public abstract class Område {

    // antall frammøtte er frammøteprosenten ganget med antall stemmeberettigede. Dersom vi er på jakt etter
    // antall frammøte for henholdsvis FHS eller VTS, så må vi forsøke å beregne en andel av stemmene
    // som går til FHS/VTS. Vi blåser opp frammøte med en faktor på 5%, for å sikre oss at opptaltprosenten
    // ikke tipper over 100% særlig ofte, det vil da gi 99.8/9% opptaltprosent selv om det kanskje er flere
    // kretser igjen på landsbasis. Da er det bedre å legge på litt ekstra i gjetting av frammøte her. Dette
    // betyr altså at opptaltprosentene som returneres er litt for små inntil man er ferdig opptalt (da blir
    // 99.8, 99.9 og 100.0 slik som det skal.
    // Denne faktoren var 1.05 under valget 2017, men dette slo uheldig ut - landets opptaltprosent ble stående på 95.1
    private static final float OPPBLÅSFAKTOR = 1.0f;
    // andrePartikoder er det sett av partikoder som har partikategori 2 og 3. Hvilke partier dette er varierer
    // fra område til område, men lar seg enkelt finne ved å gå igjennom alle partier i alle kretser i dette området.
    // Fordi denne prosessen med å finne alle andrePartikoder er litt omstendelig og i tillegg at listen
    // man finner er statisk for et gitt valg, så har vi gjort en optimalisering - vi lagrer denne listen for
    // dette området etter det er blitt hentet opp første gang.
    private List<String> andrePartikoder;

    @Getter @Setter private Land land;
    @Getter @Setter private int minimumKretserForPrognose = Konstanter.MIN_KRETSER;

    public abstract Map<String, Kommune> getKommuner();

    public abstract Valgdistrikt getValgdistrikt();

    public abstract String getNavn();

    public abstract String getNr();

    public abstract Område getOverordnetOmråde();

    public abstract Område[] getUnderordnedeRapportOmråder();

    public String getRapportid() {
        return getOverordnetOmråde().getRapportid() + "/" + getNr();
    }

    public String getRapportnavnURLEncLC() {
        try {
            return getOverordnetOmråde().getRapportnavnURLEncLC() + "/" + URLEncoder.encode(getNavn().toLowerCase(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new ValgnattException(e); // Vi forventer aldri dette - alle plattformer støtter UTF-8!!
        }
    }

    public int getAntallMandater() {
        if (dekkerMinstEttKomplettValgdistrikt()) {
            return getValgdistrikter().values().stream().mapToInt(Valgdistrikt::getAntallmandater).sum();
        }
        return 0;
    }

    // Basert på getStemmekretser() så henter vi ut valgdistrikter.
    public Map<String, Valgdistrikt> getValgdistrikter() {
        Map<String, Valgdistrikt> valgdistriktMap = new TreeMap<>();
        for (Stemmekrets stemmekrets : getStemmekretser().values()) {
            Valgdistrikt valgdistrikt = stemmekrets.getKommune().getValgdistrikt();
            valgdistriktMap.put(valgdistrikt.getId().getNr(), valgdistrikt);
        }
        return valgdistriktMap;
    }

    public abstract Map<String, Stemmekrets> getStemmekretser();

    private boolean dekkerMinstEttKomplettValgdistrikt() {
        if (this.getClass() == Land.class || this instanceof LandInndeling) {
            return true;
        }
        if (this.getClass() == Stemmekrets.class || this.getClass() == Bydel.class) {
            return false;
        }
        // eneste mulighet igjen nå er Kommune - den er bare et valgdistrikt i KO-valg, ellers ikke:
        return getValgType() == ValgType.KO;
    }

    public ValgType getValgType() {
        return land == null ? null : land.getValgType();
    }

    // Summérer summérbare tall (vts, fhs, sistevts, etc.) for partier med angitt partikoder
    // Dette brukes til å beregne Andre/Andre2-"partiene" i rapportene
    public PartiStemmer sumStemmerAndre() {
        List<PartiStemmer> partistemmerListe = aggregerStemmerPrParti(Summering.PARTIER_SOM_STILLER); // Trenger ikke partier som ikke stiller i år
        List<String> partikoderAndre = getAndrePartikoder();
        PartiStemmer andre = new PartiStemmer(Konstanter.ANDRE, Konstanter.ANDRE, Konstanter.PARTIKATEGORIANDRE, Konstanter.DEFAULT_VISNINGSSEKVENS, false);
        for (PartiStemmer partistemmer : partistemmerListe) {
            if (partikoderAndre.contains(partistemmer.getPartikode())) {
                andre.add(partistemmer);
            }
        }
        return andre;
    }

    // Finner liste over partikoder som er av partikategori 2 eller 3 -> ANDRE partier (men ikke BLANKE partiet)
    List<String> getAndrePartikoder() {
        if (andrePartikoder == null) {
            List<PartiStemmer> partistemmerListe = aggregerStemmerPrParti(Summering.PARTIER_SOM_STILLER); // Trenger ikke historikk
            List<String> partikoder = new ArrayList<>();
            for (PartiStemmer PS : partistemmerListe) {
                if (PS.getPartikode().equals(Konstanter.BLANKE)) {
                    continue;
                }
                if ("2".equals(PS.getPartikategori()) || "3".equals(PS.getPartikategori())) {
                    partikoder.add(PS.getPartikode());
                }
            }
            andrePartikoder = partikoder;
        }
        return andrePartikoder;
    }

    /**
     * En sentral metode som aggregerer stemmer fra et sett av stemmekretser. Brukstilfeller:
     * 1) Summere alle stemmer totalt: Vi trenger absolutt alle stemmer fra alle valgår.
     * 2) Summere stemmer pr parti: Vi trenger alle stemmer pr parti fra alle valgår.
     * 3) Summere stemmer for "Andre"-partier: Vi trenger alle stemmer for disse partiene for alle valgår.
     * 4) Summere stemmer for "Andre2"-partier: Vi trenger bare de stemmene fra gamle valgår som ikke har blitt tatt med i partier som stiller
     * <p/>
     * Tilfelle 1-3 er nokså likt, men tilfelle 4 skiller seg ut. Poenget er at et parti kan både
     * stille og ikke-stille i et område (f.eks. hele landet). Da telles partiet med i Andre-partier,
     * og må dermed ikke telles med i Andre2. Kun partier som overhodet ikke stiller noe sted blir talt
     * opp i Andre2.
     * <p/>
     * Logikken er som følger
     * 1. Les inn PartiStemmer pr parti/krets - fordel stemmene i 2 maps etter hvorvidt de stiller i kretsen
     * 2. Dersom TOTAL-summering, ta med alle stemmer uansett om partiet stiller liste nå eller ei -> Komplett historikk
     * 3. Dersom PARTIER_SOM_STILLER-summering, ta med alle stemmer for partier som stiller minst ett sted -> Komplett historikk for partiene
     * 4. Dersom PARITER_SOM_IKKE_STILLER-summering, fjern alle stemmer for partier soms tiller minst ett sted -> Står igjen med de som bare stilte sist.
     * Da kan brukstilfelle 1 dekkes av med en TOTAL-summering. Brukstilfelle 2 og 3 dekkes med PARTIER_SOM_STILLER-summering
     * og brukstilfelle 4 dekkes med PARTIER_SOM_IKKE_STILLER-summering.
     */
    public List<PartiStemmer> aggregerStemmerPrParti(Summering summering) {
        Map<String, PartiStemmer> stiller = new TreeMap<>();
        Map<String, PartiStemmer> stillerIkke = new TreeMap<>();
        Map<String, PartiStemmer> resultat;
        sumStemmerStillerOgStillerIkke(getStemmekretser().values(), stiller, stillerIkke);
        if (summering == Summering.TOTAL) {
            resultat = sumStemmerTotal(stiller, stillerIkke);
        } else if (summering == Summering.PARTIER_SOM_STILLER) {
            resultat = sumStemmerStiller(stiller, stillerIkke);
        } else { // Summering.PARTIER_SOM_IKKE_STILLER
            resultat = sumStemmerStillerIkke(stiller, stillerIkke);
        }
        List<PartiStemmer> resultList = new ArrayList<>(resultat.values());
        Collections.sort(resultList);
        return resultList;
    }

    private Map<String, PartiStemmer> sumStemmerStillerIkke(Map<String, PartiStemmer> stiller, Map<String, PartiStemmer> stillerIkke) {
        stillerIkke.keySet().removeIf(o -> stiller.get(o) != null);
        return stillerIkke;
    }

    private Map<String, PartiStemmer> sumStemmerStiller(Map<String, PartiStemmer> stiller, Map<String, PartiStemmer> stillerIkke) {
        for (PartiStemmer stillerIkkePS : stillerIkke.values()) {
            PartiStemmer stillerPS = stiller.get(stillerIkkePS.getPartikode());
            if (stillerPS != null) {
                stillerPS.add(stillerIkkePS);
            }
        }
        return stiller;
    }

    private Map<String, PartiStemmer> sumStemmerTotal(Map<String, PartiStemmer> stiller, Map<String, PartiStemmer> stillerIkke) {
        for (PartiStemmer stillerIkkePS : stillerIkke.values()) {
            PartiStemmer stillerPS = stiller.get(stillerIkkePS.getPartikode());
            if (stillerPS == null) {
                stiller.put(stillerIkkePS.getPartikode(), stillerIkkePS);
            } else {
                stillerPS.add(stillerIkkePS);
            }
        }
        return stiller;
    }

    // Populerer 2 maps som inneholder nøkkelen partikode og en verdi av aggregert sum av partistemmer. Dersom en partikode bare finnes i stillerIkke, så
    // vet vi sikkert at dette partiet ikke stiller overhodet noe sted i dette området.
    private void sumStemmerStillerOgStillerIkke(Collection<Stemmekrets> stemmekretser, Map<String, PartiStemmer> stiller, Map<String, PartiStemmer> stillerIkke) {
        for (Stemmekrets stemmekrets : stemmekretser) {
            for (PartiStemmer PS : stemmekrets.getPartier()) {
                Map<String, PartiStemmer> tmpMap;
                if (PS.isStillerliste()) {
                    tmpMap = stiller;
                } else {
                    tmpMap = stillerIkke;
                }
                PartiStemmer partiStemmer = tmpMap.get(PS.getPartikode());
                if (partiStemmer == null) {
                    // Lager en kopi her, for å ikke forstyrre tilstanden i de innleste stemmene
                    partiStemmer = new PartiStemmer(PS.getPartikode(), PS.getPartinavn(), PS.getPartikategori(), PS.getVisningssekvens(), PS.isStillerliste());
                    tmpMap.put(PS.getPartikode(), partiStemmer);
                }
                partiStemmer.add(PS);
            }
        }
    }

    public PartiStemmer sumStemmerAndre2() {
        List<PartiStemmer> partistemmerListe = aggregerStemmerPrParti(Summering.PARTIER_SOM_IKKE_STILLER); // Trenger historikk for å finne de partier som stilte liste
        // sist, men ikke nå!
        PartiStemmer andre = new PartiStemmer(Konstanter.ANDRE2, Konstanter.ANDRE2, Konstanter.PARTIKATEGORIANDRE, Konstanter.DEFAULT_VISNINGSSEKVENS, false);
        for (PartiStemmer partistemmer : partistemmerListe) {
            andre.add(partistemmer);
        }
        return andre;


    }

    public OpptaltStatus getLavesteOS() {
        int lavesteOS = Integer.MAX_VALUE;
        for (Stemmekrets stemmekrets : getStemmekretser().values()) {
            if (stemmekrets.getOpptaltStatus() < lavesteOS) {
                lavesteOS = stemmekrets.getOpptaltStatus();
            }
        }
        return lavesteOS == Integer.MAX_VALUE ? OpptaltStatus.INGEN : OpptaltStatus.values()[lavesteOS];
    }

    public int getAntallStemmeberettigede() {
        return getAntallStemmeberettigede(stemmekrets -> true);
    }

    /**
     * ANTALL STEMMEGIVINGER OG ANTALL STEMMESEDLER FORKASTET metoder
     */

    public int getAntallStsForkastede() {
        return getStemmekretser().values().stream().mapToInt(sk -> sk.getStsForkastede() == null ? 0 : sk.getStsForkastede()).sum();
    }

    public int getAntallStsFhsForkastede() {
        return getStemmekretser().values().stream().mapToInt(sk -> sk.getStsFhsForkastede() == null ? 0 : sk.getStsFhsForkastede()).sum();
    }

    public int getAntallStgFhsGodkjente() {
        return getStemmekretser().values().stream().mapToInt(sk -> sk.getStgFhsGodkjente() == null ? 0 : sk.getStgFhsGodkjente()).sum();
    }

    public int getAntallStgForkastede() {
        return getStemmekretser().values().stream().mapToInt(sk -> sk.getStgForkastede() == null ? 0 : sk.getStgForkastede()).sum();
    }

    public int getAntallStgFhsForkastede() {
        return getStemmekretser().values().stream().mapToInt(sk -> sk.getStgFhsForkastede() == null ? 0 : sk.getStgFhsForkastede()).sum();
    }

    public PartiMandater sumMandaterTotal() {
        PartiMandater total = new PartiMandater(Konstanter.PARTI_TOTAL, Konstanter.PARTI_TOTAL, Konstanter.DEFAULT_PARTIKATEGORI, Konstanter.DEFAULT_VISNINGSSEKVENS, false);
        if (dekkerMinstEttKomplettValgdistrikt()) {
            for (PartiMandater PM : aggregerMandaterPrParti(getValgdistrikter().values(), Summering.TOTAL, true).values()) {
                total.aggreger(PM, true);
            }
        }
        return total;
    }

    /**
     * En sentral metode som aggregerer mandater fra et sett av valgdistrikter. Brukstilfeller:
     * 1) Summere alle mandater totalt: Vi trenger absolutt alle mandater fra alle valgår.
     * 2) Summere mandater pr parti: Vi trenger alle mandater for dette partiet fra alle valgår.
     * 3) Summere mandater for "Andre"-partier: Vi trenger alle mandater for disse partiene for alle valgår.
     * 4) Summere mandater for "Andre2"-partier: Vi trenger bare de mandatene fra gamle valgår som ikke har blitt tatt med i partier som stiller
     * <p/>
     * Tilfelle 1-3 er nokså likt, men tilfelle 4 skiller seg ut. Poenget er at et parti kan både
     * stille og ikke-stille i et område (f.eks. hele landet). Da telles partiet med i Andre-partier,
     * og må dermed ikke telles med i Andre2. Kun partier som overhodet ikke stiller noe sted blir talt
     * opp i Andre2.
     * <p/>
     * Logikken er som følger
     * 1. Les inn PartiMandater pr parti/krets - fordel mandatene i 2 maps etter hvorvidt de stiller i kretsen man
     * 2. Dersom TOTAL-summering, ta med alle stemmer uansett om partiet stiller liste nå eller ei -> Komplett historikk
     * 3. Dersom PARTIER_SOM_STILLER-summering, ta med alle stemmer for partier som stiller minst ett sted -> Komplett historikk for partiene
     * 4. Dersom PARITER_SOM_IKKE_STILLER-summering, fjern alle stemmer for partier soms tiller minst ett sted -> Står igjen med de som bare stilte sist.
     * Da kan brukstilfelle 1 dekkes av med en TOTAL-summering. Brukstilfelle 2 og 3 dekkes med PARTIER_SOM_STILLER-summering
     * og brukstilfelle 4 dekkes med PARTIER_SOM_IKKE_STILLER-summering.
     */
    private Map<String, PartiMandater> aggregerMandaterPrParti(Collection<Valgdistrikt> valgdistrikter, Summering summering, boolean aggregerHistorikk) {
        Map<String, PartiMandater> stiller = new TreeMap<>();
        Map<String, PartiMandater> stillerIkke = new TreeMap<>();
        sumMandaterStillerOgStillerIkke(valgdistrikter, stiller, stillerIkke, aggregerHistorikk);
        if (summering == Summering.TOTAL) {
            return sumMandaterTotal(stiller, stillerIkke);
        } else if (summering == Summering.PARTIER_SOM_STILLER) {
            return sumMandaterStiller(stiller, stillerIkke);
        } else { // Summering.PARTIER_SOM_IKKE_STILLER
            return sumMandaterStillerIkke(stiller, stillerIkke);
        }
    }

    private Map<String, PartiMandater> sumMandaterStillerIkke(Map<String, PartiMandater> stiller, Map<String, PartiMandater> stillerIkke) {
        stillerIkke.keySet().removeIf(o -> stiller.get(o) != null);
        return stillerIkke;
    }

    private Map<String, PartiMandater> sumMandaterStiller(Map<String, PartiMandater> stiller, Map<String, PartiMandater> stillerIkke) {
        for (PartiMandater stillerIkkePM : stillerIkke.values()) {
            PartiMandater stillerPM = stiller.get(stillerIkkePM.getPartikode());
            if (stillerPM != null) {
                stillerPM.aggreger(stillerIkkePM, true);
            }
        }
        return stiller;
    }

    private Map<String, PartiMandater> sumMandaterTotal(Map<String, PartiMandater> stiller, Map<String, PartiMandater> stillerIkke) {
        for (PartiMandater stillerIkkePM : stillerIkke.values()) {
            PartiMandater stillerPM = stiller.get(stillerIkkePM.getPartikode());
            if (stillerPM == null) {
                stiller.put(stillerIkkePM.getPartikode(), stillerIkkePM);
            } else {
                stillerPM.aggreger(stillerIkkePM, true);
            }
        }
        return stiller;
    }

    private void sumMandaterStillerOgStillerIkke(Collection<Valgdistrikt> valgdistrikter, Map<String, PartiMandater> stiller, Map<String, PartiMandater> stillerIkke, boolean aggregerHistorikk) {
        for (Valgdistrikt valgdistrikt : valgdistrikter) {
            for (PartiMandater pm : valgdistrikt.getPartiMandater()) {
                Map<String, PartiMandater> tmpMap = pm.isStillerliste() ? stiller : stillerIkke;
                PartiMandater partiMandater = tmpMap.get(pm.getPartikode());
                if (partiMandater == null) {
                    // Lager en kopi her, for å ikke forstyrre tilstanden i de innleste mandatene
                    partiMandater = new PartiMandater(pm.getPartikode(), pm.getPartinavn(), pm.getPartikategori(), pm.getVisningssekvens(), pm.isStillerliste());
                    tmpMap.put(pm.getPartikode(), partiMandater);
                }
                partiMandater.aggreger(pm, aggregerHistorikk);
            }
        }
    }

    public PartiMandater sumMandaterParti(String partikode) {
        return aggregerMandaterPrParti(getValgdistrikter().values(), Summering.PARTIER_SOM_STILLER, false).get(partikode);
    }

    /**
     * Denne metoden er litt hackete - vi trenger den en eneste gang for å hente ut valgdistriktet (og dermed
     * partimandater) koblet til et område. Et område har ikke nødvendigvis ett valgdistrikt, men i det
     * tilfellet metoden her blir brukt, så har den alltid det (vi spør enten etter land, fylke eller kommune)
     * Dermed fungerer koden under...
     */
    public PartiMandater getPartiMandater(String partikode) {
        return getValgdistrikter().values().stream()
                .findAny()
                .orElse(new Valgdistrikt())
                .getPartiMandater(partikode);
    }

    public PartiMandater sumMandaterAndre() {
        PartiMandater andre = new PartiMandater(Konstanter.ANDRE, Konstanter.ANDRE, Konstanter.DEFAULT_PARTIKATEGORI, Konstanter.DEFAULT_VISNINGSSEKVENS, false);
        List<String> partikoderAndre = getAndrePartikoder();
        for (PartiMandater PM : aggregerMandaterPrParti(getValgdistrikter().values(), Summering.PARTIER_SOM_STILLER, false).values()) {
            if (partikoderAndre.contains(PM.getPartikode())) {
                andre.aggreger(PM, true);
            }
        }
        return andre;
    }

    public PartiMandater sumMandaterAndre2(PartiMandater totalPM) {
        PartiMandater andre2 = new PartiMandater(Konstanter.ANDRE2, Konstanter.ANDRE2, Konstanter.DEFAULT_PARTIKATEGORI, Konstanter.DEFAULT_VISNINGSSEKVENS, true);
        for (PartiMandater PM : aggregerMandaterPrParti(getValgdistrikter().values(), Summering.PARTIER_SOM_IKKE_STILLER, false).values()) {
            andre2.aggreger(PM, true);
        }
        andre2.getPrognoseData().setPrognose(totalPM.getPrognoseData().getPrognoseindikator());
        return andre2;
    }

    /**
     * Opptaltprosent er et tall som vises selv om man ikke har fullt opptalte
     * kretser i området. Tanken er at dette tallet skal vise en fremdrift uansett, så
     * lenge det er talt opp stemmer. I tillegg skal prosenten *ikke* bli 100 med mindre
     * alle oppgjør er gjort, og heller ikke over 99,9 med mindre alle endelige tellinger
     * er gjort i området.
     * <p/>
     * Algoritmen:
     * <p/>
     * 1. Dersom opptaltstatus >= OpptaltStatus.OPPGJØR for alle kretser, så returner 1f
     * 2. Dersom opptaltstatus >= OpptaltStatus.ALLE_ENDELIG for alle kretser, så returner 0.999f
     * 2. Dersom opptaltstatus >= OpptaltStatus.ALLE_FOPELØPIG for alle kretser, så returner 0.998f
     * 4. Dersom antall stemmeberettigede er 0, så returner 0f (kan skje i en 0000-krets)
     * 5. Dersom frammøteprosent (FP) er null, så bruk FP for landsnivå
     * 6. Dersom frammøteprosent (FP) fortsatt er null, så bruk default FP (0,7)
     * 7. Opptaltstatus = antall-stemmer-opptalt / (FP*antall-stemmberettigede)
     * 8. Dersom opptaltstatus <8 for en eller flere kretser, så returneres max 0.998f
     * <p>
     * Dette har vist seg vanskelig å forstå for folk flest, og det har dermed blitt besluttet
     * at dette tallet ikke skal vises i eksternt API. I stedet brukes følgende tall:
     * - forelopigFhs
     * - forelopigVts
     * - forelopig
     * - endelig
     * - oppgjor
     */
    public float beregnOpptaltProsent() {
        int sb = getAntallStemmeberettigede();
        boolean erOmrådeOppgjort = beregnOpptaltProsent(ForeløpigEndeligOppgjør.OPPGJØR, null) == 1f;
        if (erStortingsvalgHvorDetteFylketErFerdigOpptaltMenIkkeAlleAndre(erOmrådeOppgjort)) {
            return Konstanter.OSPROSENT_ALLE_ENDELIGE_STEMMER_OPPTALT;
        }
        if (erOmrådeOppgjort) { // 1
            return Konstanter.OSPROSENT_ALLE_OPPGJØR_FERDIG;
        }
        float endeligOpptaltProsent = beregnOpptaltProsent(ForeløpigEndeligOppgjør.ENDELIG, null);
        if (endeligOpptaltProsent == 1f) { // 2
            return Konstanter.OSPROSENT_ALLE_ENDELIGE_STEMMER_OPPTALT;
        }
        if (sb == 0) { // 4
            return 0f;
        }

        float foreløpigOpptaltProsent = beregnOpptaltProsent(ForeløpigEndeligOppgjør.FORELØPIG, null);
        float osProsent = Math.max(foreløpigOpptaltProsent, endeligOpptaltProsent);
        if (osProsent > Konstanter.OSPROSENT_ALLE_FORELØPIGE_STEMMER_OPPTALT) {
            return Konstanter.OSPROSENT_ALLE_FORELØPIGE_STEMMER_OPPTALT;
        } else {
            return osProsent;
        }
    }

    @SuppressWarnings("squid:S1244") // Verdien settes til 1f i koden
    private boolean erStortingsvalgHvorDetteFylketErFerdigOpptaltMenIkkeAlleAndre(boolean erOmrådeFerdig) {
        return getValgType() == ValgType.ST && getClass() == Fylke.class && erOmrådeFerdig && getLand().beregnOpptaltProsent() != 1f;
    }

    /**
     * METODER RELATERT TIL FRAMMØTE OG OPPTALTPROSENT
     **/

	/* Metoden er ganske lik den over, men denne beregningen skal gi en opptaltstatus
       basert på spesfikke indikatorer, mens den over en en slags oppsummering av ALLE
	   indikatorene.
	 */
    @SuppressWarnings("squid:S2164") // Verdien settes til 0f i koden, og vi benytter derfor float
    public float beregnOpptaltProsent(ForeløpigEndeligOppgjør feo, ValgtingForhand vf) {
        int sb = getAntallStemmeberettigede();
        long antallKretserSomSkalTelles = getStemmekretser().size();
        if (vf != null) { // Trekker fra noen kretser som ikke mottar FHS/VTS.
            if (ValgtingForhand.F == vf) {
                antallKretserSomSkalTelles -= getStemmekretser().values().stream().filter(sk -> sk.getOpptaltStatusMax() == OpptaltStatus.BARE_VTS_ENDELIG.getStatus())
                        .count();
            } else if (ValgtingForhand.V == vf) {
                antallKretserSomSkalTelles -= getStemmekretser().values().stream().filter(sk -> sk.getOpptaltStatusMax() == OpptaltStatus.BARE_FHS_ENDELIG.getStatus())
                        .count();
            }
        }
        long antallKretserMedGittStatus = getStemmekretser().values().stream().filter(sk -> sk.erOpptalt(feo, vf)).count();

        // Håndterer 0% og 100% tilfellene
        if (antallKretserSomSkalTelles + antallKretserMedGittStatus == 0) {
            return 0f;
        } else if (antallKretserSomSkalTelles <= antallKretserMedGittStatus) {
            return 1f;
        } else if (sb == 0) {
            return 0f;
        }

        int antallStemmer = getStemmekretser().values().stream().mapToInt(sk -> sk.getStemmer(Valg.DETTE, feo, vf)).sum();
        Float opptaltprosent = antallStemmer / beregnAntallFrammøtte(sb, vf);

        if (opptaltprosent > Konstanter.OSPROSENT_ALLE_ENDELIGE_STEMMER_OPPTALT) { // Den eneste måten å komme til 1f er ved at opptaltstatus er satt korrekt i nok
            // kretser
            return Konstanter.OSPROSENT_ALLE_ENDELIGE_STEMMER_OPPTALT;
        }
        return opptaltprosent;
    }

    @SuppressWarnings("squid:S2164") // Verdien settes til 0f i koden, og vi benytter derfor float
    private float beregnAntallFrammøtte(int sb, ValgtingForhand vf) {
        if (vf == null) {
            return OPPBLÅSFAKTOR * sb * beregnFrammøteForOpptaltProsentBeregning();
        } else if (vf == ValgtingForhand.F) {
            return OPPBLÅSFAKTOR * sb * beregnFrammøteForOpptaltProsentBeregning() * beregnAndelFhs();
        } else { // ValgtingForhand.V
            return OPPBLÅSFAKTOR * sb * beregnFrammøteForOpptaltProsentBeregning() * (1 - beregnAndelFhs());
        }
    }

    // bereger andelFhs utifra de stemmekretser som ligger under dette området - ergo
    // vil andelen FHS variere fra område til område, og slik sett påvirke opptaltandelen
    // for FHS. Dette er godt nok, vi trenger ikke å jobbe ekstremt mye med å finne
    // en perfekt prognose/beregning av forhåndsstemmeandelen - da dette kun påvirker
    // opptalt.foreløpigFhs/opptalt.foreløpigVts i rapporten.
    @SuppressWarnings("squid:S2164") // Verdien settes til 0f i koden, og vi benytter derfor float
    private float beregnAndelFhs() {
        int alleFhs = getSum(PartiStemmer::getFhs);
        int alleVts = getSum(PartiStemmer::getVts);
        if (alleFhs == 0 || alleVts == 0) {
            return Konstanter.DEFAULT_FHS_ANDEL;
        } else {
            return alleFhs / (float) (alleFhs + alleVts);
        }
    }

    private int getSum(ToIntFunction<PartiStemmer> mapper) {
        return getStemmekretser().values().stream()
                .filter(getStemmekretsPredicate())
                .flatMap(partiStemmer())
                .mapToInt(mapper)
                .sum();
    }

    private Predicate<Stemmekrets> getStemmekretsPredicate() {
        return stemmekrets -> {
            if (stemmekrets.getKommune().harKretsvisOpptelling()) {
                if (stemmekrets.erDefaultKrets()) {
                    return stemmekrets.getOpptaltStatus() >= OpptaltStatus.BARE_FHS_FORELØPIG.getStatus();
                } else {
                    return stemmekrets.getOpptaltStatus() >= OpptaltStatus.BARE_VTS_FORELØPIG.getStatus();
                }
            } else {
                return stemmekrets.getOpptaltStatus() >= OpptaltStatus.ALLE_FORELØPIG.getStatus();
            }
        };
    }

    private Function<Stemmekrets, Stream<? extends PartiStemmer>> partiStemmer() {
        return stemmekrets -> stemmekrets.getPartier().stream();
    }

    @SuppressWarnings("squid:S1244") // Verdien settes til 0f i koden
    private Float beregnFrammøteForOpptaltProsentBeregning() {
        Float frammote = beregnFrammoteProsentIaar();
        Område tmp = this;
        while (frammote == null || frammote == 0f) {
            if (tmp.getOverordnetOmråde() == null) { // omraade = Land
                break;
            }
            tmp = tmp.getOverordnetOmråde();
            frammote = tmp.beregnFrammoteProsentIaar();
        }
        if (frammote == null || frammote == 0f) {
            frammote = Konstanter.DEFAULT_FRAMMØTE_PROSENT;
        }
        return frammote;
    }

    /**
     * Frammøteprosent beregnes som et vektet gjennomsnitt av frammøteprosenten i de kommuner
     * som har ferdig opptalte kretser. Dersom ingen kretser i kommunen er ferdig opptalt så finnes
     * det ikke noe frammøteprosent.
     * <p/>
     * NB! Det finnes en konstant som heter Konstanter.DEFAULT_FRAMMØTE_PROSENT. Den brukes
     * kun til å beregne Opptaltprosent, ikke til å gi et faktisk default frammøte. Grunnen
     * er at en slik (feilaktig) antagelse om frammøte er mye mer synlig og problematisk enn
     * om man gir en opptaltprosent som er 10 pp feil.
     */
    @SuppressWarnings("squid:S2164") // Verdien settes til 0f i koden, og vi benytter derfor float
    public Float beregnFrammoteProsentIaar() {
        int totSb = 0;
        float totSum = 0f;
        for (Kommune kommune : getKommuner().values()) {
            Float fremmoteKomm = kommune.beregnFrammoteProsentIaar();
            if (fremmoteKomm != null) {
                int sb = kommune.getAntallStemmeberettigede();
                totSb += sb;
                totSum += Math.round(fremmoteKomm * sb);
            }
        }
        if (totSb > 0) {
            return totSum / totSb;
        } else {
            return null;
        }
    }

    @SuppressWarnings("squid:S2164") // Verdien settes til 0f i koden, og vi benytter derfor float
    public Float beregnFrammoteProsentNestsiste() {
        int sb = getStemmekretser().values().stream().mapToInt(Stemmekrets::getNestsisteStemmeberettigede).sum();
        return sb == 0 ? null : (float) sumStemmerTotal().getNestsisteStemmer() / sb;
    }

    /**
     * PARTISTEMMER metoder
     */

    public PartiStemmer sumStemmerTotal() {
        PartiStemmer total = new PartiStemmer(Konstanter.PARTI_TOTAL, Konstanter.PARTI_TOTAL, Konstanter.DEFAULT_PARTIKATEGORI, Konstanter.DEFAULT_VISNINGSSEKVENS,
                false);
        for (PartiStemmer partistemmer : aggregerStemmerPrParti(Summering.TOTAL)) {
            total.add(partistemmer);
            if (partistemmer.getPartikode().equals(Konstanter.BLANKE)) {
                total.setBlankePartistemmer(partistemmer);
            }
        }
        return total;
    }

    public Date beregnSistEndretTms() {
        Date sistEndretTms = null;
        for (Stemmekrets stemmekrets : getStemmekretser().values()) {
            if (stemmekrets.getEndretTms() != null && (sistEndretTms == null || stemmekrets.getEndretTms().after(sistEndretTms))) {
                sistEndretTms = stemmekrets.getEndretTms();
            }
        }
        return sistEndretTms;
    }

    @SuppressWarnings("squid:S2164") // Verdien settes til 0f i koden, og vi benytter derfor float
    public Float beregnFrammøteProsentStemmegiving() {
        Predicate<Stemmekrets> stemmekretsFilter = sk -> sk.getOS() == OpptaltStatus.OPPGJØR ||
                (sk.getStgGodkjente() != null &&
                        (sk.getOSMax() == OpptaltStatus.BARE_VTS_ENDELIG ?
                                sk.getOS().getStatus() >= OpptaltStatus.BARE_VTS_FORELØPIG.getStatus() :
                                sk.getOS().getStatus() >= OpptaltStatus.ALLE_FORELØPIG.getStatus()));
        int stgGodkjente = getAntallStgGodkjente(stemmekretsFilter);
        int sb = getAntallStemmeberettigede(stemmekretsFilter);
        if (stgGodkjente == 0 || sb == 0) {
            return null;
        } else {
            return stgGodkjente / (float) sb;
        }
    }

    public int getAntallStemmeberettigede(Predicate<Stemmekrets> stemmekretsFilter) {
        return getStemmekretser().values().stream().filter(stemmekretsFilter).mapToInt(Stemmekrets::getStemmeberettigede).sum();
    }

    public int getAntallStgGodkjente(Predicate<Stemmekrets> stemmekretsFilter) {
        return getStemmekretser().values().stream().filter(stemmekretsFilter).mapToInt(sk -> sk.getStgGodkjente() == null ? 0 : sk.getStgGodkjente()).sum();
    }

    public int getAntallStgGodkjente() {
        return getStemmekretser().values().stream().mapToInt(sk -> sk.getStgGodkjente() == null ? 0 : sk.getStgGodkjente()).sum();
    }

    public boolean isSisteSammenlignbar() {
        return true; // overrides i Stemmekrets, Bydel og Kommune
    }

    public boolean isNestSisteSammenlignbar() {
        return true; // overrides i Stemmekrets, Bydel og Kommune
    }
    public enum Summering {
        TOTAL, PARTIER_SOM_STILLER, PARTIER_SOM_IKKE_STILLER
    }

}
