#!/usr/bin/env python
# encoding: iso-8859-15
#
#Minë. Mundo Interactivo-Narrativo en Español [Tierra-Media]
#Copyright (C) 2002  Pablo Ruiz Múzquiz
#
#
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#Fichero: Guion.py

"""Clase Guion."""

import os

import logging
from utils import *
import Mundo
import Requisito
import Accion
import FechaHoraMine
import CronoMine
import xmlutils

log = logging.getLogger("Modulos")

# Version del sistema de descripciones de guión (se incrementa cada vez que cambia
# el formato de los ficheros de desc_guiones)
VERSION_DESC_GUION = '1.0.0'

# Version del sistema de guiones (se incrementa cada vez que varian los atributos
# de la clase Guion)
VERSION_GUION = '1.0.0'

DIR_DESC_GUIONES = 'desc_guiones'
EXT_DESC_GUIONES = '.xml'


class Guion(object):
    """Esta clase representa un guión, es decir, una secuencia de requisitos y acciones.

    Un guión es como un pequeño programa, compuesto por instrucciones y condiciones de tipo IF...
    THEN...ELSE. No tiene ningún tipo de bucles. Los guiones también se pueden anidar: es decir,
    un elemento de la secuencia puede ser un nodo <guion>. Si ocurre esto, en la ejecución del
    guion externo, el anidado se ejecutará como si fuera una llamada a una subrutina.

    La misión de esta clase es doble:

    a) "Parsear" un guión a partir de un conjunto de nodos XML.

    b) Ejecutar el guión, llamando por orden a todos los requisitos y acciones de la secuencia.

    La ejecución del guión se puede realizar de varias maneras:

    - Lineal: las acciones van ocurriendo en el orden especificado y de forma temporizada, con un
      retardo que depende del parámetro "retardo".
    - Aleatoria: las acciones también ocurren con un retardo, pero el orden es aleatorio.
    - Instantánea: las acciones ocurren por orden y todas seguidas, sin retardo.

    Los ficheros XML descriptores de guiones se encuentran situados debajo del directorio
    DIR_DESC_GUIONES, y dentro de otro subdirectorio con el nombre del grupo.
    Por ejemplo, el descriptor del guión con id calle_sucia-limpiar estará en 
    DIR_DESC_GUIONES/calle_sucia/calle_sucia-limpiar.xml.

    Los guiones también pueden estar descritos inline, directamente dentro del descriptor
    de otra cosa (una sala, un objeto, un pnj o un módulo). En ese caso, no tienen fichero
    propio y el id es None.

    El contexto del guion (es decir, el suceso que lo origina, o bien el guion que lo contiene)
    determina las macros que se pasarán a los requisitos y acciones que hay dentro. Además,
    cuando se hace una llamada a un guion de fichero, con un nodo <guion>, se puede indicar una
    lista adicional de macros (las macros extra) que estarán disponibles para el guión interno.
    Los valores de las macros extra se indican estáticamente en el propio nodo <guion>.

    Ejemplo::

       (fichero pan01.xml)
       ...
         <personaje_come>
	   <guion id="comida_generica-comer" puntos_hambre="-1" />
	 </personaje_come>
       ...

       (fichero comida_generica-comer.xml)
       ...
       <desc_guion id="comida_generica-comer" version=... />
         (las acciones que hay aquí tienen disponibles las macros $usuario, $sala y $objeto, por
	  estar dentro de un suceso personaje_come, y $puntos_hambre porque se ha definido esa macro
	  extra en la llamada al guion)
       </desc_guion>
       ...
    """

    # Atributos públicos de la clase

    def __leer_id(self): return self.__id 

    id = property(__leer_id,
                  doc="""(string): identifica univocamente al guión, si ha sido leido desde
                      un fichero. Si el guión es parte de otro fichero distinto, id = None.

		      No puede contener espacios y el formato debe ser:
		          nombre_de_grupo-identificador_de_guion
                      """)


    def __leer_version(self): return self.__version 

    version = property(__leer_version,
                       doc="""(int): la versión del sistema de guiones.

                           Invariantes:
                               version != None
                           """)


    def __leer_autor(self): return self.__autor 

    autor = property(__leer_autor,
                     doc="""autor (string): identifica al autor del guión.
                     
                         Invariantes:
                             autor != None
                         """)


    def __leer_comentario(self): return self.__comentario 

    comentario = property(__leer_comentario,
                          doc="""(string): un metacomentario acerca del guión.
                          
                              Invariantes:
                                  comentario != None
                              """)


    def __leer_ejecucion(self): return self.__ejecucion 

    ejecucion = property(__leer_ejecucion,
                          doc="""(string): tipo de ejecucion del guión.
                          
                              Invariantes:
                                  ejecucion in ["lineal", "aleatoria", "instantanea"]
                              """)


    def __leer_retardo(self): return self.__retardo 

    retardo = property(__leer_retardo,
                          doc="""(int): retardo base en segundos entre una acción y otra, puede
			      tener modificadores.
                          
                              Invariantes:
                                  retardo >= 0
                              """)


    def __leer_secuencia(self): return self.__secuencia

    secuencia = property(__leer_secuencia,
                    doc="""((Guion|Requisito|Accion)[]): lista de líneas del guion, cada línea puede
		    ser otro guión anidado, un requisito (que a su vez contiene otros guiones dentro
		    de <cumplido> y <no_cumplido>), o una acción.

                    Invariantes:
                        secuencia != None
                    """)


    # Constructor

    def __init__(self, id, macros_extra, fecha_fichero, version, autor, comentario, ejecucion,
	         retardo, secuencia):
        """Crea un nuevo guión a partir de sus atributos.

        Requiere:
            Los parámetros satisfacen las invariantes de los atributos.
        """

        # Atributos privados:
        #
        # __fecha_fichero (int): fecha de la ultima modificacion del fichero de descripción.
        # __macros_extra (string:any): macros que se pasan a la ejecución del guión, desde
        #                              el sitio en el fichero ha sido invocado.
	# __secuencia_generada ((Guion|Requisito|Accion)[]): secuencia de lineas, en el orden en
	#     que se van a ejecutar, según el parámetro ejecucion.
	# __linea_actual (Guion|Requisito|Accion): línea que se está ejecutando en modo temporizado.
	# __macros_total ({String:any}): copia de los valores de las macros mientras se está
	#                                ejecutando en modo temporizado
	# __retardo (int): retardo base en segundos para las acciones del guión.
	# __cb_finalizar (funcion): función llamada al terminar el guión.
        #
        # Invariantes:
        #   __id == None -> __fecha_fichero = None
        #   __id != None -> __fecha_fichero >= 0
	#   __macros_extra != None
	#   __secuencia_generada != None

	self.__id = id
	self.__macros_extra = macros_extra
	self.__fecha_fichero = fecha_fichero
	self.__version = version
	self.__autor = autor
	self.__comentario = comentario
	self.__ejecucion = ejecucion
	self.__retardo = retardo
	self.__secuencia = secuencia
	self.__secuencia_generada = None
	self.__linea_actual = None
	self.__macros_total = None
	self.__cb_finalizar = None
	self.__retardo = None


    # Métodos normales

    def ejecutar(self, macros_valores, cb_finalizar = None):
        """Ejecuta el guión en cuestion.

        macros_valores({String:any}) es un diccionario de los nombres de las macros a su valor
        final.

	Según el retardo y el tipo de ejecución del propio guión, se establece la duración de
	las acciones. Si cb_finalizar contiene una función de callback, ésta será llamada al
	terminar la ejecución, sin parámetros.

        Devuelve el resultado final de "aceptar" (1) o "cancelar" (0), o bien None
        si no se ha especificado (la serie no contiene ninguna de las dos acciones).

        Requiere:
            macros_valores != None
        """
	if self.__id == None:
	    log.info("Ejecutando guión inline de manera %s" % self.__ejecucion)
	else:
	    log.info("Ejecutando guión %s de manera %s" % (self.__id, self.__ejecucion))

	# Si hay otra ejecución en curso, se cancela.
	self.cancelar()

        macros_total = macros_valores
        macros_total.update(self.__macros_extra)

	if self.__ejecucion == "instantanea":

            resultado = None
	    for linea in self.__secuencia:
		r = linea.ejecutar(macros_total)
		if r != None:
		    resultado = r

	    if cb_finalizar != None:
		# Llamar al callback pero a través de un crono con retardo 0, para
		# permitir que se termine de ejecutar la función actual.
		CronoMine.crear(Mundo.cronoMine_control,
			        FechaHoraMine.crear_rel(segundos = 0),
				cb_finalizar)

	else:

	    self.__secuencia_generada = self.__generar_secuencia()
	    self.__macros_total = macros_total
	    self.__cb_finalizar = cb_finalizar
	    self.__siguiente_linea()
	    resultado = None

        return resultado
                
    
    def cancelar(self):
	"""Cancela la ejecución del guión temporizado. No se llama al callback."""
	if self.__linea_actual != None:
	    if self.__id == None:
		log.info("Cancelado guion inline")
	    else:
		log.info("Cancelado guion %s" % self.__id)
	    self.__linea_actual.cancelar()
	    self.__secuencia_generada = None
	    self.__linea_actual = None
	    self.__macros_total = None
	    self.__cb_finalizar = None


