'''
Module Laby
Affichage pour Labyrinthe
'''

from tkinter import Tk, Canvas
import time

class Walker:
    '''
    Classe décrivant le marcheur
    '''

    def __init__(self, x0, y0, orientation):
        self.__x = x0
        self.__y = y0
        self.__orientation = orientation

    @property
    def x(self):
      return self.__x

    @property
    def y(self):
      return self.__y

    @staticmethod
    def get_vecteur(orientation):
        '''
        orientation: orientation parmi 'D', 'B', 'G', 'D'
        Renvoie dx, dy, coordonnées du déplacement
        Calcule un vecteur de déplacement selon l'orientation.
        Par exemple si l'orientation est Droite, le marcheur envisage
        un pas à droite et 0 pas verticalement, donc dx = 1 et dy = 0
        Attention : sur les écrans, quand dy = 1, on va vers le bas.
        '''
        if orientation == "D":
            return 1, 0
        if orientation == "B":
            return 0, 1
        if orientation == "G":
            return -1, 0
        return 0, -1

    def left_side(self):
        '''
        renvoie l'orientation sur la gauche du marcheur
        '''
        if self.__orientation == "D":
            return "H"
        elif self.__orientation == "B":
            return "D"
        elif self.__orientation == "G":
            return "B"
        return "G"

    def turn_left(self):
        '''
        modifie l'orientation pour tourner à gauche
        '''
        self.__orientation = self.left_side()

    def right_side(self):
        '''
        renvoie l'orientation sur la droite du marcheur
        '''
        if self.__orientation == "D":
            return "B"
        if self.__orientation == "B":
            return "G"
        if self.__orientation == "G":
            return "H"
        return "D"

    def turn_right(self):
        '''
        modifie l'orientation pour tourner à droite
        '''
        self.__orientation = self.right_side()

    def go_forward(self):
        '''
        fait avancer le marcheur
        '''
        self.__x, self.__y = self.pos_forward()

    def pos_forward(self):
        '''
        renvoie les coordonnées de la position devant le marcheur
        '''
        dx, dy = Walker.get_vecteur(self.__orientation)
        return self.__x + dx, self.__y + dy

    def pos_left(self):
        '''
        renvoie les coordonnées de la position à gauche du marcheur
        '''
        dx, dy = Walker.get_vecteur(self.left_side())
        return self.__x + dx, self.__y + dy

    def pos_right(self):
        '''
        renvoie les coordonnées de la position à droite du marcheur
        '''
        dx, dy = Walker.get_vecteur(self.right_side())
        return self.__x + dx, self.__y + dy

    def draw_coords(self):
        '''
        renvoie les coordonnées du triangle pour dessiner le marcheur
        '''
        centerX = (self.__x + 0.5)
        centerY = (self.__y + 0.5)

        if self.__orientation == "D": # vers la droite
            return (
                centerX - 0.3,
                centerY - 0.3,
                centerX + 0.3,
                centerY,
                centerX - 0.3,
                centerY + 0.3
            )
        if self.__orientation == "B": # vers le bas
            return (
                centerX + 0.3,
                centerY - 0.3,
                centerX,
                centerY + 0.3,
                centerX - 0.3,
                centerY - 0.3
            )
        if self.__orientation == "G": # vers la gauche
            return (
                centerX + 0.3,
                centerY + 0.3,
                centerX - 0.3,
                centerY,
                centerX + 0.3,
                centerY - 0.3
            )
        return (
            centerX - 0.3,
            centerY + 0.3,
            centerX,
            centerY - 0.3,
            centerX + 0.3,
            centerY + 0.3
        )

