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

"""Clase Accion."""

import logging
import sys
from utils import *
import Idioma
import FechaHoraMine
import CronoMine
import xmlutils

log = logging.getLogger("Modulos")

# Información estática sobre todos los nombres de accion (excepto aceptar y cancelar). Sintaxis:
#
# nombres ::= { "base": nombres_clase }
#
# nombres_clase ::= { "nombre_nombre": info_nombre }
#
# info_nombre ::= [ ("nombre_parametro", tipo_parametro [, valor_defecto] ) ]
#
# tipo_parametro ::= string | numero | si_no | Personaje | Objeto | Sala | Idioma
#
NOMBRES = {

    "Personaje": {
	"mover":               [ ("id", "Personaje"), ("sala_destino", "Sala") ],
	"seguir":              [ ("id", "Personaje"), ("lider", "Personaje") ],
	"no_seguir":           [ ("id", "Personaje") ],
	"hablar":              [ ("id", "Personaje"), ("texto", "string"), ("idioma", "Idioma", "oestron") ],
	"susurrar":            [ ("id", "Personaje"), ("oyente", "Personaje"), ("texto", "string"), ("idioma", "Idioma") ],
	"gritar":              [ ("id", "Personaje"), ("texto", "string"), ("idioma", "Idioma") ],
	"actuar":              [ ("id", "Personaje"), ("texto", "string") ],
	"actuar_individual":   [ ("id", "Personaje"), ("espectador", "Personaje"), ("texto", "string") ],
	"actuar_uno_mismo":    [ ("id", "Personaje"), ("texto", "string") ],
	"coger":               [ ("id", "Personaje"), ("id_objeto", "string"), ("cantidad", "numero", "1"), ("n_orden", "numero", "1"), ("n_instancia", "numero", "1"), ("sigilo", "si_no", "no") ],
	"dejar":               [ ("id", "Personaje"), ("objeto", "Objeto"), ("cantidad", "numero", "1"), ("sigilo", "si_no", "no") ],
	"esconder":            [ ("id", "Personaje"), ("objeto", "Objeto"), ("cantidad", "numero", "1") ],
	"dar":                 [ ("id", "Personaje"), ("receptor", "Personaje"), ("objeto", "Objeto"), ("cantidad", "numero", "1") ],
	"comer":               [ ("id", "Personaje"), ("id_objeto", "string"), ("n_orden", "numero", "1"), ("n_instancia", "numero", "1"), ("sigilo", "si_no", "no") ],
	"beber":               [ ("id", "Personaje"), ("id_objeto", "string"), ("n_orden", "numero", "1"), ("n_instancia", "numero", "1"), ("sigilo", "si_no", "no") ],
	"usar":                [ ("id", "Personaje"), ("id_objeto", "string"), ("n_orden", "numero", "1"), ("n_instancia", "numero", "1"), ("sigilo", "si_no", "no") ],
	"usar_con":            [ ("id", "Personaje"), ("id_objeto", "string"), ("n_orden", "numero", "1"), ("n_instancia", "numero", "1"), ("id_objeto2", "string"), ("n_orden2", "numero", "1"), ("n_instancia2", "numero", "1"), ("sigilo", "si_no", "no") ],
	"ponerse":             [ ("id", "Personaje"), ("objeto", "Objeto"), ("posicion", "string") ],
	"quitarse":            [ ("id", "Personaje"), ("posicion", "string") ],
	"tirar_atributo":      [ ("id", "Personaje"), ("id_tirada", "string"), ("atributo", "string"), ("modif_atributo", "numero") ],
	"tirar_competicion":   [ ("id", "Personaje"), ("id_competicion", "string"), ("oponente", "Personaje"), ("atributo_este", "string"), ("atributo_oponente", "string") ],
	"cambiar_experiencia": [ ("id", "Personaje"), ("puntos", "numero") ],
	"cambiar_vida":        [ ("id", "Personaje"), ("puntos", "numero") ],
	"cambiar_atributo":    [ ("id", "Personaje"), ("nombre_atributo", "string"), ("niveles", "numero") ],
	"cambiar_idioma":      [ ("id", "Personaje"), ("idioma", "Idioma"), ("niveles", "numero") ],
	"cambiar_confianza":   [ ("id", "Personaje"), ("interlocutor", "Personaje"), ("niveles", "numero") ],
	"cambiar_renombre":    [ ("id", "Personaje"), ("puntos", "numero") ],
	"cambiar_infamia":     [ ("id", "Personaje"), ("puntos", "numero") ],
	"registrar_hazana":    [ ("id", "Personaje"), ("texto", "string"), ("anonimo", "si_no") ],
	"registrar_delito":    [ ("id", "Personaje"), ("texto", "string"), ("anonimo", "si_no") ],
	"alimentar":           [ ("id", "Personaje"), ("puntos_nutritivos", "numero") ],
	"hidratar":            [ ("id", "Personaje"), ("puntos_hidratantes", "numero") ],
	"cambiar_dinero":      [ ("id", "Personaje"), ("monedas", "numero") ],
	"pagar":               [ ("id", "Personaje"), ("otro_personaje", "Personaje"), ("monedas", "numero") ],
	"cambiar_contador":    [ ("id", "Personaje"), ("nombre_contador", "string"), ("incr_valor", "numero") ],
	"crear_contador":      [ ("id", "Personaje"), ("nombre_contador", "string"), ("valor", "numero") ],
	"borrar_contador":     [ ("id", "Personaje"), ("nombre_contador", "string") ],
	"cambiar_etiqueta":    [ ("id", "Personaje"), ("nombre_etiqueta", "string"), ("valor", "string") ],
	"crear_etiqueta":      [ ("id", "Personaje"), ("nombre_etiqueta", "string"), ("valor", "string") ],
	"borrar_etiqueta":     [ ("id", "Personaje"), ("nombre_etiqueta", "string") ]
    },

    "Objeto": {
	"mover":               [ ("id", "Objeto"), ("sala", "Sala") ],
	"cambiar_cargas":      [ ("id", "Objeto"), ("cargas", "numero") ],
	"cambiar_estado":      [ ("id", "Objeto"), ("puntos", "numero") ],
	"reemplazar":          [ ("id", "Objeto"), ("id_objeto_nuevo", "string") ],
	"actuar":              [ ("id", "Objeto"), ("texto", "string") ],
	"actuar_individual":   [ ("id", "Objeto"), ("espectador", "Personaje"), ("texto", "string") ],
	"cambiar_propietario": [ ("id", "Objeto"), ("nuevo_propietario", "Personaje") ],
	"destruir":            [ ("id", "Objeto") ],
	"cambiar_contador":    [ ("id", "Objeto"), ("nombre_contador", "string"), ("incr_valor", "numero") ],
	"crear_contador":      [ ("id", "Objeto"), ("nombre_contador", "string"), ("valor", "numero") ],
	"borrar_contador":     [ ("id", "Objeto"), ("nombre_contador", "string") ],
	"cambiar_etiqueta":    [ ("id", "Objeto"), ("nombre_etiqueta", "string"), ("valor", "string") ],
	"crear_etiqueta":      [ ("id", "Objeto"), ("nombre_etiqueta", "string"), ("valor", "string") ],
	"borrar_etiqueta":     [ ("id", "Objeto"), ("nombre_etiqueta", "string") ]

    },

    "Sala": {
	"ambiente":            [ ("id", "Sala"), ("texto", "string"), ("rango_salas", "numero", 0) ],
	"cambiar_aura":        [ ("id", "Sala"), ("puntos", "numero") ],
	"crear_objeto":        [ ("id", "Sala"), ("id_objeto", "string") ],
	"crear_pnj":           [ ("id", "Sala"), ("id_pnj", "string") ],
	"cambiar_contador":    [ ("id", "Sala"), ("nombre_contador", "string"), ("incr_valor", "numero") ],
	"crear_contador":      [ ("id", "Sala"), ("nombre_contador", "string"), ("valor", "numero") ],
	"borrar_contador":     [ ("id", "Sala"), ("nombre_contador", "string") ],
	"cambiar_etiqueta":    [ ("id", "Sala"), ("nombre_etiqueta", "string"), ("valor", "string") ],
	"crear_etiqueta":      [ ("id", "Sala"), ("nombre_etiqueta", "string"), ("valor", "string") ],
	"borrar_etiqueta":     [ ("id", "Sala"), ("nombre_etiqueta", "string") ]

    },

    "Area": {
	"ambiente":            [ ("id", "area"), ("texto", "string"), ("tipo_sala", "string"), ("excepciones", "string") ]
    },

    "Mundo": {
	"ambiente":            [ ("texto", "string"), ("tipo_sala", "string"), ("excepciones", "string") ]
    }
}

