'''
module: entrepot
'''

from tkinter import Tk, Canvas
from time import sleep

class TKEntrepot:
    '''
    Objet graphique dessinant l'entrepot
    '''
    __CELL_SIZE = 20
    __MIN_COMPART = 5
    __MARGE_TOP_CAISSE = 80
    __SORTIE_HEIGHT = 30
    __ROBOT_BLOC_SIZE = 20
    __ROBOT_CABLE = 30
    __SORTIE_COLOR = '#bbf'
    __ROBOT_COLOR = 'red'
    __CAISSE_COLOR = 'green'
    __FRAME_PER_MOVE = 0.1
    __VITESSE = 2

    def __init__(self, caisses, sortie, max_size, speed):
        '''
        caisses: 
        sortie: rampe de sortie. -1 pour pas de rampe
        max_size: nombre de caisses possibles dans chaque compartiment
        speed: vitesse d'affichage
        '''
        taille = len(caisses)
        assert taille >= self.__MIN_COMPART, \
            f"L'entrepot doit avoir au moins {self.__MIN_COMPART} compartiments."
        assert -1 <= sortie < taille
        assert max_size > 0
        if sortie != -1:
            caisses[sortie] = 0
        self.__size = taille
        self.__sortie = sortie
        self.__caisses_sprites_id = [[] for i in range(taille)]
        self.__max = max_size
        for i, c in enumerate(caisses):
            assert type(c) == int and c >= 0 and c <= self.__max
        self.set_speed(speed)

        self.__tk = Tk()                # système d'affichage dans une fenêtre
        self.__tk.title("Entrepot")     # titre de la fenêtre
        canvas_width = self.__CELL_SIZE * (self.__size + 2)
        if self.__sortie != -1:
            canvas_height = self.__CELL_SIZE * (self.__max + 2) \
                            + self.__MARGE_TOP_CAISSE \
                            + self.__SORTIE_HEIGHT
        else:
            canvas_height = self.__CELL_SIZE * (self.__max + 2) \
                            + self.__MARGE_TOP_CAISSE
        self.__canvas = Canvas(self.__tk, width = canvas_width, \
                               height = canvas_height , bd=0, bg="white")
        self.__canvas.pack(padx=10, pady=10)
        self.__position_robot = 0

        self.__draw_robot()
        self.__robot_cable = None
        self.__draw_cable()

        self.__draw_rampe_sortie()

        for i, c in enumerate(caisses):
            for k in range(c):
                self.__add_caisse_sprite(i)
        self.__robot_caisse = -1

    def __y_for_top_line_n(self,n):
        '''
        Calcule la position y pour le haut de la n-ième ligne de caisse
        '''
        return self.__MARGE_TOP_CAISSE + self.__CELL_SIZE * (self.__max - n + 1)

    def __draw_rampe_sortie(self):
        '''
        dessine la rampe de sortie
        '''
        if self.__sortie == -1:
            # rien à faire
            return
        x = (self.__sortie + 1) * self.__CELL_SIZE
        y = self.__y_for_top_line_n(1)
        x2 = self.__CELL_SIZE * (self.__size + 2)
        y2 = y + self.__SORTIE_HEIGHT
        self.__canvas.create_rectangle(
            x, y,
            x + self.__CELL_SIZE, y2,
            fill=self.__SORTIE_COLOR, width=0)
        self.__canvas.create_rectangle(
            x, y2,
            x2, y2 + self.__CELL_SIZE,
            fill=self.__SORTIE_COLOR, width=0)

    def __draw_robot(self):
        '''
        dessine le robot
        '''
        x = (self.__position_robot + 1) * self.__CELL_SIZE
        y = self.__CELL_SIZE
        self.__robot_bloc = self.__canvas.create_rectangle(
            x, y,
            x + self.__CELL_SIZE, y + self.__ROBOT_BLOC_SIZE,
            fill=self.__ROBOT_COLOR)

    def __draw_cable(self, yb = -1):
        '''
        redessine le cable
        '''
        x1, y1, x2, y2 = self.__canvas.coords(self.__robot_bloc)
        x = (x1 + x2)//2
        y = y2
        if y == -1 or self.__robot_cable is None:
            y_bottom = y + self.__ROBOT_CABLE
        else:
            y_bottom = yb
        if self.__robot_cable is not None:
            self.__canvas.delete(self.__robot_cable)
        self.__robot_cable = self.__canvas.create_line(x, y, x, y_bottom)

    def __add_caisse_sprite(self, indice_compartiment:int):
        '''
        ajoute une caisse dans le compartiment demandé,
        indice_compartiment: indice du compartiment demandé
        '''

        n = len(self.__caisses_sprites_id[indice_compartiment]) + 1
        x = (indice_compartiment + 1) * self.__CELL_SIZE
        y = self.__y_for_top_line_n(n)
        caisse_sprite_id = self.__canvas.create_rectangle(
            x, y,
            x + self.__CELL_SIZE, y + self.__CELL_SIZE,
            fill=self.__CAISSE_COLOR)
        self.__caisses_sprites_id[indice_compartiment].append(caisse_sprite_id)

    def __animation_sortie(self):
        '''
        animation de la caisse sur la rampe de sortie
        '''
        if self.__sortie == -1:
            return
        s = self.__caisses_sprites_id[self.__sortie] 
        if s == []:
            return
        caisse_sprite_id = s.pop()
        # mouvement vers le bas
        move_y = self.__SORTIE_HEIGHT
        dy = self.__VITESSE
        while move_y > 0:
            self.__canvas.move(caisse_sprite_id, 0, dy)
            move_y -= dy
            self.__tk.update()
            self.__pause(self.__FRAME_PER_MOVE)
        # mouvement vers la droite
        x_dest = (self.__size + 2) * self.__CELL_SIZE
        x, y, a, b = self.__canvas.coords(caisse_sprite_id)
        move_x = x_dest - x
        dx = self.__VITESSE
        while move_x > 0:
            self.__canvas.move(caisse_sprite_id, dx, 0)
            move_x -= dx
            self.__tk.update()
            self.__pause(self.__FRAME_PER_MOVE)
        # suppresion de la caisse
        self.__canvas.delete(caisse_sprite_id)

    def __animation_mouvement_cable(self, y_dest):
        '''
        séquence d'image faisant se dérouler le cable avec ou sans caisse
        '''
        assert self.__robot_cable is not None
        x1, y1, x2, yb = self.__canvas.coords(self.__robot_cable)
        if y_dest == yb:
            return
        if y_dest > yb:
            dy = self.__VITESSE
        else:
            dy = -self.__VITESSE
        while (y_dest - yb) * dy > 0:
            if self.__robot_caisse != -1:
                self.__canvas.move(self.__robot_caisse, 0, dy)
            self.__draw_cable(yb + dy)
            self.__tk.update()
            self.__pause(self.__FRAME_PER_MOVE)
            x1, y1, x2, yb = self.__canvas.coords(self.__robot_cable)

    def __animation_mouvement_horizontal(self, i_dest):
        self.__position_robot = i_dest
        x_dest = (i_dest + 1) * self.__CELL_SIZE
        x, y, a, b = self.__canvas.coords(self.__robot_bloc)
        if x == x_dest:
            return
        if x > x_dest:
            dx = - self.__VITESSE
        else:
            dx = self.__VITESSE
        while dx*(x_dest - x) > 0:
            self.__canvas.move(self.__robot_bloc, dx, 0)
            self.__canvas.move(self.__robot_cable, dx, 0)
            if self.__robot_caisse != -1:
                self.__canvas.move(self.__robot_caisse, dx, 0)
            self.__tk.update()
            self.__pause(self.__FRAME_PER_MOVE)
            x, y, a, b = self.__canvas.coords(self.__robot_bloc)

    def __animation_descente(self):
        '''
        animation d'une descente, avec ou sans caisse
        si caisse, elle est déposée
        '''
        s = self.__caisses_sprites_id[self.__position_robot]
        n = len(s)
        if self.__robot_caisse != -1:
            n += 1
        y_dest = self.__y_for_top_line_n(n)
        self.__animation_mouvement_cable(y_dest)
        if self.__robot_caisse != -1:
            s.append(self.__robot_caisse)
            self.__robot_caisse = -1

    def __animation_montee(self, prendre:bool):
        '''
        animation d'une remontée
        si prendre == True, on prend une caisse, si possible
        '''
        assert self.__robot_caisse == -1
        s = self.__caisses_sprites_id[self.__position_robot]
        n = len(s)
        if prendre and n > 0:
            self.__robot_caisse = s.pop()
        y_dest = self.__CELL_SIZE + self.__ROBOT_BLOC_SIZE + self.__ROBOT_CABLE
        self.__animation_mouvement_cable(y_dest)

    def __pause(self, frames):
        '''
        met l'affichage en pause. Chaque frame a une durée 1/tick secondes
        frames: nombre frames de pause
        '''
        sleep(frames/self.__tick)

    def set_speed(self, speed:int):
        '''
        règle la vitesse de l'animation
        au moins 1
        '''
        self.__tick = max(speed, 1)

    def prendre_caisse(self):
        '''
        lance l'animation de prise de caisse
        '''
        self.__animation_descente()
        self.__animation_montee(True)
    
    def deposer_caisse(self):
        '''
        lance l'animation de dépose de caisse
        '''
        self.__animation_descente()
        self.__animation_montee(False)
        self.__animation_sortie()

    def horizontal(self, i_dest):
        '''
        lance l'animation de mouvement horizontal vers l'indice i_dest
        '''
        self.__animation_mouvement_horizontal(i_dest)
    
    @property
    def position(self):
        '''
        renvoie l'indice de la position du robot
        '''
        return self.__position_robot

    @property
    def loaded(self):
        '''
        renvoie True si le robot est chargé
        '''
        return self.__robot_caisse != -1

    def get_caisses_number(self, i_compartiment:int) -> int:
        '''
        renvoie le nombre de caisses dans un compartiment
        '''
        assert 0 <= i_compartiment < len(self.__caisses_sprites_id)
        return len(self.__caisses_sprites_id[i_compartiment])

    def fin(self):
        '''
        fin de l'animation
        '''
        self.__tk.mainloop()
        # maintient l'affichage en fin

