
package de.ivu.elect.core.exchange.gebietsbaum;

import static java.util.stream.Collectors.toList;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;

import javax.inject.Inject;

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

import de.ivu.elect.business.gebietsbaum.boundary.GebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.GebietsgruppierungDAO;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietDescriptor;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformation;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationByNummerComparator;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationResolver;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationResolverContext;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGemeindeInformation;
import de.ivu.elect.business.gebietsbaum.control.EntityMitWurzelgebietDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.wahl.control.Wahldaten;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlParameterLoader;
import de.ivu.elect.util.TriFunction;

public class WahlverwaltungGebietInformationCollector implements Serializable {

  private static final long serialVersionUID = 1L;

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

  private static final Comparator<WahlverwaltungGebietInformation> STIMMZETTELGEBIET_DEFAULT_COMPARATOR = new WahlverwaltungGebietInformationByNummerComparator();

  private static final Comparator<WahlverwaltungGebietInformation> GEMEINDE_DEFAULT_COMPARATOR = new WahlverwaltungGebietInformationByNummerComparator();

  private static final Comparator<Gebiet> GEBIET_DEFAULT_COMPARATOR = new GebietByErfasungsgebietsartAndPositionComparator<>();

  private static final Predicate<Gebiet> GEBIET_DEFAULT_FILTER = gebiet -> true;

  @Inject
  private transient GebietDAO gebietDAO;

  @Inject
  private transient GebietsgruppierungDAO gebietsgruppierungDAO;

  @Inject
  private transient WahlParameterLoader wahlParameterLoader;

  @Inject
  private transient WahlverwaltungGebietInformationResolver wahlverwaltungGebietInformationResolver;

  public WahlverwaltungGebietInformationCollector() {
    super();
  }

  public WahlverwaltungGebietInformation getWurzelgebiet(Wahl wahl) {
    return new WahlverwaltungGebietInformation(wahl.getWurzelgebiet(EntityMitWurzelgebietDAO.fromDao(gebietDAO)));
  }

  /**
   * @return Erfassungsgebiete (gemappt) per Gemeindeverband/Gemeinde (oder Stadtbezirk) per Stimmzettelgebiet
   */
  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteGroupedByDefault(Wahl wahl,
      TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    return collectErfassungsgebieteGroupedByDefault(wahl, false, erfassungsgebietMapper);
  }

  /**
   * @return Erfassungsgebiete (gemappt) per Gemeindeverband/Gemeinde (oder Stadtbezirk, falls nicht ignoriert) per Stimmzettelgebiet
   */
  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteGroupedByDefault(Wahl wahl,
      boolean gemeindeStadtbezirkseinteilungIgnored, TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    Predicate<Gebiet> erfassungsgebietPredicate = Gebiet::isErfassungsgebiet;

    return collectGebieteGroupedByDefault(wahl, gemeindeStadtbezirkseinteilungIgnored, erfassungsgebietPredicate, erfassungsgebietMapper);
  }

  /**
   * @return Erfassungsgebiete (gemappt) per Gemeindeverband/Gemeinde (oder Stadtbezirk) per Stimmzettelgebiet
   */
  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteGroupedByDefault(Wahldaten< ? , ? > wahldaten,
      TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    return collectErfassungsgebieteGroupedByDefault(wahldaten, false, erfassungsgebietMapper);
  }

  /**
   * @return Erfassungsgebiete (gemappt) per Gemeindeverband/Gemeinde (oder Stadtbezirk, falls nicht ignoriert) per Stimmzettelgebiet
   */
  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteGroupedByDefault(Wahldaten< ? , ? > wahldaten,
      boolean gemeindeStadtbezirkseinteilungIgnored, TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    return collectErfassungsgebieteGroupedByDefault(wahldaten, gemeindeStadtbezirkseinteilungIgnored, GEBIET_DEFAULT_FILTER, erfassungsgebietMapper);
  }

  /**
   * @return Erfassungsgebiete (gefiltert, gemappt) per Gemeindeverband/Gemeinde (oder Stadtbezirk) per Stimmzettelgebiet
   */
  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteGroupedByDefault(Wahldaten< ? , ? > wahldaten,
      Predicate<Gebiet> erfassungsgebietFilter, TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    return collectErfassungsgebieteGroupedByDefault(wahldaten, false, erfassungsgebietFilter, erfassungsgebietMapper);
  }