TIPOS_PARAMETROS = ( "string", "numero", "si_no", "Personaje", "Objeto", "Sala", "Idioma" )


class Accion(object):
    """Esta clase contiene la descripción de una accion.

    Una accion se caracteriza por una clase base y un "nombre" de accion, que se corresponde con una
    operación a ejecutar por una instancia de dicha clase base (por ejemplo, Personaje.hablar()).

    También tiene una lista de parámetros, cada uno con un nombre y un tipo (por ejemplo, 
    oyente: Personaje, texto: String).

    Hay dos acciones especiales con nombre "aceptar" y "cancelar". Estas dos acciones no tienen base
    ni llaman a ningún método de ninguna instancia. En lugar de esto, son marcadores para indicar si
    la acción que originó el suceso dentro de cual están se debe completar o cancelar.

    Una acción está descrita por un nodo XML contenido siempre en un fichero de descripción de sala,
    pnj, objeto, etc. Los valores de los parámetros se indican en dicho nodo, y se pueden expresar
    de varias maneras:

    a) Se puede indicar el valor directamente. Por ejemplo: texto="Hola", cantidad="7".
    b) Se puede indicar en forma de macro. Una macro es un nombre precedido por el signo $, y que
       representa un valor que se calculará en el momento de ejecución de la acción. Las macros
       disponibles dependen del contexto en el que se ha definido la acción. Por ejemplo, una acción
       puede ocurrir como respuesta al suceso de que un personaje *usa* un objeto de Minë. El contexto
       proporciona una lista de macros posibles, en este caso son $usuario (de tipo Personaje),
       $sala (de tipo Sala) y $objeto (de tipo Objeto). Entonces se podrían indicar los siguientes
       valores para los parámetros: personaje_destino="$usuario", id_sala="$sala".
    c) Se puede omitir el parámetro en el nodo XML. En este caso, se cogerá como valor la primera
       macro que se encuentre con un tipo coincidente al del parámetro.

    Finalmente, una accion se puede *ejecutar*. Para la ejecución se le deben pasar los valores de las
    macros del contexto. Por ejemplo, en el caso anterior la macro $usuario contendrá un puntero a la
    instancia de Personaje que está usando el objeto; etc.

    La misión de esta clase es doble:

    a) "Parsear" una accion a partir de un nodo XML contenido en un fichero de descripción de sala,
       pnj, objeto, etc.

    b) Ejecutar una accion, calculando los valores de la instancia base y de todos los parámetros,
       y llamando al método correspondiente de la instancia base.
    """

    # Atributos públicos de la clase

    def __leer_base(self): return self.__base

    base = property(__leer_base,
            doc="""(string): el nombre de la clase base de esta acción.

            Invariantes:
              base != None
            """)

    def __leer_nombre(self): return self.__nombre

    nombre = property(__leer_nombre,
            doc="""(string): el nombre de accion (o sea, la operación que se ejecuta).

            Invariantes:
              nombre != None
            """)


    def __leer_parametros(self): return self.__parametros 

    parametros = property(__leer_parametros,
              doc="""({String:any})[]: descripción de los parámetros.
              
                  Consiste en una lista de items, y cada item es un conjunto de
                  propiedades en forma de diccionario:
                   - "nombre" (string): nombre del parámetro.
                   - "valor" (any): puede ser un literal con el valor del parámetro,
                                    un dato que permita obtener el valor cuando se
                        active la accion (por ejemplo, el id de una sala)
                        o un macro (si comienza por $).
                   - "tipo" (string): indica de que tipo es el parámetro, y determina
                                      cómo se va a calcular el valor.

                  Invariantes:
                  nombre != None
                  valor != None
                  tipo in Accion.TIPOS_PARAMETROS
                  """)

    # Constructor

    def __init__(self, base, nombre, parametros):
	# Atributos privados:
	#
	# __crono_finalizar (CronoMine): para medir la duración de la acción
	# __cb_finalizar (funcion): función llamada al finalizar la acción
	self.__base = base
	self.__nombre = nombre
	self.__parametros = parametros
	self.__crono_finalizar = None
	self.__cb_finalizar = None


    # Métodos normales

