#!/usr/bin/env python2

# =====================================================
# Auswertungs-Programm fuer verschiedene Wahl-Verfahren
# =====================================================

from __future__ import print_function
import sys
import os

# ##################
# Einlesen der Daten
# ##################
def ignoriere_kommentare(zeile):
    # Whitespace um Zeile entfernen:
    zeile = zeile.strip()
    # Leere Zeilen und Kommentare ignorieren:
    if zeile == '' or zeile.startswith('#'):
        return None
    # Sonst Zeile zurueckgeben:
    return zeile

def konfiguration_einlesen(dateiname):
    pos = 0
    neg = 0
    kandidaten = []
    namen = {}
    with open(dateiname, 'r') as datei:
        # Leere Zeilen und mit '#' beginnende Kommentarzeilen
        # ignorieren:
        zeile = None
        while not zeile:
            zeile = ignoriere_kommentare(datei.readline())
        # Erste Zeile, Anzahl der Positiv-Stufen:
        label, nummer = zeile.split(':')
        if label.strip() != 'Pos':
            print("Fehler: "
                    "Erste Zeile der Konfigurationsdatei muss Anzahl")
            print("        "
                    "der Positiv-Stufen nach Label 'Pos:' enthalten!")
            sys.exit(1)
        pos = int(nummer)
        # Leere Zeilen und mit '#' beginnende Kommentarzeilen
        # ignorieren:
        zeile = None
        while not zeile:
            zeile = ignoriere_kommentare(datei.readline())
        # Zweite Zeile, Anzahl der Negativ-Stufen:
        label, nummer = zeile.split(':')
        if label.strip() != 'Neg':
            print("Fehler: "
                    "Zweite Zeile der Konfigurationsdatei muss Anzahl")
            print("        "
                    "der Negativ-Stufen nach Label 'Neg:' enthalten!")
            sys.exit(1)
        neg = int(nummer)
        # Weitere Zeilen, Kandidaten:
        for zeile in datei:
            # Leere Zeilen und mit '#' beginnende Kommentarzeilen
            # ignorieren:
            zeile = ignoriere_kommentare(zeile)
            if not zeile:
                continue
            # Label (in Stimmzettel-Datei verwendet) und zugehoerigen
            # Namen trennen und in Ergebnis speichern:
            label, name = zeile.split(':')
            label = label.strip()
            name = name.strip()
            kandidaten.append(label)
            namen[label] = name
    return pos, neg, kandidaten, namen

def datei_einlesen(dateiname, pos, neg, kandidaten):
    # Liste aller Stimmzettel:
    stimmzettel_liste = []
    # Oeffnen der Datei:
    with open(dateiname, 'r') as datei:
        # Einlesen aller weiteren Zeilen:
        for zeile in datei:
            # Leere Zeilen und mit '#' beginnende Kommentarzeilen
            # ignorieren:
            zeile = ignoriere_kommentare(zeile)
            if not zeile:
                continue
            # Zeile in Stimmzettel umwandeln:
            stimmzettel = zeile_umwandeln(zeile, pos, neg, kandidaten)
            # Stimmzettel an Liste anhaengen:
            stimmzettel_liste.append(stimmzettel)
    print("{} Stimmzettel eingelesen.".format(len(stimmzettel_liste)))
    return stimmzettel_liste

