
package de.ivu.elect.business.sitzberechnung.control.wahlneutral;

import static de.ivu.elect.util.NotNull.notNull;
import static java.util.Collections.singletonList;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.ivu.elect.sitzberechnung.math.NachStimmenSortierer;

import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.ErgebnisLogEintragArt;
import de.ivu.elect.business.ergebnis.entity.GewaehltStatus;
import de.ivu.elect.business.ergebniskonflikt.control.util.ErgebnisKonfliktLoeser;
import de.ivu.elect.business.ergebniskonflikt.control.util.SitzverteilungAbbruchBeiErgebnisKonfliktException;
import de.ivu.elect.business.ergebniskonflikt.entity.ErgebnisKonflikt;
import de.ivu.elect.business.ergebniskonflikt.entity.KandidaturenAuswaehlenErgebnisKonfliktCustomData;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.sitzberechnung.control.util.GewaehlteNachStimmenzahlErmittler;
import de.ivu.elect.business.sitzberechnung.control.util.GewaehlteNachStimmenzahlErmittlerCallback;
import de.ivu.elect.business.sitzberechnung.control.util.SitzberechnungProGebietHelper;
import de.ivu.elect.business.sitzberechnung.entity.SitzberechnungsalgorithmusKonfiguration;
import de.ivu.elect.business.sitzberechnung.entity.Sitzberechnungseingabe;
import de.ivu.elect.business.sitzberechnung.entity.Sitzberechnungsergebnis;

/**
 * Verfahren zur Bestimmung je eines Direktkandidaten in den Wahlkreisen (genauer: "Direktkandidaten-Gebieten").
 * <p/>
 * Es werden keine {@link Sitze}-Objekte erzeugt. Für jeden Direktkandidaten wird ein {@link Gewaehlt}-Objekt erzeugt mit folgendem {@link GewaehltStatus} und Rang:
 * <ul>
 * <li>NICHT_ZUGELASSEN mit Rang = 0, falls nicht zugelassen</li>
 * <li>GEWAEHLT mit Rang = 1 für den Kandidaten mit höchster Stimmenzahl jedes Wahlkreises</li>
 * <li>LOS_OFFEN mit Rang = 0 für die Kandidaten mit höchster Stimmenzahl jedes Wahlkreises, falls es wegen Stimmengleichheit mehrere gibt (durch den Variantenberechner)</li>
 * <li>NICHT_GEWAEHLT für alle anderen. Der Rang wird pro Wahlkreis nach Stimmenzahl vergeben, bei Stimmengleichheit gleicher Rang.</li>
 * </ul>
 * <p/>
 * Gelost wird ausschließlich pro Wahlkreis um den einen Sitz (Auswahl eines Kandidaten aus der Menge der Stimmengleichen). Der Kern-Algorithmus bricht beim ersten Konflikt ab. Alle Konflikte sind
 * unabhängig, aber bisher wird kein {@link HinweisAnVariantenberechner} erzeugt, mit dem man dem Variantenberechner eine Optimierung ermöglichen könnte.
 */
public abstract class AbstractDirektwahlSitzberechnungsalgorithmusInternal<K extends SitzberechnungsalgorithmusKonfiguration> implements MehrheitswahlSitzberechnungsalgorithmusInternal<K> {

  protected final Sitzberechnungseingabe<K> sitzberechnungseingabe;

  protected final SitzberechnungProGebietHelper helper;

  protected final Map<Gebiet, Map<PolitischeGruppe, Long>> gebiet2PolitischeGruppe2AnzahlSitze = new HashMap<>();

  protected AbstractDirektwahlSitzberechnungsalgorithmusInternal(Sitzberechnungseingabe<K> sitzberechnungseingabe) {
    super();

    this.sitzberechnungseingabe = sitzberechnungseingabe;

    this.helper = new SitzberechnungProGebietHelper(sitzberechnungseingabe, getWahlgebiet(sitzberechnungseingabe));
  }

  protected boolean isStichwahl() {
    return helper.getErgebnis().getWahl().getWahlgang().isStichwahl();
  }

  /**
   * Kann in Subklassen überschrieben werden
   *
   * @return false, um die Erzeugung von Sitze-Objekten zu unterdrücken
   */
  public boolean isMitSitzeErzeugung() {
    return false;
  }

