
package de.ivu.elect.business.ergebnis.control;

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

import de.ivu.elect.business.ergebnis.entity.Stimmen;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.util.MultiMapUtil;

/**
 * Sammelt und sortiert die aktuellen Stimmen und die Vorperioden-Stimmen und kumuliert sie auf die übergeordneten
 * Gebiete, so dass die Anfragen der Hochrechnung damit effizient beantwortet werden können.
 * <p>
 * Das Kumulieren erfolgt so, dass zunächst die aktuellen Stimmen auf den Erfassungsgebieten eingetragen und auf allen
 * übergeordneten Gebieten kumuliert werden. Außerdem wird bei jedem Gebiet gemerkt, ob es dort schon Listenstimmen und
 * / oder Direktstimmen gibt. Danach werden die Vorperioden-Stimmen auf den Erfassungsgebieten eingetragen. Auf den
 * übergeordneten Gebieten kumuliert werden sie aber nur, wenn es in dem Erfassungsgebiet ebenfalls Listenstimmen bzw.
 * Direktstimmen gibt.
 * <p>
 * Die Stimmen der Systemgruppe "Gueltige" wird wie alle anderen Stimmen auch behandelt, obwohl sie für die Hochrechnung
 * eine besondere Rolle spielt.
 * <p>
 *  on 19.02.2016.
 */
class StimmenFuerHochrechnung {

  private final Map<Gebiet, List<Stimmen>> gebiet2erfassteStimmen = new HashMap<>();
  private final Map<Gebiet, StimmenProGebietFuerHochrechnung> gebiet2gruppe2stimmen = new HashMap<>();

  public StimmenFuerHochrechnung(StimmergebnisDaten stimmergebnisDaten, Collection<Stimmen> vorperiodenStimmen) {
    stimmergebnisDaten.getStimmen().getListenstimmenStream().forEach(e -> addListenstimmen(e));
    stimmergebnisDaten.getStimmen().getDirektstimmenStream().forEach(e -> addDirektstimmen(e));

    vorperiodenStimmen.stream().forEach(e -> addVorperiodenStimmen(e));
  }

  private void addListenstimmen(Stimmen stimmen) {
    getErfassteStimmen(stimmen.getGebiet()).add(stimmen);
    get(stimmen.getGebiet()).addListenstimmen(stimmen);
  }

  private void addDirektstimmen(Stimmen stimmen) {
    getErfassteStimmen(stimmen.getGebiet()).add(stimmen);
    get(stimmen.getGebiet()).addDirektstimmen(stimmen);
  }

  private void addVorperiodenStimmen(Stimmen stimmen) {
    get(stimmen.getGebiet()).addVorperiodenStimmen(stimmen);
  }

  public List<Stimmen> getErfassteStimmen(Gebiet erfassungsgebiet) {
    return MultiMapUtil.getArrayList(gebiet2erfassteStimmen, erfassungsgebiet);
  }

  /**
   * @return Stimmen für die aktuelle Wahl auf einem Erfassungsgebiet
   */
  public Stimmenzaehler getStimmenAktuell(Gruppe gruppe, Wahldurchfuehrungsgebiet gebiet, Stimmenart stimmenart) {
    return get(gebiet).get(gruppe).get(stimmenart);
  }

  /**
   * @return Stimmen für die aktuelle Wahl auf einem übergeordneten Gebiet
   */
  public Stimmenzaehler getStimmenKumuliert(Gruppe gruppe, Wahldurchfuehrungsgebiet gebiet, Stimmenart stimmenart) {
    return get(gebiet).get(gruppe).get(stimmenart);
  }

  /**
   * @return Stimmen der Vorperiode auf einem Erfassungsgebiet
   */
  public Stimmenzaehler getStimmenVorperiode(Gruppe gruppe, Wahldurchfuehrungsgebiet gebiet, Stimmenart stimmenart) {
    return get(gebiet).get(gruppe).getVorperiode(stimmenart);
  }

  /**
   * @return Stimmen der Vorperiode auf einem übergeordneten Gebiet kumuliert, wobei in die Summe nur die Untergebiete
   *         einfließen, für die auch bei der aktuellen Wahl ein Ergebnis vorliegt
   */
  public Stimmenzaehler getStimmenVorperiodeKumuliert(Gruppe gruppe, Wahldurchfuehrungsgebiet gebiet,
      Stimmenart stimmenart) {
    return get(gebiet).get(gruppe).getVorperiode(stimmenart);
  }

  /**
   * @return Summe der gültigen Stimmen der Vorperiode auf einem Erfassungsgebiet
   */
  public Stimmenzaehler getGueltigeStimmenVorperiode(Gruppe gueltige, Wahldurchfuehrungsgebiet gebiet,
      Stimmenart stimmenart) {
    return get(gebiet).get(gueltige).getVorperiode(stimmenart);
  }

  /**
   * @return Summe der gültigen Stimmen der Vorperiode auf einem übergeordneten Gebiet kumuliert, wobei in die Summe nur
   *         die Untergebiete einfließen, für die auch bei der aktuellen Wahl ein Ergebnis vorliegt
   */
  public Stimmenzaehler getGueltigeStimmenVorperiodeKumuliert(Gruppe gueltige, Wahldurchfuehrungsgebiet gebiet,
      Stimmenart stimmenart) {
    return get(gebiet).get(gueltige).getVorperiode(stimmenart);
  }

