
package de.ivu.elect.dokumente.ergebnis.zusammenstellung;

import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformation;
import de.ivu.elect.business.gebietsbaum.entity.Verwaltungsart;
import de.ivu.elect.dokumente.ergebnis.AbstractErgebnisTabellarischDocumentZeile;
import de.ivu.elect.dokumente.ergebnis.ErgebnisDocumentDatenArt;
import de.ivu.elect.dokumente.ergebnis.zusammenstellung.ErgebnisTabellarischTabelleSortierung.ErgebnisTabellarischTabelleSortierungGruppierung;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungGruppe;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungTabelle;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungZeileDatengebiet;

public class ErgebnisTabellarischTabelle {

  public static final Comparator<WahlverwaltungGebietInformation> GEBIET_INFORMATION_DEFAULT_COMPARATOR = AbstractErgebnisTabellarischDocumentZeile.GEBIET_INFORMATION_DEFAULT_COMPARATOR;

  public static final Comparator<WahlverwaltungGebietInformation> GEMEINDE_INFORMATION_DEFAULT_COMPARATOR = AbstractErgebnisTabellarischDocumentZeile.GEMEINDE_INFORMATION_DEFAULT_COMPARATOR;

  public static final Comparator<ErgebnisZusammenstellungGruppe> GRUPPE_DEFAULT_COMPARATOR = (one, another) -> Long.compare(one.getPosition(), another.getPosition());

  private static final Logger LOG = LoggerFactory.getLogger(ErgebnisTabellarischTabelle.class);

  private final ErgebnisZusammenstellungTabelle template;

  private List<ErgebnisTabellarischZeile> zeilen;

  private final ErgebnisTabellarischTabelleSortierung tabelleSortierung;

  private final ErgebnisDocumentDatenArt tabelleDatenArt;

  public ErgebnisTabellarischTabelle(ErgebnisZusammenstellungTabelle template, ErgebnisTabellarischTabelleSortierung tabelleSortierung, ErgebnisDocumentDatenArt tabelleDatenArt) {
    this(template, tabelleSortierung, tabelleDatenArt, null);
  }

  public ErgebnisTabellarischTabelle(ErgebnisTabellarischTabelle template, List<ErgebnisTabellarischZeile> zeilen) {
    this(template.getTemplate(), template.getTabelleSortierung(), template.getTabelleDatenArt(), zeilen);
  }

  private ErgebnisTabellarischTabelle(ErgebnisZusammenstellungTabelle template, ErgebnisTabellarischTabelleSortierung tabelleSortierung, ErgebnisDocumentDatenArt tabelleDatenArt,
      @Nullable List<ErgebnisTabellarischZeile> zeilen) {
    super();

    this.template = template;

    this.tabelleSortierung = tabelleSortierung;

    this.tabelleDatenArt = tabelleDatenArt;

    this.zeilen = zeilen;
  }

  private ErgebnisZusammenstellungTabelle getTemplate() {
    return template;
  }

  private ErgebnisTabellarischTabelleSortierung getTabelleSortierung() {
    return tabelleSortierung;
  }

  public ErgebnisDocumentDatenArt getTabelleDatenArt() {
    return tabelleDatenArt;
  }

  public Optional<ErgebnisTabellarischZeile> getZeileSpaltensummen() {
    return ErgebnisTabellarischZeile.sum(ErgebnisZusammenstellungZeileDatengebiet.create(getTemplate(), "Summe", "Summe"), Stimmabgabeart.GESAMT, Optional.empty(), getZeilen());
  }

  public List<ErgebnisTabellarischZeile> getZeilen() {
    if (zeilen == null) {
      zeilen = unmodifiableList(collectZeilenSortedZwischensummenIncluded());
    }

    return zeilen;
  }

