# -*- coding: utf-8 -*-

# Copyright 2009 Manik Bhattacharjee (manik-listes at altern.org)
#
# This file is part of PyVote.
#
# PyVote is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyVote is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyVote.  If not, see <http://www.gnu.org/licenses/>.

from operator import itemgetter

#####################################
#
# Calcul du résultat d'un scrutin
# de type Vote Unique Non transférable
#
#####################################
def calculVoteUnique(votes, scrutin):
  nbVotes = len(votes)
  resultats = {}
  # Initialisation du score de chaque candidat à 0
  for c in scrutin["Candidats"]:
     resultats[c] = 0
  resultats[u"Vote Blanc"] = 0
  # Comptage des votes par candidat
  for k, v in votes.iteritems():
    resultats[v["choix"]] = resultats[v["choix"]] + 1

  # Qui est élu ?
  aPourvoir = int(scrutin["Parametres Scrutin"]["Sieges a pourvoir"])
  # Classer les candidats en nombre de votes décroissant
  paires = sorted(resultats.iteritems(), key=itemgetter(1), reverse=True)
  # Prendre les aPourvoir premiers candidats
  elus = paires [0:aPourvoir] # Les paires [(candidat1, note1),...]
  elus = [el[0] for el in elus] # Les noms des candidats
  # Détail des votes
  details =u""
  for candidat, score in resultats.iteritems():
    pourcentage = float(score)*100.0 / float(nbVotes)
    details = details + u"<font color=green>" + unicode(candidat)\
              + u"</font> : <font color=blue>" + unicode("%.2f" % pourcentage)\
              + u"%</font> - <font color=orange>" + unicode(score) +" votes</font><br>\n"
  return {"Elus" : elus,\
          "Details" : details\
         }



#####################################
#
# Pour le Vote Unique Transférable
# -> Compte les voix de chaque candidat
#     à l'itération courante
# -> Renvoie le nombre de voix pour tous
#     les candidats restants
#
#####################################
def comptePremiersVotes(votesPonderes, candidats):
  # On initialise les résultats de chaque candidat à 0 voix
  result = {}
  for c in candidats:
    result[c] = 0
  # On parcourt les votes (k est leur identifiant, v le vote proprement dit)  
  for k,v in votesPonderes.iteritems():
    if v[0] == []:#Tous les candidats choisis par l'électeur ont été éliminés -> On ajoute à chaque candidat p/nbCandidats votes
      aDistribuer = v[1]/len(candidats) # Correspond aux votes restants de cet électeur répartis également entre les candidats restants
      for c in candidats:
         result[c] += aDistribuer
    else:# Le premier candidat de la liste de cet électeur reçoit ses p voix
      print u"DEBUG Candidat 1 : "+repr(v[0][0])
      result[v[0][0]] += v[1] # On ajoute le poids à ce candidat

  return result



#####################################
#
# Pour le Vote Unique Transférable
# -> enlever les candidats élus des
#    bulletins de votes en transférant
#    les votes en excédent par rapport
#    au seuil
#
#####################################
def vireGagnants(votesPonderes, gagnants, seuil):

  for k,v in votesPonderes.iteritems(): # Sur tous les votes
    for gagnant, score in gagnants.iteritems(): # Pour chaque nouvel élu
      if gagnant in v[0]: # Si l'élu est présent dans ce bulletin de vote
        if gagnant == v[0][0]: # S'il est en première place, on change le poids
          votesPonderes[k][1] *= float((score - seuil))/float(score) # Le poids de ce vote s'allège de la quantité utilisée pour élire l'élu
          votesPonderes[k][0].remove(gagnant)  # On vire l'élu de la liste
        else: # S'il n'est pas en première place, on le vire juste de la liste
          votesPonderes[k][0].remove(gagnant)
  return votesPonderes



#####################################
#
# Pour le Vote Unique Transférable
# -> enlever le candidat ayant le
#    score le plus bas quand aucun
#    candidat ne passe le seuil
#    On redistribue ses voix en 
#    intégralité (il n'est pas élu,
#    donc les voix de ses électeurs 
#    n'ont pas servi)
#
#####################################
def virePerdant(votesPonderes, perdant):
  print "DEBUG vire Perdant"
  for k,v in votesPonderes.iteritems(): # Sur tous les votes
    if perdant in v[0]: # Si l'éliminé est présent dans ce bulletin de vote 
      votesPonderes[k][0].remove(perdant) # On le vire
  return votesPonderes