def zeile_umwandeln(zeile, pos, neg, kandidaten):
    # Dictionary Kandidat -> Stufe fuer diesen Stimmzettel:
    stimmzettel = {}
    # Zeile in Positiv, Enthaltung und Negativ aufteilen:
    pos_string, ent_string, neg_string = zeile.split('/')
    # Positiv, Enthaltung und Negativ in Stufen aufteilen:
    pos_stufen = pos_string.split(';')
    ent_stufen = ent_string.split(';')
    neg_stufen = neg_string.split(';')
    neg_stufen.pop()
    # Anzahl der Stufen auf Konsistenz pruefen:
    if len(pos_stufen) != pos:
        print("Fehler: Falsche Anzahl Positiv-Stufen!")
        print(zeile)
        sys.exit(1)
    if len(ent_stufen) != 1:
        print("Fehler: Mehr als eine Enthaltungs-Stufe!")
        print(zeile)
        sys.exit(1)
    if len(neg_stufen) != neg:
        print("Fehler: Falsche Anzahl Negativ-Stufen!")
        print(zeile)
        sys.exit(1)
    # Stufen in einer Liste zusammenfuehren:
    alle_stufen = []
    alle_stufen.extend(pos_stufen)
    alle_stufen.extend(ent_stufen)
    alle_stufen.extend(neg_stufen)
    # Stufen in Kandidaten aufteilen:
    stufen_nummer = pos
    for stufe in alle_stufen:
        stufen_kandidaten = stufe.split(',')
        for kandidat in stufen_kandidaten:
            kandidat = kandidat.strip()
            if kandidat == '':
                continue
            # Kandidat auf Existenz in Konfiguration pruefen:
            if not kandidat in kandidaten:
                print("Fehler: "
                        "Kandidat '{}' nicht in Konfiguration!".format(
                            kandidat))
                print(zeile)
                sys.exit(1)
            # Kandidat in Stimm-Dictionary eintragen:
            if kandidat in stimmzettel:
                print("Fehler: "
                        "Kandidat '{}' mehrfach aufgefuehrt!".format(
                            kandidat))
                print(zeile)
                sys.exit(1)
            stimmzettel[kandidat] = stufen_nummer
        stufen_nummer -= 1
    # Alle Kandidaten auf Existenz auf Stimmzettel pruefen:
    for kandidat in kandidaten:
        if not kandidat in stimmzettel:
            print("Fehler: "
                    "Kandidat '{}' nicht aufgefuehrt!".format(
                        kandidat))
            print(zeile)
            sys.exit(1)
    return stimmzettel

# ############
# Range Voting
# ############
def range_voting(kandidaten, namen, stimmzettel_liste):
    summe, ja, nein = rng_stimmzettel_auswerten(kandidaten,
            stimmzettel_liste)
    sortiert = rng_sortieren(kandidaten, summe, ja, nein)
    liste, abgelehnt = rng_aufteilen(sortiert, ja, nein)
    rng_ergebnis_ausgeben(namen, summe, ja, nein, liste, abgelehnt)

def rng_stimmzettel_auswerten(kandidaten, stimmzettel_liste):
    # Dictionaries fuer Punkte, Ja- und Nein-Stimmen initialisieren:
    summe = {}
    ja = {}
    nein = {}
    for kandidat in kandidaten:
        summe[kandidat] = 0
        ja[kandidat] = 0
        nein[kandidat] = 0
    # Stimmzettel auswerten:
    for stimmzettel in stimmzettel_liste:
        for kandidat in kandidaten:
            punkte = stimmzettel[kandidat]
            summe[kandidat] += punkte
            if punkte > 0:
                ja[kandidat] += 1
            if punkte < 0:
                nein[kandidat] += 1
    return summe, ja, nein

def rng_sortieren(kandidaten, summe, ja, nein):
    # Nach Punktsumme sortieren:
    kandidaten_nach_punkten = {}
    for kandidat in kandidaten:
        punkte = summe[kandidat]
        if not punkte in kandidaten_nach_punkten:
            kandidaten_nach_punkten[punkte] = []
        kandidaten_nach_punkten[punkte].append(kandidat)
    sortiert = []
    for punkte in sorted(kandidaten_nach_punkten, reverse=True):
        if len(kandidaten_nach_punkten[punkte]) == 1:
            sortiert.extend(kandidaten_nach_punkten[punkte])
        else:
            # Nach Ja-Nein-Differenz sortieren:
            kandidaten_nach_differenz = {}
            for kandidat in kandidaten_nach_punkten[punkte]:
                diff = ja[kandidat] - nein[kandidat]
                if not diff in kandidaten_nach_differenz:
                    kandidaten_nach_differenz[diff] = []
                kandidaten_nach_differenz[diff].append(kandidat)
            for diff in sorted(kandidaten_nach_differenz,
                    reverse=True):
                if len(kandidaten_nach_differenz[diff]) == 1:
                    sortiert.extend(kandidaten_nach_differenz[diff])
                else:
                    # Nach Ja sortieren:
                    kandidaten_nach_ja = {}
                    for kandidat in kandidaten_nach_differenz[diff]:
                        j = ja[kandidat]
                        if not j in kandidaten_nach_ja:
                            kandidaten_nach_ja[j] = []
                        kandidaten_nach_ja[j].append(kandidat)
                    for j in sorted(kandidaten_nach_ja, reverse=True):
                        sortiert.extend(kandidaten_nach_ja[j])
    return sortiert