#%%    def leer_parametro(self, nombre, default=None):
#%%        """Extrae el valor de un parametro a partir de su nombre"""
#%%        resultados = [parametro["valor"] \
#%%                        for parametro in self.__parametros \
#%%                        if parametro["nombre"] == nombre]
#%%        if resultados:
#%%            return resultados[0]
#%%        else:
#%%            return default


    def ejecutar(self, macros_valores, retardo = None, cb_finalizar = None):
        """Realiza la ejecución de la accion (es decir, que la accion ocurre).
    
        Aquí se calculan los valores reales de la ejecución. Los valores pueden incluir
        macros, que se resuelven con el parámetro "macros_valores" de esta función. Dicho
        parámetro es un diccionario con pares "$nombre_macro": valor.
    
        Ejemplo -> Hirunatan se bebe la pocion en la posada
        
           macros_valores: {"$usuario": <instancia Hirunatan>,
                            "$sala": <instancia posada01>)
                            "$objeto": <instancia pocion01>}

	Si se indican un retardo y una función callback, se establece la duración de la acción
	en función del retardo y del tipo de acción. Y al finalizar la misma, se llama al callback,
	sin parámetros.
    
        Requiere:
            macros_valores != None
	    retardo == None or retardo >= 0
        """
	# Si hay un temporizador pendiente, se cancela.
	self.cancelar()

        if self.__nombre == "aceptar" or self.__nombre == "cancelar":

		# Las acciones especiales no tienen más ejecución que calcular el resultado.
                result = (self.__nombre == "aceptar")
                
        else:
            
            # Resolver los parámetros, y colocarlos en un diccionario {nombre: valor}.
            # El parámetro llamado "id" no es en realidad un parámetro, sino la instancia
            # base sobre el cual se aplicará la operación.
            parametros_resueltos = {}
            instancia_base = None
            for parametro in self.__parametros:
            
                t = parametro["tipo"]
                v = parametro["valor"]
                
                if t == "numero":
                    if v and v[0] == "$":
                        valor_str = macros_valores[v]
                    else:
                        valor_str = v
                
                    if valor_str == "*" or valor_str == "+*":
                        valor = sys.maxint
                    elif valor_str == "-*":
                        valor = -sys.maxint
                    else:
                        valor = float(valor_str)
                
                elif t == "string":
                    if v and v[0] == "$":
                        valor = macros_valores[v]
                    else:
                        valor = v
                
                elif t == "si_no":
                    if v and v[0] == "$":
                        valor_str = macros_valores[v].lower()
                    else:
                        valor_str = v.lower()
                    if valor_str == 's' or valor_str == 'si' or valor_str == 'sí' \
                       or valor_str == '1':
                        valor = 1
                    else:
                        valor = 0
                    
                elif t == "Personaje":
                    if v and v[0] == "$":
                        valor = macros_valores[v]
                    else:
			sala = None
			valor = None
			
			for macro in macros_valores.values():
			    if macro.__class__.__name__ == "Sala":
				sala = macro
				break

			if sala == None:
			    raise ValueError("Ejecutada acción %s en un guion sin contexto de sala" %
			                      self.__nombre)

			for personaje in sala.personajes.values():
			    if personaje.id.split("%")[0].lower() == v.lower():
				valor = personaje
				break

			if valor == None:
			    raise ValueError("No se encuentra personaje con id %s en acción %s" %
			                     (v, self.__nombre))
                    
                elif t == "Sala":
                    if v and v[0] == "$":
                        valor = macros_valores[v]
                    else:
                        import Mundo
                        valor = Mundo.localizar_sala(v)
                    
                elif t == "Objeto":
                    if v and v[0] == "$":
                        valor = macros_valores[v]
                    else:
                        valor = None # %% La verdad, no sé qué hacer aquí :-?
                    
                elif t == "Idioma":
                    if v and v[0] == "$":
                        valor = macros_valores[v]
                    else:
                        valor = Idioma.buscar(v)
                
                else:
                    raise ValueError("Encontrado parametro %s con tipo desconocido: %s" % 
                                     (parametro["nombre"], t))
                
                if parametro["nombre"] == "id":
                    instancia_base = valor
                else:
                    parametros_resueltos[parametro["nombre"]] = valor
    
            # Obtener la función sobre la instancia base, y llamarla.
	    if self.base == "Mundo":
		log.info("Ejecutando accion %s.%s sobre el mundo " % (self.base, self.nombre))
		import Mundo
		funcion = getattr(Mundo, self.Nombre, None)
	    else:
		log.info("Ejecutando accion %s.%s sobre la entidad %s " % (self.base, self.nombre, instancia_base.id))
		funcion = getattr(instancia_base, self.nombre, None)
		
            if funcion != None:
                apply(funcion, (), parametros_resueltos)
	    else:
		log.warning("ATENCIÓN: acción %s.%s no implementada" % (self.base, self.nombre))
            
            result = True

	if retardo != None and cb_finalizar != None:
	    import Mundo
	    self.__cb_finalizar = cb_finalizar
	    self.__crono_finalizar = CronoMine.crear(Mundo.cronoMine_control,
		                                     FechaHoraMine.crear_rel(segundos = retardo),
					             self.__fin_accion)

	return result

    
    def cancelar(self):
	"""Cancela el temporizador. No se llama a la función de callback."""
	if self.__crono_finalizar != None:
	    self.__crono_finalizar.parar()
	    self.__crono_finalizar = None
	    self.__cb_finalizar = None
	    

    # Métodos privados

    def __fin_accion(self, crono):
	"""Registra que la acción ha terminado y llama al callback."""
	self.__cb_finalizar()
	self.__crono_finalizar = None
	self.__cb_finalizar = None


