package de.ivu.elect.sitzberechnung.control;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import javax.annotation.Nullable;

import de.ivu.elect.sitzberechnung.Gebiet;
import de.ivu.elect.sitzberechnung.Gruppe;
import de.ivu.elect.sitzberechnung.Kandidat;
import de.ivu.elect.sitzberechnung.Kandidatur;
import de.ivu.elect.sitzberechnung.Liste;
import de.ivu.elect.sitzberechnung.Listenverbindung;
import de.ivu.elect.sitzberechnung.SitzberechnungEntity;
import de.ivu.elect.sitzberechnung.Sitzberechnungseingabe;
import de.ivu.elect.sitzberechnung.konflikt.ErgebnisKonflikt;

public class Sitzberechnungsdaten extends AbstractSitzberechnungsdaten {

  private final Sitzberechnungseingabe sitzberechnungeingabe;

  private List<Gruppe> gruppen;

  private List<Listenverbindung> listenverbindungen;

  private Set<Kandidat> kandidaten;

  private Map<Kandidat, List<Liste>> listenPerKandidat;

  private Map<Kandidat, Map<Gebiet, Kandidatur>> listenkandidaturenMap;

  public Sitzberechnungsdaten(Sitzberechnungseingabe sitzberechnungeingabe) {
    super();

    this.sitzberechnungeingabe = sitzberechnungeingabe;
  }

  public Sitzberechnungseingabe getSitzberechnungeingabe() {
    return sitzberechnungeingabe;
  }

  public List<Gruppe> getGruppenSortiertNachKey() {
    if (gruppen == null) {
      gruppen = sitzberechnungeingabe.getListen().stream() //
          .map(Liste::getGruppe) //
          .distinct() //
          .sorted(Comparator.comparing(Gruppe::getKey)) //
          .collect(toList());
    }

    return gruppen;
  }

  public List<Listenverbindung> getListenverbindungenSortiertNachKey() {
    if (listenverbindungen == null) {
      listenverbindungen = sitzberechnungeingabe.getListen().stream() //
          .map(Liste::getListenverbindung) //
          .filter(Objects::nonNull) //
          .distinct() //
          .sorted(Comparator.comparing(Listenverbindung::getKey)) //
          .collect(toList());
    }

    return listenverbindungen;
  }

  public List<Gebiet> getListenwahlgebieteSortiertNachKey() {
    return sitzberechnungeingabe.getGebiete().stream() //
        .filter(Gebiet::isListenwahlgebiet) //
        .sorted(Comparator.comparing(Gebiet::getKey)) //
        .collect(toList());
  }

  public List<Gebiet> getDirektwahlgebieteSortiertNachKey() {
    return sitzberechnungeingabe.getGebiete().stream() //
        .filter(Gebiet::isDirektwahlgebiet) //
        .sorted(Comparator.comparing(Gebiet::getKey)) //
        .collect(toList());
  }

  public Optional<ErgebnisKonflikt> getFachlichenGleichenKonflikt(ErgebnisKonflikt ergebnisKonflikt) {
    return sitzberechnungeingabe.getKonflikte().stream().filter(ergebnisKonflikt::isGleicherKonflikt).findAny();
  }

  public List<ErgebnisKonflikt> getGeloesteKonflikteSortiertNachPosition() {
    return sitzberechnungeingabe.getKonflikte().stream()
            .filter(ErgebnisKonflikt::isGeloest)
            .sorted(Comparator.comparing(ErgebnisKonflikt::getPosition))
            .collect(toList());
  }

  public List<Liste> getListenFuerKandidat(Kandidat kandidat) {
    if (listenPerKandidat == null) {
      listenPerKandidat = sitzberechnungeingabe.getListen().stream()
              .collect(groupingBy(Liste::getListenkandidaturen)).entrySet().stream()
              .flatMap(e -> e.getKey().stream().map(k -> new AbstractMap.SimpleImmutableEntry<>(k.getKandidat(), e.getValue()))) //Einen neuen Mapeintrag für jeden Kandidaten im List<Kandidat> anlegen, i.e. die Liste glätten
              .collect(toMap(Map.Entry::getKey, e -> new ArrayList<>(e.getValue()), (v1, v2) -> {
                v1.addAll(v2);
                return v1;
              }));
    }
    return listenPerKandidat.get(kandidat);
  }