  private List<ErgebnisTabellarischZeile> collectZeilenSortedZwischensummenIncluded() {
    List<ErgebnisTabellarischZeile> result = new ArrayList<>();

    List<ErgebnisTabellarischZeile> zeilenSortedZwischensummenExcluded = collectZeilenSortedZwischensummenExcluded();
    List<ErgebnisTabellarischZeile> zeilenSortedZwischensummenIncluded = new ArrayList<>(zeilenSortedZwischensummenExcluded);

    List<ErgebnisTabellarischTabelleSortierungGruppierung> gruppierungshierarchieDesc = getTabelleSortierung().collectGruppierungshierarchieDesc();

    ErgebnisTabellarischTabelleSortierungGruppierung gruppierungObersteEbene = gruppierungshierarchieDesc.get(0);
    ErgebnisTabellarischTabelleSortierungGruppierung gruppierungUntersteEbene = gruppierungshierarchieDesc.get(gruppierungshierarchieDesc.size() - 1);

    int anzahlGruppierungUntersteEbene = zeilenSortedZwischensummenExcluded.stream().map(zeile -> gruppierungUntersteEbene.getAccessor().apply(zeile)).collect(toSet()).size();
    int anzahlStimmabgabearten = zeilenSortedZwischensummenExcluded.stream().map(ErgebnisTabellarischZeile::getStimmabgabeart).collect(toSet()).size();

    if (anzahlStimmabgabearten == 1) {
      List<ErgebnisTabellarischTabelleSortierungGruppierung> gruppierungshierarchieAsc = new ArrayList<>(gruppierungshierarchieDesc);
      Collections.reverse(gruppierungshierarchieAsc);

      if (gruppierungshierarchieAsc.size() == 2 && anzahlGruppierungUntersteEbene == 1 && Verwaltungsart.GEMEINDE.equals(gruppierungUntersteEbene.getVerwaltungsart())) {
        // LEGACY, nur eine einzelne Gemeinde -> keine weiteren Zeilen erforderlich...
      } else {
        for (ErgebnisTabellarischTabelleSortierungGruppierung gruppierung : gruppierungshierarchieAsc) {
          if (!Objects.equals(gruppierung, gruppierungObersteEbene)) { // Oberste Ebene gesamt == Summenzeile!!!
            zeilenSortedZwischensummenIncluded = insertZeilenZwischensummenGruppierungGesamt(gruppierung.getZwischensummenGesamt(), gruppierung.getAccessor(), zeilenSortedZwischensummenIncluded);
          }
        }
      }
    } else {
      List<ErgebnisTabellarischTabelleSortierungGruppierung> gruppierungshierarchieAsc = new ArrayList<>(gruppierungshierarchieDesc);
      Collections.reverse(gruppierungshierarchieAsc);

      // Urnen- und Briefwahl gesamt auf unterster Ebene einfügen...
      zeilenSortedZwischensummenIncluded = insertZeilenZwischensummenGruppierungUntersteEbeneStimmabgabearten(gruppierungUntersteEbene.getZwischensummenStimmabgabeart(),
          gruppierungUntersteEbene.getAccessor(), zeilenSortedZwischensummenIncluded);

      if (gruppierungshierarchieAsc.size() == 2 && anzahlGruppierungUntersteEbene == 1 && Verwaltungsart.GEMEINDE.equals(gruppierungUntersteEbene.getVerwaltungsart())) {
        // LEGACY, nur eine einzelne Gemeinde -> keine weiteren Zeilen erforderlich...
      } else {
        zeilenSortedZwischensummenIncluded = insertZeilenZwischensummenGruppierungGesamt(gruppierungUntersteEbene.getZwischensummenGesamt(), gruppierungUntersteEbene.getAccessor(),
            zeilenSortedZwischensummenIncluded);

        for (Iterator<ErgebnisTabellarischTabelleSortierungGruppierung> iter = gruppierungshierarchieAsc.iterator(); iter.hasNext();) {
          ErgebnisTabellarischTabelleSortierungGruppierung gruppierung = iter.next();

          if (!Objects.equals(gruppierung, gruppierungUntersteEbene)) { // Unterste Ebene Urnen-/Briefwahl und Gesamt schon hinzugefügt...
            zeilenSortedZwischensummenIncluded = insertZeilenZwischensummenGruppierungUebergeordnetStimmabgabearten(gruppierung.getZwischensummenStimmabgabeart(), gruppierung.getAccessor(),
                zeilenSortedZwischensummenIncluded);

            if (iter.hasNext()) {
              zeilenSortedZwischensummenIncluded = insertZeilenZwischensummenGruppierungGesamt(gruppierung.getZwischensummenGesamt(), gruppierung.getAccessor(), zeilenSortedZwischensummenIncluded);
            }
          }
        }
      }
    }

    result.addAll(zeilenSortedZwischensummenIncluded);

    return result;
  }

  private List<ErgebnisTabellarischZeile> collectZeilenSortedZwischensummenExcluded() {
    Comparator<ErgebnisTabellarischZeile> comparator = getTabelleSortierung().comparator();

    return getTemplate().getZeilen().stream() //
        .map(zeile -> new ErgebnisTabellarischZeile(zeile, Optional.empty())) //
        .sorted(comparator) //
        .collect(toList());
  }