  /**
   * Das Wahlgebiet spielt hier bei der Sitzverteilung eigentlich keine Rolle.
   *
   * @return das Wahlgebiet
   */
  public Wahldurchfuehrungsgebiet getWahlgebiet(Sitzberechnungseingabe<K> sitzberechnungseingabe) {
    return sitzberechnungseingabe.getStimmergebnisDaten().getWurzelgebiet();
  }

  protected long getAnzahlSitzeImGebiet(Wahldurchfuehrungsgebiet direktwahlgebiet) {
    return 1L; // Standard der meisten Wahlen
  }

  /**
   * @return den Typ für den ErgebnisKonflikt zur Auswahl des Wahlsiegers aus den erstplatzierten Kandidaten bei Stimmengleichheit.
   */
  protected abstract Enum< ? > getLosentscheidTypFuerAuswahlKonflikt();

  @Override
  public Sitzberechnungsergebnis berechneSitzverteilung() {
    return berechneSitzverteilung(collectDirektwahlgebiete(sitzberechnungseingabe));
  }

  public Sitzberechnungsergebnis berechneSitzverteilung(List<Wahldurchfuehrungsgebiet> direktwahlgebiete) {
    try {
      for (Wahldurchfuehrungsgebiet direktwahlgebiet : direktwahlgebiete) {
        List<Direktkandidatur> direktkandidaturen = collectDirektkandidaturen(sitzberechnungseingabe, direktwahlgebiet);

        berechneSitzverteilungImGebiet(helper, direktwahlgebiet, direktkandidaturen);
      }

      if (isMitSitzeErzeugung()) {
        createSitze();
      }

      postBerechneSitzverteilungFuerDirektmandate();

      return helper.getSitzberechnungsergebnis();
    } catch (SitzverteilungAbbruchBeiErgebnisKonfliktException e) {
      // Fasse Konflikte zusammen: die gelösten aus der Sitzberechnung und die neuen ungelösten
      return helper.createSitzberechnungsergebnisNurKonflikte(e.getKonflikte());
    }
  }

  protected void berechneSitzverteilungImGebiet(SitzberechnungProGebietHelper helper, Wahldurchfuehrungsgebiet direktwahlgebiet, List<Direktkandidatur> direktkandidaturen) {
    NachStimmenSortierer<Direktkandidatur> sortierer = new NachStimmenSortierer<>(getAnzahlSitzeImGebiet(direktwahlgebiet));

    for (Direktkandidatur direktkandidatur : direktkandidaturen) {
      if (direktkandidatur.isZugelassen()) {
        long anzahlStimmen = getAnzahlStimmen(sitzberechnungseingabe, direktkandidatur);

        sortierer.add(anzahlStimmen, direktkandidatur);
      } else {
        helper.createGewaehltNichtZugelassen(direktkandidatur);
      }
    }

    Ergebnis ergebnis = sitzberechnungseingabe.getErgebnis();

    helper.log(ErgebnisLogEintragArt.TITEL, "sitzberechnung.algorithmus.protokoll.titel", direktwahlgebiet.getName(), ergebnis.getWahl().getWahlart().getNameGui(),
        helper.getMessages().lokalisiere(ergebnis.getErgebnisart()));

    if (sortierer.isEmpty()) {
      helper.log(ErgebnisLogEintragArt.WARNUNG, "sitzberechnung.algorithmus.protokoll.kein.zugelassener.direktkandidat.in.wahlkreis", direktwahlgebiet.getName());

      return;
    }

    if (sortierer.isAlleStimmenNull()) {
      helper.log(ErgebnisLogEintragArt.WARNUNG, "sitzberechnung.algorithmus.protokoll.anzahl.stimmen.ist.0.in.gebiet", direktwahlgebiet.getName());

      return;
    }

    markiereGewaehlte(helper, direktwahlgebiet, sortierer);
  }

  protected void markiereGewaehlte(SitzberechnungProGebietHelper helper, Wahldurchfuehrungsgebiet direktwahlgebiet, NachStimmenSortierer<Direktkandidatur> sortierer) {
    GewaehlteNachStimmenzahlErmittler<Direktkandidatur> gewaehlteErmittler = new GewaehlteNachStimmenzahlErmittler<>(sortierer, new DirektwahlCallback(helper));
    gewaehlteErmittler.ermittleGewaehlte(); // Hier werden die Methoden des Callbacks aufgerufen und dort Gewaehlte erzeugt
  }

