import os
import subprocess
import numpy as np
import uuid  # Import uuid to generate unique IDs
from moviepy.editor import VideoFileClip
from scipy.io import wavfile
import random

temporary_audio_files = []

# Define video file extensions and the output folder
video_extensions = ['.mp4', '.mkv', '.wmv', '.avi']
output_folder = 'Output'

def update_max_segments_for_selected_video():
    global selected_video_path, max_segments, segment_duration, starting_offset_seconds, ending_offset_seconds, num_moments
    if selected_video_path in video_durations:
        video_duration = video_durations[selected_video_path]
        max_segments = calculate_max_segments(video_duration, segment_duration, starting_offset_seconds, ending_offset_seconds)
        num_moments = min(num_moments, max_segments)  # Ajustez num_moments si nécessaire
    else:
        max_segments = 0

def sorting_preference_to_string(pref):
    sorting_options = {
        1: "Par ordre de lecture de la vidéo",
        2: "Par ordre inverse de lecture de la vidéo",
        3: "Par volume croissant",
        4: "Par volume décroissant",
        5: "Aléatoire"
    }
    return sorting_options.get(pref, "Non défini")

def peak_position_to_string(pos):
    peak_options = {
        '1': "À 1/4 du temps de lecture de la vidéo",
        '2': "À 1/2 du temps de lecture de la vidéo",
        '3': "À 3/4 du temps de lecture de la vidéo"
    }
    return peak_options.get(pos, "Non défini")
    
def choose_video(videos):
    # Separate root files from subdirectory files
    root_files = [video for video in videos if os.path.dirname(os.path.relpath(video)) == '']
    subdirectory_files = [video for video in videos if os.path.dirname(os.path.relpath(video)) != '']

    # Sort the files in subdirectories
    subdirectory_files_sorted = sorted(subdirectory_files, key=lambda x: (os.path.dirname(x).lower(), os.path.basename(x).lower()))

    # Combine lists: root files first, then sorted subdirectory files
    combined_videos = root_files + subdirectory_files_sorted

    print("Liste des vidéos disponibles :")
    for i, video in enumerate(combined_videos):
        # Get the relative path for printing
        rel_path = os.path.relpath(video)
        print(f"{i + 1}- {rel_path}")

    while True:
        choice = input("Veuillez choisir la vidéo à extraire (entrez le numéro) : ")
        try:
            choice_idx = int(choice) - 1
            if 0 <= choice_idx < len(combined_videos):
                return combined_videos[choice_idx]
            else:
                print("Le numéro doit être valide. Veuillez réessayer.")
        except ValueError:
            print("Veuillez entrer un nombre valide.")

def print_menu(selected_video_path):
    global max_segments
    global include_subfolders, starting_offset_seconds, ending_offset_seconds
    global segment_duration, num_moments, sorting_preference, peak_position
    video_name = os.path.basename(selected_video_path) if selected_video_path else "Aucune vidéo sélectionnée"

    print("\nMenu des options :")
    print(f"1. Traiter les sous-dossiers ou non ({str(include_subfolders)})")
    print(f"2. Effectuer un retrait temporel (début: {str(starting_offset_seconds)}s, fin: {str(ending_offset_seconds)}s)")
    print(f"3. Changer la durée des segments ({str(segment_duration)}s | max segments extractibles: {str(max_segments)})")
    print(f"4. Changer le nombre de segments à extraire ({str(num_moments)})")
    print(f"5. Changer l'ordre de tri ({sorting_preference_to_string(sorting_preference)})")
    print(f"6. Changer l'emplacement du pic sonore ({peak_position_to_string(peak_position)})")
    print(f"7. Lancer l'extraction")
    print(f"8- ({video_name}) - Modifier")
    print(f"9. Quitter")


def ask_for_number_of_moments(max_segments):
    while True:
        num_input = input(f"Veuillez entrer le nombre de moments forts à extraire (maximum {max_segments}): ")
        try:
            num = int(num_input)
            if num > 0 and num <= max_segments:
                return num
            else:
                print(f"Le nombre doit être supérieur à 0 et inférieur ou égal à {max_segments}. Veuillez réessayer.")
        except ValueError:
            print("Entrée non valide, veuillez réessayer avec un nombre entier.")

def ask_yes_no_question(question):
    answer = None
    while answer not in ('1', '2'):
        print(question)
        print("1- Oui")
        print("2- Non")
        answer = input("Veuillez entrer le numéro de votre choix (1 ou 2) : ").strip()
        if answer not in ('1', '2'):
            print("Entrée non valide, veuillez réessayer.")
    return answer == '1'
    