def rng_aufteilen(sortiert, ja, nein):
    # Aufteilen in Liste und Abgelehnte:
    liste = []
    abgelehnt = []
    for kandidat in sortiert:
        if ja[kandidat] - nein[kandidat] > 0:
            liste.append(kandidat)
        else:
            abgelehnt.append(kandidat)
    return liste, abgelehnt

def rng_ergebnis_ausgeben(namen, summe, ja, nein, liste, abgelehnt):
    # Ergebnis ausgeben:
    print("")
    print("Liste:")
    print("======")
    print("Plz: Schnitt: Summe: Diff:   Ja: Enth: Nein: Kandidat*in:")
    platz = 0
    letzte_summe = 0
    letzte_diff = 0
    for kandidat in liste:
        platz += 1
        if (summe[kandidat] == letzte_summe and
                ja[kandidat] - nein[kandidat] == letzte_diff):
            print("  -- {:8.4f} {:6} {:5} {:5} {:5} {:5} {:>3} ({})".
                    format(float(summe[kandidat]) /
                        float(len(stimmzettel_liste)), summe[kandidat],
                        ja[kandidat] - nein[kandidat], ja[kandidat],
                        len(stimmzettel_liste) - ja[kandidat] -
                        nein[kandidat], nein[kandidat],
                        kandidat, namen[kandidat]))
        else:
            print("{:3}. {:8.4f} {:6} {:5} {:5} {:5} {:5} {:>3} ({})".
                    format(platz, float(summe[kandidat]) /
                        float(len(stimmzettel_liste)), summe[kandidat],
                        ja[kandidat] - nein[kandidat], ja[kandidat],
                        len(stimmzettel_liste) - ja[kandidat] -
                        nein[kandidat], nein[kandidat],
                        kandidat, namen[kandidat]))
        letzte_summe = summe[kandidat]
        letzte_diff = ja[kandidat] - nein[kandidat]
    print("")
    print("Abgelehnt:")
    print("==========")
    print("Plz: Schnitt: Summe: Diff:   Ja: Enth: Nein: Kandidat*in:")
    for kandidat in abgelehnt:
        platz += 1
        if (summe[kandidat] == letzte_summe and
                ja[kandidat] - nein[kandidat] == letzte_diff):
            print("  -- {:8.4f} {:6} {:5} {:5} {:5} {:5} {:>3} ({})".
                    format(float(summe[kandidat]) /
                        float(len(stimmzettel_liste)), summe[kandidat],
                        ja[kandidat] - nein[kandidat], ja[kandidat],
                        len(stimmzettel_liste) - ja[kandidat] -
                        nein[kandidat], nein[kandidat],
                        kandidat, namen[kandidat]))
        else:
            print("{:3}. {:8.4f} {:6} {:5} {:5} {:5} {:5} {:>3} ({})".
                    format(platz, float(summe[kandidat]) /
                        float(len(stimmzettel_liste)), summe[kandidat],
                        ja[kandidat] - nein[kandidat], ja[kandidat],
                        len(stimmzettel_liste) - ja[kandidat] -
                        nein[kandidat], nein[kandidat],
                        kandidat, namen[kandidat]))
        letzte_summe = summe[kandidat]
        letzte_diff = ja[kandidat] - nein[kandidat]

# #################
# Majority Judgment
# #################
def majority_judgment(pos, neg, kandidaten, namen, stimmzettel_liste):
    noten = maj_stimmzettel_auswerten(pos, neg, kandidaten,
            stimmzettel_liste)
    minmaj = maj_minmaj_ermitteln(pos, neg, kandidaten, noten)
    liste = maj_liste_erstellen(kandidaten, minmaj)
    maj_ergebnis_ausgeben(namen, minmaj, liste)

def maj_stimmzettel_auswerten(pos, neg, kandidaten, stimmzettel_liste):
    # Dictionary fuer Noten initialisieren:
    noten = {}
    for kandidat in kandidaten:
        noten[kandidat] = {}
        for note in range(-neg,pos+1):
            noten[kandidat][note] = 0
    # Stimmzettel auswerten:
    for stimmzettel in stimmzettel_liste:
        for kandidat in kandidaten:
            noten[kandidat][stimmzettel[kandidat]] += 1
    return noten