class LabyDisplay:
    '''
    Classe de gestion de l'affichage du labyrinthe
    '''
    PIXEL_SIZE = 10  # taille d'une brique constitutive des disques
    BACK_COLOR = "white"
    WALL_COLOR = "black"
    END_COLOR = "green"
    WALKER_LINE_COLOR = '#f11'
    WALKER_FILL_COLOR = '#1f1'
    MAX_SPEED = 100
    __speed = 1

    def __init__(self, width:int, height:int):
        '''
        width: largeur en nombre de cellules
        height: hauteur en nombre de cellules
        '''
        self.__tkEngine = Tk()
        self.__tkEngine.title("Labyrinthe")

        canvasWidth = self.PIXEL_SIZE * width
        canvasHeight = self.PIXEL_SIZE * height

        self.__canvas = Canvas(
            self.__tkEngine,
            width = canvasWidth,
            height = canvasHeight,
            bd=0,
            bg=self.BACK_COLOR
        )
        self.__canvas.pack(padx=10,pady=10)

        self.__walker_sprite = None



    def draw_wall(self, x:int, y:int):
        '''
        x, y: coordonnées
        trace un mur à ces coordonnées
        '''
        self.__canvas.create_rectangle(
            x*self.PIXEL_SIZE,
            y*self.PIXEL_SIZE,
            (x + 1) * self.PIXEL_SIZE,
            (y + 1) * self.PIXEL_SIZE,
            fill=self.WALL_COLOR
        )

    def draw_end(self, x:int, y:int):
        '''
        x, y: coordonnées
        trace une case de fin à ces coordonnées
        '''
        self.__canvas.create_rectangle(
            x*self.PIXEL_SIZE,
            y*self.PIXEL_SIZE,
            (x + 1)* self.PIXEL_SIZE,
            (y + 1) * self.PIXEL_SIZE,
            fill=self.END_COLOR
        )

    def draw_walker(self, walker):
        '''
        walker: objet décrivant le marcheur
        dessine le sprite du marcheur dans le canvas
        '''
        if self.__walker_sprite is not None:
            self.__canvas.delete(self.__walker_sprite)

        coords = [coord*self.PIXEL_SIZE for coord in walker.draw_coords()]
        self.__walker_sprite = self.__canvas.create_polygon(
            coords,
            outline = self.WALKER_LINE_COLOR,
            fill = self.WALKER_FILL_COLOR,
            width = 1
        )

    def affichage(self):
        '''
        provoque l'affichage de la fenêtre et temporise
        réactualise a fenêtre pour maintenir son affichage
        tout le temps de la temporisation
        La temporisation permet de réguler la vitesse de l'animation
        '''
        countDown = int(LabyDisplay.MAX_SPEED/self.__speed)
        while countDown>0 :
            self.__tkEngine.update()
            time.sleep(0.025)
            countDown -= 1

    def update_speed(self, s:int):
        '''
        accélère l'animation
        '''
        self.__speed = max(min(s, 100), 1)

    def fin(self):
        '''
        arrête définitivement le marcheur
        maintient l'affichage de la fenêtre
        '''
        self.__tkEngine.mainloop()


class Laby:
    '''
    Classe de gestion d'un labyrinthe
    '''

    START_SYMB = 'D'
    END_SYMB = 'A'
    WALL_SYMB = '*'
    NOT_WALLS = 'DA '
    LAB = None
    speed = 1

    @staticmethod
    def load(filename):
        '''
        filename: nom de fichier texte
        '''
        file = open(filename, 'r', encoding='utf8')
        grid = file.readlines()
        file.close()
        grid = [line.strip() for line in grid]
        while len(grid) > 0 and grid[-1] == "":
            grid.pop()
        Laby(grid)
        return Laby.LAB

    @staticmethod
    def set_speed(s:int):
        '''
        règle la vitesse d'affichage, entre 1 et 100
        '''
        Laby.speed = max(min(s, 100), 1)
        if Laby.LAB is not None:
            Laby.LAB.display.update_speed(s)

    @property
    def display(self):
        return self.__display

    def __init__(self, grid): # constructeur
        '''
        grille = liste de chaînes de texte représentant le labyrinthe
        Le caractère espace est une case vide
        Tout autre caractère est un mur
        '''
        assert Laby.LAB is None, "vous ne pouvez pas initialiser deux fois le Laby !"
        Laby.LAB = self
        self.__grid = grid
        self.__height = len(grid)       # hauteur de la grille
        assert self.__height > 0          # hauteur de la grille pas nulle
        self.__width = len(grid[0])     # largeur de la grille
        assert self.__width > 0           # largeur de la grille pas nulle
        for ligne in grid:
            assert len(ligne) == self.__width # toutes les lignes ont la même longueur

        self.__clean_grid()

        # walker désigne le marcheur
        xS, yS = self.__get_start_pos()
        self.__walker = Walker(xS, yS, "D")

        self.__display = LabyDisplay(self.__width, self.__height)

        for y, line in enumerate(grid):
            for x, cell in enumerate(line):
                if cell == Laby.END_SYMB:
                    self.__display.draw_end(x,y)
                elif cell == Laby.WALL_SYMB:
                    self.__display.draw_wall(x,y)

        self.__display.draw_walker(self.__walker)
        self.__display.update_speed(Laby.speed)
        self.__display.affichage()



    def __clean_grid(self):
        '''
        remplace tous les caractères de la grille non autorisés par des murs
        '''
        for line in range(self.__height):
            for col in range(self.__width):
                cell = self.__grid[line][col]
                if cell not in self.NOT_WALLS:
                    self.__grid[line] = self.__grid[line].replace(cell, self.WALL_SYMB)

    def __get_start_pos(self):
        '''
        Renvoie la position x, y de la case de départ
        '''
        start_found = False # marqueur indiquant que départ n'est pas encore trouvé
        for x in range(self.__width):         # parcours des x
            for y in range(self.__height):     # parcours des y
                if self.__grid[y][x] == self.START_SYMB: # départ trouvé
                    assert not start_found     # assume qu'on en n'avait pas déjà un
                    x_start = x
                    y_start = y
        return x_start, y_start

    def __get_cell(self, line_index:int, col_index:int):
        '''
        line_index: numéro de ligne
        col_index: numéro de colonne
        renvoie le caractère à cette position dans la grille.
        Renvoie un mur les la position est hors grille
        '''
        if not (0 <= col_index < self.__width):
            return self.WALL_SYMB
        if not (0 <= line_index < self.__height):
            return self.WALL_SYMB
        return self.__grid[line_index][col_index]



    def __is_wall(self, line_index:int, col_index:int):
        '''
        line_index: numéro de ligne
        col_index: numéro de colonne
        '''
        cell = self.__get_cell(line_index, col_index)
        return cell == self.WALL_SYMB


    '''
    Fonctions à usage de l'utilisateur
    '''

    def avancer(self):
        '''
        Fait avancer le marcheur d'un pas en avant
        Erreur s'il y a un mur
        '''

        assert not self.mur_devant()
        self.__walker.go_forward()
        self.__display.draw_walker(self.__walker)
        self.__display.affichage()

    def tourner_gauche(self):
        '''
        Fait tourner le marcheur d'un quart de tour vers la gauche
        '''
        self.__walker.turn_left()
        self.__display.draw_walker(self.__walker)
        self.__display.affichage()

    def tourner_droite(self):
        '''
        Fait tourner le marcheur d'un quart de tour vers la droite
        '''
        self.__walker.turn_right()
        self.__display.draw_walker(self.__walker)
        self.__display.affichage()

    def est_A(self):
        '''
        Renvoie True si le marcheur est arrivé sur une case d'arrivée
        False sinon
        '''
        cell = self.__get_cell(self.__walker.y, self.__walker.x)
        return cell == self.END_SYMB

    def mur_devant(self):
        '''
        Renvoie True si le marcheur a un mur devant lui
        False sinon
        '''
        x_devant, y_devant = self.__walker.pos_forward()
        return self.__is_wall(y_devant, x_devant)

    def mur_a_gauche(self):
        '''
        Renvoie True si le marcheur a un mur sur sa gauche
        False sinon
        '''
        x_gauche, y_gauche = self.__walker.pos_left()
        return self.__is_wall(y_gauche, x_gauche)

    def mur_a_droite(self):
        '''
        Renvoie si le marcheur a un mur sur sa droite
        False sinon
        '''
        x_droite, y_droite = self.__walker.pos_right()
        return self.__is_wall(y_droite, x_droite)