  private List<ErgebnisTabellarischZeile> insertZeilenZwischensummenGruppierungUntersteEbeneStimmabgabearten(ErgebnisTabellarischZwischensummen zwischensummen,
      Function<ErgebnisTabellarischZeile, WahlverwaltungGebietInformation> gruppierungFct, List<ErgebnisTabellarischZeile> zeilen) {
    List<ErgebnisTabellarischZeile> result = new ArrayList<>();

    List<ErgebnisTabellarischZeile> zeilenZuSummieren = new ArrayList<>();
    ErgebnisTabellarischZeile previous = null;

    BiFunction<WahlverwaltungGebietInformation, Stimmabgabeart, String> labelFct = tabelleSortierung.getLabelFctZwischensummenGruppierungUntersteEbeneStimmabgabearten();

    for (Iterator<ErgebnisTabellarischZeile> iter = zeilen.iterator(); iter.hasNext();) {
      ErgebnisTabellarischZeile zeile = iter.next();

      if (previous != null) {
        boolean gruppierungWechselt = !Objects.equals(gruppierungFct.apply(zeile), gruppierungFct.apply(previous));
        boolean stimmabgabeartWechselt = !Objects.equals(zeile.getStimmabgabeart(), previous.getStimmabgabeart());

        if (gruppierungWechselt || stimmabgabeartWechselt) {
          LOG.info("Erzeuge Zwischensummenzeile Stimmabgabeart Gruppierung unterste Ebene [ursachen = [gruppierung gewechselt = {}, stimmabgabeart gewechselt = {}]]", gruppierungWechselt,
              stimmabgabeartWechselt);

          createZeileZwischensummenGruppierungUntersteEbeneStimmabgabeart(zwischensummen, previous.getDatengebiet(), previous.getStimmabgabeart(), zeilenZuSummieren, labelFct).ifPresent(result::add);

          zeilenZuSummieren = new ArrayList<>();
        }
      }

      result.add(zeile);

      previous = zeile;
      zeilenZuSummieren.add(zeile);

      boolean letzteZeileErreicht = !iter.hasNext();

      if (letzteZeileErreicht) {
        LOG.info("Erzeuge Zwischensummenzeile Stimmabgabeart Gruppierung unterste Ebene [ursachen = [letzte zeile = {}]]", letzteZeileErreicht);

        createZeileZwischensummenGruppierungUntersteEbeneStimmabgabeart(zwischensummen, previous.getDatengebiet(), previous.getStimmabgabeart(), zeilenZuSummieren, labelFct).ifPresent(result::add);
      }
    }

    return result;
  }

  private List<ErgebnisTabellarischZeile> insertZeilenZwischensummenGruppierungGesamt(ErgebnisTabellarischZwischensummen zwischensummen,
      Function<ErgebnisTabellarischZeile, WahlverwaltungGebietInformation> gruppierungFct, List<ErgebnisTabellarischZeile> zeilen) {
    List<ErgebnisTabellarischZeile> result = new ArrayList<>();

    ErgebnisTabellarischZeile previous = null;
    List<ErgebnisTabellarischZeile> zeilenZuSummieren = new ArrayList<>();

    BiFunction<WahlverwaltungGebietInformation, Stimmabgabeart, String> labelFct = tabelleSortierung.getLabelFctZwischensummenGruppierungGesamt();

    for (Iterator<ErgebnisTabellarischZeile> iter = zeilen.iterator(); iter.hasNext();) {
      ErgebnisTabellarischZeile zeile = iter.next();

      if (previous != null) {
        boolean gruppierungWechselt = !Objects.equals(gruppierungFct.apply(zeile), gruppierungFct.apply(previous));

        if (gruppierungWechselt) {
          LOG.info("Erzeuge Zwischensummenzeile Gruppierung gesamt [ursachen = [gruppierung gewechselt = {}]]", gruppierungWechselt);

          createZeileZwischensummenGruppierungUntersteEbeneGesamt(zwischensummen, previous.getDatengebiet(), zeilenZuSummieren, labelFct).ifPresent(result::add);

          zeilenZuSummieren = new ArrayList<>();
        }
      }

      result.add(zeile);

      previous = zeile;
      zeilenZuSummieren.add(zeile);

      boolean letzteZeileErreicht = !iter.hasNext();

      if (letzteZeileErreicht) {
        LOG.info("Erzeuge Zwischensummenzeile Gruppierung gesamt [ursachen = [letzte zeile = {}]]", letzteZeileErreicht);

        createZeileZwischensummenGruppierungUntersteEbeneGesamt(zwischensummen, previous.getDatengebiet(), zeilenZuSummieren, labelFct).ifPresent(result::add);
      }
    }

    return result;
  }

