from tkinter import ttk
import tkinter as tk
import tkinter.filedialog
from PIL import ImageTk, Image


LARGEUR_MAX_ZONE_IMAGE = 500      # Taille de la zone de dessin
HAUTEUR_MAX_ZONE_IMAGE = 500
LARGEUR_CADRE_TRAITEMENTS = 200
PADDING = 10      # Espacement entre les objets
BLANC = 255
NOIR = 0
LIMITE_BLANC_NOIR = (BLANC*3)//2
IMAGE_PAR_DEFAUT = "castle_orig.png"


class App():
    def __init__(self):
        self.initialiseUI()
        try :
            self.image = Image.open(IMAGE_PAR_DEFAUT).convert("RGB")
        except:
            self.image = None
        self.affiche_image()
        self.fenetre.mainloop()

    def affiche_image(self) -> None:
        """Affiche l'image PIL dans la zone dédiée de l'affichage."""
        global im_travail    # Nécessaire pour que im_travail ne soit pas supprimé par le ramasse-miette
        if self.image is None: return  # Si vide, on ne fait rien
        if self.image.width == 0 or self.image.height == 0:
            largeur = min(self.image.width, LARGEUR_MAX_ZONE_IMAGE)
            hauteur = min(self.image.height, HAUTEUR_MAX_ZONE_IMAGE)
        else:
            ratio = self.image.width/self.image.height
            largeur = min(self.image.width, LARGEUR_MAX_ZONE_IMAGE)
            hauteur = largeur / ratio
            if hauteur > HAUTEUR_MAX_ZONE_IMAGE:
                hauteur = HAUTEUR_MAX_ZONE_IMAGE
                largeur = hauteur * ratio
        im_travail = ImageTk.PhotoImage(self.image.copy().resize((int(largeur), int(hauteur))))
        self.affichage_image.configure(image = im_travail, width=largeur, height=hauteur)


    def initialiseUI(self) -> None:
        """Crée la fenêtre principale et ses widgets."""

        # Création de la fenêtre principale
        self.fenetre = tk.Tk()
        self.fenetre.title("Seam Carving")
        
        # Création du menu
        barreMenu = tk.Menu(self.fenetre)
        # Menu pour la taille de la grille
        menu_fichier = tk.Menu(barreMenu)
        menu_fichier.add("command", label="Charger une image",
                          command=self.charge_image)
        menu_fichier.add("command", label="Sauvegarder une image",
                          command=self.sauvegarde_image)
        barreMenu.add_cascade(label="Fichiers", menu=menu_fichier)

        self.fenetre.config(menu=barreMenu)      # Attache la barre de menu à la fenêtre
        

        # Cadre de gauche (image)
        cadre_gauche = tk.LabelFrame(self.fenetre)
        cadre_gauche.pack(side="left", padx=PADDING, pady=PADDING)
        
        # Cadre de droite (boutons)
        cadre_droit = tk.LabelFrame(self.fenetre, text="Traitements",
                                    width=LARGEUR_CADRE_TRAITEMENTS)
        cadre_droit.pack(side="right", padx=PADDING, pady=PADDING)
        # Bouton noir & blanc
        btn_noir_et_blanc = tk.Button(cadre_droit, text="Noir et blanc",
                                      command=self.noir_et_blanc)
        btn_noir_et_blanc.pack(padx=PADDING, pady=PADDING)
        # Bouton niveaux de gris
        btn_niveaux_de_gris = tk.Button(cadre_droit, text="Niveaux de gris",
                                      command=self.niveaux_de_gris)
        btn_niveaux_de_gris.pack(padx=PADDING, pady=PADDING)

        # Cadre rétrécissement
        cadre_retrecissement = tk.LabelFrame(cadre_droit)
        cadre_retrecissement.pack(padx=PADDING, pady=PADDING)
        quantite_retrecissement = tk.IntVar(value=10)
        slider_quantite_retrecissement = tk.Scale(cadre_retrecissement, resolution=1,
                    orient="horizontal", from_=0, to=200, variable=quantite_retrecissement)
        slider_quantite_retrecissement.pack(padx=PADDING, pady=0)
        btn_retrecissement = tk.Button(cadre_retrecissement, text="Rétrécissement",
                command=lambda:self.retrecissement(quantite_retrecissement.get()))
        btn_retrecissement.pack(padx=PADDING, pady=PADDING//2)
           
        # Barre de progression
        self.barre_progression = ttk.Progressbar(cadre_droit, mode="determinate",
                                           orient="horizontal")
        self.barre_progression.pack(padx=PADDING, pady=PADDING)

        # Création du canevas
        vide = tk.PhotoImage()
        self.affichage_image = tk.Label(cadre_gauche, bg="white", image = vide,
                        width=LARGEUR_MAX_ZONE_IMAGE, height=HAUTEUR_MAX_ZONE_IMAGE)
        self.affichage_image.pack(padx=PADDING, pady=PADDING)


    def demande_fichier(self, ouverture=True) -> str:
        """Demande un nom de fichier image pour chargement si le paramètre
        ouverture est à True ou pour sauvegarde s'il est à False."""
        association =[("Fichiers png", ".png"), ("Fichiers jpg", ".jp*g"),
                      ("Fichiers gif", ".gif"),("Fichiers bmp", ".bmp"),
                      ("Fichiers tiff", ".tif*"),("Tous les fichiers", ".*")]
        titre = "Sélectionner le fichier image"
        if ouverture:
            return tkinter.filedialog.askopenfilename(filetypes=association,
                                            title=titre, multiple=False)
        else:
            return tkinter.filedialog.asksaveasfilename(filetypes=association,
                                            title=titre)

    def charge_image(self) -> None:
        nom_fichier = self.demande_fichier()
        if nom_fichier != "":
            self.image = Image.open(nom_fichier).convert("RGB")
            self.affiche_image()


    def sauvegarde_image(self) -> None:
        nom_fichier = self.demande_fichier(ouverture=False)
        if nom_fichier != "":
            self.image.save(nom_fichier)


    def noir_et_blanc(self) -> None:
        """Passe l'image en noir et blanc"""
        for y in range(self.image.height):
            for x in range(self.image.width):
                pixel = self.image.getpixel((x,y))
                if sum(pixel) > LIMITE_BLANC_NOIR:
                    pixel = (BLANC, BLANC, BLANC)
                else:
                    pixel = (NOIR, NOIR, NOIR)
                self.image.putpixel((x,y),pixel)
        self.affiche_image()

    def niveaux_de_gris(self) -> None:
        """Passe l'image en niveaux de gris"""
        for y in range(self.image.height):
            for x in range(self.image.width):
                pixel = self.image.getpixel((x,y))
                moyenne = sum(pixel) // 3
                pixel = (moyenne, moyenne, moyenne)
                self.image.putpixel((x,y),pixel)
        self.affiche_image()

    def est_dans_image(self, coords:tuple, largeur:int, hauteur:int) -> bool:
        """Renvoie True si le pixel dont les coordonnées (x,y) sont dans le tuple
        coords est compris dans l'image largeur x hauteur."""
        return 0 <= coords[0] < largeur and 0 <= coords[1] < hauteur

    def distance_pixels(self, c1:tuple, c2:tuple) -> int:
        """Renvoie la distance entre les couleurs c1 et c2 données sous forme de
        tuples (r,v,b).
        La distance correspond à la somme des valeurs absolues des différences
        entre c1 et c2 pour chaque composante de couleur."""
        return abs(c1[0]-c2[0]) + abs(c1[1]-c2[1]) + abs(c1[2]-c2[2])

    def tableau_energies(self, img:Image) -> list:
        """Renvoi un tableau contenant les énergies des pixels de l'image img.
        l'énergie calculée correspond au constraste du pixel, c'est à dire la
        différence entre sa couleur et celles de ses 8 voisins immédiats."""
        # Couples (Δx, Δy) pour les voisins autour du point courant
        DELTAS = [(-1,-1),(0,-1),(1,-1), (-1,0),(1,0), (-1,1),(0,1), (1,1)]
        largeur, hauteur = img.size
        # Initialisation du tableau des énergies
        energies = [[None]*largeur for _ in range(hauteur)]
        for y in range(hauteur):
            for x in range(largeur):
                distance = 0    # Distance entre les couleurs des pixels environnants
                nb_voisins = 0  # Nombre de voisins (peut être différent sur les bords)
                couleur_reference = img.getpixel((x,y))  # Couleur du pixel courant
                for delta in DELTAS:    # On examine les voisins un par un
                    voisin = (x+delta[0], y+delta[1])
                    if self.est_dans_image(voisin, largeur, hauteur):
                        distance += self.distance_pixels(couleur_reference,
                                                         img.getpixel(voisin))
                        nb_voisins += 1
                # L'énergie est la moyenne des distances
                energies[y][x] = distance // nb_voisins
        return energies

    def retire_colonne_image(self, img:Image, a_enlever:list) -> Image:
        """Renvoie l'image img à laquelle on a retirée une colonne en supprimant le
        pixel à l'abscisse donné par le tableau a_enlever."""
        largeur, hauteur = img.size
        if largeur == 0:    # Si la largeur est déjà nulle
            return img      # On ne touche pas à l'image
        img_reduite = Image.new(img.mode, (largeur-1,hauteur))

        # TODO

        return img_reduite

    def retrecissement(self, nb_colonnes_supprimees:int) -> None:
        """Supprime le nombre de colonnes passé en argument à l'image actuelle
        en utilisant une méthode de 'seam carving'.
        On peut utiliser la notation TP.xyz pour désigner une variable ou une
        fonction du fichier principal car on a fait 'import __main__ as TP' au
        début de ce fichier."""
        self.barre_progression.configure(maximum=nb_colonnes_supprimees)
        for _ in range(nb_colonnes_supprimees):
            # Détermination de l'énergie des pixels
            # TODO ( 1 ligne)
            # Détermination des chemins minimaux
            # TODO ( 1 ligne)
            # Élimination du chemin de poids le plus faible
            # TODO ( 1 ligne)
            # Et suppression des colonnes correspondantes de l'image
            # TODO ( 1 ligne)
            
            # On met à jour la barre de progression
            self.barre_progression.step(1)
            self.barre_progression.update()
            # Et l'affichage de l'image
            self.affiche_image()


if __name__ == "__main__":
    # Si c'est ce fichier qui est exécuté, on importe le fichier avec les
    # algorithme écrits précédemment sous le nom "TP" pour pouvoir les utiliser ici.
    import TP_Seam_carving as TP
    app = App()     # Puis on lance l'application
else:
    # Sinon, c'est que l'application est lancée depuis le fichier du TP
    # et que ce fichier et la classe App est simplement importée. Dans ce cas
    # on fait référence au fichier appelant sous la dénomination "TP" pour
    # pouvoir utiliser ses fonctions dans la classe App.
    import __main__ as TP