def maj_minmaj_ermitteln(pos, neg, kandidaten, noten):
    # Median-Noten aller Stufen ermitteln und
    # in MinMaj-Tupel-Dict merken:
    minmaj = {}
    for kandidat in kandidaten:
        # Von aussen nach innen Paare maximaler gleichgrosser Mengen
        # gleicher Noten von oben (Major) und von unten (Minor) finden:
        minor = -neg
        major = pos
        minmaj[kandidat] = []
        while True:
            # Finde nicht-leere Minor- und Major-Noten-Mengen
            # (mit Major beginnend, damit Minor dominant bei leerer
            # Haupt-Median-Menge):
            while noten[kandidat][major] == 0 and minor < major:
                major -= 1
            while noten[kandidat][minor] == 0 and minor < major:
                minor += 1
            # Minor und Major gleich => Haupt-Median-Menge erreicht:
            if minor == major:
                break
            # Maximale Anzahl Minor- und Major-Noten
            # in MinMaj-Tupel merken:
            anzahl = min(noten[kandidat][minor],
                    noten[kandidat][major])
            minmaj[kandidat].append((minor, major, 2*anzahl))
            # Maximale Anzahl Minor- und Major-Noten entfernen:
            noten[kandidat][minor] -= anzahl
            noten[kandidat][major] -= anzahl
        # Haupt-Median (Minor=Major) als innerstes MinMaj-Tupel
        # anhaengen:
        minmaj[kandidat].append((minor, major, noten[kandidat][minor]))
        #                     = (minor, major, temp_noten[major])
        # MinMaj-Tupel-Liste umdrehen => von innen nach aussen:
        minmaj[kandidat].reverse()
    return minmaj

def maj_liste_erstellen(kandidaten, minmaj):
    # Kandidaten nach Hauptmedian einsortieren:
    kandidaten_nach_hauptmedian = {}
    for kandidat in kandidaten:
        (median, _, _) = minmaj[kandidat][0]
        # Kandidat nach Haupt-Median einsortieren:
        if not median in kandidaten_nach_hauptmedian:
            kandidaten_nach_hauptmedian[median] = []
        kandidaten_nach_hauptmedian[median].append(kandidat)
    # Listen von Kandidaten mit gleichem Hauptmedian sortiert in Liste:
    liste = []
    for median in sorted(kandidaten_nach_hauptmedian, reverse=True):
        liste.append(kandidaten_nach_hauptmedian[median])
    # Liste iterativ nach spaeteren MinMaj-Tupeln sortieren:
    akt_stufe = 0
    fertig = False
    while not fertig:
        fertig = True
        naechste_liste = []
        for bisher_gleich in liste:
            # Wenn bereits nur ein Kandidat, ist nichts zu tun:
            if len(bisher_gleich) == 1:
                naechste_liste.append(bisher_gleich)
                continue
            # Liste fuer mittlere Kandidaten
            # (keine weiteren MinMaj-Tupel):
            mitte = []
            # Dictionaries fuer untere und obere Kandidaten:
            unten = {}
            oben = {}
            # Ansonsten Kandidaten einzeln angucken:
            for kandidat in bisher_gleich:
                # Kandidaten ohne weitere MinMaj-Tupel an Mitte
                # anhaengen:
                if akt_stufe + 1 >= len(minmaj[kandidat]):
                    mitte.append(kandidat)
                    continue
                # Weitere MinMaj-Tupel vorhanden => noch nicht fertig:
                fertig = False
                # Aktuelles MinMaj-Tupel:
                (akt_minor, akt_major, distanz) = \
                        minmaj[kandidat][akt_stufe]
                # Naechstes MinMaj-Tupel:
                (nst_minor, nst_major, _) = \
                        minmaj[kandidat][akt_stufe + 1]
                # Bei kleinerem Minor unten, bei gleichem Minor und
                # groesserem Major oben einsortieren:
                if nst_minor < akt_minor:
                    # Einsortieren nach Distanz, naechstem Minor
                    # und Major:
                    if not distanz in unten:
                        unten[distanz] = {}
                    if not nst_minor in unten[distanz]:
                        unten[distanz][nst_minor] = {}
                    if not nst_major in unten[distanz][nst_minor]:
                        unten[distanz][nst_minor][nst_major] = []
                    unten[distanz][nst_minor][nst_major].append(
                            kandidat)
                elif nst_major > akt_major:
                    # Einsortieren nach Distanz und naechstem Major:
                    if not distanz in oben:
                        oben[distanz] = {}
                    if not nst_major in oben[distanz]:
                        oben[distanz][nst_major] = []
                    oben[distanz][nst_major].append(kandidat)
                else:
                    print("Fehler: Weder Minor noch Major geaendert!")
                    sys.exit(1)
            # Oben nach steigender Distanz und fallendem Major
            # anhaengen:
            for distanz in sorted(oben):
                for major in sorted(oben[distanz], reverse=True):
                    naechste_liste.append(oben[distanz][major])
            # Mitte anhaengen:
            if mitte:
                naechste_liste.append(mitte)
            # Unten nach fallender Distanz, steigendem Minor
            # und fallendem Major anhaengen:
            for distanz in sorted(unten, reverse=True):
                for minor in sorted(unten[distanz]):
                    for major in sorted(unten[distanz][minor],
                            reverse=True):
                        naechste_liste.append(
                                unten[distanz][minor][major])
        liste = naechste_liste
        akt_stufe += 1
    return liste

