#!/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: Modulo.py

"""Clase Modulo."""

import logging
from random import random
import os
import glob

from utils import *
import Opcion
import Guion
import Requisito
import Accion
import xmlutils

log = logging.getLogger("Modulos")

# Version del sistema de descripciones de módulo (se incrementa cada vez que cambia
# el formato de los ficheros de desc_modulos)
VERSION_DESC_MODULO = '1.0.0'

# Version del sistema de módulos (se incrementa cada vez que varian los atributos
# de la clase Modulo)
VERSION_MODULO = '1.0.0'

DIR_DESC_MODULOS = 'desc_modulos'
EXT_DESC_MODULOS = '.xml'

# Constantes especiales para las conversaciones
TEMA_NO_ENCONTRADO = 'tema_no_encontrado'
TEMA_AGOTADO = 'tema_agotado'
FUERA_DE_TEMA = 'fuera_de_tema'
INICIATIVA_PROPIA = 'iniciativa_propia'
CONVERSACION_GENERICA = 'conversacion_generica' 


class Modulo(object):
    """Esta clase representa un módulo, es decir, un conjunto de guiones.

    Un módulo es un ladrillo para la construccion de la Inteligencia Artificial de un PNJ. Se
    puede definir un comportamiento complejo de un personaje, un objeto, etc. a base de combinar
    módulos. Cada módulo almacena la coleccion de posibles respuestas de la entidad ante un
    suceso determinado. 
    
    Cuando se activa el suceso, el módulo escoge de entre los guiones almacenados el más
    adecuado y lo pone en marcha.

    La misión de esta clase es doble:

    a) "Parsear" un módulo a partir de una lista de nodos XML contenidos en un fichero de
       descripción de sala, pnj, objeto, etc., o de un fichero XML propio.

    b) Activar el módulo y seleccionar el guión mas adecuado para ejecutarlo.

    Un módulo está siempre bajo un nodo XML de suceso, dentro del descriptor de una sala, un
    objeto o un pnj. Puede estar definido inline, es decir, la lista de opciones está colocada
    directamente debajo del suceso, en cuyo caso el módulo no tiene fichero propio y el id es
    None. Ejemplo:

      (en DIR_DESC_SALAS/guarida_secreta/guarida_secreta-entrada.xml)
      <personaje_cruza>
        <opcion contexto="inicial">...aquí va un guión...</opcion>
	<opcion contexto="ya_cruzado">...otro guión...</opcion>
      </personaje_cruza>

    También puede estar en un fichero XML descriptor de módulo, debajo del directorio
    Modulo.DIR_DESC_MODULOS, y dentro de otro subdirectorio con el nombre del grupo.
    Ejemplo:

      (en DIR_DESC_SALAS/guarida_secreta/guarida_secreta-entrada.xml)
      <personaje_cruza>
        <modulo id="guarida_secreta-cruzar">
      </personaje_cruza>

      (en DIR_DESC_MODULOS/guarida_secreta/guarida_secreta-cruzar.xml)
      <desc_modulo id="guarida_secreta-cruzar" autor=...>
        <opcion contexto="inicial">...aquí va un guión...</opcion>
	<opcion contexto="ya_cruzado">...otro guión...</opcion>
      </desc_modulo>

    Observar que debajo del nodo de suceso también puede ir colocado directamente un
    guión, en cuyo caso no existe módulo. Se distingue este caso porque el primer nodo
    de debajo del suceso es distinto de <modulo> y de <opción>.
    """

    # Atributos públicos de la clase

    def __leer_id(self): return self.__id 

    id = property(__leer_id,
                  doc="""(string): identifica univocamente al módulo, si ha sido leido desde
                      un fichero. Si el módulo está descrito inline dentro de otro descriptor,
		      id = None.

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

    def __leer_version(self): return self.__version 

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

                           Invariante:
                               version != None
                           """)

    def __leer_autor(self): return self.__autor 

    autor = property(__leer_autor,
                     doc="""(string): identifica al autor del módulo.
                     
                         Invariante:
                             autor != None
                         """)

    def __leer_comentario(self): return self.__comentario 

    comentario = property(__leer_comentario,
                          doc="""(string): un metacomentario acerca del módulo.
                          
                              Invariante:
                                  comentario != None
                              """)


    def __leer_opciones(self): return self.__opciones

    opciones = property(__leer_opciones,
			doc="""(Opcion[]): lista de opciones de módulo

			Invariante:
			  opciones != None
			""")

    def __leer_agotado(self): return (self.mejor_opcion() == None)

    agotado = property(__leer_agotado,
	               doc="""(bool): indica si no hay ninguna opción disponible.""")


    # Constructor

    def __init__(self, id, macros_extra, fecha_fichero, version, autor, comentario, opciones):
        """Crea un nuevo módulo 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 guion, desde
        #                              el sitio en el fichero ha sido invocado.
        #
        # Invariantes:
        #   __id == None -> __fecha_fichero = None
        #   __id != None -> __fecha_fichero >= 0
        
	self.__id = id
	self.__macros_extra = macros_extra
	self.__fecha_fichero = fecha_fichero
	self.__version = version
	self.__autor = autor
	self.__comentario = comentario
	self.__opciones = opciones
	self.__guion_actual = None


    # Métodos normales

    def mejor_opcion(self):
	"""Devuelve la opción más adecuada en la situación actual."""

	# ESTO ESTÁ SIN HACER, ME TENGO QUE ENTERAR MEJOR DEL TEMA DE LOS CONTEXTOS.
	# De momento, devuelve la primera opción no agotada y ya ta.
	for (i, opcion) in enumerate(self.__opciones):
	    if not opcion.agotada():
		log.info("Elegida opción %d" % i)
		return opcion

	return None
    

    def ejecutar(self, macros_valores):
        """Ejecuta el guion en cuestion.

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

        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 módulo inline")
	else:
	    log.info("Ejecutando módulo %s" % self.__id)

        macros_total = macros_valores
        macros_total.update(self.__macros_extra)

	opcion = self.mejor_opcion()
	if opcion:
	    return opcion.ejecutar(macros_total)
	else:
	    return True
                       

    # 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 módulos leídos de fichero).
            if not self.__dict__.has_key("_Modulo__version"):
                raise ValueError("Cargado módulo " + self.id + " obsoleto: sin versión," +
                                 " cuando se esperaba " + VERSION_MODULO)

            assert(VERSION_MODULO == "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 módulo " + self.__id + " con versión desconocida: " +
                                 str(self.__version) + " cuando se esperaba " + VERSION_MODULO)