#%%    def activar(self):
#%%     """Realiza algunos chequeos periodicos de integridad del guión.
#%%     Los chequeos se pueden hacer, por ejemplo, al coger el guión.
#%%     """
#%%     archivo = os.path.join(DIR_DESC_GUIONES, self.__id + EXT_DESC_GUIONES)
#%%     if self.__fecha_fichero < os.path.getmtime(archivo):
#%%         # Si se modifica el fichero descriptor, recargarlo de nuevo
#%%         # reseteando completamente el guión
#%%         escribir("Recargando guión " + self.__id + " desde el fichero de descripcion")
#%%         self.__inicializar_guion(???????)


    # Métodos privados
    
    def __generador_secuencia(self):
	"""Construye un iterador sobre la secuencia de líneas, en un orden determinado por
	el parámetro "ejecución".
	"""
	if self.__ejecucion == "aleatoria":
	    lineas = shuffle(self.__secuencia[:])
	    for linea in lineas:
		yield linea
	else:
	    for linea in self.__secuencia:
		yield linea
        

#%%    def __calcular_retardo(self, linea):
#%%	"""Calcula el retardo producido por la línea en cuestión hasta la siguiente línea."""
#%%	if isinstance(linea, Guion):
#%%	    return 0
#%%	elif isinstance(linea, Requisito):
#%%	    return 0
#%%	else:
#%%	    return self.__retardo
#%%      
#%%	#%% Esto hay que repensarlo un poco más
#%%	#
#%%        #texto = linea.leer_parametro("texto")
#%%        #personaje = linea.leer_parametro("id","$interlocutor")
#%%        #idioma = linea.leer_parametro("idioma", "oestron")
#%%        #log.info("el texto vale: %s" % texto)
#%%        #log.info("el id es: %s" % personaje)
#%%        #log.info("el idioma es: %s" % idioma)
#%%        #log.info("el personaje es: %s" % self.__macros_valores[personaje])
#%%        #if personaje and personaje[0] == "$":
#%%        #    fluidez = 5 - self.__macros_valores[personaje].idiomas.get(idioma, 4)
#%%        #else:
#%%        #    sala = self.__macros_valores["$sala"]
#%%        #    fluidez = 5 - sala.personajes[personaje].idiomas.get(idioma, 4)
#%%        #
#%%        #return ((1.5 * len(texto) + self.__velocidad ) / float(self.__velocidad)) + fluidez
        

    def __siguiente_linea(self):
        """Ejecuta una línea del guión y establece el temporizador para la siguiente"""
	try:

	    self.__linea_actual = self.__sucesos.next()
	    self.__linea_actual.ejecutar(self.__macros_total, self.__retardo, 
		                         self.__siguiente_linea)

	except StopIteration:

	    # Final de la secuencia: limpia el estado y llama al callback registrado.
	    if self.__cb_finalizar:
		self.__cb_finalizar()
	    self.__secuencia_generada = None
	    self.__linea_actual = None
	    self.__macros_total = None
	    self.__cb_finalizar = None
	    self.__retardo = None


    # Métodos especiales

    def __setstate__(self, atributos):
        """Método llamado para restaurar los atributos provinientes de un pickle."""

        self.__dict__ = atributos
 
        if self.__id != None:
            # Verificar la versión (sólo para guiones leídos de fichero).
            if not self.__dict__.has_key("_Guion__version"):
                raise ValueError("Cargado guión " + self.id + " obsoleto: sin versión," +
                                 " cuando se esperaba " + VERSION_GUION)

            assert(VERSION_GUION == "1.0.0")
            while self.__version != "1.0.0":
     
                # Aquí se irá poniendo el código para convertir automáticamente a versiones futuras.

                raise ValueError("Cargado guión " + self.__id + " con versión desconocida: " +
                                 str(self.__version) + " cuando se esperaba " + VERSION_GUION)


