import threading
import random
import time
import string
#
#   BANCONE DEL BAR
#
#   Questo codice simula il workflow tipico dell'arrivo dei clienti in un bar
#   e la loro gestione da parte del barista.
#
#   Il bancone del bar è rappresentato da un vettore di liste, dove ogni lista
#   rappresenta una "colonna" del bancone e cioè un certo numero di clienti che si accodano sulla stessa fila
#   Ogni colonna può contenere al massimo 
#   un numero di elementi pari al numero di "righe" del bancone.
#
#   I clienti che arrivano al bar vengono inseriti in una delle colonne del bancone
#   tra quelle che hanno meno elementi. Se ci sono più colonne con lo stesso
#   numero minimo di elementi, viene scelta una di queste a caso. Se il bancone è pieno,
#   la procedura di inserimento viene messa in attesa.
#
#   Il barista, quando è libero, prende un cliente a caso che trova sulla fila 0
#   e lo serve.  Se non ci sono clienti, la procedura di estrazione viene posta in attesa.
#
#  Ad esempio, lo stato del bancone in un certo momento potrebbe essere:
#
#   OOOOO
#   OO-OO
#   OO--O    
#   -O--O
# 
#  dove O indica che c'è un cliente e - indica che la posizione corrispondente è vuota. Il barista
#  serve prima i clienti sulla prima riga, scegliendo a caso tra quelli che trova.
#  
#  I clienti in arrivo preferiscono accodarsi sulle colonne che hanno meno elementi.  
#    

#
# Classe di supporto per la gestione di un elemento del bancone. 
#
class DatiElemento:
    def __init__(self,invisibile,elemento):
        self.invisibile = invisibile
        self.elemento = elemento
        self.condition = None
        self.monitorato = False 
        self.estratto = False
 