  private List<ErgebnisTabellarischZeile> insertZeilenZwischensummenGruppierungUebergeordnetStimmabgabearten(ErgebnisTabellarischZwischensummen zwischensummen,
      Function<ErgebnisTabellarischZeile, WahlverwaltungGebietInformation> gruppierungFct, List<ErgebnisTabellarischZeile> zeilen) {
    List<ErgebnisTabellarischZeile> result = new ArrayList<>();

    List<ErgebnisTabellarischZeile> zeilenZuSummierenUrnenwahl = new ArrayList<>();
    List<ErgebnisTabellarischZeile> zeilenZuSummierenBriefwahl = new ArrayList<>();
    ErgebnisTabellarischZeile previous = null;

    BiFunction<WahlverwaltungGebietInformation, Stimmabgabeart, String> labelFct = tabelleSortierung.getLabelFctZwischensummenGruppierungUebergeordnetStimmabgabearten();

    for (Iterator<ErgebnisTabellarischZeile> iter = zeilen.iterator(); iter.hasNext();) {
      ErgebnisTabellarischZeile zeile = iter.next();

      if (previous != null) {
        boolean gruppierungWechselt = !Objects.equals(gruppierungFct.apply(zeile), gruppierungFct.apply(previous));

        if (gruppierungWechselt) {
          LOG.info("Erzeuge Zwischensummenzeilem Stimmabgabearten Gruppierung übergeordnet [ursachen = [gruppierung gewechselt = {}]]", gruppierungWechselt);

          createZeileZwischensummenGruppierungUebergeordnetStimmabgabeart(zwischensummen, previous.getDatengebiet(), Stimmabgabeart.URNE, zeilenZuSummierenUrnenwahl, labelFct).ifPresent(result::add);
          createZeileZwischensummenGruppierungUebergeordnetStimmabgabeart(zwischensummen, previous.getDatengebiet(), Stimmabgabeart.BRIEF, zeilenZuSummierenBriefwahl, labelFct).ifPresent(result::add);

          zeilenZuSummierenUrnenwahl = new ArrayList<>();
          zeilenZuSummierenBriefwahl = new ArrayList<>();
        }
      }

      result.add(zeile);

      previous = zeile;

      if (zeile.isStimmabgabeartUrnenwahl()) {
        zeilenZuSummierenUrnenwahl.add(zeile);
      }

      if (zeile.isStimmabgabeartBriefwahl()) {
        zeilenZuSummierenBriefwahl.add(zeile);
      }

      boolean letzteZeileErreicht = !iter.hasNext();

      if (letzteZeileErreicht) {
        LOG.info("Erzeuge Zwischensummenzeilem Stimmabgabearten Gruppierung übergeordnet [ursachen = [letzte zeile = {}]]", letzteZeileErreicht);

        createZeileZwischensummenGruppierungUebergeordnetStimmabgabeart(zwischensummen, previous.getDatengebiet(), Stimmabgabeart.URNE, zeilenZuSummierenUrnenwahl, labelFct).ifPresent(result::add);
        createZeileZwischensummenGruppierungUebergeordnetStimmabgabeart(zwischensummen, previous.getDatengebiet(), Stimmabgabeart.BRIEF, zeilenZuSummierenBriefwahl, labelFct).ifPresent(result::add);
      }
    }

    return result;
  }

  private static Optional<ErgebnisTabellarischZeile> createZeileZwischensummenGruppierungUebergeordnetStimmabgabeart(ErgebnisTabellarischZwischensummen zwischensummen,
      ErgebnisZusammenstellungZeileDatengebiet datengebiet, Stimmabgabeart stimmabgabeart, List<ErgebnisTabellarischZeile> zeilen,
      BiFunction<WahlverwaltungGebietInformation, Stimmabgabeart, String> labelFct) {
    return zwischensummen.createZeile(datengebiet, stimmabgabeart, zeilen, labelFct);
  }

  private static Optional<ErgebnisTabellarischZeile> createZeileZwischensummenGruppierungUntersteEbeneStimmabgabeart(ErgebnisTabellarischZwischensummen zwischensummen,
      ErgebnisZusammenstellungZeileDatengebiet datengebiet, Stimmabgabeart stimmabgabeart, List<ErgebnisTabellarischZeile> zeilen,
      BiFunction<WahlverwaltungGebietInformation, Stimmabgabeart, String> labelFct) {
    return zwischensummen.createZeile(datengebiet, stimmabgabeart, zeilen, labelFct);
  }

  private static Optional<ErgebnisTabellarischZeile> createZeileZwischensummenGruppierungUntersteEbeneGesamt(ErgebnisTabellarischZwischensummen zwischensummen,
      ErgebnisZusammenstellungZeileDatengebiet datengebiet, List<ErgebnisTabellarischZeile> zeilen, BiFunction<WahlverwaltungGebietInformation, Stimmabgabeart, String> labelFct) {
    return zwischensummen.createZeile(datengebiet, Stimmabgabeart.GESAMT, zeilen, labelFct);
  }
}
