# Herencia dinámica en Python


import sys
from datetime import datetime


"""
Crea un programa en Python que modele el proceso de un paquete.
Este tiene las propiedades de solo lectura direccion y peso,
cuyos valores se asignan en el momento de crearlo.
Habrá un archivo de log llamado "paquetes.log" en el que se recogerán
los estados de cada paquete. Cada entrada se prefija con la fecha
(en formato ISO 8601: yyyy-mm-dd) y la hora.
Cada entrada debe contener el nuevo estado como #PRE#, #ENV#, #TRA# y #ENT#.
El coste puede asignarse posteriormente mediante el setter "_coste".
La interfaz del paquete es: envia(), entrega() y retorna().
Los estados por los que pasa el paquete son los de:
preparación, listo para el envío, tránsito, y entregado.
El estado de preparación consiste en el cálculo del coste del envío,
una vez hecho el cálculo se añade su información en el log.
En el estado de preparado para el envío, está hasta que el paquete se envía,
momento en el que este se añade al log. En el estado de tránsito,
puede pasar a preparación de nuevo si se retorna (se añade al log de retornos),
o puede pasar al estado de entregado si se entrega, momento en el que
el paquete se añade al log.
"""


class Log:
    ARCHIVO_LOG = "" # "paquetes.log"
    
    @staticmethod
    def inscribe(pq: "Paquete"):
        def now():
            dt = datetime.now()
            tm = dt.time()
            return f"{dt.date()} {dt.hour:02d}:{dt.minute:02d}:{dt.second:02d}"
        
        log_entry = f"{now()} {pq}\n"
        
        if not Log.ARCHIVO_LOG:
            sys.stdout.write(log_entry)
        else:                   
            with open(Log.ARCHIVO_LOG, "at") as f:
                f.write(log_entry)

class Paquete:
    """Representa un paquete de correos."""
    def __init__(self, direccion: str, peso: float):
        """Crea un nuevo paquete de correos.
            :param direccion: La dirección de envío.
            :param peso: El peso, en kg.
        """
        self.__direccion = direccion
        self.__peso = peso
        self.__coste = 0
        
    @property
    def direccion(self) -> str:
        return self.__direccion
    
    @property
    def peso(self) -> float:
        return self.__peso
    
    @property
    def coste(self) -> float:
        return self.__coste
    
    @coste.setter
    def _coste(self, v: float):
        self.__coste = v
    
    def __str__(self):
        return f"Pq {self.coste:5.2f}€ {self.peso:5.2f}kg./>\"{self.direccion}\""


class PaqueteEnPreparacion(Paquete):
    COSTE_POR_KG = 1.10
    
    def __init__(self, direccion, peso):
        """Todos los nuevos paquetes se crean como paquete en preparación.
            :param direccion: La dirección de envío.
            :param peso: El peso, en kg.
        """
        super().__init__(direccion, peso)
        
    def prepara(self):
        self.calcula_coste()
        self.__class__ = PaqueteListoEnvio
        Log.inscribe(self)
        
    def envia(self):
        raise ValueError("El paquete no está listo para su envío.")
    
    def retorna(self):
        raise ValueError("El paquete no ha sido enviado.")
    
    def entrega(self):
        raise ValueError("El paquete no ha sido enviado.")
    
    def calcula_coste(self):
        self._coste = self.peso * PaqueteEnPreparacion.COSTE_POR_KG
        return self.coste
    
    def __str__(self):
        return "#PRE# " + super().__str__()



class PaqueteListoEnvio(Paquete):
    """Representan paquetes que están listos para su envío.
       No se crean objetos de esta clase directamente,
       sino que llegan desde PaqueteEnPreparacion.prepara().
    """
    def envia(self):
        self.__class__ = PaqueteEnTransito
        Log.inscribe(self)
    
    def retorna(self):
        raise ValueError("El paquete no ha sido enviado.")
    
    def entrega(self):
        raise ValueError("El paquete no ha sido enviado.")
    
    def __str__(self):
        return "#ENV# " + super().__str__()
    
    
class PaqueteEnTransito(Paquete):
    """Representan paquetes que están en camino.
       No se crean objetos de esta clase directamente,
       sino que llegan desde PaqueteEnPreparacion.prepara().
    """
    def envia(self):
        raise ValueError("El paquete ya ha sido enviado.")
    
    def retorna(self):
        self.__class__ = PaqueteEnPreparacion
        Log.inscribe(self)
        
    def entrega(self):
        self.__class__ = PaqueteEntregado
        Log.inscribe(self)
            
    def __str__(self):
        return "#TRA# " + super().__str__()


class PaqueteEntregado(Paquete):
    """Representan paquetes que han sido entregados.
       No se crean objetos de esta clase directamente,
       sino que llegan desde PaqueteEnPreparacion.prepara().
    """
    def envia(self):
        raise ValueError("El paquete ya ha sido enviado.")
    
    def retorna(self):
        raise ValueError("El paquete ya ha sido recepcionado.")
    
    def entrega(self):
        raise ValueError("El paquete ya ha sido entregado.")
    
    def __str__(self):
        return "#ENT# " + super().__str__()


if __name__ == "__main__":
    pq = PaqueteEnPreparacion("Rue Percebe, 13", 2.5)
    
    print(pq)
    pq.prepara()
    
    print(pq)
    pq.envia()
    
    print(pq)
    pq.entrega()
    
    print(pq)