# Funciones públicas

def nuevo_xml(elem_suceso, macros_tipos):
    """Crea un módulo a partir de un nodo XML de suceso.
    
    El primer subnodo puede ser de tipo <opcion>. Entonces es un módulo inline, no tiene
    id y la lista de opciones está contenida directamente en los subnodos.

    El primer subnodo puede ser también <modulo id="xxx" macro1="yyy" macro2="zzz" />.
    Entonces se leen las macros extra de dicho nodo, luego se abre el fichero descriptor
    XML correspondiente, y se crea el módulo a partir de ese fichero.

    La estructura del fichero de descripción es:

      <desc_modulo id="xxx" version="xxx" autor="xxx" comentario="xxx">
        <opcion contexto="xxx" duracion_recuerdo="xxx" repeticiones="xxx">
	  ...guion...
	</opcion>
	...
      </desc_modulo>

    Finalmente, el primer subnodo puede ser otra cosa, en cuyo caso asumimos que no existe
    módulo y que el suceso contiene un único guión. Entonces esta función devuelve un guión
    en vez de un módulo.

    macros_tipos((string, string)[]) es una lista con los nombres y tipos de las macros
    definidas en el suceso. Serán pasadas a los guiones incluidos.

    Requiere:
	elem_suceso != None
	macros_tipos != None

    Asegura:
	return != None
    """

    if len(xmlutils.subelementos(elem_suceso)) > 0:
	primer_subnodo = xmlutils.subelementos(elem_suceso)[0]
	nombre_primer_subnodo = xmlutils.nombre_elemento(primer_subnodo)
    else:
	primer_subnodo = None
	nombre_primer_subnodo = ""

    if nombre_primer_subnodo == "opcion":
	# Módulo inline
	log.debug("Creando módulo inline")
	id = None
	macros_extra = {}
	fecha_fichero = None
	version = None
	autor = None
	comentario = None
	elem_raiz = elem_suceso
    elif nombre_primer_subnodo == "modulo":
	# Módulo en fichero
	(id, macros_extra, archivo) = _parsear_nodo_modulo(primer_subnodo)
	log.debug("Creando módulo %s" % id)
	(fecha_fichero, version, autor, comentario, elem_raiz) = _parsear_archivo(archivo)
    else:
	# Esto es extremadamente poco ortodoxo :-P, pero queda muy compacto. Supongo que
	# se podría hacer de otra manera, el que tenga ganas que lo mejore ;)
	return Guion.nuevo_xml(elem_suceso, macros_tipos, {})

    opciones = _parsear_opciones(elem_raiz, macros_tipos, macros_extra)

    modulo = Modulo(id, macros_extra, fecha_fichero, version, autor, comentario, opciones)

    log.debug("Módulo creado correctamente")

    return modulo


