from random import *
from math import *
from PIL import Image
import numpy as np
from scipy.ndimage import gaussian_filter
import threading
import sys
import cv2
import os

'''
PROCEDE :
1- Générer une image aléatoirement
2- Appliquer successivement :
    - un flou de l'image
    - une multiplication des couleurs
De manière à faire grossir les symboles
'''


class Process:

    def aff(self, value):
        '''Affiche le pourcentage de chargement correspondant à value'''
        percent = int((value - self.debut)/self.fin * 100) + 1

        if percent > self.percent:
            self.percent = percent

            print("\033[2K\033[A", end='')


            print(self.percent, '% effectué')

            self.previous = len(str(self.percent)) + 11


    def __init__(self, message, debut, fin):
        '''initialise un objet de classe Process'''
        self.debut = debut
        self.fin = fin
        self.previous = 0
        self.percent = 0
        print(message + '\n')
    
    def end(self):
        print("\033[2K\033[A")







# VARIABLE GLOBALE CONTENANT LE PRECEDENT NOMBRE ALEATOIRE


def tuple_int(t):
    return (int(t[0]), int(t[1]), int(t[2]))



def save_image(rgb_array, filename):
    """
    Sauvegarde un tableau 2D de couleurs RGB dans un fichier PNG.
    
    :param rgb_array: Liste de listes contenant des tuples RGB.
    :param filename: Nom du fichier PNG de sortie.
    """
    # Obtenir les dimensions de l'image
    height = len(rgb_array)
    width = len(rgb_array[0])

    # Créer une nouvelle image avec les dimensions spécifiées
    image = Image.new("RGB", (width, height))

    # Charger les pixels de l'image avec les valeurs RGB du tableau
    for y in range(height):
        for x in range(width):
            image.putpixel((x, y), tuple_int(rgb_array[y][x]))

    # Sauvegarder l'image dans un fichier PNG
    image.save(filename)


def number(name):
    return int(name[5:len(name)-4])


def sort_names(list):
    nouvliste = []
    while len(list) > 0 :
        i_min = 0
        for i in range(len(list)):
            if number(list[i_min]) > number(list[i]):
                i_min = i
        
        nouvliste.append(list[i_min])
        del list[i_min]
    
    return nouvliste




def save_images_to_video(image_list, output_filename, fps=30):
    if not image_list:
        raise ValueError("La liste d'images est vide")

    # Convertir la première image en tableau NumPy pour obtenir les dimensions
    first_image = np.array(image_list[0], dtype=np.uint8)
    height, width, layers = first_image.shape
    size = (width, height)

    # Initialiser le VideoWriter
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 'mp4v' pour .mp4
    out = cv2.VideoWriter(output_filename, fourcc, fps, size)

    for img in image_list:
        # Convertir chaque image en tableau NumPy
        image = np.array(img, dtype=np.uint8)
        
        # Vérifier que l'image a les mêmes dimensions que la première
        if image.shape[0] != height or image.shape[1] != width:
            raise ValueError("Toutes les images doivent avoir la même dimension")
        
        out.write(image)

    out.release()









def ran_couleur():
    return (randint(0, 256), randint(0, 256), randint(0, 256))


add = lambda c1, c2 : (c1[0] + c2[0], c1[1] + c2[1], c1[2] + c2[2])
mul = lambda x1, x2 : (x1 * x2[0], x1 * x2[1], x1 * x2[2]) if type(x1) in [int, float]  else (x2 * x1[0], x2 * x1[1], x2 * x1[2])
mod = lambda c : (c[0]%256, c[1]%256, c[2]%256)
remove_zero = lambda c : ((c[0] == 0)*1 + c[0], (c[1] == 0)*1 + c[1], (c[2] == 0)*1 + c[2])




def image_ranGen(hauteur, largeur):
    return [[ran_couleur() for j in range(largeur)] for i in range(hauteur)]




def dim(image): # hauteur, largeur
    return (len(image), len(image[0]))


def image_vide(image):
    hauteur, largeur = dim(image)
    return [[None for j in range(largeur)] for i in range(heuteur)]








def split_rgb_channels(image):
    height = len(image)
    width = len(image[0])

    red_channel = [[0]*width for _ in range(height)]
    green_channel = [[0]*width for _ in range(height)]
    blue_channel = [[0]*width for _ in range(height)]

    for i in range(height):
        for j in range(width):
            red_channel[i][j] = image[i][j][0]
            green_channel[i][j] = image[i][j][1]
            blue_channel[i][j] = image[i][j][2]

    return red_channel, green_channel, blue_channel

def apply_gaussian_blur(image, sigma):
    image_array = np.array(image)
    blurred_image = gaussian_filter(image_array, sigma=sigma)
    return blurred_image

def combine_rgb_channels(red_channel, green_channel, blue_channel):
    height = len(red_channel)
    width = len(red_channel[0])

    image = [[(0, 0, 0)] * width for _ in range(height)]

    for i in range(height):
        for j in range(width):
            image[i][j] = (int(red_channel[i][j]), int(green_channel[i][j]), int(blue_channel[i][j]))

    return image