def load(filename:str):
    '''
    filename: nom du fichier texte contenant le labyrinthe
    initialise le labyrinthe
    '''
    if Laby.LAB is not None:
        print("Erreur load : un labyrinthe est déjà chargé !")
        return
    Laby.load(filename)

def mur_devant() -> bool:
    '''
    renvoie True s'il y a un mur devant
    False sinon
    '''
    assert Laby.LAB is not None, "mur_devant: labyrinthe pas initialisé !"
    return Laby.LAB.mur_devant()

def mur_a_gauche() -> bool:
    '''
    renvoie True s'il y a un mur à gauche
    False sinon
    '''
    assert Laby.LAB is not None, "mur_a_gauche: labyrinthe pas initialisé !"
    return Laby.LAB.mur_a_gauche()

def mur_a_droite() -> bool:
    '''
    renvoie True s'il y a un mur à gauche
    False sinon
    '''
    assert Laby.LAB is not None, "mur_a_droite: labyrinthe pas initialisé !"
    return Laby.LAB.mur_a_droite()

def est_A() -> bool:
    '''
    renvoie True si on est sur une case A
    False sinon
    '''
    assert Laby.LAB is not None, "est_A: labyrinthe pas initialisé !"
    return Laby.LAB.est_A()

def tourner_gauche():
    '''
    Fait pivoter le marcheur d'un quart de tour à gauche
    '''
    assert Laby.LAB is not None, "tourner_gauche: labyrinthe pas initialisé !"
    Laby.LAB.tourner_gauche()

def tourner_droite():
    '''
    Fait pivoter le marcheur d'un quart de tour à droite
    '''
    assert Laby.LAB is not None, "tourner_droite: labyrinthe pas initialisé !"
    Laby.LAB.tourner_droite()

def avancer():
    '''
    Fait avancer le marcheur d'une case
    '''
    assert Laby.LAB is not None, "avancer: labyrinthe pas initialisé !"
    if mur_devant():
        print("Erreur avancer: mur devant !")
        return
    Laby.LAB.avancer()

def fin():
    '''
    Maintient l'affichage de la fenêtre en fin de programme
    '''
    assert Laby.LAB is not None, "fin: labyrinthe pas initialisé !"
    Laby.LAB.display.fin()

def speed(s):
    '''
    règle la vitesse d'affichage, de 1 à 100
    '''
    Laby.set_speed(s)