import csv

try: # Barres de progression si les modules sont installés
  from tqdm import tqdm
  from scipy.special import binom
  progression = True
except:
  progression = False

def devs_par_lecon(recasages: dict, lecons: list) -> dict:
  """ Renvoie, pour chaque leçon, la liste des développements qui s'y recasent.
    Autrement dit, inverse le dictionnaire 'recasages'.
    Entrées:
        - recasages (dict): format {"nom développement": {liste recasages}}
        - lecons (list): liste de numéros de leçons
    Sortie:
        - (dict): format {numéro de leçon: ["nom développement"]}
  """
  # Initialisation de la valriable retour
  parLecon = {}
  for lecon in lecons:
      parLecon[lecon] = []
  # Vérification des recasages
  for dev, listeLecons in recasages.items():
      for lecon in listeLecons:
          if lecon in lecons and dev not in parLecon[lecon]:
              parLecon[lecon].append(dev)
  return parLecon

def nb_devs_par_lecon(recasages: dict, lecons: list) -> dict:
  """ Renvoie le nombre de lecons recasées par lecons. """
  devsParLecon = devs_par_lecon(recasages, lecons)
  nbParLecon = {lecon: len(devs) for lecon, devs in devsParLecon.items()}
  return nbParLecon

def dict_moins_1_cle(D):
  cles = list(D.keys())
  return {k: D[k] for k in cles[1:]}, cles[0]

def extraire_sous_dict_taille_donnee(D, n):
  """ Générateur des sous-dictionnaires de taille n de D """
  if n == 0:
      yield {}
  elif len(D) > n:
      Dm1c, c = dict_moins_1_cle(D)
      for subcombi in extraire_sous_dict_taille_donnee(Dm1c, n-1):
          subcombi[c] = D[c]
          yield subcombi
      for combi in extraire_sous_dict_taille_donnee(Dm1c, n):
          yield combi
  else:
      yield D

# Liste des leçons: si vous voulez tester sur une partie des leçons uniquement, remplacez les listes
# suivantes par les listes des leçons correspondantes
algebre = [101, 102, 103, 104, 105, 106, 108, 120, 121, 122, 123, 125, 126, 141, 142, 144, 148, 149, 151, 152, 153, 154, 155, 
        156, 157, 158, 159, 160, 161, 162, 170, 171, 181, 190, 191] 
analyse = [201, 203, 204, 205, 206, 208, 209, 213, 214, 215, 219, 220, 221, 223, 224, 226, 228, 229, 230, 234, 235, 236, 239, 
        241, 243, 245, 246, 250, 253, 261, 262, 264, 265, 266, 267]

recasagesImportes = {}
with open('recasages.csv', mode='r', encoding='utf-8') as flux:
  entree = csv.reader(flux)
  for ligne in entree:
      rec = list(filter(('').__ne__, ligne))
      recasagesImportes[rec[0]] = list(map(int, rec[1:]))
      
print(f"Importation réussie ({len(recasagesImportes)} développements) !\n")
# NOTE: C'est long. Vraiment. Il risque d'y avoir des saturations de mémoire: il est donc conseillé de
# faire tourner l'algorithme plusieurs fois, pour plusieurs plages de valeurs.
print(" REKASATOR (tm) ".center(60, '#'))
print("\nSélectionnez le type de test:")
aTester = input("Tout tester: 0\nAlgèbre uniquement: 1\nAnalyse uniquement: 2\n>>> ")

if aTester != "0":
  recasages = {}
  for dev, recs in recasagesImportes.items():
      recs.sort()
      i = len(recs) - 1
      while i >= 0 and int(recs[i]) >= 200:
          i -= 1
      if aTester == "1" and i == len(recs) - 1:
          recasages[dev] = recs
      elif aTester == "2" and i == -1:
          recasages[dev] = recs
      elif i >= 0 and i < len(recs) - 1:
          recasages[dev] = recs
  if aTester == "1":
      lecons = algebre
  elif aTester == "2":
      lecons = analyse
  else:
      lecons = algebre + analyse
else:
  recasages = recasagesImportes
  lecons = algebre + analyse
  
  
N = len(recasages)
print(f"({N} recasages correspondants.)\n")

print("/!\\ NOTE: aucun document ne sera produit si le programme ne termine pas correctement. " +
  "En raison du volume de données, privilégiez des plages de test de faible amplitude, " +
  "quitte à faire tourner le programme plusieurs fois.")
inf = int(input("Nb min de développements à tester >>> "))
sup = int(input("Nb max de développements à tester >>> "))

optimisationsPossibles = []
stop = False
n = min(sup, N)
while not stop and n >= inf: # On explore toutes les possibilités
  print(f">>> Test: {n} développements...")
  extraction = extraire_sous_dict_taille_donnee(recasages, n)
  stop = True
  if progression:
      t = tqdm(total=binom(N,n))
      prog = 0
  for extrait in extraction:
      nbDevsParLecon = list(nb_devs_par_lecon(extrait, lecons).values())
      i = 0
      ok = True
      while i < len(nbDevsParLecon) and ok: # On teste si toutes les leçons ont au moins 2 développements
          ok = nbDevsParLecon[i] >= 2
          i += 1
      if ok: # Si c'est bien le cas, on enregistre le résultat
          optimisationsPossibles.append(list(set(recasages.keys()) - set(extrait.keys())))
          stop = False
      if progression:
          prog += 1
          t.update(1)
  if progression: t.close()
  n -= 1
optimisationsPossibles.reverse()        

with open('optimisations.csv', mode='w', encoding="utf-8", newline='') as flux:
  sortie = csv.writer(flux, delimiter=',')
  for opti in optimisationsPossibles:
      sortie.writerow(opti)
      
print("Fin de la recherche !", end=" ")
if stop:
  print(f"(Pas de possibilités à moins de {n+1} développements)")
else:
  print("(Il est conseillé de relancer le programme avec un borne inférieure plus petite.)")