# Funciones públicas

def nueva_xml(elem_accion, macros_tipos):
    """Crea una accion nueva a partir de un nodo XML leido de un fichero de descripción.

    La estructura del nodo es:

      <accion base="xxx" nombre="xxx" PARAMETRO="xxx".../>

    Parámetros:
	elem_accion: el nodo XML
	macros_tipos ((string, string)[]): lista con los nombres y tipos de los macros
					   permitidas en el contexto de esta accion.

    Ejemplo -> accion dentro del uso "personaje_bebe" del objeto "pocion01"

      elem_accion: <accion base="Personaje" nombre="cambiar_vida" puntos="+15">
      macros_tipos: [("$usuario", "Personaje"), ("$sala", "Sala"), ("$objeto", "Objeto")]

    Requiere:
	elem_accion != None
	macros_tipos != None
    """
    (base, nombre, parametros) = _parsear_accion(elem_accion, macros_tipos)
    log.debug("Creada acción %s.%s. Parámetros:" % (base, nombre))
    if len(parametros) == 0:
	log.debug("  (vacío)")
    else:
	for param in parametros:
	    log.debug("  - %s(%s) = %s" % (param["nombre"], param["tipo"], str(param["valor"])))
    acc = Accion(base, nombre, parametros)
    return acc


def nueva_datos(base, nombre, parametros):
    """Crea una acción nueva aportando directamente sus datos.

    Requiere:
	base != None
	nombre != None
	parametros != None
	len(parametros) > 0
    """
    log.debug("Creada acción %s.%s automática. Parámetros:" % (base, nombre))
    if len(parametros) == 0:
	log.debug("  (vacío)")
    else:
	for param in parametros:
	    log.debug("  - %s(%s) = %s" % (param["nombre"], param["tipo"], str(param["valor"])))
    acc = Accion(base, nombre, parametros)
    return acc


