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

from utils import *
import Sala
import FechaHoraMine
import CronoMine


"""El mundo tal y como lo conocemos.

No es clase, ya que sólo hay un mundo en una determinada ejecución de Minë. Aquí
sólo hay variables y funciones globales de módulo.

Contiene todas las salas del juego, pero no las mantiene a todas en memoria, sino
que utiliza una cache con un tamaño máximo. Cuando la cache se llena, se liberan
las salas que estén vacías (sin ningún personaje jugador).

Hay una sala especial (el "limbo") donde se encuentran todos los PJ que no
están siendo manejados en un momento determinado por su jugador (a traves de un
InterfazPJ). Hay dos métodos para traer un PJ de y enviarlo a esta sala.
El limbo no es accesible desde ningún lugar (no tiene salidas, y ninguna otra
sala conduce a él).

Otra sala especial es la "entrada", que es allí donde los personajes empiezan
a jugar cuando han terminado de ser creados.

Una tercera sala especial es la "muerte", que es a donde van por ahora los
personajes cuando mueren. Se entra alli, pero no se puede salir.

Nota: las salas especiales deben tener capacidad de volumen infinita (en la
práctica usaremos volumen="99999999") porque hay mucho trasiego de entrada y
salida de gente. En muchas partes del programa se lleva a personajes a ellas
sin preguntar por el volumen.

El Mundo tiene un reloj que va marcando "ticks" cada hora. Cualquier objeto que
quiera ser notificado del paso del tiempo puede registrarse.

Invariantes:
  salas != None
  pjs_activos != None
  suscritos_al_tick != None
"""

TAMANO_CACHE_SALAS = 50

ID_SALA_LIMBO = "limbo-limbo"

ID_SALA_ENTRADA = "entrada-entrada"

ID_SALA_MUERTE = "muerte"

# Lista (usada en forma de cola) con las salas, para implementar LRU (ver abajo)
salas = []

# ConnectionHandler accesible globalmente, para manejar conexiones via socket
conn_handler = None

# CronoMineControl accesible globalmente, para manejar cronometros
cronoMine_control = None

# Diccionario con todos los personajes jugadores en activo (que no están en el Limbo
# ni en la Muerte). El diccionario es {id : personaje}. Se inicializa a vacío porque
# al arrancar Mine, todos los personajes están en el limbo.
pjs_activos = {}

# Lista de todas las funciones que se han suscrito al "tick" del reloj global.
suscritos_al_tick = []

# FechaHora del último tick.
tick_anterior = FechaHoraMine.FechaHoraMine(0)

# Contador para ir asignando ids a los pjs peregrinos nuevos
contador_de_peregrinos = 0
    

# Funciones públicas

def inicializar():
    """Echa el mundo a rodar..."""
    global cronoMine_control

    # El primer tick tiene que saltar al principio de la próxima hora.
    ahora = FechaHoraMine.ahora()
    primer_tick = ahora + FechaHoraMine.crear_rel(signo=1, horas=1, segundos=-ahora.segundo())
    CronoMine.crear(cronoMine_control, primer_tick - ahora, tick)