  protected void postBerechneSitzverteilungFuerDirektmandate() {
    // Tue nichts - kann von Subklassen implementiert werden
  }

  protected void createSitze() {
    for (Gebiet gebiet : gebiet2PolitischeGruppe2AnzahlSitze.keySet()) {
      Map<PolitischeGruppe, Long> politischeGruppe2AnzahlSitze = gebiet2PolitischeGruppe2AnzahlSitze.get(gebiet);

      for (PolitischeGruppe politischeGruppe : politischeGruppe2AnzahlSitze.keySet()) {
        Long anzahlSitze = politischeGruppe2AnzahlSitze.get(politischeGruppe);

        helper.createSitze(politischeGruppe, gebiet, anzahlSitze, anzahlSitze);
      }
    }
  }

  /**
   * Merkt sich pro Wahlgebiet und politischer Gruppe die Anzahl der Gewaehlten, um damit später die Sitze-Objekte zu erzeugen
   */
  protected void addSitz(Gebiet direktwahlgebiet, PolitischeGruppe politischeGruppe) {
    Gebiet wahlgebiet = direktwahlgebiet.getUebergeordnetesWahlgebiet();

    if (wahlgebiet != null) {
      gebiet2PolitischeGruppe2AnzahlSitze.computeIfAbsent(wahlgebiet, egal -> new HashMap<>()).compute(politischeGruppe, (egal, oldValue) -> notNull(oldValue) + 1);
    }
  }

  /**
   * Innere Klasse. Helper für die Erzeugung von {@link Gewaehlt}en und Konfliktbehandlung bei der Stimmengleichheit
   */
  private class DirektwahlCallback implements GewaehlteNachStimmenzahlErmittlerCallback<Direktkandidatur> {

    private final SitzberechnungProGebietHelper helper;

    public DirektwahlCallback(SitzberechnungProGebietHelper helper) {
      super();

      this.helper = helper;
    }

    /**
     * Wer nicht gewählt ist, wird NICHT_GEWAEHLT. Es werden keine Nachrücker bestimmt.
     *
     * @see de.ivu.elect.business.sitzberechnung.control.util.GewaehlteNachStimmenzahlErmittlerCallback#createGewaehlt(de.ivu.elect.business.kandidatur.entity.Kandidatur, boolean, long)
     */
    @Override
    public void createGewaehlt(Direktkandidatur direktkandidatur, boolean isGewaehlt, long rang) {
      helper.createGewaehlt(direktkandidatur, isGewaehlt ? GewaehltStatus.GEWAEHLT : GewaehltStatus.NICHT_GEWAEHLT, rang);

      if (isGewaehlt) {
        addSitz(direktkandidatur.getGebiet(), direktkandidatur.getGruppe());
      }
    }

    @Override
    public List<Direktkandidatur> bestimmeLetzteGewaehlteBeiStimmengleichheit(List<Direktkandidatur> direktkandidaturen, long anzahlZuLosen) {
      ErgebnisKonflikt neuerKonflikt = helper.createKandidaturAuswahlKonflikt(new KandidaturenAuswaehlenErgebnisKonfliktCustomData(), anzahlZuLosen, direktkandidaturen,
          getLosentscheidTypFuerAuswahlKonflikt());
      ErgebnisKonflikt geloesterKonflikt = helper.loeseKonflikt(neuerKonflikt, new ErgebnisKonfliktLoeser(), true);

      // Gewählte für die per Los gewählten erzeugen
      KandidaturenAuswaehlenErgebnisKonfliktCustomData customData = geloesterKonflikt.getCustomData(KandidaturenAuswaehlenErgebnisKonfliktCustomData.class);

      return customData.getGelosteKandidaturen(direktkandidaturen);
    }

    /**
     * Die Reihenfolge der nicht Gewaehlten bei Stimmengleichheit wird nicht bestimmt. Sie erhalten den gleichen Rang.
     */
    @Override
    public List<List<Direktkandidatur>> bestimmeNachrueckerReihenfolge(List<Direktkandidatur> direktkandidaturen) {
      // Liefert eine Liste mit der Liste der Kandidaten. Das bedeutet, dass alle Kandidaten den gleichen Rang erhalten.
      return singletonList(direktkandidaturen);
    }
  }
}