# Funciones públicas

def nuevo_xml(elem_guion, macros_tipos, macros_extra):
    """Crea un guión a partir de un nodo XML.

    El nodo puede ser un suceso, una opción de módulo o una parte de un requisito.
    En estos casos se asume que es un guión inline, y la lista de elementos está
    directamente en los nodos hijos de éste. Ejemplo:

      (dentro de un fichero XML de descripción de objeto)
      ...
      <personaje_come>         <- elem_guion
        <requisito base="xxx" nombre="xxx" PARAMETRO="xxx"...>
	  <cumplido>
	     <accion base="xxx" nombre="xxx" PARAMETRO="xxx".../>
	  </cumplido>
	</requisito>
	<accion base="xxx" nombre="xxx" PARAMETRO="xxx".../>
      </personaje_come>
      ...
    
    También puede ser un nodo <guion id="xxx" macro1="yyy" macro2="zzz" />, incluido como
    línea de otro guion. Entonces se leen las macros extra de dicho nodo, luego se abre
    el fichero descriptor XML correspondiente, y se crea el guion a partir de ese fichero.

    La estructura del fichero de descripción es:

      <desc_guion id="xxx" version="xxx" autor="xxx" comentario="xxx"
                  ejecucion="xxx" retardo="xxx">
        ...guion...
      </desc_guion>

    macros_tipos ((string, string)[]) es una lista con los nombres y tipos de las
    macros permitidas en el contexto de este guión. Serán pasadas a los requisitos y
    acciones del guion, y también a los guiones incluidos.

    macros_extra ({string:any}) es un diccionario con los nombres y valores de las macros
    extra que tiene el guion contenedor. Si este guion está directamente debajo de un
    suceso, no hay macros extra, el parámetro es un {}.

    Requiere:
	elem_guion != None
	macros_tipos != None
	macros_extra != None
    """

    if xmlutils.nombre_elemento(elem_guion) != 'guion':
	# Guion inline
	log.debug("Creando guión inline")
	id = None
	fecha_fichero = None
	version = None
	autor = None
	comentario = None
	ejecucion = "instantanea"
	retardo = 0
	elem_raiz = elem_guion
    else:
	# Guion en fichero
	(id, macros_extra2, fecha_fichero, version, autor, comentario, ejecucion, 
	    retardo, elem_raiz) =  _parsear_nodo_guion(elem_guion)
	log.debug("Creando guión %s" % id)
	macros_extra.update(macros_extra2)

    log.debug("Macros-tipos:")
    if len(macros_tipos) == 0:
	log.debug("  (vacío)")
    else:
	for macro in macros_tipos:
	    log.debug("  - %s (%s)" % (macro[0], macro[1]))

    log.debug("Macros-extra:")
    if len(macros_extra) == 0:
	log.debug("  (vacío)")
    else:
	for nombre in macros_extra.keys():
	    log.debug("  - %s (%s)" % (str(nombre), macros_extra[nombre]))
	         # BUG RARO DE PYTHON: el nombre extraido de las keys es de tipo
		 # unicode, al juntarlo con el string de macros_extra casca si lleva
		 # acentos, por eso los convierto los dos a str

    secuencia = _parsear_secuencia(id, elem_raiz, macros_tipos, macros_extra)

    guion = Guion(id, macros_extra, fecha_fichero, version, autor, comentario, ejecucion,
		  retardo, secuencia)

    log.debug("Guion creado correctamente")

    return guion