def ask_offset_type():
    print("Souhaitez-vous un décalage temporel relatif ou absolu ?")
    print("1- Relatif (pourcentage)")
    print("2- Absolu (secondes)")
    while True:
        choice = input("Veuillez entrer le numéro de votre choix (1 ou 2) : ").strip()
        if choice in ('1', '2'):
            return choice
        else:
            print("Entrée non valide, veuillez réessayer.")
            
def get_offset_value(video_duration, offset_type):
    if offset_type == '1':  # Relative offset
        while True:
            percent = input("Entrez le pourcentage du temps vidéo à ignorer : ")
            try:
                percent_value = float(percent)
                return percent_value * video_duration / 100
            except ValueError:
                print("Veuillez entrer un nombre valide.")
    else:  # Absolute offset
        while True:
            seconds = input("Entrez le nombre de secondes à ignorer : ")
            try:
                return float(seconds)
            except ValueError:
                print("Veuillez entrer un nombre valide.")
                
def ask_for_segment_duration(allowable_duration, video_duration, starting_offset_seconds, ending_offset_seconds):
    # Cette fonction a été modifiée pour répondre plus précisément aux contraintes de durée.
    while True:
        duration = input("Quelle est la durée (en secondes) de chaque segment vidéo à extraire (Entrez un nombre positif et inférieur ou égal à {allowable_duration}) ? ")
        try:
            segment_duration = float(duration)
            if 0 < segment_duration <= allowable_duration:
                # Calculez le nombre maximal de segments pour une vidéo
                available_duration = video_duration - (starting_offset_seconds + ending_offset_seconds)
                max_segments = int(available_duration // segment_duration)
                return segment_duration, max_segments
            else:
                print(f"La durée doit être un nombre positif et moins ou égale à {allowable_duration} secondes.")
        except ValueError:
            print("Veuillez entrer un nombre valide.")

def ask_directory_preference():
    print("Souhaitez-vous inclure les sous-dossiers dans la recherche des vidéos ?")
    print("1- Oui")
    print("2- Non")
    choice = input("Veuillez entrer le numéro de votre choix (1 ou 2) : ")
    return choice.strip() == '1'  # Retourne True si l'utilisateur choisit '1' (Oui), False sinon

def calculate_loudness(audio_data):
    if audio_data.ndim == 1:
        volume = audio_data.astype('float32') ** 2
    else:
        volume = np.mean(audio_data.astype('float32') ** 2, axis=1)
    volume_dB = 10 * np.log10(volume + 1e-9)  # +1e-9 to avoid log(0) and convert to dB
    return volume_dB
    
def calculate_max_segments(video_duration, segment_duration, starting_offset, ending_offset):
    allowable_duration = video_duration - (starting_offset + ending_offset)
    if allowable_duration > 0:
        return int(allowable_duration // segment_duration)
    else:
        return 0

def find_loudest_moments(audio_filename, num_moments, segment_duration, video_duration, starting_offset, ending_offset):
    rate, audio_data = wavfile.read(audio_filename)

    # Convert stereo to mono if necessary
    if audio_data.ndim == 2:
        audio_data = np.mean(audio_data, axis=1)

    volume_dB = calculate_loudness(audio_data)
    segment_half_duration = segment_duration / 2.0
    start_index = int(starting_offset * rate)
    end_index = int((video_duration - ending_offset) * rate)

    moments = []
    volumes = []

    while len(moments) < num_moments and (end_index - start_index) > 0:
        index = np.argmax(volume_dB[start_index:end_index])
        print(f"Current index: {index}, start_index: {start_index}, end_index: {end_index}")  # Ajouté pour le débogage
        moment = (start_index + index) / rate
        print(f"Added moment at {moment} seconds")  # Ajouté pour le débogage
        moment_volume = volume_dB[start_index + index]

        if moment - segment_half_duration < starting_offset or moment + segment_half_duration > video_duration - ending_offset:
            volume_dB[start_index + index] = -np.inf
            continue

        moments.append(moment)
        print(f"Added moment at {moment} seconds") # Ajouté pour le débogage
        volumes.append(moment_volume)

        # Clear the volume around the found moment to prevent picking up nearby moments
        # Increment the global index by adding the start_index
        global_index = start_index + index

        # Définir le facteur de neutralisation (par exemple, 0.25 pour un quart de la durée du segment)
        neutralization_factor = 0.1  # Ajuster ce facteur en fonction de vos besoins
        
        # Calculer le rayon de neutralisation en appliquant le facteur à la durée du segment
        neutralization_radius = segment_duration * neutralization_factor

        # Déterminer les indices de début et de fin de la plage de neutralisation
        clear_range_start = max(0, global_index - int(rate * neutralization_radius))
        clear_range_end = min(len(volume_dB), global_index + int(rate * neutralization_radius))

        # Mettre la plage de neutralisation à -inf pour éviter de sélectionner à nouveau des moments proches
        volume_dB[clear_range_start:clear_range_end] = -np.inf
        
        print(f"Volume after clearing at index {global_index}: {volume_dB[global_index]}")

    return moments, volumes
    
def perform_extraction():
    global starting_offset_seconds, ending_offset_seconds
    global segment_duration, num_moments, sorting_preference, peak_position, processed_videos, selected_video_path, max_segments

    # Vérifiez si une vidéo a été sélectionnée.
    if not selected_video_path:
        print("Aucune vidéo sélectionnée pour l'extraction.")
        return

    # Vérifiez si la vidéo sélectionnée est contenue dans video_durations.
    if selected_video_path not in video_durations:
        print(f"La vidéo sélectionnée '{selected_video_path}' n'est pas disponible.")
        return

    # Obtenez la durée de la vidéo sélectionnée.
    duration = video_durations[selected_video_path]
    available_duration = duration - (starting_offset_seconds + ending_offset_seconds)

    if available_duration <= segment_duration:
        print(f"La vidéo {selected_video_path} est trop courte après les décalages. Ignorer.")
        return

    if sorting_preference == 5:  # Aléatoire
        all_possible_moments = np.arange(starting_offset_seconds, duration - ending_offset_seconds - segment_duration, segment_duration) + (segment_duration / 2)
        np.random.shuffle(all_possible_moments)
        selected_moments = all_possible_moments[:num_moments]
        volumes = [0] * len(selected_moments)  # Les volumes sont justes pour la compatibilité avec extract_segments.
    else:
        audio_path = f'temp_audio_{uuid.uuid4().hex}.wav'
        try:
            with VideoFileClip(selected_video_path) as video_clip:
                video_clip.audio.write_audiofile(audio_path, codec='pcm_s16le', fps=44100, nbytes=2, buffersize=2000)

                moments, volumes = find_loudest_moments(audio_path, num_moments, segment_duration, duration, starting_offset_seconds, ending_offset_seconds)
                sorted_moments, sorted_volumes = sort_moments(moments, volumes, sorting_preference)
                selected_moments = sorted_moments
                volumes = sorted_volumes

        finally:
            if os.path.exists(audio_path):
                os.remove(audio_path)
                
    # Juste avant l'appel à extract_segments, mettez à jour num_moments pour être sûr qu'il ne dépasse pas max_segments
    num_moments = min(num_moments, max_segments)
    extract_segments(selected_video_path, selected_moments, segment_duration, duration, peak_position)
    processed_videos += 1
    print(f"Extraction terminée pour la vidéo '{selected_video_path}'")

def extract_segments(video_path, moments, segment_duration, video_duration, peak_position):
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
    base_name = os.path.splitext(os.path.basename(video_path))[0]
    half_segment_duration = segment_duration / 2
    
    for i, moment in enumerate(moments):
        if peak_position == '1':  # 1/4
            start_time = max(moment - segment_duration * 0.25, 0)
        elif peak_position == '2':  # 1/2
            start_time = max(moment - segment_duration * 0.5, 0)
        elif peak_position == '3':  # 3/4
            start_time = max(moment - segment_duration * 0.75, 0)
        end_time = min(start_time + segment_duration, video_duration)

        output_filename = f"{base_name}_moment{i + 1}.mp4"
        output_path = os.path.join(output_folder, output_filename)

        command = [
            "ffmpeg",
            "-y",                     # Overwrite output files without asking
            "-ss", str(start_time),   # Start time
            "-i", video_path,         # Input file
            "-t", str(min(segment_duration, video_duration - start_time)),  # Duration or remaining video
            "-c:v", "libx264",        # Specify video codec for output
            "-preset", "medium",      # Specify the encoding preset (trade-off between encoding speed and quality)
            "-crf", "23",             # Specify the Constant Rate Factor for quality (lower means better quality)
            "-c:a", "aac",            # Specify audio codec for output
            "-strict", "-2",          # Necessary for some versions of ffmpeg to use experimental aac encoder
            "-b:a", "192k",           # Specify the audio bitrate
            output_path               # Output path
        ]

        try:
            subprocess.run(command, check=True, stderr=subprocess.PIPE)
            print(f"Extracted and re-encoded {output_filename}")
        except subprocess.CalledProcessError as e:
            # Catch the exception for this process, print an error message,
            # but don't break from the for loop.
            err_msg = e.stderr.decode('utf-8') if e.stderr else 'Unknown error'
            print(f"Failed to extract segment from {video_path}: {err_msg}")
        
def store_segment_info(video_path, moment, volume, order):
    base_name = os.path.splitext(os.path.basename(video_path))[0]
    output_filename = f"{base_name}_moment{order}.mp4"
    output_path = os.path.join(output_folder, output_filename)
    extracted_segments.append({
        'path': output_path,
        'timestamp': moment,
        'volume': volume
    })
        
def ask_sorting_preference():
    print("Comment souhaitez-vous trier les vidéos extraites ?")
    print("1- Par ordre de lecture de la vidéo")
    print("2- Par ordre inverse de lecture de la vidéo")
    print("3- Par volume croissant")
    print("4- Par volume décroissant")
    print("5- Aléatoire")
    choice = int(input("Veuillez entrer le numéro de votre choix : "))
    return choice
    
def sort_moments(moments, volumes, choice):
    if choice == 1:  # Par ordre de lecture de la vidéo
        zipped = sorted(zip(moments, volumes), key=lambda x: x[0])
    elif choice == 2:  # Par ordre inverse de lecture de la vidéo
        zipped = sorted(zip(moments, volumes), key=lambda x: x[0], reverse=True)
    elif choice == 3:  # Par volume croissant
        zipped = sorted(zip(moments, volumes), key=lambda x: x[1])
    elif choice == 4:  # Par volume décroissant
        zipped = sorted(zip(moments, volumes), key=lambda x: x[1], reverse=True)
    elif choice == 5:  # Pas de tri, sélection aléatoire
        zipped = list(zip(moments, volumes))
        random.shuffle(zipped)
    else:
        zipped = zip(moments, volumes)

    # Unzip the list of tuples to two lists
    sorted_moments, sorted_volumes = zip(*zipped) if zipped else ([], [])
    return list(sorted_moments), list(sorted_volumes)

def get_video_durations(include_subfolders):
    video_durations = {}
    for root, dirs, files in os.walk('.', topdown=True):
        # Si l'utilisateur ne souhaite pas inclure les sous-dossiers, nous modifions dirs sur place
        if not include_subfolders:
            dirs[:] = []  # Cela empêchera os.walk de descendre dans tous les sous-dossiers
        # La suite du traitement des fichiers reste inchangée
        for file in files:
            if file.lower().endswith(tuple(video_extensions)):
                video_path = os.path.join(root, file)
                try:
                    # Essayez d'ouvrir et d'extraire les métadonnées du fichier vidéo.
                    video_clip = VideoFileClip(video_path)
                    video_duration = video_clip.duration
                    video_durations[video_path] = video_duration
                except Exception as e:
                    # Si une erreur survient, affichez un message et ignorez le fichier vidéo.
                    print(f"Error processing video {video_path}: {e}")
                finally:
                    # Assurez-vous de fermer le clip pour libérer les ressources.
                    video_clip.close()
    return video_durations
    
def confirm_segment_number_or_ask_again(video_duration, starting_offset_seconds, ending_offset_seconds):
    allowable_duration = video_duration - (starting_offset_seconds + ending_offset_seconds)
    while True:
        segment_duration, max_segments = ask_for_segment_duration(allowable_duration, video_duration, starting_offset_seconds, ending_offset_seconds)
        print(f"Avec cette durée, vous pouvez extraire jusqu'à {max_segments} segments.")
        confirmation = ask_yes_no_question("Voulez-vous continuer avec ce nombre de segments ?")
        if confirmation:
            return segment_duration, max_segments
            
def ask_peak_position():
    print("Où doit être situé le pic sonore dans la vidéo extraite ?")
    print("1- A 1/4 du temps de lecture de la vidéo")
    print("2- A 1/2 du temps de lecture de la vidéo")
    print("3- A 3/4 du temps de lecture de la vidéo")
    while True:
        choice = input("Veuillez entrer le numéro de votre choix (1, 2, ou 3) : ").strip()
        if choice in ('1', '2', '3'):
            return choice
        else:
            print("Entrée non valide, veuillez réessayer.")
         
def main():
    global include_subfolders, starting_offset_seconds, ending_offset_seconds
    global segment_duration, num_moments, sorting_preference, peak_position
    global processed_videos, video_durations, selected_video_path, max_segments

    # Initialize the variables with default values
    include_subfolders = False
    starting_offset_seconds = 0
    ending_offset_seconds = 0
    segment_duration = 5.0  # Default value, e.g., 5 seconds
    num_moments = 20        # Default value, e.g., 20 moments
    sorting_preference = 4  # Default value, e.g., sorting by descending volume
    peak_position = '2'     # Default value, e.g., peak at the middle of the segment
    selected_video_path = ""   # Initialement vide
    processed_videos = 0
    
    # Load video durations at the start of the script
    video_durations = get_video_durations(include_subfolders)
    if not video_durations:
        print("Aucune vidéo trouvée pour l'analyse.")
        exit()
        
    video_list = sorted(list(video_durations.keys()), key=lambda x: os.path.basename(x))
    selected_video_path = video_list[0] if video_list else ""  # Select the first video by default, if available.
    
    # Calculate max_segments with the newly obtained video_durations
    video_duration = video_durations[selected_video_path]  # Obtenez la durée de la vidéo sélectionnée
    max_segments = calculate_max_segments(video_durations[selected_video_path], segment_duration, starting_offset_seconds, ending_offset_seconds)

    while True:
        # Recalculate the max segments using the selected video before displaying the menu
        update_max_segments_for_selected_video()

        # Display the menu with the updated max segments
        print_menu(selected_video_path)

        choice = input("Veuillez entrer le numéro de votre choix : ").strip()

        # Handle user choices
        if choice == '1':
            # Update include_subfolders and recalculate video durations
            include_subfolders = ask_directory_preference()
            video_durations = get_video_durations(include_subfolders)
            # Optionally, reset selected_video_path if include_subfolders changed
            video_list = sorted(list(video_durations.keys()), key=lambda x: os.path.basename(x))
            selected_video_path = video_list[0] if video_list else ""
            update_max_segments_for_selected_video()

        elif choice == '2':
            # Récupérez les nouvelles valeurs.
            offset_type = ask_offset_type()
            # Utilisez une valeur minimale mise à jour si nécessaire.
            min_duration = min(video_durations.values())
            print("Configuration du retrait temporel pour le début de la vidéo:")
            starting_offset_seconds = get_offset_value(min_duration, offset_type)
            print("Configuration du retrait temporel pour la fin de la vidéo:")
            ending_offset_seconds = get_offset_value(min_duration, offset_type)

        elif choice == '3':
            # L'utilisateur définit la durée des segments et potentiellement le nombre de moments
            video_duration = min(video_durations.values())  # Min. duration among all videos
            allowable_duration = video_duration - (starting_offset_seconds + ending_offset_seconds)
            segment_duration, max_segments = ask_for_segment_duration(
                allowable_duration,
                video_duration,
                starting_offset_seconds,
                ending_offset_seconds
            )
            if num_moments > max_segments:
                num_moments = max_segments
            # Ce bloc assure que num_moments n'excède jamais max_segments après un changement de durée des segments
        
        elif choice == '4':
            num_moments = ask_for_number_of_moments(max_segments)
        
        elif choice == '5':
            sorting_preference = ask_sorting_preference()
        
        elif choice == '6':
            peak_position = ask_peak_position()
        
        elif choice == '7':
            # Exécutez vos sous-fonctions pour effectuer l'extraction des segments.
            # Vous souhaiterez probablement encapsuler cela dans une autre fonction.
            perform_extraction()  # Call perform_extraction instead of extract_segments
        
        elif choice == '8':
            video_list = sorted(list(video_durations.keys()), key=lambda x: os.path.basename(x))
            prev_video_path = selected_video_path
            selected_video_path = choose_video(video_list)
            if selected_video_path != prev_video_path:
                print(f"Vidéo sélectionnée pour l'extraction : {os.path.basename(selected_video_path)}")
                update_max_segments_for_selected_video() # Mise à jour des segments max après le choix.
        
        elif choice == '9':
            print("Fin du programme.")
            break
        
        else:
            print("Choix non valide. Veuillez réessayer.")
        
    print(f"Le traitement de toutes les vidéos est terminé. {processed_videos} vidéos ont été traitées.")

if __name__ == "__main__":
    main()