class Entrepot:
    '''
    Un entrepot est constitué de compartiments.
    - Chaque compartiment peut contenir des caisses jusqu'à un nombre maximal.
    - Un des compartiment peut servir de rampe de sortie : un tapis roulant vide
      ce compartiement en continu.
    - Un robot peut circuler d'un compartiment à l'autre
    - Le robot peut prendre une caisse dans un compartiment où y déposer une caisse
    - Le robot ne peut convoyer qu'une caisse à la fois
    '''
    __DEFAULT_SIZE = 10
    __DEFAULT_MAX = 5
    __DEFAULT_SPEED = 2

    def __init__(self):
        self.__speed = self.__DEFAULT_SPEED
        self.__sortie = -1
        self.__taille = self.__DEFAULT_SIZE
        self.__max = self.__DEFAULT_MAX
        self.__tk = None
        self.__caisses =  [0]

    def __init_tk(self):
        if self.__tk is not None:
            return
        c = self.__caisses.copy()
        assert len(c) == self.__taille
        self.__tk = TKEntrepot(c, self.__sortie, self.__max, self.__speed)

    def set_speed(self, s):
        '''
        définit la vitesse de l'animation
        '''
        if s > 0:
            self.__speed = s
        if self.__tk is not None:
            self.__tk.set_speed(self.__speed)
    
    def set_sortie(self, sortie:int):
        '''
        définit l'indice de compartiment recevant la rampe de sortie
        -1 pour pas de rampe
        sans effet après lancement de l'animation
        '''
        if self.__tk is not None:
            print("set_sortie: sans effet après le début de l'animation")
            return
        self.__sortie = -1 if sortie < 0 else sortie
        if self.__sortie > self.taille - 1:
            self.set_taille(self.__sortie+1)

    def set_taille(self, taille:int):
        '''
        définit le nombre de compartiments.
        sans effet après lancement de l'animation
        '''
        if self.__tk is not None:
            print("set_taille: sans effet après le début de l'animation")
            return
        self.__taille = min(taille, 1, len(self.__caisses))
        while self.__taille > len(self.__caisses):
            self.__caisses.append(0)

    def set_max(self, m:int):
        '''
        définit le nombre maximal de caisses pouvant être contenues dans un compartiment
        sans effet après lancement de l'animation
        '''
        if self.__tk is not None:
            print("set_max: sans effet après le début de l'animation")
            return
        self.__max = max(m, max(self.__caisses), 1)

    def set_caisses(self, caisses:list):
        '''
        définit le nombre de caisses dans les compartiments
        Par exemple si caisses = [1, 3, 2, 0], alors
          le compartiment 0 reçoit 1 caisse,
          le compartiment 1 reçoit 3 caisses,
          le compartiment 2 reçoit 2 caisses,
          le compartiment 3 reçoit 0 caisses,
        sans effet après lancement de l'animation
        '''
        if self.__tk is not None:
            print("set_caisses: sans effet après le début de l'animation")
            return
        if caisses == []:
            caisses = [0]
        self.__caisses = caisses
        if self.__taille < len(caisses):
            self.__taille = len(caisses)
        while self.__taille > len(self.__caisses):
            self.__caisses.append(0)
        
    @property
    def sortie(self) -> int:
        '''
        renvoie l'indice du compartiment recevant la rampe de sortie
        -1 si pas de rampe
        '''
        if self.__sortie < 0 or self.__sortie >= self.__taille:
            return -1
        return self.__sortie
    
    @property
    def taille(self) -> int:
        '''
        renvoie le nombre de compartiments de l'entrepot
        '''
        return self.__taille

    @property
    def position(self) -> int:
        '''
        renvoie la position courante du robot
        '''
        if self.__tk is None:
            return 0
        return self.__tk.position

    def caisses_dans_compartiment(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie le nombre de caisse dans le compartiment demandé
        indice_compartiment: indice du compartiment demandé
        Si pas indiqué, utilise la position courante du robot
        '''
        if indice_compartiment < 0:
            indice_compartiment = self.position
        if self.__tk is not None:
            return self.__tk.get_caisses_number(indice_compartiment)
        if 0 <= indice_compartiment < len(self.__caisses):
            return self.__caisses[indice_compartiment]
        return 0

    def caisse_présente(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment contient une caisse
        indice_compartiment: indice de la compartiment demandé.
        si pas indiqué, utilise la position courante du robot
        '''
        return self.caisses_dans_compartiment(indice_compartiment) > 0

    def compartiment_est_plein(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment demandé est plein (atteint le max)
        indice_compartiment: indice de la compartiment demandée
        si pas indiqué, utilise la position courante du robot
        '''
        return self.caisses_dans_compartiment(indice_compartiment) >= self.__max

    def compartiment_est_vide(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment demandé est vide
        indice_compartiment: indice du compartiment demandé
        Si pas indiqué, utilise la position courante du robot
        '''
        return self.caisses_dans_compartiment(indice_compartiment) == 0

    def compartiment_est_sortie(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment demandé est un dépôt
        indice_compartiment: indice du compartiment demandé
        Si pas indiqué, utilise la position courante du robot
        '''
        if indice_compartiment < 0:
            indice_compartiment = self.position
        return self.__sortie != -1 and self.__sortie == indice_compartiment

    def prendre_caisse(self):
        '''
        Le robot prend une caisse à la position courante
        '''
        self.__init_tk()
        self.__tk.prendre_caisse()

    def deposer_caisse(self):
        '''
        Le robot dépose une caisse à la position courante
        '''
        self.__init_tk()
        self.__tk.deposer_caisse()

    def gauche(self):
        '''
        Le robot fait un pas à gauche
        '''
        if self.position<=0:
            raise RuntimeError("Le robot est complètement à gauche et vous le faites poursuivre à gauche !") # noqa: E501
        self.__init_tk()
        self.__tk.horizontal(self.position - 1)

    def droite(self):
        '''
        Le robot fait un pas à droite
        '''
        if self.position >= self.taille-1:
            raise RuntimeError("Le robot est complètement à droite et vous le faites poursuivre à droite !") # noqa: E501
        self.__init_tk()
        self.__tk.horizontal(self.position + 1)
    
    def fin(self):
        '''
        assure le maintient de l'affichage en fin d'animation
        '''
        self.__init_tk()
        self.__tk.fin()
    
class Docks:
    '''
    raccourci pour désigner directement les docks de l'entrepot
    '''
    def __init__(self, e:Entrepot):
        self.__entrepot = e

    def set_sortie(self, sortie:int):
        '''
        définit l'indice de compartiment recevant la rampe de sortie
        -1 pour pas de rampe
        sans effet après lancement de l'animation
        '''
        self.__entrepot.set_sortie(sortie)

    def set_taille(self, taille:int):
        '''
        définit le nombre de compartiments.
        sans effet après lancement de l'animation
        '''
        self.__entrepot.set_taille(taille)

    def set_max(self, m:int):
        '''
        définit le nombre maximal de caisses pouvant être contenues dans un compartiment
        sans effet après lancement de l'animation
        '''
        self.__entrepot.set_max(m)

    def set_caisses(self, caisses:list):
        '''
        définit le nombre de caisses dans les compartiments
        Par exemple si caisses = [1, 3, 2, 0], alors
          le compartiment 0 reçoit 1 caisse,
          le compartiment 1 reçoit 3 caisses,
          le compartiment 2 reçoit 2 caisses,
          le compartiment 3 reçoit 0 caisses,
        sans effet après lancement de l'animation
        '''
        self.__entrepot.set_caisses(caisses)

    @property
    def taille(self) -> int:
        '''
        renvoie le nombre de compartiments de l'entrepot
        '''
        return self.__entrepot.taille

    @property
    def sortie(self) -> int:
        '''
        renvoie l'indice du compartiment recevant la rampe de sortie
        -1 si pas de rampe
        '''
        return self.__entrepot.sortie

    def fin(self):
        '''
        assure le maintient de l'affichage en fin d'animation
        '''
        self.__entrepot.fin()

    def occupation(self, indice_compartiment:int=-1) -> int:
        '''
        renvoie le nombre de caisse dans le compartiment demandé
        indice_compartiment: indice du compartiment demandé
        Si pas indiqué, utilise la position courante du robot
        '''
        return self.__entrepot.caisses_dans_compartiment(indice_compartiment)

    def est_plein(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment demandé est plein (atteint le max)
        indice_compartiment: indice de la compartiment demandée
        si pas indiqué, utilise la position courante du robot
        '''
        return self.__entrepot.compartiment_est_plein(indice_compartiment)

    def est_vide(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment demandé est vide
        indice_compartiment: indice du compartiment demandé
        Si pas indiqué, utilise la position courante du robot
        '''
        return self.__entrepot.compartiment_est_vide(indice_compartiment)

    def est_sortie(self, indice_compartiment:int=-1) -> bool:
        '''
        renvoie True si le compartiment demandé est un dépôt
        indice_compartiment: indice du compartiment demandé
        Si pas indiqué, utilise la position courante du robot
        '''
        return self.__entrepot.compartiment_est_sortie(indice_compartiment)

class Robot:
    '''
    raccourci pour désigner directement le robot de l'entrepot
    '''
    def __init__(self, e:Entrepot):
        self.__entrepot = e
    
    def fin(self):
        '''
        assure le maintient de l'affichage en fin d'animation
        '''
        self.__entrepot.fin()

    def prendre(self):
        '''
        prend une caisse à la position courante
        précondition: le robot ne porte pas déjà une caisse,
        '''
        self.__entrepot.prendre_caisse()

    def poser(self):
        '''
        pose la caisse à la position courante
        '''
        self.__entrepot.deposer_caisse()

    def gauche(self):
        '''
        déplace le robot d'un compartiment à gauche
        '''
        self.__entrepot.gauche()

    def droite(self):
        '''
        Déplace le robot d'un compartiment à droite
        '''
        self.__entrepot.droite()

    @property
    def position(self):
        '''
        renvoie la position courante du robot
        '''
        return self.__entrepot.position

    def set_speed(self, s):
        '''
        définit la vitesse de l'animation
        '''
        self.__entrepot.set_speed(s)

entrepot = Entrepot()
robot = Robot(entrepot)
docks = Docks(entrepot)

if __name__ == '__main__':
    robot.set_speed(10)
    docks.set_sortie(9)
    docks.set_caisses([1, 3, 2, 0, 0, 0, 5])
    robot.prendre()
    robot.droite()
    robot.droite()
    robot.gauche()
    for i in range(8):
        robot.droite()
    robot.poser()
    robot.fin()