#####################################
#
# Calcul du résultat d'un scrutin
# de type Vote Unique Transférable
#
#####################################
def calculVoteUniqueTransferable(votes, scrutin):

  ## Les résultats à renvoyer : élus et description détaillée des calculs
  detail = u""
  elus=[]

  # On met les votes sous la forme {1:[[C1,C2,C3],1.0], 2:[[C3,C4,C1],1.0], ....}
  # liste ordonnée des candidats d'un vote avec un poids de 1.0 pour chaque vote
  votesPonderes = {}
  candidatsRestants = scrutin["Candidats"] # On stocke les candidats ni élus ni éliminé

  # Chaque vote est un dictionnaire {candidat1:classement1, ...} -> conversion vers la forme votes pondérés
  num = 0 # Numero du vote (obligatoire car les dictionnaires python n'acceptent pas une liste de candidats comme clef)
  for idVote,vote in votes.iteritems():
    num += 1
    paires = sorted(vote.iteritems(), key=itemgetter(1), reverse=False)
    # Les paires [(candidat, classement),...] sont ordonnées par classement
    # Un vote devient [C1,C3,C2]
    v = [p[0] for p in paires]
    # Chaque vote est composé d'une liste ordonnée de candidats (v) et d'un poids (1 au départ)
    votesPonderes[num] = [v, float(1.0)]

  # On commence par calculer le nombre de votes seuil pour être élu(Droop Quota)
  seuil = (len(votesPonderes) / (scrutin["Parametres Scrutin"]["Sieges a pourvoir"] +1))  + 1
  detail +=  u"------------------------------------------<br>\n"\
           + u"Seuil de votes nécessaire pour être élu : nombre de votes ("+unicode(len(votesPonderes))\
           + u") divisé par le nombre de sièges ("+unicode(scrutin["Parametres Scrutin"]["Sieges a pourvoir"])\
           + u") +1, auquel on ajoute 1<br><font color=blue>Seuil = " + unicode(seuil)\
           + u" votes pour obtenir un siège</font><br>" + u"------------------------------------------<br>\n"

  # On entre dans une boucle pour remplir tous les sièges disponibles
  candidatsElus=0


  while candidatsElus < scrutin["Parametres Scrutin"]["Sieges a pourvoir"]:
    # Quel est le score des candidats à cette itération ?
    score = comptePremiersVotes(votesPonderes, candidatsRestants)
    gagnants = {}
    for cand, sc in score.iteritems():#On parcourt le score des candidats
      if sc >= seuil:#Ce candidat dépasse le seuil -> il est élu
        gagnants[cand] = sc
        candidatsElus += 1
        detail += u"Le candidat <font color=green>"+unicode(cand)+"</font> passe le seuil avec "+unicode(sc)+" voix<br>\n"
        candidatsRestants.remove(cand) # On vire le candidat de la liste des candidats encore non déterminés
      else:
        detail += u"Le candidat <font color=orange>"+unicode(cand)+"</font> ne passe pas avec "+unicode(sc)+" voix<br>\n"

    # On regarde s'il y a des élus on les ajoute dans la liste finale en fonction de leur score
    if len(gagnants) > 0:
      paires = sorted(gagnants.iteritems(), key=itemgetter(1), reverse=True) # On les trie par ordre décroissant
      elus.extend([p[0] for p in paires])# On les ajoute dans la liste des élus définitifs
      # On vire les gagnants des listes en redistribuant leurs votes en trop (ceux qui dépassent le seuil)
      votesPonderes = vireGagnants(votesPonderes, gagnants, seuil)
    else: # Aucun gagnant
      # On vire le plus mal classé des candidats et on redistribue les votes
      scoreMin = min([sc for c,sc in score.iteritems()])
      aVirer = [c for c, sc in score.iteritems() if sc == scoreMin]
      if len(aVirer) > 1: # Il y a plusieurs candidats avec le score éliminatoire. Or il ne faut en virer qu'un !
        detail += u"<font color=red>Attention : plusieurs candidats sont éliminables avec un score égal :<br>\n"
        for av in aVirer:
          detail += unicode(av) + "<br>\n"
        detail += u"----> " + unicode(aVirer[0]) + u" éliminé !</font><br>\n" #TODO Arbitrairement on prend celui-là -> plutôt faire un tirage au sort ?
      elif len(aVirer) < 1: # Ne devrait jamais arriver
        detail += u"<font color=red>BUG : pas de candidat à éliminer alors qu'il n'y a pas de gagnant !</font><br>\n"
        return {"Elus" : elus, "Details":detail}
      else:
        detail += u"Pas d'élu ! Le candidat <font color=red>" + unicode(aVirer[0]) + u"</font> est éliminé (score le plus faible)<br>\n"

      # On élimine le candidat des bulletins (redistribution de ses voix)
      votesPonderes = virePerdant(votesPonderes, aVirer[0])
      candidatsRestants.remove(aVirer[0])

    detail += u"<br>"


  return {"Elus" : elus, "Details":u"<font size=10>"+detail+"</font>\n"}



