from random import *
from math import *
from PIL import Image
from time import monotonic
import numpy as np
import sys
import cv2

'''
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 __init__(self, message, debut, fin):
        '''initialise un objet de classe Process'''
        self.debut = debut
        self.fin = fin
        self.previous = 0
        self.percent = 0
        self.start_time = monotonic()
        print(message + '\n')
    
    def aff(self, value):
        '''Affiche le pourcentage de chargement correspondant à value'''
        fact = (value - self.debut)/(self.fin-self.debut)
        fact += (fact==0.0)*0.01

        spent_time = monotonic() - self.start_time

        remaining = int(spent_time/fact*(1-fact))

        hours = remaining//3600
        minutes = (remaining - hours*3600)//60
        seconds = remaining - hours*3600 - minutes*60
        
        percent = int(fact * 100)

        if percent > self.percent:
            self.percent = percent
            print("\033[2K\033[A", end='')
            write_str = str(self.percent) + " % effectué -- fin estimée dans "
            if hours != 0:
                write_str += str(hours) + "h "
            if minutes != 0:
                write_str += str(minutes) + "min "
            
            write_str += str(seconds) + "s"

            print(write_str, "          ")

            self.previous = len(write_str)
    
    def end(self):
        self.aff(self.fin)
        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 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()





kernel_size = lambda sigma : 2*int(4*sigma)+1
remove_zero = lambda c : (c == 0)*1 + c

vect_remove_zero = np.vectorize(remove_zero)



def image_ranGen(hauteur, largeur):
    image = np.random.randint(0, 255, (hauteur, largeur, 3))
    return image




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





def calc_dim(width, height, n):
    taille = sqrt(height*width)
    if taille <= 100 or n < 3: # cas de base
        return width, height
    else:
        return calc_dim(width//2, height//2, n-3)



def make_iterations(image, taille, K, deb, n):
    image = image.astype(np.uint8)
    #on effectue n itérations
    for i in range(deb, n):
        size = kernel_size(i*taille/K)
        image = cv2.GaussianBlur(image, (size, size), i*taille/K)
        
        image = image * (2 + 5*(n-i)/n)
        image = image%256
        image = vect_remove_zero(image)
    return image



def generer(depart, width, height, K, n, n_base): # on part de l'image de départ
    '''
    Cette fonction permet d'exécuter les itérations finales sur une image intermédiaire
    En particulier, depart <- $ et n_base = 3 correspondent au cas où l'on génère tout d'un coup
    [depart] : image utilisée comme cas de base
    [width] : largeur de l'image finale voulue
    [height] : hauteur de l'image finale voulue
    [K] : Paramètre de taille de forme lors de la génération
    [n] : nombre d'itérations. Ce nombre est normalement calculé en fonction de K
    [n_base] : nombre d'itérations restantes au bout duquel on applique le cas de base
    
    '''
    taille = sqrt(height*width)

    if taille <= 100 or n < n_base: # cas de base
        #on effectue n itérations
        image = make_iterations(depart, taille, K, 1, n)
        
    else:
        image = generer(depart, width//2, height//2, K, n-3, n_base)
        # on agrandit l'image
        image = image_resized = np.repeat(np.repeat(image, 2, axis=0), 2, axis=1)
        # on effectue trois itérations
        image = make_iterations(image, taille, K, n-3, n)

    return image







def next_pixel(pix0, pix1):
    i = randint(0,2)
    mut = 3 # plus c'est petit, moins la vidéo évolue
    return (
        (i==0)*(pix1[0]+mut)%256 + (i!=0)*pix1[0],
        (i==1)*(pix1[1]+mut)%256 + (i!=1)*pix1[1],
        (i==2)*(pix1[2]+mut)%256 + (i!=2)*pix1[2]
    )




def next_image(image0, image1): # on change juste n pixels choisis aléatoirement
    n = int(0.05 * len(image0) * len(image0[0])) # nombre de pixels à modifier
    nimage = image1.copy()
    for i in range(n):
        coord = (randint(0, len(image1)-1), randint(0, len(image1[0])-1))
        nimage[coord] = next_pixel(image0[coord], image1[coord])

    return nimage


def etendre(images, nb_images):
    p = Process(f"Dérivation de l'image de départ en {nb_images} images...", 0, nb_images)
    for i in range(nb_images):
        images.append(next_image(images[-2], images[-1]))
        p.aff(i)
    p.end()
    return images


def images_finales(images, K, deb, n):
    finales = []
    P = Process(f"Calcul des {len(images)} images finales...", 0, len(images))

    for i in range(len(images)):
        finales.append(make_iterations(images[i], sqrt(len(images[i])*len(images[i][0])), K, deb, n))
        P.aff(i)
    
    P.end()

    return finales


def name():
    T = localtime()
    name = str(T[2]) + '-' + str(T[1]) + '-' + str(T[0]) + '_' + str(T[3]) + ':' + str(T[4]) + '.mp4'
    return name



def main(args):
    time = 30 # temps de la vidéo en secondes
    K = 110 # coefficient de taille des formes
    width = 640
    height = 360
    fps = 16
    nb_img = fps * time # nombre d'images nécessaires en fonction du temps voulu pour 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 = max(5,int(3000/K)) # calcul d'un nombre d'itérations en fonction de la taille des objets
    n_base = n-3 # plus on soustrait, plus la vidéo évolue vite -> plus chaotique

    width_b, height_b = calc_dim(width, height, n) # calcul des dimensions des images de base

    print(f"Création d'une image de départ...")
    seed = image_ranGen(height_b, width_b) # image de base pour toute la vidéo

    print("Pré-traitement de cette image...")
    # calcul de quelques itérations
    seed = generer(seed, width, height, K, n_base, 3)

    images = [seed] * 2 # on en met deux pour avoir un vecteur de direction

    images = etendre(images, nb_img) # on rajoute le nombre d'images qu'il faut par continuité


    finales = images_finales(images, K, n_base, n) # itérations individuelles restantes

    print("Création de la vidéo...")

    if len(args) > 1:
        save_images_to_video(finales, args[1], fps=fps)
    else:
        save_images_to_video(finales, name(), fps=fps)
    
    print("Terminé")





if __name__ == '__main__':
    '''for i in range(1, 51):
        K = 'K=' + str(randint(100, 120))
        print('\nParamétrage de la nouvelle image', K)
        main(['ranimg.py', K, name()])
        print(f'Image n°{i} terminée !')'''
    
    main(sys.argv)