def nueva_conversacion(nombre, macros_tipos):
    """Carga una conversación completa, y devuelve los módulos.
    
    Una conversación es un grupo de módulos que sigue unas normas especiales: todos ellos
    están definidos en ficheros que se encuentran debajo de un directorios cuyo nombre es
    el nombre de la conversación. Los nombres de los módulos siguen la forma

	<nombre_conversacion>-preguntar_<tema>
	<nombre_conversacion>-decir_<tema>
	
    Es decir, cada módulo contiene la lista de posibles respuestas a un tema de conversación,
    que puede ser preguntado o dicho.
    
    Además hay cuatro módulos especiales:

	<nombre_conversacion>-tema_no_encontrado: se activa cuando alguien pregunta por un
						  tema que no existe.

	<nombre_conversacion>-tema_agotado: se activa cuando alguien pregunta por un tema
					    agotado (al módulo no le queda ninguna opción
					    sin agotar).

	<nombre_conversacion>-fuera_de_tema: se activa cuando alguien pregunta por un tema
					     con un contexto no permitido. A continuación se
					     activa el tema normal.

	<nombre_conversacion>-iniciativa_propia: se activa sin que nadie pregunte, a
						 iniciativa del pnj.

    Si el grupo de conversación no contiene ficheros para los módulos especiales, éstos se
    buscan en el grupo genérico.

    La lista de módulos se compone de tres diccionarios indexado por nombre de tema, uno para
    "preguntar", otro para "decir" y otro para los temas especiales.

    macros_tipos((string, string)[]) es una lista con los nombres y tipos de las macros
    definidas en los sucesos preguntar y decir. Serán pasadas a los guiones incluidos.

    Requiere:
	nombre != None
	macros_tipos != None

    Asegura:
	return != None
    """
    log.debug("Creando conversación %s" % nombre)

    preguntar = {}
    decir = {}
    especiales = {}

    directorio = os.path.join(DIR_DESC_MODULOS, nombre)

    for archivo in glob.glob(os.path.join(directorio, "%s-preguntar_*%s" % 
			     (nombre, EXT_DESC_MODULOS))):

	tema = archivo[len(directorio) + len(nombre) + len('-preguntar_') + 1: -len(EXT_DESC_MODULOS)]
	id = archivo[len(directorio) + 1: -len(EXT_DESC_MODULOS)]

	(fecha_fichero, version, autor, comentario, elem_raiz) = _parsear_archivo(archivo)
	opciones = _parsear_opciones(elem_raiz, macros_tipos, {})
	modulo = Modulo(id, {}, fecha_fichero, version, autor, comentario, opciones)

	log.debug("Cargado tema %s para preguntar, modulo %s." % (tema, id))
	preguntar[tema] = modulo

    for archivo in glob.glob(os.path.join(directorio, "%s-decir_*%s" % 
			     (nombre, EXT_DESC_MODULOS))):

	tema = archivo[len(directorio) + len(nombre) + len('-decir_') + 1: -len(EXT_DESC_MODULOS)]
	id = archivo[len(directorio) + 1: -len(EXT_DESC_MODULOS)]

	(fecha_fichero, version, autor, comentario, elem_raiz) = _parsear_archivo(archivo)
	opciones = _parsear_opciones(elem_raiz, macros_tipos, {})
	modulo = Modulo(id, {}, fecha_fichero, version, autor, comentario, opciones)

	log.debug("Cargado tema %s para decir, modulo %s." % (tema, id))
	decir[tema] = modulo

    for tema in ['tema_no_encontrado', 'tema_agotado', 'fuera_de_tema', 'iniciativa_propia']:

	try:
	    archivo = os.path.join(directorio, "%s-%s%s" % (nombre, tema, EXT_DESC_MODULOS))
	    id = archivo[len(directorio) + 1: -len(EXT_DESC_MODULOS)]
	    (fecha_fichero, version, autor, comentario, elem_raiz) = _parsear_archivo(archivo)
	except OSError:
	    # Si no existe el tema especial, lo buscamos en conversaciones genéricas
	    directorio2 = os.path.join(DIR_DESC_MODULOS, CONVERSACION_GENERICA)
	    archivo = os.path.join(directorio2, "%s-%s%s" % (CONVERSACION_GENERICA, tema, 
							     EXT_DESC_MODULOS))
	    id = archivo[len(directorio2) + 1: -len(EXT_DESC_MODULOS)]
	    (fecha_fichero, version, autor, comentario, elem_raiz) = _parsear_archivo(archivo)

	opciones = _parsear_opciones(elem_raiz, macros_tipos, {})
	modulo = Modulo(id, {}, fecha_fichero, version, autor, comentario, opciones)

	log.debug("Cargado tema %s especial, modulo %s." % (tema, id))
	especiales[tema] = modulo

    log.debug("Conversación creada correctamente")

    return (preguntar, decir, especiales)