  /**
   * @return Erfassungsgebiete (gefiltert, gemappt) per Gemeindeverband/Gemeinde (oder Stadtbezirk, falls nicht ignoriert) per Stimmzettelgebiet
   */
  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteGroupedByDefault(Wahldaten< ? , ? > wahldaten,
      boolean gemeindeStadtbezirkseinteilungIgnored, Predicate<Gebiet> erfassungsgebietFilter,
      TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    WahlverwaltungGebietInformationResolverContext wahlverwaltungGebietInformationResolverContext = new WahlverwaltungGebietInformationResolverContext(wahldaten.getWahl(), wahlParameterLoader,
        wahldaten);

    List<Gebiet> erfassungsgebiete = new ArrayList<>(wahldaten.getErfassungsgebiete());

    return collectGebieteGroupedByDefault(wahlverwaltungGebietInformationResolverContext, gemeindeStadtbezirkseinteilungIgnored, erfassungsgebiete, erfassungsgebietFilter, erfassungsgebietMapper);
  }

  public <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectGebieteGroupedByDefault(Wahl wahl, boolean gemeindeStadtbezirkseinteilungIgnored,
      Predicate<Gebiet> gebietPredicate, TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> gebietMapper) {
    WahlverwaltungGebietInformationResolverContext wahlverwaltungGebietInformationResolverContext = new WahlverwaltungGebietInformationResolverContext(wahl, wahlParameterLoader, gebietDAO,
        gebietsgruppierungDAO);

    Collection<Gebiet> gebiete = gebietDAO.findByWahl(wahl).stream() //
        .filter(gebietPredicate) //
        .collect(toList());

    return collectGebieteGroupedByDefault(wahlverwaltungGebietInformationResolverContext, gemeindeStadtbezirkseinteilungIgnored, gebiete, GEBIET_DEFAULT_FILTER, gebietMapper);
  }

  private <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectGebieteGroupedByDefault(
      WahlverwaltungGebietInformationResolverContext wahlverwaltungGebietInformationResolverContext, boolean gemeindeStadtbezirkseinteilungIgnored, Collection<Gebiet> gebiete,
      Predicate<Gebiet> gebietFilter, TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> gebietMapper) {
    return collectGebieteGroupedByGemeindeAndStimmzettelgebiet(wahlverwaltungGebietInformationResolverContext, STIMMZETTELGEBIET_DEFAULT_COMPARATOR, gemeindeStadtbezirkseinteilungIgnored,
        GEMEINDE_DEFAULT_COMPARATOR, gebiete, gebietFilter, GEBIET_DEFAULT_COMPARATOR, gebietMapper);
  }

  private <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectGebieteGroupedByGemeindeAndStimmzettelgebiet(
      WahlverwaltungGebietInformationResolverContext wahlverwaltungGebietInformationResolverContext, Comparator<WahlverwaltungGebietInformation> stimmzettelgebietInformationComparator,
      boolean gemeindeStadtbezirkseinteilungIgnored, Comparator<WahlverwaltungGebietInformation> gemeindeInformationComparator, Collection<Gebiet> gebiete, Predicate<Gebiet> gebietFilter,
      Comparator<Gebiet> gebietComparator, TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> gebietMapper) {
    SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> result = new TreeMap<>(stimmzettelgebietInformationComparator);

    for (Entry<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>>> entryPerStimmzettelgebiet : groupGebieteByGemeindeAndStimmzettelgebiet(
        wahlverwaltungGebietInformationResolverContext, stimmzettelgebietInformationComparator, gemeindeStadtbezirkseinteilungIgnored, gemeindeInformationComparator, gebiete, gebietFilter)
            .entrySet()) {
      WahlverwaltungGebietInformation stimmzettelgebietInformation = entryPerStimmzettelgebiet.getKey();

      result.put(stimmzettelgebietInformation, new TreeMap<>(gemeindeInformationComparator));

      for (Entry<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> gebietePerGemeinde : entryPerStimmzettelgebiet.getValue().entrySet()) {
        WahlverwaltungGebietInformation gemeindeInformation = gebietePerGemeinde.getKey();

        List<T> gebieteMapped = gebietePerGemeinde.getValue().stream() //
            .sorted((one, another) -> gebietComparator.compare(one.getGebiet(), another.getGebiet())) // Erst sortieren!
            .map(gebietDescriptor -> gebietMapper.apply(stimmzettelgebietInformation, gemeindeInformation, gebietDescriptor)) //
            .collect(toList());

        result.get(stimmzettelgebietInformation).put(gemeindeInformation, gebieteMapped);
      }
    }

    return result;
  }

