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

import java.util.ArrayList;
import java.util.List;

import de.ivu.elect.sitzberechnung.math.Fraction;
import de.ivu.elect.sitzberechnung.math.FractionConverter;
import de.ivu.elect.sitzberechnung.verteilung.AbstractVerteilung;
import de.ivu.elect.sitzberechnung.verteilung.Teilnehmer;
import de.ivu.elect.sitzberechnung.verteilung.VerteilungContext;

/**
 * Implementation der Verteilung nach Sainte-Laguë (Divisorverfahren mit Standardrundung) durch Anwendung des Divisorverfahrens.
 *
 * <p>
 * Die Verteilung von Restsitzen per Los ist nicht enthalten. Es werden im Ergebnisobjekt lediglich die dafür erforderlichen Informationen geliefert.
 *
 * <p>
 * Es wird ein geeigneter Zuteilungsdivisor vorausgesetzt, der BEREITS IM VORFELD so gewählt sein muss, dass genauso viele Sitze verteilt werden, wie Sitze zu verteilen sind. Werden weniger oder mehr
 * Sitze verteilt, wird eine {@link IllegalStateException} geworfen!
 *
 * <p>
 * Ein geeigneter Zuteilungsdivisor lässt sich bspw. als "Beifang" bei der Verteilung nach Sainte-Laguë durch Anwendung des Höchstzahlverfahrens bestimmen. IST DER ZUTEILUNGSDIVISOR ALSO NICHT
 * VORGEGEBEN, sollte bevorzugt eine Verteilung nach Sainte-Laguë durch Anwendung des Höchstzahlverfahrens durchgeführt werden!!!
 *
 * @see SainteLagueVerteilung
 * @see SainteLagueHoechstzahlErgebnis#getDivisorInterval
 */
public class SainteLagueVerteilungMitVorgegebenemDivisor extends AbstractVerteilung<SainteLagueDivisorTeilnehmerErgebnis, SainteLagueDivisorErgebnis> {

  private final Fraction divisorVorgegeben;

  public SainteLagueVerteilungMitVorgegebenemDivisor(Fraction divisorVorgegeben) {
    this(FractionConverter.convertingExakt(), divisorVorgegeben);
  }

  public SainteLagueVerteilungMitVorgegebenemDivisor(FractionConverter fractionConverter, Fraction divisorVorgegeben) {
    super(fractionConverter);

    this.divisorVorgegeben = divisorVorgegeben;
  }

  private Fraction getDivisorVorgegeben() {
    return divisorVorgegeben;
  }

  @Override
  protected SainteLagueDivisorErgebnis verteileSitzeKeineSitze(VerteilungContext context, List< ? extends Teilnehmer> teilnehmerInVerteilung) {
    return SainteLagueDivisorErgebnis.ohneVerteilung(context, getDivisorVorgegeben(), teilnehmerInVerteilung);
  }

  @Override
  protected SainteLagueDivisorErgebnis verteileSitzeKeineStimmen(VerteilungContext context, List< ? extends Teilnehmer> teilnehmerInVerteilung) {
    return SainteLagueDivisorErgebnis.ohneVerteilung(context, getDivisorVorgegeben(), teilnehmerInVerteilung);
  }

  @Override
  protected SainteLagueDivisorErgebnis verteileSitze(VerteilungContext context, List< ? extends Teilnehmer> teilnehmerInVerteilung) {
    return verteileSitze(context, teilnehmerInVerteilung, getDivisorVorgegeben());
  }

  protected SainteLagueDivisorErgebnis verteileSitze(VerteilungContext context, List< ? extends Teilnehmer> teilnehmerInVerteilung, Fraction divisorInitial) {
    Fraction divisor = context.getFractionConverter().asFractionConverted(divisorInitial);

    List<SainteLagueDivisorTeilnehmerErgebnis> teilnehmerErgebnisse = new ArrayList<>();

    long anzahlSitze = context.getAnzahlSitze();
    long anzahlSitzeNachAbrundungVerteilt = 0L;

    /*
     * Hier wird künstlich zwischen Sitzen nach Abrundung (== Sitze, die ein Teilnehmer auf jeden Fall bekommt) und Restsitzen (== zusätzliche Sitze ohne Losverfahren) unterschieden, um den
     * Erwartungen von TeilnehmerErgebnis#getSitze zu entsprechen. An der Verteilung der Sitze ändert sich nichts!
     */

    for (Teilnehmer teilnehmer : teilnehmerInVerteilung) {
      SainteLagueDivisorTeilnehmerErgebnis teilnehmerErgebnisNachAbrundung = new SainteLagueDivisorTeilnehmerErgebnis(teilnehmer, divisor);
      teilnehmerErgebnisse.add(teilnehmerErgebnisNachAbrundung);

      anzahlSitzeNachAbrundungVerteilt += teilnehmerErgebnisNachAbrundung.getSitzeAusAbrundung();
    }

    if (anzahlSitzeNachAbrundungVerteilt > anzahlSitze) {
      throw new IllegalStateException(
          "Zu VIELE Sitze nach Abrundung verteilt - Zuteilungsdivisor neu bestimmen [divisor [vorgegeben = " + getDivisorVorgegeben() + ", verwendet = " + divisor + "], sitze [anzahl = " + anzahlSitze
              + ", nach abrundung verteilt = " + anzahlSitzeNachAbrundungVerteilt + ", nach abrundung zu viel verteilt = " + (anzahlSitzeNachAbrundungVerteilt - anzahlSitze) + "]]");
    }

    return verteileRestsitze(context, divisor, teilnehmerErgebnisse, anzahlSitzeNachAbrundungVerteilt);
  }