class BanconeBar:
    def __init__(self, righe, colonne):
        self.righe = righe
        self.colonne = colonne
        self.bancone = [[] for _ in range(colonne)]
        self.lock = threading.Lock()
        self.ceElemento = threading.Condition(self.lock)
        self.cePostoLibero = threading.Condition(self.lock)

    def __cisonoElementi(self):
        for c in range(self.colonne):
            if len(self.bancone[c]) > 0:
                return True
        return False
    
    def __tuttoPieno(self):
        for c in range(self.colonne):
            if len(self.bancone[c]) < self.righe:
                return False
        return True
    
    def __getIndiciFilePiuCorte(self):
        minimo = len(self.bancone[0])
        for i in range(1, self.colonne):
            if len(self.bancone[i]) < minimo:
                minimo = len(self.bancone[i])
        return [i for i in range(self.colonne) if len(self.bancone[i]) == minimo]
    
    def get_originale(self):
        with self.lock:
            while not self.__cisonoElementi():
                self.ceElemento.wait()

            indici_non_nulli = [i for i in range(self.colonne) if len(self.bancone[i]) > 0]
            indice_scelto = random.choice(indici_non_nulli)
            elemento = self.bancone[indice_scelto].pop(0)
            self.cePostoLibero.notify_all()
            return elemento
    #
    # Soluzione non ottimale al punto "Uomo invisibile". Si rappresenta un cliente invisibile con "*"
    #
    def get_sciue_sciue(self):
        with self.lock:
            while not self.__cisonoElementi():
                self.ceElemento.wait()

            #indici_non_nulli = [i for i in range(self.colonne) if len(self.bancone[i]) > 0]
            
            indici_da_cui_scegliere = [i for i in range(self.colonne) if len(self.bancone[i]) > 0 and self.bancone[i][0] != '*']
            
            if len(indici_da_cui_scegliere) == 0:
                indici_da_cui_scegliere = [i for i in range(self.colonne) if len(self.bancone[i]) > 0 and self.bancone[i][0] == '*']
            

            indice_scelto = random.choice(indici_da_cui_scegliere)
            elemento = self.bancone[indice_scelto].pop(0)
            self.cePostoLibero.notify_all()
            return elemento
    #
    # Get modificata per gestire clienti invisibili ed elementi monitorati
    #
    def get(self):
        with self.lock:
            while not self.__cisonoElementi():
                self.ceElemento.wait()

            #
            # Esploro la situazione in prima fila, raccogliendo prima la posizione dei visibili ed eventualmente quella degli invisibili
            #
            indiciDaCuiScegliere = [i for i in range(self.colonne) if len(self.bancone[i]) > 0 and not self.bancone[i][0].invisibile]
            #
            # Se non ci sono elementi visibili, prendo quelli invisibili
            #
            if len(indiciDaCuiScegliere) == 0:
                indiciDaCuiScegliere = [i for i in range(self.colonne) if len(self.bancone[i]) > 0 and self.bancone[i][0].invisibile]
            
            
            indice_scelto = random.choice(indiciDaCuiScegliere)
            datiElemento = self.bancone[indice_scelto].pop(0)
            self.cePostoLibero.notify_all()
            datiElemento.estratto = True
            if datiElemento.monitorato:
                datiElemento.condition.notify_all()

            return datiElemento.elemento
        
   

    def put(self, elemento):
        with self.lock:
            while self.__tuttoPieno():
                self.cePostoLibero.wait()
            #
            # Per gestire l'invisibilità casuale e i meccanismi di attesa legati al Punto 3, incapsulo l'elemento in un oggetto DatiElemento
            # In questa maniera tutti questi aspetti saranno del tutto trasparenti al codice che usa la classe BanconeBar
            #
            d = DatiElemento(random.random() >= 0.9, elemento)    
            self.bancone[random.choice(self.__getIndiciFilePiuCorte())].append(d)
            self.ceElemento.notify_all()
        
    
    
    def print_bancone(self):
        with self.lock:
            for r in range(self.righe):
                for c in range(self.colonne):
                    if len(self.bancone[c]) >= r+1:
                        toPrint = self.bancone[c][r].elemento
                        #
                        # Se l'elemento è invisibile, lo stampo in minuscolo
                        #
                        if self.bancone[c][r].invisibile:
                            toPrint = toPrint.lower()
                    else:
                        toPrint = '-'
                    print(toPrint, end = '') 
                print()

    def miglioraPosizione(self,r,c):
        with self.lock:
            if 0 <= r < len(self.bancone[c]):
                colonnaMigliore = c
                migliorRiga = r
                
                if c-1 >= 0 and len(self.bancone[c-1]) < migliorRiga:
                    colonnaMigliore = c-1
                    migliorRiga = len(self.bancone[c-1])

                if c+1 < self.colonne and len(self.bancone[c+1]) < migliorRiga: 
                    colonnaMigliore = c+1
                    migliorRiga = len(self.bancone[c+1])
                
                if colonnaMigliore != c:
                    self.bancone[colonnaMigliore].append(self.bancone[c].pop(r))


    def __is_in_bancone(self,E):
            trovato = False
            for c in range(self.colonne):
                for r in range(len(self.bancone[c])):
                    if self.bancone[r][c].elemento is E:
                        trovato = True
                        break
            return trovato

    def __get_elemento(self,E):
            ret_elemento = None
            for c in range(self.colonne):
                for r in range(len(self.bancone[c])):
                    if self.bancone[r][c].elemento is E:
                        ret_elemento = self.bancone[r][c]
                        break
            return ret_elemento
    
    def attendiServizio_dignitosa(self,E):

        with self.lock:

            trovato = self.__is_in_bancone(E)
            if trovato:            
                while self.__is_in_bancone(E):
                    self.cePostoLibero.wait()

            return trovato

    def attendiServizio(self,E):

        with self.lock:

            dato_elemento = self.__get_elemento(E)
            trovato = dato_elemento != None
            if dato_elemento != None:      

                dato_elemento.monitorato = True
                if dato_elemento.condition == None:
                    dato_elemento.condition = threading.Condition(self.lock)
                
                while self.__get_elemento(E) != None:
                    dato_elemento.condition.wait()

            return trovato
def prendi_elementi(bancone):
    while True:
        elemento = bancone.get()
        print("Elemento prelevato:", elemento)
        time.sleep(1)  # Simula un tempo di elaborazione

def inserisci_elementi(bancone):
    while True:
        elemento = random.choice(string.ascii_uppercase)
        bancone.put(elemento)
        #
        # Thread inseritore modificato per testare i nuovi metodi
        #
        print(f"Elemento inserito: {elemento}")
        if random.random() >= 0.95:
            r = random.randint(0, bancone.righe-1)
            c = random.randint(0, bancone.colonne-1)
            print(f"Provo a migliorare posizione ({r},{c})")
            bancone.miglioraPosizione(r,c)
        if random.random() >= 0.95:
            print(f"Attendo che venga servito elemento {elemento}")
            if bancone.attendiServizio(elemento):
                out = ""
            else: 
                out = "già "
            print(f"Fine attesa elemento {out}servito:", elemento)
        time.sleep(0.5)  # Simula un tempo di elaborazione

def stampa_bancone(bancone):
    while True:
        bancone.print_bancone()
        time.sleep(1)

bancone = BanconeBar(7, 5)


#
# Un modo diverso per creare i thread senza dovere dichiarare una classe a parte,
# consiste nel passare come target una funzione che si vuole eseguire al posto del metodo run
#
thread_barista = threading.Thread(target=prendi_elementi, args=(bancone,))
thread_barista.start()

thread_creaClienti = threading.Thread(target=inserisci_elementi, args=(bancone,))
thread_creaClienti.start()


thread_stampab = threading.Thread(target=stampa_bancone, args=(bancone,))
thread_stampab.start()