  private StimmenProGebietFuerHochrechnung createStimmenProGebietFuerHochrechnung(Gebiet gebiet) {
    StimmenProGebietFuerHochrechnung result = new StimmenProGebietFuerHochrechnung();
    Gebiet uebergeordnetesGebiet = gebiet.getUebergeordnetesGebiet();
    if (uebergeordnetesGebiet != null) {
      result.setStimmenFuerUebergeordnetesGebiet(get(uebergeordnetesGebiet));
    }
    return result;
  }

  StimmenProGebietFuerHochrechnung get(Gebiet gebiet) {
    return gebiet2gruppe2stimmen.computeIfAbsent(gebiet, key -> createStimmenProGebietFuerHochrechnung(key));
  }

  /**
   * Listenstimmen, Direktstimmen, Vorperiodenstimmen, ggf. auch kumuliert, einschließlich der Summe der gültigen für
   * alle Wahlvorschläge für ein Gebiet.
   */
  private static class StimmenProGebietFuerHochrechnung {

    private StimmenProGebietFuerHochrechnung stimmenFuerUebergeordnetesGebiet = null;
    private final Map<Gruppe, StimmenProGruppeUndGebietFuerHochrechnung> map = new HashMap<>();
    private boolean hatDirektstimmen = false;
    private boolean hatListenstimmen = false;

    public void setStimmenFuerUebergeordnetesGebiet(StimmenProGebietFuerHochrechnung uebergeordnetesGebiet) {
      this.stimmenFuerUebergeordnetesGebiet = uebergeordnetesGebiet;
    }

    public StimmenProGruppeUndGebietFuerHochrechnung get(Gruppe gruppe) {
      return map.computeIfAbsent(gruppe, key -> new StimmenProGruppeUndGebietFuerHochrechnung());
    }

    public void addListenstimmen(Stimmen stimmen) {
      hatListenstimmen = true;
      get(stimmen.getGruppe()).addListenstimmen(stimmen);
      if (stimmenFuerUebergeordnetesGebiet != null) {
        stimmenFuerUebergeordnetesGebiet.addListenstimmen(stimmen);
      }
    }

    public void addDirektstimmen(Stimmen stimmen) {
      hatDirektstimmen = true;
      get(stimmen.getGruppe()).addDirektstimmen(stimmen);
      if (stimmenFuerUebergeordnetesGebiet != null) {
        stimmenFuerUebergeordnetesGebiet.addDirektstimmen(stimmen);
      }
    }

    public void addVorperiodenStimmen(Stimmen stimmen) {
      if (stimmen.getStimmenart().isDirektstimme()) {
        addVorperiodenDirektstimmen(stimmen);
      } else if (stimmen.getStimmenart().isListenstimme() && stimmen.getListenkandidatur() == null) {
        addVorperiodenListenstimmen(stimmen);
      }
    }

    private void addVorperiodenDirektstimmen(Stimmen stimmen) {
      get(stimmen.getGruppe()).addVorperiodenDirektstimmen(stimmen);
      if (stimmenFuerUebergeordnetesGebiet != null && hatDirektstimmen) {
        stimmenFuerUebergeordnetesGebiet.addVorperiodenDirektstimmen(stimmen);
      }
    }

    private void addVorperiodenListenstimmen(Stimmen stimmen) {
      get(stimmen.getGruppe()).addVorperiodenListenstimmen(stimmen);
      if (stimmenFuerUebergeordnetesGebiet != null && hatListenstimmen) {
        stimmenFuerUebergeordnetesGebiet.addVorperiodenListenstimmen(stimmen);
      }
    }
  }

  /**
   * Listenstimmen, Direktstimmen, Vorperiodenstimmen usw. , ggf. auch kumuliert, für eine GruppeImGebiet.
   */
  private static class StimmenProGruppeUndGebietFuerHochrechnung {

    private Stimmenzaehler listenstimmen = new Stimmenzaehler();
    private Stimmenzaehler direktstimmen = new Stimmenzaehler();
    private Stimmenzaehler vorperiodenListenstimmen = new Stimmenzaehler();
    private Stimmenzaehler vorperiodenDirektstimmen = new Stimmenzaehler();

    public void addListenstimmen(Stimmen stimmen) {
      listenstimmen.add(stimmen.getAnzahl());
    }

    public void addDirektstimmen(Stimmen stimmen) {
      direktstimmen.add(stimmen.getAnzahl());
    }

    public void addVorperiodenListenstimmen(Stimmen stimmen) {
      vorperiodenListenstimmen.add(stimmen.getAnzahl());
    }

    public void addVorperiodenDirektstimmen(Stimmen stimmen) {
      vorperiodenDirektstimmen.add(stimmen.getAnzahl());
    }

    public Stimmenzaehler get(Stimmenart stimmenart) {
      if (stimmenart.isDirektstimme()) {
        return direktstimmen;
      } else if (stimmenart.isListenstimme()) {
        return listenstimmen;
      } else {
        return new Stimmenzaehler();
      }
    }

    public Stimmenzaehler getVorperiode(Stimmenart stimmenart) {
      if (stimmenart.isDirektstimme()) {
        return vorperiodenDirektstimmen;
      } else if (stimmenart.isListenstimme()) {
        return vorperiodenListenstimmen;
      } else {
        return new Stimmenzaehler();
      }
    }
  }

}