  public Set<Kandidatur> getKandidaturenFuerGebiet(Gebiet gebiet) {
    if (listenkandidaturenMap == null) {
      initListenkandidaturenMap();
    }
    Set<Kandidatur> kandidaturenAufGebiet = new HashSet<>(sitzberechnungeingabe.getDirektkandidaturen().stream().filter(direktkandidatur -> direktkandidatur.getGebiet().equals(gebiet)).collect(toSet()));

    getAlleKandidaten().stream()
            .map(kandidat -> listenkandidaturenMap.get(kandidat).getOrDefault(gebiet, null))
            .filter(Objects::nonNull)
            .forEach(kandidaturenAufGebiet::add);

    return kandidaturenAufGebiet;
  }

  public Set<Kandidat> getAlleKandidaten() {
    if (kandidaten == null) {
      Set<Kandidat> result = new HashSet<>(getAlleKandidatenVonDirektkandidaturen());
      result.addAll(getAlleKandidatenVonListenkandidaturen());
      kandidaten = Collections.unmodifiableSet(result);
    }

    return kandidaten;
  }

  private Set<Kandidat> getAlleKandidatenVonListenkandidaturen() {
    return sitzberechnungeingabe.getListen().stream()
            .flatMap(liste -> liste.getListenkandidaturen().stream())
            .map(Kandidatur::getKandidat)
            .collect(toSet());
  }

  private Set<Kandidat> getAlleKandidatenVonDirektkandidaturen() {
    return sitzberechnungeingabe.getDirektkandidaturen().stream()
        .map(Kandidatur::getKandidat)
        .collect(toSet());
  }

  public Liste findListeNachKey(String listeId) {
    return getSitzberechnungeingabe().getListen().stream()
            .filter(liste -> liste.getKey().equals(listeId)).findAny().orElse(null);
  }

  public Liste findListeNachGruppeKeyUndGebietKey(String gruppeKey, String gebietKey) {
    return getSitzberechnungeingabe().getListen().stream()
            .filter(liste -> liste.getGruppe().getKey().equals(gruppeKey) && liste.getGebiet().getKey().equals(gebietKey)).findAny().orElse(null);
  }

  public Gruppe findGruppeNachKey(String gruppeId) {
    return getSitzberechnungeingabe().getListen().stream().map(Liste::getGruppe)
            .filter(gruppe -> gruppe.getKey().equals(gruppeId)).findAny().orElse(null);
  }

  public Gebiet findGebietNachKey(String gebietKey) {
    return getSitzberechnungeingabe().getGebiete().stream()
            .filter(gebiet -> gebiet.getKey().equals(gebietKey))
            .findAny().orElse(null);
  }

  public List<Kandidat> getSortierteKandidaten(Liste liste) {
    return liste.getListenkandidaturen().stream()
            .sorted(Comparator.comparing(liste::getPositionInListe))
            .map(Kandidatur::getKandidat)
            .collect(toList());
  }

  public Kandidatur getKandidaturFuerKandidatAufGebiet(Kandidat kandidat, Gebiet gebiet) {
    if (listenkandidaturenMap == null) {
      initListenkandidaturenMap();
    }
    return listenkandidaturenMap.getOrDefault(kandidat, Collections.emptyMap()).get(gebiet);
  }

  private void initListenkandidaturenMap() {
    listenkandidaturenMap = new HashMap<>();
    for (Liste liste : sitzberechnungeingabe.getListen()) {
      for (Kandidatur kandidatur : liste.getListenkandidaturen()) {
        listenkandidaturenMap.computeIfAbsent(kandidatur.getKandidat(), m -> new HashMap<>()).put(kandidatur.getGebiet(), kandidatur);
      }
    }
  }


