
package de.ivu.elect.sitzberechnung.math;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptySortedMap;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.annotation.Nullable;

/**
 * Utility-Klasse zum Sortieren und Gruppieren von Kandidaten (oder Listen oder anderen Objekten) vom Typ V nach einer Zahl (z.B. Stimmenzahl) vom Typ N
 * <p/>
 * Benutzung:
 *
 * <pre>
 * NachStimmenSortierer&lt;V&gt; sortierer = new NachStimmenSortierer&lt;&gt;();
 * for (V v : listOfVs) {
 *   sortierer.add(v, getVotes(v));
 * }
 * System.out.println(&quot;Gewonnen haben: &quot; + sortierer.getErstplatzierte());
 * </pre>
 *
 *
 * @see VordereInRangfolgeErmittler
 * @param <V>
 *          Klasse der zu sortierenden Objekte
 * @param <N>
 *          Klasse der Anzahl, nach der Sortiert wird (meist Long oder Integer)
 */
public class NachNummerSortierer<V, N extends Comparable<N>> {

  private final long anzahlSitze;
  protected final SortedMap<N, List<V>> map = createSortedMap();

  private TreeMap<N, List<V>> createSortedMap() {
    return new TreeMap<>(Collections.reverseOrder());
  }

  public NachNummerSortierer() {
    this.anzahlSitze = 0;
  }

  public NachNummerSortierer(long anzahlSitze) {
    this.anzahlSitze = anzahlSitze;
  }

  public void add(N stimmen, V kandidatur) {
    map.computeIfAbsent(stimmen, ignored -> new ArrayList<>()).add(kandidatur);
  }

  /**
   * @return Set mit den verschienden Stimmenzahlen in absteigender Reihenfolge
   */
  public Set<N> getStimmenZahlenAbsteigend() {
    return map.keySet();
  }

  public boolean isEmpty() {
    return map.isEmpty();
  }

  @Nullable
  public N getHoechsteStimmenzahl() {
    if (isEmpty()) {
      return null;
    } else {
      return map.keySet().iterator().next();
    }
  }

  public List<V> getErstplatzierte() {
    if (isEmpty()) {
      return emptyList();
    } else {
      return map.getOrDefault(getHoechsteStimmenzahl(), emptyList());
    }
  }

  public List<V> getZweitplatzierte() {
    Iterator<N> iterator = map.keySet().iterator();
    if (!iterator.hasNext()) {
      return emptyList();
    } else {
      iterator.next();
    }
    if (!iterator.hasNext()) {
      return emptyList();
    } else {
      return map.getOrDefault(iterator.next(), emptyList());
    }
  }

  public boolean isErstplatzierterEindeutig() {
    return getErstplatzierte().size() == 1;
  }

  @Override
  public String toString() {
    return super.toString() + ": map = " + map;
  }

  public List<V> getElementeMitStimmenzahl(N anzahlStimmen) {
    return map.computeIfAbsent(anzahlStimmen, ignored -> new ArrayList<>());
  }

  public long getAnzahlSitze() {
    return anzahlSitze;
  }

  /**
   * @return die sortierte Liste der Elemente, die auf jeden Fall zu den <code>anzahlSitze</code> vordersten gehören
   */
  public List<V> getVordere() {
    List<V> result = new ArrayList<>();
    int count = 0;
    for (List<V> list : map.values()) {
      count += list.size();
      if (count > anzahlSitze) {
        return result;
      } else {
        result.addAll(list);
      }
    }
    return result;
  }

  public List<V> getAlle() {
    List<V> result = new ArrayList<>();
    for (List<V> list : map.values()) {
      result.addAll(list);
    }
    return result;

  }

  public SortedMap<N, List<V>> getAlleGruppiert() {
    return map;
  }

  /**
   * @return die sortierte Liste der Elemente, die auf jeden Fall zu den <code>anzahlSitze</code> vordersten gehören
   */
  public SortedMap<N, List<V>> getVordereGruppiert() {
    SortedMap<N, List<V>> result = createSortedMap();
    int count = 0;
    for (Entry<N, List<V>> entry : map.entrySet()) {
      List<V> list = entry.getValue();
      count += list.size();
      if (count > anzahlSitze) {
        return result;
      } else {
        result.put(entry.getKey(), new ArrayList<>(list));
      }
    }
    return result;
  }

  /**
   * @return die Liste der Elemente, die in der Rangfolge gleich sind und nur zum Teil zu den <code>anzahlSitze</code> vordersten gehören können.
   */
  public List<V> getUnentschiedene() {
    if (anzahlSitze <= 0) {
      return emptyList();
    }
    int count = 0;
    for (List<V> list : map.values()) {
      count += list.size();
      if (count == anzahlSitze) {
        return emptyList();
      }
      if (count > anzahlSitze) {
        return new ArrayList<>(list);
      }
    }
    return emptyList();
  }

  /**
   * @return die Liste der Elemente, die in der Rangfolge gleich sind und nur zum Teil zu den <code>anzahlSitze</code> vordersten gehören können.
   */
  public SortedMap<N, List<V>> getUnentschiedeneGruppiert() {
    if (anzahlSitze <= 0) {
      return emptySortedMap();
    }
    int count = 0;
    for (Entry<N, List<V>> entry : map.entrySet()) {
      List<V> list = entry.getValue();
      count += list.size();
      if (count == anzahlSitze) {
        return emptySortedMap();
      }
      if (count > anzahlSitze) {
        SortedMap<N, List<V>> result = createSortedMap();
        result.put(entry.getKey(), new ArrayList<>(list));
        return result;
      }
    }
    return emptySortedMap();
  }

  public boolean isEntschieden() {
    return getUnentschiedene().isEmpty();
  }

  /**
   * @return die sortierte Liste der Elemente, die auf keinen Fall zu den <code>anzahlSitze</code> vordersten gehören
   */
  public List<V> getHintere() {
    List<V> result = new ArrayList<>();
    int count = 0;
    for (List<V> list : map.values()) {
      if (count >= anzahlSitze) {
        result.addAll(list);
      }
      count += list.size();
    }
    return result;
  }

  /**
   * @return die sortierte Liste der Elemente, die auf keinen Fall zu den <code>anzahlSitze</code> vordersten gehören
   */
  public SortedMap<N, List<V>> getHintereGruppiert() {
    SortedMap<N, List<V>> result = createSortedMap();
    int count = 0;
    for (Entry<N, List<V>> entry : map.entrySet()) {
      List<V> list = entry.getValue();
      if (count >= anzahlSitze) {
        result.put(entry.getKey(), new ArrayList<>(list));
      }
      count += list.size();
    }
    return result;
  }

  /**
   * @return eine sortierte Map mit Listen von je mindestens zwei Elementen, die jeweils der gleichen Nummer zugeordnet sind.
   */
  public SortedMap<N, List<V>> getRangGleiche() {
    SortedMap<N, List<V>> result = createSortedMap();
    for (Entry<N, List<V>> entry : map.entrySet()) {
      List<V> list = entry.getValue();
      if (list.size() > 1) {
        result.put(entry.getKey(), new ArrayList<>(list));
      }
    }
    return result;
  }

  public long getAnzahlZuLosen() {
    return Math.min(getUnentschiedene().size(), anzahlSitze - getVordere().size());
  }
}