def maj_ergebnis_ausgeben(namen, minmaj, liste):
    # Ergebnis ausgeben:
    print("")
    print("Liste:")
    print("======")
    platz = 0
    abgelehnt = False
    for gleichrangig in liste:
        erster_im_rang = True
        for kandidat in gleichrangig:
            (median, _, distanz) = minmaj[kandidat][0]
            if not abgelehnt:
                # Pruefen, ob dies erster abgelehnter Kandidat:
                if median < 0:
                    # Wenn Median im Nein-Bereich, klar:
                    abgelehnt = True
                elif median == 0:
                    # Wenn Median Enthaltung ...
                    if len(minmaj[kandidat]) <= 1:
                        # ... und keine Nicht-Enthaltung:
                        abgelehnt = True
                    else:
                        # ... und naechstes MinMaj-Tupel
                        # Minor im Nein-Bereich:
                        (minor, _, _) = minmaj[kandidat][1]
                        if minor < 0:
                            abgelehnt = True
                # Wenn erster abgelehnter Kandidat,
                # Ueberschrift ausgeben:
                if abgelehnt:
                    print("")
                    print("Abgelehnt:")
                    print("==========")
            platz += 1
            if erster_im_rang:
                print("{:3}. {:>3} ({})".format(platz, kandidat,
                    namen[kandidat]))
                erster_im_rang = False
            else:
                print("  -- {:>3} ({})".format(kandidat,
                    namen[kandidat]))
            print("     {:+3} -{:-^4}-".format(median, distanz),
                    end="")
            i = 1
            while i < len(minmaj[kandidat]):
                (minor, major, distanz) = minmaj[kandidat][i]
                print("> {:>+3}/{:<+3}".format(minor, major),
                        end="")
                i += 1
                if i < len(minmaj[kandidat]):
                    if i % 4 == 1:
                        print("\n        ", end="")
                    else:
                        print(" ", end="")
                    print("-{:-^4}-".format(distanz), end="")
                else:
                    print(" -{:-^4}-".format(distanz), end="")
            print("|")

# #############
# Hauptprogramm
# #############
if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Fehler: Falsche Anzahl Argumente!")
        print("Benutzung: auswertung "
                "<Konfigurations-Datei> <Stimm-Datei> <Verfahren>")
        sys.exit(1)
    # Konfiguration aus erstem Parameter:
    dateiname = sys.argv[1]
    if not os.path.isfile(dateiname):
        print("Fehler: "
                "Konfigurations-Datei '{}' existiert nicht!".format(
                    dateiname))
        sys.exit(1)
    pos, neg, kandidaten, namen = konfiguration_einlesen(dateiname)
    # Stimmzettel aus zweitem Parameter:
    dateiname = sys.argv[2]
    if not os.path.isfile(dateiname):
        print("Fehler: Stimm-Datei '{}' existiert nicht!".format(
            dateiname))
        sys.exit(1)
    stimmzettel_liste = datei_einlesen(dateiname, pos, neg, kandidaten)
    # Wahlverfahren aus drittem Parameter:
    verfahren = sys.argv[3]
    if verfahren == 'range':
        range_voting(kandidaten, namen, stimmzettel_liste)
    elif verfahren == 'majority':
        majority_judgment(pos, neg, kandidaten, namen,
                stimmzettel_liste)
    else:
        print("Fehler: Verfahren '{}' ist unbekannt!".format(
            verfahren))
        print("Zur Verfuegung stehende Verfahren:"
            "range, majority")
        sys.exit(1)