def _parsear_nodo_guion(elem_guion):
    """Analiza el nodo <guion> y carga las macros extra. Luego carga el fichero y analiza
       el nodo <desc_guion>.

    Devuelve:
	- id
	- macros_extra
	- fecha_fichero
	- version
	- autor
	- comentario
	- ejecucion
	- retardo
	- elem_raiz
    """
    id = xmlutils.atributo(elem_guion, 'id')     

    macros_extra = {}
    for i in range(0, elem_guion.attributes.length):
	attr = elem_guion.attributes.item(i)
	if attr.name != 'id':
	    macros_extra["$" + attr.name] = xmlutils.atributo(elem_guion, attr.name)

    archivo = os.path.join(DIR_DESC_GUIONES, id.split('-')[0], id + EXT_DESC_GUIONES)
    fecha_fichero = os.path.getmtime(archivo)

    elem_raiz = xmlutils.parsear_fichero(archivo)

    version_desc_guion = xmlutils.atributo(elem_raiz, 'version')
    if version_desc_guion != VERSION_DESC_GUION:
	raise ValueError("Version incorrecta del fichero descriptor de guión " +
			 id + ": " + version_desc_guion + " cuando se esperaba " +
			 VERSION_DESC_GUION)
			 
    version = VERSION_GUION
    
    autor = xmlutils.atributo(elem_raiz, 'autor')
    comentario = xmlutils.atributo_def(elem_raiz, 'comentario', '')
    ejecucion = xmlutils.atributo_def(elem_raiz, 'ejecucion', 'instantanea')
    retardo = xmlutils.atributo_int_def(elem_raiz, 'retardo', '30',
  	            "Retardo incorrecto en guion %s. Tiene que ser un número" % id)

    return (id, macros_extra, fecha_fichero, version, autor, comentario, ejecucion, 
	    retardo, elem_raiz)


def _parsear_secuencia(id, elem_raiz, macros_tipos, macros_extra):
    """Parsea la lista de nodos hijos del raiz, construyendo la secuencia de elementos."""

    secuencia = []

    for elem in elem_raiz.childNodes:
	nombre = xmlutils.nombre_elemento(elem)
	if nombre == "guion":
	    secuencia.append(nuevo_xml(elem, macros_tipos, macros_extra))
	elif nombre == "requisito":
	    secuencia.append(Requisito.nuevo_xml(elem, macros_tipos, macros_extra))
	elif nombre == "accion":
	    secuencia.append(Accion.nueva_xml(elem, macros_tipos))
	elif nombre == "#text" or nombre == "#comment":
	    pass
	else:
	    raise AttributeError("Elemento incorrecto dentro de guion %s: leido %s cuando " 
				 "se esperaba accion, requisito, guion o modulo." %
				 (id, nombre))

    return secuencia


# Código para pruebas unitarias del módulo.

if (__name__ == '__main__'):
    print "No hay prueba unitaria de módulo. Prueba con objeto.py"