def localizar_sala(id):
    """Localiza una sala por el id.
    
    Si está en memoria, la devuelve directamente. Si no, la carga de disco. Si no existe,
    la crea nueva a partir de su descripción.

    Requiere:
      id != None
      id existe en cache o disco, o existe una descripcion para crearla

    Asegura:
      return != None
    """
    global salas

    la_sala = None

    # Primero se busca en la cache
    for i in range(0, len(salas)):
	if salas[i].id == id:
	    # Si esta en la cache, ya la tenemos. Unicamente, moverla al principio
	    # de la cola (algoritmo LRU - Less Recently Used: cuando haya que quitar
	    # una sala de la cache, que sea la que lleva más tiempo sin utilizarse).
	    la_sala = salas[i]
	    del salas[i]
	    salas.insert(0, la_sala)
	    break

    if la_sala == None:
	
	try:
	
	    # Si no, ver si se puede cargar de disco. Si es posible, añadirla al
	    # principio de la cola.
	    la_sala = Sala.cargar(id)
	    salas.insert(0, la_sala)

	except IOError:
	
	    # En otro caso, crearla a partir de su descripción, guardarla
	    # en disco y añadirla a la cache

	    la_sala = Sala.nueva_xml(id)
	    la_sala.salvar()
	    salas.insert(0, la_sala)
	    
		    
    # Y si hay demasiadas salas cargadas, descartar una de la cache (salvarla primero)
    # Si todas las salas tienen gente dentro, no se descartará ninguna, pero eso es muy
    # improbable y además se arregla solo en cuanto alguien se salga de una sala.
    if (len(salas) > TAMANO_CACHE_SALAS):
	for i in range(len(salas)-1, -1, -1):
	    if salas[i].n_jugadores == 0:
		sala_descartada = salas[i]
		sala_descartada.salvar()
		del salas[i]
		break
	    

    la_sala.activar()

    return la_sala


def personaje_activado(personaje):
    """Un PJ informa que se ha activado (pasa a ser controlado por un jugador)

    Requiere:
      personaje != None
 
    Asegura:
      personaje in Mundo.pjs_activos.values()
    """
    global pjs_activos
    pjs_activos[personaje.id] = personaje
    

def personaje_desactivado(personaje):
    """Un PJ informa que se ha desactivado (su jugador se ha desconectado o el PJ ha muerto).

    Requiere:
      personaje != None
  
    Asegura:
      personaje not in Mundo.pjs_activos.values()
    """
    global pjs_activos
    if pjs_activos.has_key(personaje.id):
	del pjs_activos[personaje.id]


def salvar_todo():
    """Salva todas las salas en memoria, junto con todo su contenido."""
    global salas
    for sala in salas:
	sala.salvar()


def suscribirse_al_tick(funcion_tick):
    """Para añadirse a la lista de funciones llamadas con el "tick" del reloj global.

    A esta función se le pasarán dos parámetros: el primero es la FechaHoraMine actual,
    y el otro la que era actual en la llamada anterior (sirve para comprobar si ha habido
    cambio de dia, por ejemplo).
    """
    global suscritos_al_tick
    suscritos_al_tick.append(funcion_tick)


def borrarse_del_tick(funcion_tick):
    """Para borrarse de la lista anterior."""
    global suscritos_al_tick
    suscritos_al_tick.remove(funcion_tick)


def tick(crono):
    """Función que es llamada con cada tick del reloj global.

    Se encarga de llamar a todos los suscritos.
    """
    global cronoMine_control, suscritos_al_tick, tick_anterior

    ahora = FechaHoraMine.ahora()

    for suscrito in suscritos_al_tick:
	suscrito(ahora, tick_anterior)

    tick_anterior = ahora

    siguiente_tick = ahora + FechaHoraMine.crear_rel(signo=1, horas=1, segundos=-ahora.segundo())
    CronoMine.crear(cronoMine_control, siguiente_tick - ahora, tick)


def nuevo_id_peregrino():
    """Devuelve un número único para usarlo en el id de un nuevo pj peregrino."""
    global contador_de_peregrinos
    contador_de_peregrinos += 1
    return str(contador_de_peregrinos)


def hora():
    """Devuelve el número de la hora actual del día.

    Asegura:
	return in range(0, FechaHoraMine.HORAS_POR_DIA - 1)
    """
    fecha_actual = FechaHoraMine.ahora()
    return fecha_actual.dia()


def seccion_del_dia():
    """Devuelve el nombre de la sección actual del día.

    Asegura:
	return in FechaHoraMine.SECCIONES
    """
    fecha_actual = FechaHoraMine.ahora()
    return fecha_actual.desc_seccion()


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

if (__name__ == '__main__'):
    sala = localizar_sala(ID_SALA_ENTRADA)
    print 'Cargada sala ' + sala.id + ' con exito.'

