
package de.ivu.elect.sitzberechnung.verteilung.dhondt;

import static java.util.Collections.emptyList;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;

import de.ivu.elect.sitzberechnung.math.Fraction;
import de.ivu.elect.sitzberechnung.math.NachNummerSortierer;
import de.ivu.elect.sitzberechnung.verteilung.AbstractVerteilungErgebnis;
import de.ivu.elect.sitzberechnung.verteilung.Teilnehmer;
import de.ivu.elect.sitzberechnung.verteilung.VerteilungContext;

/**
 * Das Ergebnis einer Sitzverteilung nach D’Hondt. Sie wird als Quasi-Quotenverfahren (http://www.wahlrecht.de/verfahren/quotenverfahren.html) umgesetzt. D.h. zunächst wird die Stimmenzahl durch den
 * Hare-Quotienten dividiert und auf die nächste ganze Zahl abgerundet. Anschließend werden Restsitze vergeben. Man erhält es nach Verteilung der abgerundeten Sitze und der Restsitze von einer
 * {@link DHondtVerteilung}. Durch eine evtl. spätere Verteilung nach Los (oder anderen Kriterien) werden nachträglich noch Sitze vergeben und damit dieses Objekt modifiziert. Es enthält für jeden
 * Teilnehmer an der Verteilung ein {@link DHondtTeilnehmerErgebnis}, das ebenfalls noch durch spätere Verteilung nach Los (oder anderen Kriterien) modifiziert werden kann.
 */
public class DHondtErgebnis extends AbstractVerteilungErgebnis<DHondtTeilnehmerErgebnis> {

  /**
   * Ergebnis der DHondtVerteilung ohne dass ein Losverfahren nötig war
   */
  public DHondtErgebnis(VerteilungContext context, List<DHondtTeilnehmerErgebnis> teilnehmerErgebnisse) {
    this(context, teilnehmerErgebnisse, 0L, emptyList());
  }

  public DHondtErgebnis(VerteilungContext context, List<DHondtTeilnehmerErgebnis> teilnehmerErgebnisse, long anzahlSitzeZuVerlosen, List<DHondtTeilnehmerErgebnis> teilnehmerErgebnisseImLosverfahren) {
    super(context, teilnehmerErgebnisse, anzahlSitzeZuVerlosen, teilnehmerErgebnisseImLosverfahren);
  }

  /**
   * @return den Quotienten, mit dem der nächste Sitz zugeteilt worden wäre
   */
  public Fraction getNaechsterQuotient() {
    Fraction max = Fraction.ZERO;

    for (DHondtTeilnehmerErgebnis teilnehmerErgebnis : getTeilnehmerErgebnisse()) {
      Fraction quotient = teilnehmerErgebnis.getNaechsterQuotient(getFractionConverter());

      if (max.compareTo(quotient) < 0) {
        max = quotient;
      }
    }

    return max;
  }

  /**
   * @return den Quotienten, mit dem der letzte Sitz zugeteilt wurde. null, wenn kein Sitz zugeteilt wurde.
   */
  public Fraction getLetzterQuotient() {
    Fraction min = null;

    for (DHondtTeilnehmerErgebnis teilnehmerErgebnis : getTeilnehmerErgebnisse()) {
      Fraction quotient = teilnehmerErgebnis.getLetzterQuotient(getFractionConverter());

      if (quotient != null && (min == null || min.compareTo(quotient) > 0)) {
        min = quotient;
      }
    }

    return min;
  }

  /**
   * Die Anzahl der {@link DHondtQuotient}en pro Teilnehmer ist um 1 größer, als die maximale Anzahl Sitze eine Teilnehmers.
   *
   * @param nurBeiVerteilungQuotientenErzeugen
   *          wenn dies true ist, ist das Ergebnis leer in dem Fall, dass keine richtige Verteilung stattgefunden hat (wegen 0 Sitze oder 0 Stimmen für alle)
   * @return eine Map mit einer Liste von {@link DHondtQuotient}en aller Teilnehmer an der Verteilung.
   */
  public Map<Teilnehmer, List<DHondtQuotient>> getDHondtQuotientenProTeilnehmer(boolean nurBeiVerteilungQuotientenErzeugen) {
    Map<Teilnehmer, List<DHondtQuotient>> map = new LinkedHashMap<>();

    createDHondtQuotienten(map, nurBeiVerteilungQuotientenErzeugen);

    return map;
  }