def _parsear_accion(elem_accion, macros_tipos):
    """Extrae la información del elemento <accion>:
	- base
	- nombre
	- parametros

    Requiere:
	elem_accion != None
	macros_tipos != None
    """
    nombre = xmlutils.atributo(elem_accion, 'nombre')

    if nombre == "aceptar" or nombre == "cancelar":
	base = ""
	parametros = []
    else:
	# Obtenemos la información del nombre de accion (es decir, la lista
	# de parámetros y sus tipos correspondientes).
	base = xmlutils.atributo(elem_accion, 'base')
	try:
	    info_nombre = NOMBRES[base][nombre]
	except KeyError:
	    raise ValueError("No existe la accion %s.%s" % (base, nombre))
	
	# Obtenemos la lista de parámetros, sus valores y tipos.
	parametros = []
	
	for info_parametro in info_nombre:
	    param_nombre = info_parametro[0]
	    param_tipo = info_parametro[1]
	    param_valor = None
	    
	    if elem_accion.hasAttribute(param_nombre):
		# Si el atributo está presente en la definición XML, extraer el valor.
		param_valor = xmlutils.atributo(elem_accion, param_nombre)
		param_valor = " ".join(param_valor.split())
	    else:
		# Si no, inventarse un valor a partir del primer macro que tenga
		# un tipo compatible.
		for macro_tipo in macros_tipos:
		    if macro_tipo[1] == param_tipo:
			param_valor = macro_tipo[0]
			break
	    
	    if param_valor == None:
		# Si no hay ningún valor, ver si el parámetro tiene valor por defecto.
		if len(info_parametro) > 2:
		    param_valor = info_parametro[2]
		else:
		    raise ValueError("No se encuentra un valor para el parámetro %s de la accion %s.%s." %
			 (param_nombre, base, nombre))
	    
	    parametros.append({"nombre": param_nombre,
			       "valor": param_valor,
			       "tipo": param_tipo})

    return (base, nombre, parametros)


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

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

