#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""This script is to help EPFL CS-112(g) students to create
   their project ZIP file to be delivered.
"""

import os.path
import string
import glob
import shutil
import zipfile
import magic
import codecs

__author__    = 'J.-C. Chappelier'
__version__   = '1.0.2026'
__copyright__ = '(c) EPFL 2026, except is_binary_file()'
__license__   = 'All rights reserved (except for is_binary_file()).'

## ======================================================================
## This year class

students = {
    'Ahouidi Ambroise Raymond' : 45,
    'Albasini Joachim' : 14,
    'Antonino Madrid Alex Christian' : 14,
    'Arnoult Mendoza Carla Angela' : 53,
    'Atlab Elias Wael' : 43,
    'Augé Clarisse Caiqing' : 10,
    'Beaucamp Mathilde Marie Sonia' : 10,
    'Bessadi Justin Elie Claude' : 29,
    'Bevk Ana' : 62,
    'Billat-rossi Paolo' : 70,
    'Blasco L\'arvor Thomas Benjamin' : 35,
    'Blondel Romain Arnaud Victor' : 46,
    'Boero Anna' : 57,
    'Bohbot Tancrède Luc François-Antoine' : 50,
    'Bomo-Leducq Romain Arnaud Lucien Robert' : 44,
    'Boniol Morgane' : 58,
    'Borras Eliot Camille Robert' : 44,
    'Bourgeois Belotz Darwin Benjamin' : 8,
    'Bovet Cynthia' : 27,
    'Bruchez Nicolas Marie-Joseph' : 34,
    'Bryson Leo Cat' : 61,
    'Byrne Alexis Rian' : 38,
    'Cantoni Lopez Maxence' : 23,
    'Carrera Hanus Alex' : 73,
    'Carrier Emile' : 13,
    'Cavini Matteo Charles Ezio' : 75,
    'Chalabi Wassim' : 83,
    'Cherrier Prune Nicole Marima' : 60,
    'Chizhov Alexandre' : 52,
    'Courtois Garance Sarah' : 57,
    'Darbellay Pierre-Alexis Enrico Valéry' : 59,
    'Debernardi Nils Matteo' : 9,
    'Debono Alexandre François Junior' : 5,
    'Declerck Jean Alfred Pierre Marie' : 28,
    'Delaloye Julien' : 34,
    'De Luca Emmanuel Pio' : 3,
    'Deniset Claire Jeanne Louise' : 68,
    'Di Meglio Matteo' : 38,
    'Diquattro Augustin Pierre Robert' : 76,
    'Divall Benjamin' : 37,
    'Doffey Trouiller Elena' : 17,
    'Doutriaux Max Emmanuel' : 40,
    'Ducommun Sarah' : 54,
    'Dufey Marc Philippe Thibaud' : 82,
    'Eloy Charlène Lana' : 28,
    'Fasel Alizée Sophie' : 20,
    'Felder Baptiste Nicolas Gilles' : 42,
    'Ferreira Noa' : 15,
    'Figueiredo Saraiva Nuno Duarte' : 47,
    'Flament Konstantin Aleksandr' : 64,
    'Fleuriau Kléa' : 21,
    'Friot Archibald Benjamin Mathieu' : 6,
    'Fuz Valentin Pierre Thomas' : 59,
    'Garcia Juliette Catherine' : 48,
    'Garcia Valentine Christiane' : 8,
    'Gasch Valentino' : 30,
    'Gashi Aguesa' : 27,
    'Gauch Maé Rebecca' : 17,
    'Gay Lauriane Alizée Emmanuelle' : 78,
    'Genoud Thomas' : 1,
    'Gereben Julia' : 49,
    'Gonin Youssef' : 67,
    'Gosselin Mathyas Gabriel' : 77,
    'Guichard Romain Vincenzo Gabriel' : 4,
    'Guillard Jean Émile Pierre' : 72,
    'Guzman Milo Nathan Pierre' : 75,
    'Hamdouna Akram' : 81,
    'Harf-Wilwers Théodore Eliot' : 61,
    'Helfand Elias Antasena' : 32,
    'Henriot Timothé Bertrand Christophe' : 21,
    'Hersch Jonas' : 31,
    'Houngbo-Fitremann Edenson Samuel Léo' : 56,
    'Huk Adam Elliot' : 63,
    'Hu Wentao' : 25,
    'Ismaili Albin' : 12,
    'Jehanno Marin' : 83,
    'Jung Zoé Brigitte Jeanette' : 39,
    'Kado Akira Ray Lorenzo' : 69,
    'Kessler Paul Antoine Ange' : 81,
    'Khlifi Taghzouti Badra' : 11,
    'Kuster Axelle Joséphine' : 78,
    'Laamarti Nassim' : 22,
    'Lahlou Elias' : 19,
    'Lamanna Adriano Mattia' : 69,
    'Lamboray Marguerite' : 36,
    'Laurrin Emillie Claire Marie' : 54,
    'Lemieszonek Maksymilian' : 74,
    'Levard Romain Eliott Emmanuel' : 50,
    'Li Sirui' : 55,
    'Lorenzini Arno Ambrosio' : 52,
    'Luisoni Alessandro Giovanni Gelindo' : 11,
    'Lummert Louise Magda Cécilia' : 41,
    'Maier Colin Elia' : 36,
    'Maître Julien' : 7,
    'Malleval Bastian' : 4,
    'Mandriota Federico' : 66,
    'Marande Pauline Francoise Marie' : 48,
    'Maurer Olivia' : 41,
    'Mc Laughlin Charlie Pierre' : 73,
    'Meoni Louis Carlos Yvan' : 15,
    'Michel Anastasia Marie Cassiopée' : 24,
    'Mira Mathis Noah' : 43,
    'Moisan Adrien Paul' : 71,
    'Monsigny Victor Yves Marie' : 6,
    'Mubalama Louise Nyota' : 29,
    'Neronova Elizaveta' : 51,
    'Ngayo Fotso Niels-Alexandre Christophe' : 5,
    'Nidegger Samuel Max' : 33,
    'Oioli Stella Carla' : 65,
    'Oldewurtel Jona Felix' : 30,
    'Papaldo Alessia' : 20,
    'Péré Sébastien André' : 26,
    'Pierrard Terence Philippe C.' : 2,
    'Pignat Adrien' : 18,
    'Pizzaia Ethan Luca' : 2,
    'Platanakis Nicolas' : 80,
    'Pokorska Natalia' : 49,
    'Polselli Lorenzo Vincent Sébastien' : 16,
    'Polster Aurélien Clément A' : 46,
    'Progin Emilien' : 1,
    'Räss Matthieu Paul Antoine Marie' : 25,
    'Raymond Thomas Yvan Jean' : 22,
    'Reid Alexander Phoenix' : 31,
    'Relander Emma Sofia' : 53,
    'Roche Maxime Jean' : 79,
    'Rohrbach Leo Faustino' : 40,
    'Roman Adrian' : 77,
    'Romat-Gosset Fantin' : 74,
    'Roset Pastor Amalia' : 45,
    'Roy Alexandre' : 33,
    'Ruffiner Lynn Valentine' : 62,
    'Salcedo Perez Diego' : 79,
    'Sanders Nicholas-George' : 47,
    'Sauser Luke Tristan' : 64,
    'Sauton Roméo Pierre David' : 42,
    'Savary Loïc' : 3,
    'Schmid Nicolas Mehdi Adam' : 66,
    'Scolari Sonja Alma' : 55,
    'Shepard Garance Rachael' : 68,
    'Simonazzi Benoît' : 84,
    'Solazzo Romain Benjamin Elia' : 35,
    'Sormani Mathieu Marc Michel' : 26,
    'Spagnulo Carolina Francesca Maria' : 65,
    'Staub Noé Ismaël' : 67,
    'Stausbøll James Sebastian' : 13,
    'Steiner Joseph Oscar' : 58,
    'Sternberg Yarden' : 23,
    'Strmecki Tin' : 37,
    'Surdez David' : 39,
    'Szyper Shai Simon O' : 76,
    'Talidec Joshua' : 63,
    'Tinel Stephan Renaud Pierre' : 32,
    'Trivedi Suvarn Kishore' : 82,
    'Trommsdorff Florian Mathis' : 70,
    'Unterkircher Fabienne' : 12,
    'Valent Eliza Charlotte Martine Lucie' : 60,
    'Vasquez Merlos Daniel Enrique' : 72,
    'Vermot-Petit-Outhenin Hansen' : 51,
    'Viale Guillaume Valentin Roch' : 18,
    'Vidal Serrate Alejandro' : 19,
    'Vilela Guillaume Felix Gérard' : 24,
    'Viricel Clément Orion' : 80,
    'Willa Matteo Baptiste Jonas' : 9,
    'Wohnlich Arthur' : 56,
    'Xu Thomas Lin' : 7,
    'Yurchenko Maksymilian' : 16,
    'Zniber Yasine' : 71,
}

coaches = (
    'nocoach',
    'tom',
    'lowen',
    'maud',
    'cedric',
    'amir',
    'valentin',
    'johan',
    'sébastien',
    'thomas',
    'ayrton-maya',
    'lowen',
    'maud',
    'amir',
    'valentin',
    'johan',
    'sébastien',
    'thomas',
    'ayrton-maya',
    'lowen',
    'tom',
    'maud',
    'amir',
    'valentin',
    'johan',
    'sébastien',
    'thomas',
    'cedric',
    'ayrton-maya',
    'lowen',
    'maud',
    'amir',
    'tom',
    'cedric',
    'valentin',
    'johan',
    'tom',
    'sébastien',
    'thomas',
    'ayrton-maya',
    'cedric',
    'lowen',
    'maud',
    'tom',
    'amir',
    'valentin',
    'johan',
    'sébastien',
    'thomas',
    'ayrton-maya',
    'lowen',
    'cedric',
    'maud',
    'amir',
    'valentin',
    'johan',
    'sébastien',
    'thomas',
    'tom',
    'ayrton-maya',
    'lowen',
    'maud',
    'amir',
    'cedric',
    'valentin',
    'johan',
    'tom',
    'cedric',
    'sébastien',
    'thomas',
    'ayrton-maya',
    'lowen',
    'maud',
    'tom',
    'cedric',
    'amir',
    'valentin',
    'johan',
    'sébastien',
    'thomas',
    'ayrton-maya',
    'lowen',
    'maud',
    'amir',
    'valentin',
)

garbage = {
    '*.o' : 'des fichiers objets',
    '*~'  : 'des fichiers de backup',
    '#*#' : 'des fichiers de backup',
    '*.h.gch' : 'des headers compilés',
    'core' : 'des « core dump »',

    'html'  : 'de la doc en HTML (ne rendez que le Doxyfile et indiquez comment générer la doc dans votre fichier README)',
    'latex' : 'de la doc en LaTeX (ne rendez que le Doxyfile et indiquez comment générer la doc dans votre fichier README, de préférence en HTML plutôt qu\'en LaTeX)',

    # default message
    'cmake-build-debug' : '',
    '.clang-*'          : '',
    '*.dSYM'            : '',
    '.fuse_*'           : '',
    '.*.swp'            : '',
    '*.md5'             : '',
    '.DS_Store'         : '',
    '.idea'             : '',
    '.vs'               : '',
    '.vscode'           : ''
}

## ======================================================================
def is_binary_file(name, chunk_size = 1024):
## There may be better solutions...
## This one is based on file(1) command
## see https://github.com/file/file/blob/f2a6e7cb7db9b5fd86100403df6b2f830c7f22ba/src/encoding.c#L151-L228
## and https://stackoverflow.com/a/7392391/14800316

    decoder = codecs.getincrementaldecoder('utf-8')()
    
    textchars = bytearray({7,8,9,10,12,13,27} | set(range(0x20, 0x100)) - {0x7f})
    is_binary_string = lambda bytes: bool(bytes.translate(None, textchars))

    with open(name, 'rb') as checked_file:
        chunk = checked_file.read(chunk_size)

    if b'\x00' in chunk:
        return True

    try:
        decoder.decode(chunk, final=False) # final=False allows to finish in the middle of an UTF-8 sequence
        return False
    except UnicodeDecodeError:
        return is_binary_string(chunck)

## ======================================================================
def is_exec_file(name):
    if not os.path.isfile(name):
        return False

    _, ext = os.path.splitext(name)
    return (ext == '.exe') \
        or ((ext == '') and is_binary_file(name))

## ======================================================================
def is_acceptable_file(name, acceptable_mimetypes = \
                        ('text/plain', 'application/pdf'), \
                       verbose=True):
    filetype = magic.from_file(name, mime=True)
    if filetype in acceptable_mimetypes:
        return True
    if verbose:
        print(f'Le fichier "{name}" n\'est pas dans un format accepté :')
        print(f'  il est au format "{filetype}" (nous n\'acceptons que des')
        print('    formats ouverts : texte, PDF)')
    return False

## ======================================================================
def contains(directory, pattern):
    return any(True for _ in \
                glob.iglob(os.path.join(os.path.join(directory, '**'), pattern),
                           recursive=True))

## ======================================================================
def check_files(directory, files_i, description):
    # I will go through all of them anyway and peekable() is not part of the standard,
    # so let's make it a list
    files_l = list(files_i)
    if files_l:
        print(f'Le répertoire "{directory}" contient {description} :')
        max=11
        nb=0
        for filename in files_l:
            nb += 1
            if nb < max: print(f'  - {filename}')
            elif nb == max: print(f'  [...] (more that {max-1} files)')
        if nb >= max: print(f'  - {filename}')
        print('Veuillez les supprimer !')
        return True
    return False

## ======================================================================
def contains_garbage(directory, pattern, description):
    return check_files(directory,
                       glob.iglob(os.path.join(os.path.join(directory, '**'), pattern),
                                               recursive=True),
                       description)

## ======================================================================
def has_some_source_file(directory):
    if contains(directory, '*.cc') or contains(directory, '*.cpp'):
        return True
    print(f'Le répertoire "{directory}" ne semble contenir aucun code C++ !')
    return False

## ======================================================================
def has_some_make_file(directory):
    if   contains(directory, 'makefile') \
      or contains(directory, 'Makefile') \
      or contains(directory, '*.pro')    \
      or contains(directory, 'CMakeLists.txt'):
        return True
    print(f'Le répertoire "{directory}" ne semble contenir aucun Makefile ou similaire !')
    return False

## ======================================================================
def does_not_have_exec_file(directory):
    return not check_files(directory,
                           (some_file for some_file in glob.iglob(os.path.join(os.path.join(directory, '**'), '*'),
                                                       recursive=True) if is_exec_file(some_file)),
                           'des fichiers exécutables')

## ======================================================================
def does_not_have_garbage(directory):
    reply = True
    for pattern, message in garbage.items():
        if message == '':
            message = 'du matériel non désiré'
        if contains_garbage(directory, pattern, message):
            reply = False

    return does_not_have_exec_file(directory) and reply

## ======================================================================
def has_txt_file_at_root(directory, root_filename):
    possible_files = (os.path.join(directory, filename)
    for filename in (root_filename, root_filename + '.txt',
                     root_filename.capitalize(), root_filename.capitalize() + '.txt') if os.path.isfile(os.path.join(directory, filename)))
    desired_file = next(possible_files, False)
    if not desired_file:
        print(f'Le fichier attendu "{root_filename}" n\'existe pas (à la racine du répertoire "{directory}").\nVeuillez l\'ajouter.')
        return False
    return is_acceptable_file(desired_file)

## ======================================================================
def has_acceptable_file(directory, filename):
    if any(is_acceptable_file(possible_file, verbose=False)
           for possible_file in \
      glob.iglob(os.path.join(os.path.join(directory, '**'), filename + '*'),
                              recursive=True)):
        return True

    print(f'Le fichier attendu "{filename}" n\'a pas été trouvé. Veuillez l\'ajouter.')
    return False

## ======================================================================
def has_required_files(directory):
    return True if all(has_txt_file_at_root(directory, filename)
                       for filename in ('NOMS', 'README')) \
               and all(has_acceptable_file(directory, filename)
                       for filename in ('JOURNAL', 'REPONSES', 'CONCEPTION')) \
            else False

## ======================================================================
def has_valid_content(directory):
    return has_required_files(directory)   \
      and  has_some_source_file(directory) \
      and  has_some_make_file(directory)   \
      and  does_not_have_garbage(directory)

## ======================================================================
def is_valid_directory(directory = '.'):
    if not os.path.isdir(directory):
        print(f'"{directory}" n\'est pas un répertoire !')
    print(f'Je vérifie le contenu du répertoire "{directory}"')
    if has_valid_content(directory):
        print('  ---> OK')
        return True
    return False

## ======================================================================
def find_group(family_name):
    group = { value:key for key,value in students.items() if key.startswith(family_name + ' ') }
    if not group:
        print(f'Désolé, mais je ne trouve pas le nom "{family_name}" dans la classe :-(')
        return False

    if len(group) > 1:
        print('Il y a plusieurs noms de famille qui correspondent :')
        for num in sorted(group):
            print(f'  {num} - {group[num]}')
        reply = int(input('Quel est votre groupe ? (le numéro devant votre nom ; 0 si aucun)\n'))
        if not reply in group:
            print(f'{reply} n\'est pas un numéro de groupe que je vous ai proposé !')
            print('Veuillez recommencer.')
            reply = 0
        return reply

    return next(iter(group))

## ======================================================================
def make_zipfile(group, directory):
    zipfilename = f'{coaches[group]}-g{group:03d}'

    print(f'Je lance la création du fichier Zip "{zipfilename}.zip".')
    shutil.make_archive(zipfilename, 'zip', directory)

    zipfilename += '.zip'
    print(f'Le fichier Zip "{zipfilename}" a été créé. Voici son contenu :\n')
    with zipfile.ZipFile(zipfilename, 'r') as our_zipfile:
        for some_file in our_zipfile.namelist():
            if not our_zipfile.getinfo(some_file).is_dir():
                print(f'\t- {some_file}')
    print('\nSI CE N\'EST PAS CE QUE VOUS VOULEZ RENDRE, recommencez la procédure.\n')

## ======================================================================
def main():
    group = find_group(string.capwords(input('Quel est votre nom de famille ? ')))
    if not group:
        return

    directory = input('Quel répertoire voulez-vous rendre ? ')
    if not is_valid_directory(directory):
        return

    make_zipfile(group, directory)

## ======================================================================
if __name__ == '__main__':
    main()