  private SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>>> groupGebieteByGemeindeAndStimmzettelgebiet(
      WahlverwaltungGebietInformationResolverContext wahlverwaltungGebietInformationResolverContext, Comparator<WahlverwaltungGebietInformation> stimmzettelgebietInformationComparator,
      boolean gemeindeStadtbezirkseinteilungIgnored, Comparator<WahlverwaltungGebietInformation> gemeindeInformationComparator, Collection<Gebiet> gebiete, Predicate<Gebiet> gebietFilter) {
    SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>>> result = new TreeMap<>(stimmzettelgebietInformationComparator);

    for (Gebiet gebiet : gebiete) {
      if (gebietFilter.test(gebiet)) {
        WahlverwaltungGebietDescriptor gebietDescriptor = wahlverwaltungGebietInformationResolver.createDescriptor(wahlverwaltungGebietInformationResolverContext, gebiet);

        WahlverwaltungGebietInformation stimmzettelgebietInformation = gebietDescriptor.getStimmzettelgebiet();
        WahlverwaltungGebietInformation gemeindeInformation = extractGemeindeInformation(gebietDescriptor, gemeindeStadtbezirkseinteilungIgnored);

        if (result.putIfAbsent(stimmzettelgebietInformation, new TreeMap<>(gemeindeInformationComparator)) == null) {
          info(
              "Stimmzettelgebiet gefunden [art = " + stimmzettelgebietInformation.getArt() + "/" + stimmzettelgebietInformation.getArtIndex() + ", nummer = " + stimmzettelgebietInformation.getNummer()
                  + ", name = " + stimmzettelgebietInformation.getName() + ", gebiet in gebietsbaum = " + stimmzettelgebietInformation.getGebiet().isPresent() + "]");
        }

        SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> gebietePerGemeinde = result.get(stimmzettelgebietInformation);

        if (gebietePerGemeinde.putIfAbsent(gemeindeInformation, new ArrayList<>()) == null) {
          info("Gemeinde" + (gemeindeStadtbezirkseinteilungIgnored ? "" : "/Stadtbezirk") + " gefunden [art = " + gemeindeInformation.getArt() + "/" + gemeindeInformation.getArtIndex() + ", nummer = "
              + gemeindeInformation.getNummer() + ", name = " + gemeindeInformation.getName() + ", gebiet in gebietsbaum = " + gemeindeInformation.getGebiet().isPresent()
              + ", stadtbezirkseinteilung ignored = " + gemeindeStadtbezirkseinteilungIgnored + "]");
        }

        gebietePerGemeinde.get(gemeindeInformation).add(gebietDescriptor);
      }
    }

    return result;
  }

  private WahlverwaltungGebietInformation extractGemeindeInformation(WahlverwaltungGebietDescriptor gebietDescriptor, boolean gemeindeStadtbezirkseinteilungIgnored) {
    Gebiet gebiet = gebietDescriptor.getGebiet();

    if (!gemeindeStadtbezirkseinteilungIgnored) {
      WahlverwaltungGebietInformation stadtbezirkInformation = gebietDescriptor.getStadtbezirk();

      if (!stadtbezirkInformation.isUnbekannt()) {
        info("\tGebiet mit (übergeordnetem) Stadtbezirk gefunden [nummer = " + gebiet.getNummer() + ", name = " + gebiet.getName() + ", gebiet in gebietsbaum = true]");

        return stadtbezirkInformation;
      }
    }

    WahlverwaltungGemeindeInformation gemeindeverbandInformation = gebietDescriptor.getGemeindeverband();

    if (!gemeindeverbandInformation.isUnbekannt()) { // ELECT-8697
      info("\tGebiet mit (übergeordnetem) Gemeindeverband gefunden [nummer = " + gebiet.getNummer() + ", name = " + gebiet.getName() + ", gebiet in gebietsbaum = true]");

      return gemeindeverbandInformation;
    }

    return gebietDescriptor.getGemeinde();
  }

  private void info(String format, Object... params) {
    LOG.info(format, params);
  }
}