def gaussian_blur_rgb_image(image, sigma):
    red_channel, green_channel, blue_channel = split_rgb_channels(image)
    
    blurred_red_channel = [None]
    blurred_green_channel = [None]
    blurred_blue_channel = [None]

    def blur_channel(channel, sigma, result):
        result[0] = apply_gaussian_blur(channel, sigma)

    red_thread = threading.Thread(target=blur_channel, args=(red_channel, sigma, blurred_red_channel))
    green_thread = threading.Thread(target=blur_channel, args=(green_channel, sigma, blurred_green_channel))
    blue_thread = threading.Thread(target=blur_channel, args=(blue_channel, sigma, blurred_blue_channel))

    red_thread.start()
    green_thread.start()
    blue_thread.start()

    red_thread.join()
    green_thread.join()
    blue_thread.join()
    blurred_image = combine_rgb_channels(blurred_red_channel[0], blurred_green_channel[0], blurred_blue_channel[0])
    return blurred_image







def iter(image, fonction):
    return [[fonction(x) for x in ligne] for ligne in image]





def generer(width, height, K, n):
    taille = sqrt(height*width)

    if taille <= 100 or n < 3: # cas de base
        image = image_ranGen(height, width)
        
        #on effectue n itérations
        for i in range(1,n):
            image = gaussian_blur_rgb_image(image, i*taille/K)
            
            image = iter(image, lambda couleur : mul(2 + 5*(n-i)/n,couleur))
            image = iter(image, mod)
            image = iter(image, remove_zero)
        
    else:
        image = generer(width//2, height//2, K, n-3)
        
        # on agrandit l'image
        image = [[image[x//2][y//2] for y in range(len(image[0])*2)] for x in range(len(image)*2)]
        
        # on effectue trois itérations
        #         
        for i in range(n-3,n):
            image = gaussian_blur_rgb_image(image, i*taille/K)
            
            image = iter(image, lambda couleur : mul(2 + 5*(n-i)/n,couleur))
            image = iter(image, mod)
            image = iter(image, remove_zero)

    return image




def distance(c1, c2): # calcule la distance entre deux couleurs
    return sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2 + (c1[2]-c2[2])**2)


def vect_dir(c1, c2): # renvoie la moitié du vecteur pour aller de c1 vers c2
    return ((c2[0] - c1[0])/2, (c2[1] - c1[1])/2, (c2[2] - c1[2])/2)



def completer(img1, img2, n, C):
    '''
    Fonction qui cree un continuum d'images pour passer de img1 à img2 avec n images
    Cette fonction est récursive et génère une image pile entre img1 et img2. Appel récursif entre img1 et celle générée et img2 et celle générée
    '''

    img = [[(0, 0, 0) for x in range(len(img1[0]))] for y in range(len(img1))]

    for x in range(len(img)):
        for y in range(len(img[0])):
            img[x][y] = add(img1[x][y], vect_dir(img1[x][y], img2[x][y]))

    if n==1:
        return [img]
    else:
        return completer(img1, img, n//2, 1) + [img]*C + completer(img, img2, n//2, 1)




def generer_flou(width, height, K, coeff):
    n = max(5,int(3000/K)) # calcul d'un nombre d'itérations en fonction de la taille des objets
    image = generer(width, height, K, n)
    image = gaussian_blur_rgb_image(image, (n*sqrt(width*height)/K)*coeff)
    return image





def enrichir(images, nb_frames, C):
    # fonction qui récupère une liste d'images toutes différentes et qui va insérer entre chaque image un continuum d'images
    liste = []
    for i in range(len(images)-1):
        liste += completer(images[i], images[i+1], nb_frames, C)

    return liste





def main(args):
    time = 15 # temps de la vidéo en secondes
    K = 200 # coefficient de taille des formes
    width = 340
    height = 480
    coeff = 0.5 # plus coeff est important, plus la vidéo est floutée
    fps = 24
    C = 1 # nombre de fois que l'on met chaque image pilier
    nb_frames = 20 # nombre de frames entre deux images totalement différentes
    nb_img = int((time * fps)/(nb_frames + C) + 1) # nombre d'images 'pilier' en fonction du temps voulu de la vidéo
    
    # plus K est grand, plus les symboles que l'on va observer seront petits
    # apparition des symboles : 100
    # mélange de flou et de taches : 400 et +
    n_frame = 1
    
    print(f"Creation de {nb_img} images originales...")
    images = [generer_flou(width, height, K, coeff) for i in range(nb_img)]

    print("Raccordement des images...")
    images = enrichir(images, nb_frames, C)

    print("Création de la vidéo...")
    save_images_to_video(images, "Video.mp4")
    
    print("Terminé")



if __name__ == '__main__':
    '''for i in range(1, 15):
        K = 'K=' + str(randint(100, 110))
        main(['ranimg.py', K, 'image' + str(i) + '.png'])'''
    
    main(sys.argv)