def conversacion_nula(macros_tipos):
    """Crea una conversación para los PNJs que no tienen conversación definida.

    Contiene sólo los módulos genéricos.

    Requiere:
	macros_tipos != None

    Asegura:
	return != None
    """
    log.debug("Creando conversación nula")

    preguntar = {}
    decir = {}
    especiales = {}

    for nombre_especial in ['tema_no_encontrado', 'tema_agotado', 'fuera_de_tema', 'iniciativa_propia']:

	directorio = os.path.join(DIR_DESC_MODULOS, CONVERSACION_GENERICA)
	archivo = os.path.join(directorio, "%s-%s%s" % (CONVERSACION_GENERICA, nombre_especial, 
							EXT_DESC_MODULOS))
	id = archivo[len(directorio) + 1: -len(EXT_DESC_MODULOS)]
	(fecha_fichero, version, autor, comentario, elem_raiz) = _parsear_archivo(archivo)

	opciones = _parsear_opciones(elem_raiz, macros_tipos, {})
	modulo = Modulo(id, {}, fecha_fichero, version, autor, comentario, opciones)

	especiales[tema] = modulo

    log.debug("Conversación creada correctamente")

    return (preguntar, decir, especiales)


def _parsear_nodo_modulo(elem_modulo):
    """Analiza el nodo <modulo> y carga las macros extra y el nombre del archivo a cargar.

    Devuelve:
      - id
      - macros_extra
      - archivo
    """
    id = xmlutils.atributo(elem_modulo, 'id') 

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

    archivo = os.path.join(DIR_DESC_MODULOS, id.split('-')[0], id + EXT_DESC_MODULOS)

    return (id, macros_extra, archivo)


def _parsear_archivo(archivo):
    """Abre un archivo de módulo y analiza los datos del nodo <desc_modulo>.

    Devuelve:
      - fecha_fichero
      - version
      - autor
      - comentario
      - elem_raiz
    """
    fecha_fichero = os.path.getmtime(archivo)

    elem_raiz = xmlutils.parsear_fichero(archivo)

    version_desc_modulo = xmlutils.atributo(elem_raiz, 'version')
    if version_desc_modulo != VERSION_DESC_MODULO:
	raise ValueError("Version incorrecta del fichero descriptor de módulo " +
			 id + ": " + version_desc_modulo + " cuando se esperaba " +
			 VERSION_DESC_MODULO)
			 
    version = VERSION_MODULO
    
    autor = xmlutils.atributo_def(elem_raiz, 'autor', '')
    
    comentario = xmlutils.atributo_def(elem_raiz, 'comentario', '')

    return (fecha_fichero, version, autor, comentario, elem_raiz)


def _parsear_opciones(elem_raiz, macros_tipos, macros_extra):
    """Parsea la lista de nodos hijos del raíz, construyendo el conjunto de opciones.
    
    Devuelve:
      - opciones
    """
    opciones = []
    
    for elem in xmlutils.subelementos(elem_raiz, 'opcion'):
	opciones.append(Opcion.nueva_xml(elem, macros_tipos, macros_extra))

    return opciones


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

if (__name__ == '__main__'):
    archivo = os.path.join(DIR_DESC_MODULOS, "prueba" + EXT_DESC_MODULOS)
    elem_desc_modulo = xmlutils.parsear_fichero(archivo)
    macros_tipos = [ ("$usuario", "Personaje"),  ("$sala", "Sala"), ("$objeto", "Objeto") ]
    macros_extra = {}
    m = Modulo(elem_desc_modulo, macros_tipos, macros_extra)
    print m.id
    from mundo import Mundo        
    Mundo.inicializar()     
    for g in m._Modulo__opciones:
	print g
	g.ejecutar(macros_valores=None)