  /**
   * @param nurBeiVerteilungQuotientenErzeugen
   *          wenn dies true ist, ist das Ergebnis leer in dem Fall, dass keine richtige Verteilung stattgefunden hat (wegen 0 Sitze oder 0 Stimmen für alle)
   * @return Die {@link DHondtQuotient}en aller Teilnehmer gruppiert und absteigend sortiert nach Rang
   */
  public List<List<DHondtQuotient>> getDHondtQuotientenNachRang(boolean nurBeiVerteilungQuotientenErzeugen) {
    Map<Teilnehmer, List<DHondtQuotient>> map = new LinkedHashMap<>();

    SortedMap<Fraction, List<DHondtQuotient>> result = createDHondtQuotienten(map, nurBeiVerteilungQuotientenErzeugen);

    return new ArrayList<>(result.values());
  }

  /**
   * @param nurBeiVerteilungQuotientenErzeugen
   *          wenn dies true ist, ist das Ergebnis leer in dem Fall, dass keine richtige Verteilung stattgefunden hat (wegen 0 Sitze oder 0 Stimmen für alle)
   * @return Die {@link DHondtQuotient}en aller Teilnehmer
   */
  public List<DHondtQuotient> getDHondtQuotienten(boolean nurBeiVerteilungQuotientenErzeugen) {
    List<DHondtQuotient> result = new ArrayList<>();

    for (List<DHondtQuotient> dHondtQuotienten : getDHondtQuotientenProTeilnehmer(nurBeiVerteilungQuotientenErzeugen).values()) {
      result.addAll(dHondtQuotienten);
    }

    return new ArrayList<>(result);
  }

  /**
   * Wenn <code>isOhneVerteilung() == true && nurBeiVerteilungQuotientenErzeugen == true</code>, dann werden keine Quotienten erzeugt. ansonsten werden für jeden Teilnehmer ein Quotient mehr erzeugt
   * als die maximale Sitzanzahl der Teilnehmer.
   */
  private SortedMap<Fraction, List<DHondtQuotient>> createDHondtQuotienten(Map<Teilnehmer, List<DHondtQuotient>> map, boolean nurBeiVerteilungQuotientenErzeugen) {
    NachNummerSortierer<DHondtQuotient, Fraction> dHondtQuotientSortierer = new NachNummerSortierer<>();

    long anzahlDHondtQuotienten = getMaximaleAnzahlSitzeEinesTeilnehmers() + 1L;

    if (isOhneVerteilung() && nurBeiVerteilungQuotientenErzeugen) {
      anzahlDHondtQuotienten = 0L; // Keine Quotienten erzeugen!!!
    }

    for (DHondtTeilnehmerErgebnis teilnehmerErgebnis : getTeilnehmerErgebnisse()) {
      List<DHondtQuotient> dHondtQuotienten = teilnehmerErgebnis.createDHondtQuotienten(anzahlDHondtQuotienten);

      map.put(teilnehmerErgebnis.getTeilnehmer(), dHondtQuotienten);

      for (DHondtQuotient dHondtQuotient : dHondtQuotienten) {
        dHondtQuotientSortierer.add(dHondtQuotient.toFraction(getFractionConverter()), dHondtQuotient);
      }
    }

    SortedMap<Fraction, List<DHondtQuotient>> rangGleiche = dHondtQuotientSortierer.getHintereGruppiert();

    long rang = 1L;

    for (List<DHondtQuotient> dHondtQuotienten : rangGleiche.values()) {
      for (DHondtQuotient dHondtQuotient : dHondtQuotienten) {
        dHondtQuotient.setRang(rang);
      }

      rang += dHondtQuotienten.size();
    }

    return rangGleiche;
  }

  /**
   * @return <code>true</code>, wenn kein Teilnehmer eine Sitz erhalten hat und keine Losentscheid mehr offen ist. Das ist nur der Fall, wenn keine Verteilung stattgefunden hat, entweder weil keine
   *         Sitze zu verteilen waren oder weil kein Teilnehmer eine Stimme erhalten hat.
   */
  public boolean isOhneVerteilung() {
    return getMaximaleAnzahlSitzeEinesTeilnehmers() == 0L && isEntschieden();
  }

  /**
   * @return Ergebnis der Verteilung, wenn keine richtige Verteilung stattgefunden hat
   */
  public static DHondtErgebnis ohneVerteilung(VerteilungContext context, List< ? extends Teilnehmer> teilnehmerInVerteilung) {
    List<DHondtTeilnehmerErgebnis> teilnehmerErgebnisse = new ArrayList<>();

    for (Teilnehmer teilnehmer : teilnehmerInVerteilung) {
      teilnehmerErgebnisse.add(DHondtTeilnehmerErgebnis.ohneVerteilung(teilnehmer));
    }

    return new DHondtErgebnis(context, teilnehmerErgebnisse);
  }
}