#####################################
#
# Calcul du résultat d'un scrutin
# de type Vote par Assentiment
#
#####################################
def calculVoteAssentiment(votes, scrutin):

  methode = scrutin["Parametres Scrutin"]["Methode de calcul"]
  nbVotes = len(votes)
  resultats={}
  noteFinale = {}

  if methode == u"Note Moyenne":
    # Initialisation du score de chaque candidat à 0
    for c in scrutin["Candidats"]:
       noteFinale[c] = float(0.0)

    # Calcul de la moyenne des notes de chaque candidat
    for k, v in votes.iteritems():
      for candidat, note in v.iteritems():
        noteFinale[candidat] = noteFinale[candidat] + float(note)/float(nbVotes)

  elif methode == u"Note Médiane":

    # Initialisation de chaque candidat à 0 voix pour chaque note
    for c in scrutin["Candidats"]:
       resultats[c] = {}
       for note in xrange(scrutin["Parametres Scrutin"]["Note minimale"], scrutin["Parametres Scrutin"]["Note maximale"] + 1):
        resultats[c][note] = 0

    # Calcul du nombre de chaque note pour chaque candidat
    for k,v in votes.iteritems():
      for candidat, note in v.iteritems():
        if not(resultats[candidat].has_key(note)):
          print u"BUG : un électeur a rentré une note invalide pour le candidat "+repr(candidat)
        else:
          resultats[candidat][note] += 1

    # Trouver la médiane puis pondérer entre les notes M-0,5 et M+0,5 
    #   en fonction de la position de la médiane dans la note 
    #   (Interpolation linéaire) 
    positionMediane = float(nbVotes)/2.0
    # Itération sur les candidats
    for candidat, notes in resultats.iteritems():
      cumulVotes = 0
      # On ordonne par note
      clefs = notes.keys()
      clefs.sort()
      #On compte le cumul de voix jusqu'à la médiane
      for k in clefs:
        if cumulVotes + notes[k] >= positionMediane: #On se trouve dans la note médiane (k)
          p = float(positionMediane - cumulVotes)/float(notes[k]) # interpolation linéaire
          # ATTENTION : valide uniquement pour des notes entières espacées de 1 en 1
          mini = float(k)-0.5
          maxi = float(k)+0.5
          #print "DEBUG Mini : "+ repr(mini)+ ", maxi : "+repr(maxi)+" et clef "+repr(k)
          noteFinale[candidat] = p*maxi + (1.0-p)*mini # Pondération
          break # On sort de cette boucle pour passer au candidat suivant
        cumulVotes = cumulVotes + notes[k]

  else: # Méthode inconnue
    return {}

  # Qui est élu ?
  aPourvoir = int(scrutin["Parametres Scrutin"]["Sieges a pourvoir"])
  # Classer les candidats en notes décroissantes
  paires = sorted(noteFinale.iteritems(), key=itemgetter(1), reverse=True)
  # Prendre les aPourvoir premiers candidats
  elus = paires [0:aPourvoir] # Les paires [(candidat1, note1),...]
  elus = [el[0] for el in elus] # Les noms des candidats

  # Détail des votes
  details =u""
  for candidat, score in noteFinale.iteritems():
    details = details + u"<font color=green>" + unicode(candidat)\
              + u"</font> - note <font color=blue>" + unicode(score)\
              + u"</font><br>\n"
    if methode == u"Note Médiane":
      details = details + "<font color=orange>"
      for k in clefs:
        details = details + u"---> Note " + unicode(k) + ": "+ unicode(resultats[candidat][k])+" votes<br>\n"
      details = details + "</font>\n"

  return {"Elus" : elus,\
          "Details" : details\
         }


#####################################
#
# Cette fonction renvoie un dictionnaire
#  qui à chaque type de scrutin associe
#  la fonction calculant son résultat.
#
#####################################
def getAllMethods():
  return {"Vote unique" : calculVoteUnique,\
          "Vote unique transferable" : calculVoteUniqueTransferable,\
          "Vote par assentiment" : calculVoteAssentiment\
         }