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

"""Clase Requisito."""

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

log = logging.getLogger("Modulos")

# Información estática sobre todos los nombres de requisito. 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": {
	"nombre":                [ ("id", "Personaje"), ("comparador", "string"), ("valor", "string") ],
	"nivel":                 [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"vida":                  [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"volumen":               [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"estado":                [ ("id", "Personaje"), ("comparador", "string"), ("valor", "string") ],
	"aura":                  [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"atributo":              [ ("id", "Personaje"), ("nombre_atributo", "string"), ("comparador", "string"), ("valor", "numero") ],
	"idioma":                [ ("id", "Personaje"), ("idioma", "Idioma"), ("comparador", "string"), ("valor", "numero") ],
	"renombre":              [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"infamia":               [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"es_jugador":            [ ("id", "Personaje"), ("comparador", "string"), ("valor", "si_no") ],
	"es_maia":               [ ("id", "Personaje"), ("comparador", "string"), ("valor", "si_no") ],
	"raza":                  [ ("id", "Personaje"), ("comparador", "string"), ("valor", "string") ],
	"profesion":             [ ("id", "Personaje"), ("comparador", "string"), ("valor", "string") ],
	"sexo":                  [ ("id", "Personaje"), ("comparador", "string"), ("valor", "string") ],
	"objeto_en_mochila":     [ ("id", "Personaje"), ("nombre_objeto", "string"), ("comparador", "string"), ("valor", "si_no") ],
	"objeto_puesto":         [ ("id", "Personaje"), ("nombre_objeto", "string"), ("posicion", "string"), ("comparador", "string"), ("valor", "si_no") ],
	"confianza":             [ ("id", "Personaje"), ("personaje_objeto", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"resultado_tirada":      [ ("id", "Personaje"), ("id_tirada", "string"), ("dificultad", "numero"), ("comparador", "string"), ("valor", "numero") ],
	"resultado_competicion": [ ("id", "Personaje"), ("id_competicion", "string"), ("comparador", "string"), ("valor", "numero") ],
	"comparacion":           [ ("id", "Personaje"), ("oponente", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"hambre":                [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"sed":                   [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"dinero":                [ ("id", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"precio_compra_objeto":  [ ("id", "Personaje"), ("objeto", "Objeto"), ("vendedor", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"precio_venta_objeto":   [ ("id", "Personaje"), ("objeto", "Objeto"), ("comprador", "Personaje"), ("comparador", "string"), ("valor", "numero") ],
	"contador":              [ ("id", "Personaje"), ("nombre_contador", "string"), ("comparador", "string"), ("valor", "numero") ],
	"etiqueta":              [ ("id", "Personaje"), ("nombre_etiqueta", "string"), ("comparador", "string"), ("valor", "string") ]
    },

    "Objeto": {
	"nombre":                [ ("id", "Objeto"), ("comparador", "string"), ("valor", "string") ],
	"tipo":                  [ ("id", "Objeto"), ("comparador", "string"), ("valor", "string") ],
	"categoria":             [ ("id", "Objeto"), ("comparador", "string"), ("valor", "string") ],
	"peso":                  [ ("id", "Objeto"), ("comparador", "string"), ("valor", "numero") ],
	"cargas":                [ ("id", "Objeto"), ("comparador", "string"), ("valor", "numero") ],
	"valor":                 [ ("id", "Objeto"), ("comparador", "string"), ("valor", "numero") ],
	"estado":                [ ("id", "Objeto"), ("comparador", "string"), ("valor", "numero") ],
	"contador":              [ ("id", "Objeto"), ("nombre_contador", "string"), ("comparador", "string"), ("valor", "numero") ],
	"etiqueta":              [ ("id", "Objeto"), ("nombre_etiqueta", "string"), ("comparador", "string"), ("valor", "string") ]
    },

    "Sala": {
	"nombre":                [ ("id", "Sala"), ("comparador", "string"), ("valor", "string") ],
	"tipo":                  [ ("id", "Sala"), ("comparador", "string"), ("valor", "string") ],
	"subtipo":               [ ("id", "Sala"), ("comparador", "string"), ("valor", "string") ],
	"volumen":               [ ("id", "Sala"), ("comparador", "string"), ("valor", "numero") ],
	"luz":                   [ ("id", "Sala"), ("comparador", "string"), ("valor", "numero") ],
	"aura":                  [ ("id", "Sala"), ("comparador", "string"), ("valor", "numero") ],
	"objeto_en_sala":        [ ("id", "Sala"), ("nombre_objeto", "string"), ("comparador", "string"), ("valor", "si_no") ],
	"contador":              [ ("id", "Sala"), ("nombre_contador", "string"), ("comparador", "string"), ("valor", "numero") ],
	"etiqueta":              [ ("id", "Sala"), ("nombre_etiqueta", "string"), ("comparador", "string"), ("valor", "string") ]
    },

    "Mundo": {
	"hora":                  [ ("comparador", "string"), ("valor", "string") ],
	"seccion_del_dia":       [ ("comparador", "string"), ("valor", "string") ],
	"fecha":                 [ ("comparador", "string"), ("valor", "string") ]
    }
}

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


class Requisito(object):
    """Esta clase contiene la descripción de un requisito.

    Un requisito se caracteriza por una clase base y un "nombre" de requisito, que se corresponde con una
    propiedad a consultar en una instancia de dicha clase base (por ejemplo, Personaje.vida). A la
    propiedad se le pueden pasar argumentos (por ejemplo, un índice si es una propiedad compuesta).
    Finalmente, el requisito tiene siempre un comparador y un valor, y produce como resultado un valor
    booleano (la comparación se cumple o no se cumple). Por ejemplo, "Personaje.vida > 10".

    Los parámetros del requisito tienen todos un nombre y un tipo (por ejemplo, oyente: Personaje,
    valor: String).

    Un requisito está descrito 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 del requisito. Las macros
       disponibles dependen del contexto en el que se ha definido el requisito. Por ejemplo, un requisito
       puede estar colocado como condición 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, un requisito se puede *ejecutar*. Para la comprobació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" un requisito a partir de un nodo XML contenido en un fichero de descripción de sala,
       pnj, objeto, etc.

    b) Ejecutar un requisito, calculando los valores de la instancia base y de todos los parámetros,
       leyendo la propiedad correspondiente de la instancia base y comparándola con el valor. A
       continuación, se ejecuta el guión correspondiente a <cumplido> o a <no_cumplido>.
    """

    # 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 este requisito.

                    Requiere:
                      base != None
                    """)


    def __leer_nombre(self): return self.__nombre

    nombre = property(__leer_nombre,
                      doc="""(string): el nombre de requisito (o sea, la propiedad que se comprueba).

                      Requiere:
                        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
                                                compruebe el requisito (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.

                              Requiere:
                                  nombre != None
                                  valor != None
                                  tipo in Requisito.TIPOS_PARAMETROS
                              """)


    def __leer_cumplido(self): return self.__cumplido

    cumplido = property(__leer_cumplido,
			doc="""(Guion): contenido de la sección <cumplido> del requisito.

			    Contiene el guion que se ejecuta si este requisito se cumple. Puede
			    ser None si no se debe ejecutar nada.
			    """)


    def __leer_no_cumplido(self): return self.__no_cumplido

    no_cumplido = property(__leer_no_cumplido,
			   doc="""(Guion): contenido de la sección <no_cumplido> del requisito.

			       Contiene el guion que se ejecuta si este requisito no se cumple.
			       Puede ser None si no se debe ejecutar nada.
			       """)

    # Constructor

    def __init__(self, base, nombre, parametros, cumplido, no_cumplido):
	# Atributos privados:
	#
	# __guion_actual (Guion): el guión que se está ejecutando en modo temporizado
	# __cb_finalizar (funcion): función llamada al finalizar la ejecución
	self.__base = base
	self.__nombre = nombre
	self.__parametros = parametros
	self.__cumplido = cumplido
	self.__no_cumplido = no_cumplido
	self.__guion_actual = None
	self.__cb_finalizar = None


    # Métodos normales

    def ejecutar(self, macros_valores, cb_finalizar = None):
	"""Realiza la comprobación de la requisito (es decir, comparar la propiedad con el valor)
	y ejecuta un guión u otro según el resultado.

	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>}

	Además, devuelve si la acción que originó el suceso dentro del cual está este requisito
	debe ser aceptada (True) o cancelada (False), o None si no se especifica.

	Si se indica una función callback, al finalizar la ejecución, se llama a la misma, sin
	parámetros.

	Requiere:
	    macros_valores != None
	"""
	# Si hay otra ejecución en curso, se cancela.
	self.cancelar()

	# 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' or valor_str == 'true':
		    valor = True
		else:
		    valor = False
		
	    elif t == "Personaje":
		if v and v[0] == "$":
		    valor = macros_valores[v]
		else:
		    sala = macros_valores["$sala"]
		    valor = sala.personajes[v]
		
	    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"], parametro["tipo"]))

	    if parametro["nombre"] == "id":
		instancia_base = valor
	    elif parametro["nombre"] == "comparador":
	        comparador = valor
		if comparador == "mayor":
		    comparador = ">"
		elif comparador == "menor":
		    comparador = "<"
		elif comparador == "igual" or comparador == "=":
		    comparador = "=="
		elif comparador == "distinto" or comparador == "<>":
		    comparador = "!="
		elif comparador == "mayor_o_igual":
		    comparador = ">="
		elif comparador == "menor_o_igual":
		    comparador = "<="
	    elif parametro["nombre"] == "valor":
		if parametro["tipo"] == "string" and comparador != "es":
		    valor_requisito = "'" + valor + "'"
		else:
		    valor_requisito = valor
	    else:
	        parametros_resueltos[parametro["nombre"]] = valor

        # Obtener el valor de la propiedad y comprobar el resultado de la comparación
	if self.base == "Mundo":
	    log.info("Comprobando requisito %s.%s %s %s sobre el mundo" %
		     (self.base, self.nombre, str(comparador), str(valor_requisito)))
	    import Mundo
	    funcion = getattr(Mundo, self.nombre, None)
	    if funcion != None:
	        valor_propiedad = apply(funcion, (), parametros_resueltos)
	    else:
		valor_propiedad = None
		log.warning("ERROR: requisito no implementado")
	else:
	    log.info("Comprobando requisito %s.%s %s %s sobre la entidad %s" %
		     (self.base, self.nombre, str(comparador), str(valor_requisito),
		      instancia_base.id))
	    if len(parametros_resueltos) == 0:
		valor_propiedad = getattr(instancia_base, self.nombre, None)
	    else:
		funcion = getattr(instancia_base, self.nombre, None)
		if funcion != None:
		    valor_propiedad = apply(funcion, (), parametros_resueltos)
		else:
		    valor_propiedad = None

	if valor_propiedad != None:
	    if comparador == "es":
		expresion = str(valor_requisito)
		# Convertir la expresion regular de tipo ?* al estilo del módulo sre.
		expresion = "^" + expresion.replace("?", ".").replace("*", ".*") + "$"
		comparacion_verdadera = sre.match(expresion, valor_propiedad)
	    elif comparador == "en":
	        try:
		    val = int(valor_propiedad)
		except ValueError:
		    val = 0
	        rangos = valor_requisito.split(",")
		comparacion_verdadera = False
		for rango in rangos:
		    aux = rango.split("-")
		    ini = int(aux[0])
		    if len(aux) > 1:
		        fin = int(aux[1])
		    else:
		        fin = ini
		    if val >= ini and val <= fin:
		        comparacion_verdadera = True
			break
	    else:
		comparacion_verdadera = eval("valor_propiedad " + str(comparador) + " " +
			                     str(valor_requisito))
	else:
	    comparacion_verdadera = False

	log.info("Valor propiedad = %s, resultado = %s" % 
	         (str(valor_propiedad), str(comparacion_verdadera)))

	if comparacion_verdadera:
	    guion = self.__cumplido
	else:
	    guion = self.__no_cumplido

	if cb_finalizar == None:
	    # Ejecución instantánea
	    if guion != None:
		return guion.ejecutar(macros_valores)
	    else:
		return comparacion_verdadera
	else:
	    # Ejecución temporizada
	    if guion != None:
		self.__guion_actual = guion
		self.__cb_finalizar = cb_finalizar
		self.__guion_actual.ejecutar(macros_valores, self.__fin_ejecucion)
		return None
	    else:
		self.__guion_actual = None
		self.__cb_finalizar = cb_finalizar
		# Creamos un crono de duración 0. Es necesario así, en vez de llamar aquí al callback
		# para que la llamada sea asíncrona.
		import Mundo
		crono = CronoMine.crear(Mundo.cronoMine_control,
			                FechaHoraMine.crear_rel(segundos = 0),
					self.__fin_accion)
		return comparacion_verdadera


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


    # Métodos privados

    def __fin_ejecucion(self):
	"""Registra que ha terminado la ejecución y llama al callback."""
	if self.__cb_finalizar != None:
	    self.__cb_finalizar()
	self.__guion_actual = None
	self.__cb_finalizar = None
	

# Funciones públicas

def nuevo_xml(elem_requisito, macros_tipos, macros_extra):
    """Crea un requisito nuevo a partir de un nodo XML leido de un fichero de descripción.

    La estructura del nodo es:

      <requisito base="xxx" nombre="xxx" PARAMETRO="xxx"...>
        <cumplido>
	   ...guion...
	</cumplido>
        <no_cumplido>
	   ...guion...
	</no_cumplido>
      </requisito>

    Parámetros:
	elem_requisito: el nodo XML
	macros_tipos ((string, string)[]): lista con los nombres y tipos de los macros
					   permitidos en el contexto de este requisito.
	macros_extra ({string:any}): macros extra que serán pasados a los guiones internos.

    Ejemplo -> requisito dentro de la accion "beber" del objeto "pocion01"

      elem_requisito: <requisito base="Personaje" nombre="atributo" atributo="sabiduria"
				 id="$usuario" comparador=">" valor="10">
      macros_tipos: [("$usuario", "Personaje"), ("$sala", "Sala"), ("$objeto", "Objeto")]

    Requiere:
	elem_requisito != None
	macros_tipos != None
    """
    (base, nombre, parametros, cumplido, no_cumplido) = \
	    _parsear_requisito(elem_requisito, macros_tipos, macros_extra)
    log.debug("Creado requisito %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"])))
    req = Requisito(base, nombre, parametros, cumplido, no_cumplido)
    return req


def nuevo_datos(base, nombre, parametros, cumplido, no_cumplido):
    """Crea un requisito nuevo aportando directamente sus datos.

    Requiere:
	base != None
	nombre != None
	parametros != None
	len(parametros) > 0
    """
    log.debug("Creado requisito %s.%s automático. 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"])))
    req = Requisito(base, nombre, parametros, cumplido, no_cumplido)
    return req


def _parsear_requisito(elem_requisito, macros_tipos, macros_extra):
    """Extrae la información del elemento <requisito>:
	- base
	- nombre
	- parametros
	- cumplido
	- no_cumplido

    Requiere:
	elem_requisito != None
	macros_tipos != None
    """
    base = xmlutils.atributo(elem_requisito, 'base')
    nombre = xmlutils.atributo(elem_requisito, 'nombre')
    
    try:
	# Obtenemos la información del nombre de requisito (es decir, la lista
	# de parámetros y sus tipos correspondientes).
	info_nombre = NOMBRES[base][nombre]
    except KeyError:
	raise ValueError("No existe el requisito %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_requisito.hasAttribute(param_nombre):
	    # Si el atributo está presente en la definición XML, extraer el valor.
	    param_valor = xmlutils.atributo(elem_requisito, param_nombre)
	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 del requisito %s.%s." %
				 (param_nombre, base, nombre))

	parametros.append({"nombre": param_nombre,
			   "valor": param_valor,
			   "tipo": param_tipo})

    # Ahora obtenemos el contenido de los subnodos <cumplido> y <no_cumplido>
    import Guion

    elems_cumplido = xmlutils.subelementos(elem_requisito, 'cumplido')
    if len(elems_cumplido) > 0:
	cumplido = Guion.nuevo_xml(elems_cumplido[0], macros_tipos, macros_extra)
    else:
	cumplido = None

    elems_no_cumplido = xmlutils.subelementos(elem_requisito, 'no_cumplido')
    if len(elems_no_cumplido) > 0:
	no_cumplido = Guion.nuevo_xml(elems_no_cumplido[0], macros_tipos, macros_extra)
    else:
	no_cumplido = None

    return (base, nombre, parametros, cumplido, no_cumplido)


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

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