  private SainteLagueDivisorErgebnis verteileRestsitze(VerteilungContext context, Fraction divisor, List<SainteLagueDivisorTeilnehmerErgebnis> teilnehmerErgebnisse,
      long anzahlSitzeNachAbrundungVerteilt) {
    long anzahlSitze = context.getAnzahlSitze();

    long anzahlRestsitze = anzahlSitze - anzahlSitzeNachAbrundungVerteilt;
    long anzahlRestsitzeVerbleibend = anzahlRestsitze;

    FractionConverter fractionConverter = context.getFractionConverter();

    List<SainteLagueDivisorTeilnehmerErgebnis> teilnehmerErgebnisseMitRestsitz = new ArrayList<>();
    List<SainteLagueDivisorTeilnehmerErgebnis> teilnehmerErgebnisseUnentschieden = new ArrayList<>();
    List<SainteLagueDivisorTeilnehmerErgebnis> teilnehmerErgebnisseOhneRestsitz = new ArrayList<>();

    for (SainteLagueDivisorTeilnehmerErgebnis teilnehmerErgebnis : teilnehmerErgebnisse) {
      int restCompared = teilnehmerErgebnis.getRest(fractionConverter).compareTo(Fraction.ONE_HALF);

      if (restCompared < 0) { // Kein Restsitz...
        teilnehmerErgebnisseOhneRestsitz.add(teilnehmerErgebnis);
      } else if (restCompared == 0) { // Vielleicht Restsitz...
        teilnehmerErgebnisseUnentschieden.add(teilnehmerErgebnis);
      } else { // Restsitz...
        teilnehmerErgebnisseMitRestsitz.add(teilnehmerErgebnis);

        teilnehmerErgebnis.vergibRestsitz();

        anzahlRestsitzeVerbleibend--;
      }
    }

    if (anzahlRestsitzeVerbleibend < 0) {
      throw new IllegalStateException("Zu VIELE Restsitze verteilt - Zuteilungsdivisor neu bestimmen [divisor [vorgegeben = " + getDivisorVorgegeben() + ", verwendet = " + divisor
          + "], sitze [anzahl = " + anzahlSitze + ", nach abrundung verteilt = " + anzahlSitzeNachAbrundungVerteilt + ", restsitze = " + anzahlRestsitze + ", nach resten zu viel verteilt = "
          + Math.abs(anzahlRestsitzeVerbleibend) + "], teilnehmer = [anzahl = " + teilnehmerErgebnisse.size() + ", rest < 0,5 = " + teilnehmerErgebnisseOhneRestsitz.size() + ", rest == 0,5 = "
          + teilnehmerErgebnisseUnentschieden.size() + ", rest > 0,5 = " + teilnehmerErgebnisseMitRestsitz.size() + "]]");
    }

    // Szenario anzahlRestsitzeVerbleibend == 0 -> KEIN Restsitz für ALLE mit Rest 0,5...

    if (anzahlRestsitzeVerbleibend > 0) {
      int anzahlRestsitzeZuteilbar = teilnehmerErgebnisseUnentschieden.size();

      if (anzahlRestsitzeVerbleibend > anzahlRestsitzeZuteilbar) {
        throw new IllegalStateException("Zu VIELE Restsitze zu verteilen - Zuteilungsdivisor neu bestimmen [divisor [vorgegeben = " + getDivisorVorgegeben() + ", verwendet = " + divisor
            + "], sitze [anzahl = " + anzahlSitze + ", nach abrundung verteilt = " + anzahlSitzeNachAbrundungVerteilt + ", restsitze = " + anzahlRestsitze + ", nach resten nicht zuteilbar = "
            + (anzahlRestsitzeVerbleibend - anzahlRestsitzeZuteilbar) + "], teilnehmer = [anzahl = " + teilnehmerErgebnisse.size() + ", rest < 0,5 = " + teilnehmerErgebnisseOhneRestsitz.size()
            + ", rest == 0,5 = " + teilnehmerErgebnisseUnentschieden.size() + ", rest > 0,5 = " + teilnehmerErgebnisseMitRestsitz.size() + "]]");
      }

      if (anzahlRestsitzeVerbleibend == anzahlRestsitzeZuteilbar) { // Restsitz für ALLE mit Rest 0,5...
        for (SainteLagueDivisorTeilnehmerErgebnis teilnehmerErgebnis : teilnehmerErgebnisseUnentschieden) {
          teilnehmerErgebnis.vergibRestsitz();

          anzahlRestsitzeVerbleibend--;
        }
      } else {
        List<SainteLagueDivisorTeilnehmerErgebnis> teilnehmerErgebnisseImLosverfahren = teilnehmerErgebnisseUnentschieden;
        long anzahlSitzeZuVerlosen = anzahlRestsitzeVerbleibend;

        for (SainteLagueDivisorTeilnehmerErgebnis teilnehmerErgebnis : teilnehmerErgebnisseImLosverfahren) {
          teilnehmerErgebnis.seiTeilnehmerAmLosverfahren();
        }

        return new SainteLagueDivisorErgebnis(context, getDivisorVorgegeben(), teilnehmerErgebnisse, anzahlSitzeZuVerlosen, teilnehmerErgebnisseImLosverfahren);
      }
    }

    return new SainteLagueDivisorErgebnis(context, getDivisorVorgegeben(), teilnehmerErgebnisse);
  }
}