  @Nullable
  public Kandidatur findListenkandidaturNachKandidatUndGruppeKey(String kandidatKey, String gruppeKey) {
    return sitzberechnungeingabe.getListen().stream()
            .filter(liste -> liste.getGruppe().getKey().equals(gruppeKey))
            .flatMap(l -> l.getListenkandidaturen().stream())
            .filter(k -> k.getKandidat().getKey().equals(kandidatKey))
            .findFirst()
            .orElse(null);
  }

  public void validate() {
    validateListen();

    validateGruppen();

    validateGebiete();
    verifyJedeListeAufEinemGebiet();
    verifyGruppenHabenNichtMehrereListenInEinemGebiet();


    validateKandidaten();
    verifyEindeutigeKandidatenpositionenInJederListe();
  }

  void validateListen() {
    verifyEindeutigenKeys(sitzberechnungeingabe.getListen());
    verifyJedeListeHatEineGruppe();
  }

  void validateKandidaten() {
    if (getAlleKandidaten() == null || getAlleKandidaten().isEmpty()) {
      throw new IllegalStateException("Es liegen keine Kandidaten vor.");
    }
    verifyEindeutigenKeys(getAlleKandidaten());
  }

  void validateGebiete() {
    if (sitzberechnungeingabe.getGebiete() == null || sitzberechnungeingabe.getGebiete().isEmpty()) {
      throw new IllegalStateException("Es liegen keine Gebiete vor.");
    }
    verifyEindeutigenKeys(sitzberechnungeingabe.getGebiete());
  }

  void validateGruppen() {
    verifyEindeutigenKeys(sitzberechnungeingabe.getListen().stream()
            .map(Liste::getGruppe)
            .distinct()
            .collect(toList()));
  }

  private void verifyJedeListeHatEineGruppe() {
    String listenOhneGruppeString = sitzberechnungeingabe.getListen().stream().filter(liste -> liste.getGruppe() == null)
            .map(SitzberechnungEntity::getKey).collect(joining(", "));
    if (!listenOhneGruppeString.isEmpty()) {
      throw new IllegalStateException("Die folgende Listen gehören zu keiner Gruppe: " + listenOhneGruppeString);
    }
  }

  private void verifyJedeListeAufEinemGebiet() {
    String listenOhneGebietString = sitzberechnungeingabe.getListen().stream().filter(liste -> liste.getGebiet() == null)
            .map(SitzberechnungEntity::getKey).collect(joining(", "));
    if (!listenOhneGebietString.isEmpty()) {
      throw new IllegalStateException("Die folgende Listen befinden sich auf keinem Gebiet: " + listenOhneGebietString);
    }
  }


  void verifyGruppenHabenNichtMehrereListenInEinemGebiet() {
    Map<Gruppe, List<Liste>> gruppeListMap = getSitzberechnungeingabe().getListen().stream()
            .collect(groupingBy(Liste::getGruppe));
    List<String> fehlerhafteGruppen = new ArrayList<>();

    for (Map.Entry<Gruppe, List<Liste>> listenFuerGruppe : gruppeListMap.entrySet()) {
      Gruppe gruppe = listenFuerGruppe.getKey();
      List<Liste> listen = listenFuerGruppe.getValue();
      if (listen.stream().map(Liste::getGebiet).distinct().count() != listen.size()) {
        fehlerhafteGruppen.add(gruppe.toString());
      }
    }

    if (!fehlerhafteGruppen.isEmpty()) {
      throw new IllegalStateException("Die folgende Gruppen haben mehrere Listen auf einem Gebiet: " + String.join(", ", fehlerhafteGruppen));
    }

  }

  private void verifyEindeutigeKandidatenpositionenInJederListe() {
    for (Liste liste : getSitzberechnungeingabe().getListen()) {
      long eindeutigeKandidaturenpositionenFuerListe = liste.getListenkandidaturen().stream()
              .map(liste::getPositionInListe)
              .distinct().count();

      if (eindeutigeKandidaturenpositionenFuerListe != liste.getListenkandidaturen().size()) {
        throw new IllegalStateException("Kandidaten auf der folgenden Liste haben keine eindeutigen Positionen: " + liste);
      }
    }
  }
}
