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

"""Módulo para manejar fechas y horas del calendario de Minë.

Contiene dos clases:
 - FechaHoraMine
 - FechaHoraMineRel
"""

import time
    
# Los siguientes valores representan la correspondencia Tiempo Real - Tiempo Minë.
# Pueden ser modificados libremente.

FECHA_R_ANNO    = 1500     # Fecha R = 1 de Enero del 1500 T.E., a las 0:000h
FECHA_R_MES     = 1
FECHA_R_DIA     = 1
FECHA_R_HORA    = 0
FECHA_R_SEGUNDO = 0

FECHA_R_REAL_ANNO    = 2002 # Fecha R real = 8 de Septiembre de 2002 D.C. a las 0:0:0h WET
FECHA_R_REAL_MES     = 9    # (Western European Time, 1 hora más que GMT)
FECHA_R_REAL_DIA     = 8
FECHA_R_REAL_HORA    = 0
FECHA_R_REAL_MINUTO  = 0
FECHA_R_REAL_SEGUNDO = 0

MESES_POR_ANNO    = 12    # 1 año Minë = 45 dias reales (3.888.000 segundos)
DIAS_POR_MES      = 30    # 1 mes Minë = 3,75 dias reales (324.000 segundos)
HORAS_POR_DIA     = 24    # 1 dia Minë = 3 horas reales (10.800 segundos)
SEGUNDOS_POR_HORA = 450   # 1 hora Minë = 7,5 minutos reales (450 segundos)

# La siguiente lista es para identificar secciones dentro de un dia. El
# número indica la hora a la que empieza la sección.

SECCIONES = [ (0,  "Noche cerrada"),
	      (4,  "Madrugada"),
	      (8,  "Por la mañana"),
	      (12, "Mediodia"),
	      (16, "Por la tarde"),
	      (20, "Por la noche") ]

# La siguiente lista contiene los nombres de los meses, van ordenados de 1 a
# MESES_POR_ANNO.

NOMBRES_MES = [ "enero",
		"febrero",
		"marzo",
		"abril",
		"mayo",
		"junio",
		"julio",
		"agosto",
		"septiembre",
		"octubre",
		"noviembre",
		"diciembre" ]

# Los siguientes valores son usados internamente para los cálculos. Se obtienen
# automáticamente. No tocar.

FECHA_R_REAL_SEGUNDOS_TOTALES = -1
SEGUNDOS_POR_ANNO = -1
SEGUNDOS_POR_MES = -1
SEGUNDOS_POR_DIA = -1


class FechaHoraMine(object):
    """Objeto que contiene una fecha y hora dentro de juego.

    Externamente el tiempo se identifica como el año, mes, dia, hora y segundo, desde el
    inicio de la Tercera Edad (dia 1 del mes 1 del año 0, a las 0:000 horas). En Minë no se
    cuentan los minutos.

    Internamente almacena la información como un entero largo con el número de segundos
    transcurridos desde la Fecha R (la fecha usada para sincronizar el mundo de Minë con
    el real). Tiene fórmulas para transformar en año, mes, dia, hora y segundo.  

    La equivalencia con el tiempo real es 1 segundo Minë = 1 segundo real. A partir de ahí,
    las constantes SEGUNDOS_POR_HORA, etc. definen la cuenta del tiempo.

    Contiene los siguientes atributos:
        - __anno (int): el año dentro de la Tercera Edad
        - __mes (int): el número del mes
        - __dia (int): el número del día
        - __hora (int): la hora del día
        - __segundo (int): el segundo dentro de la hora
        - __segundos_totales (int): el número de segundos transcurridos desde la Fecha R

    Invariantes:
        __anno >= 0  (en realidad podría ser < 0, pero no consideraremos fechas anteriores a la T.E.)
        __mes in range(1,MESES_POR_ANNO)
        __dia in range(1,DIAS_POR_MES)
        __hora in range(0,HORAS_POR_DIA-1)
        __segundo in range(0,SEGUNDOS_POR_HORA-1)
    """

    def __init__(self, segundos_totales):
        """Constructor a partir del número de segundos totales.

        Normalmente desde fuera no se usará este constructor, sino más bien los
        métodos crear() o ahora().
        """
        self.__segundos_totales = segundos_totales
        self.__descomponer_segundos()


    def __repr__(self):
        """Devuelve un string representando la instancia, de una forma evaluable."""
        return "%d" % self.__segundos_totales


    def __str__(self):
        """Devuelve un string con la fecha en formato imprimible."""
        return "%d de %s de %d T.E., %02dh" % (self.dia(),
                                               self.nombre_mes(),
                                               self.anno(),
                                               self.hora())

    def anno(self):
        return self.__anno


    def mes(self):
        return self.__mes


    def nombre_mes(self):
        return NOMBRES_MES[self.__mes - 1]


    def dia(self):
        return self.__dia


    def hora(self):
        return self.__hora


    def segundo(self):
        return self.__segundo


    def seccion(self):
        """Devuelve el índice de la sección del día en que estamos."""
        for i in range(len(SECCIONES)-1, -1, -1):
            seccion = SECCIONES[i]
            if self.__hora >= seccion[0]:
                return i


    def desc_seccion(self):
        """Devuelve la descripción de la sección del día en que estamos."""
        for i in range(len(SECCIONES)-1, -1, -1):
            seccion = SECCIONES[i]
            if self.__hora >= seccion[0]:
                return seccion[1]


    def __descomponer_segundos(self):
        """Extrae los valores desglosados a partir de los segundos totales."""
        
        self.__anno = (self.__segundos_totales / SEGUNDOS_POR_ANNO) + FECHA_R_ANNO

        resto = self.__segundos_totales % SEGUNDOS_POR_ANNO

        self.__mes = (resto / SEGUNDOS_POR_MES) + FECHA_R_MES

        resto = resto % SEGUNDOS_POR_MES

        self.__dia = (resto / SEGUNDOS_POR_DIA) + FECHA_R_DIA

        resto = resto % SEGUNDOS_POR_DIA

        self.__hora = (resto / SEGUNDOS_POR_HORA) + FECHA_R_HORA

        self.__segundo = (resto % SEGUNDOS_POR_HORA) + FECHA_R_SEGUNDO


    def __cmp__(self, otra):
        """Compara dos fechas.
        
        Devuelve -1, 0 o 1 si la segunda fecha es menor, igual o mayor que esta.
        """
	if otra == None:
	    return -1
        if self.__segundos_totales < otra.__segundos_totales:
            return -1
        elif self.__segundos_totales > otra.__segundos_totales:
            return 1
        else:
            return 0


    def __sub__(self, otra):
        """Resta esta fecha menos la otra.
        
        Devuelve la diferencia como una FechaHoraMineRel.
        """
        if self >= otra:
            signo = 1
            mayor = self
            menor = otra
        else:
            signo = -1
            mayor = otra
            menor = self

        return FechaHoraMineRel(signo,
                                mayor.__anno - menor.__anno,
                                mayor.__mes - menor.__mes,
                                mayor.__dia - menor.__dia,
                                mayor.__hora - menor.__hora,
                                mayor.__segundo - menor.__segundo)


    def __add__(self, relativa):
        """Suma una fecha relativa a esta.

        Devuelve otra fecha normal.
        """
        if relativa.signo() >= 0:
            return crear(self.__anno + relativa.annos(),
                         self.__mes + relativa.meses(),
                         self.__dia + relativa.dias(),
                         self.__hora + relativa.horas(),
                         self.__segundo + relativa.segundos())
        else:
            return crear(self.__anno - relativa.annos(),
                         self.__mes - relativa.meses(),
                         self.__dia - relativa.dias(),
                         self.__hora - relativa.horas(),
                         self.__segundo - relativa.segundos())



def ahora():
    """Devuelve una FechaHoraMine correspondiente a la fecha y hora en este momento."""
    return FechaHoraMine(int(time.time() + 0.5) - FECHA_R_REAL_SEGUNDOS_TOTALES)


def crear(anno = FECHA_R_ANNO, \
          mes = FECHA_R_MES, \
          dia = FECHA_R_DIA, \
          hora = FECHA_R_HORA, \
          segundo = FECHA_R_SEGUNDO):
    """Devuelve una FechaHoraMine correspondiente a los datos pasados."""
    segundos_totales = (anno - FECHA_R_ANNO) * SEGUNDOS_POR_ANNO \
                     + (mes - FECHA_R_MES) * SEGUNDOS_POR_MES \
                     + (dia - FECHA_R_DIA) * SEGUNDOS_POR_DIA \
                     + (hora - FECHA_R_HORA) * SEGUNDOS_POR_HORA \
                     + (segundo - FECHA_R_SEGUNDO)
    return FechaHoraMine(segundos_totales)

    

class FechaHoraMineRel(object):
    """Objeto que contiene LA DIFERENCIA entre dos fechas y horas dentro de juego.

    Mientras que FechaHoraMine mide la distancia entre una fecha y el origen de la T.E.,
    esta clase mide la distancia entre dos fechas arbitrarias, medida en años, meses,
    dias, horas y segundos.

    La distancia puede ser positiva, o negativa, según si la fecha final es posterior o
    anterior a la origen, respectivamente. El 0 se considera positivo.

    Internamente NO guarda la conversión a número de segundos. Debido a la duración de los
    meses, años bisiestos y otras irregularidades, el número de segundos a que corresponde
    una FechaHoraMineRel es variable y depende de cuál sea la fecha origen. Por ejemplo,
    una diferencia de un mes no dura lo mismo en segundos si el mes origen tiene 30 días o
    si tiene 31.

    Nótese que mientras en FechaHoraMine los meses van de 1 a MESES_POR_ANNO, en ésta
    clase van de 0 a MESES_POR_ANNO-1, dado que es una diferencia. Lo mismo con los dias.

    Puede haber números fuera de rango, cuando provienen de una suma o resta de fechas.
    Por ejemplo, si el año tiene 12 meses, la fecha relativa meses=14, equivale a
    annos=1,meses=2. Otro ejemplo: si el mes tiene 30 dias, la fecha relativa
    meses=3,dias=-5 equivale a meses=2,dias=25.

    Se admiten cantidades fraccionarias, que serán convertidas a la unidad inferior. Por
    ejemplo, 1.5 dias se convierte a 1 dia y 12 horas.

    Invariantes:
        __signo in [-1, 1]
        __annos >= 0
    """

    def __init__(self, signo=1, annos=0, meses=0, dias=0, horas=0, segundos=0):
        """Constructor a partir de los componentes.

        Requiere:
            signo in [-1, 1]
            annos >= 0
        """
        self.__signo = signo
        self.__annos = int(annos)
        self.__meses = int(meses)
        self.__dias = int(dias)
        self.__horas = int(horas)
        self.__segundos = int(segundos)

        # Asignar las partes fraccionarias a la unidad inferior
        self.__meses += int((annos - int(annos)) * MESES_POR_ANNO)
        self.__dias += int((meses - int(meses)) * DIAS_POR_MES)
        self.__horas += int((dias - int(dias)) * HORAS_POR_DIA)
        self.__segundos += int((horas - int(horas)) * SEGUNDOS_POR_HORA)
        self.__horas += int((dias - int(dias)) * HORAS_POR_DIA)


    def __repr__(self):
        """Devuelve un string representando la instancia, de una forma evaluable."""
        return "%s%04d/%02d/%02d %02d:%03d" % (((self.__signo < 0) and "(-)" or ""),
                                               self.__annos, self.__meses, self.__dias,
                                               self.__horas, self.__segundos)


    def __str__(self):
        """Devuelve un string con la fecha en formato imprimible."""
        return "%s%04d/%02d/%02d %02d:%03d" % (((self.__signo < 0) and "(-)" or ""),
                                               self.__annos, self.__meses, self.__dias,
                                               self.__horas, self.__segundos)

    def signo(self):
        return self.__signo


    def annos(self):
        return self.__annos


    def meses(self):
        return self.__meses


    def dias(self):
        return self.__dias


    def horas(self):
        return self.__horas


    def segundos(self):
        return self.__segundos


    def inverso(self):
        """Devuelve el mismo intervalo con el signo invertido."""
        if otra.signo() >= 0:
            signo_inverso = -1
        else:
            signo_inverso = 1

        return FechaHoraMineRel(signo_inverso,
                                self.annos(),
                                self.meses(),
                                self.dias(),
                                self.horas(),
                                self.segundos())


    def abs(self):
        """Devuelve el mismo intervalo eliminando el signo."""
        return FechaHoraMineRel(1,
                                self.annos(),
                                self.meses(),
                                self.dias(),
                                self.horas(),
                                self.segundos())


    def __cmp__(self, otra):
        """Compara dos intervalos de fechas.
        
        Devuelve -1, 0 o 1 si el segundo intervalo es más, igual o menos largo que éste.
        """
        # Esto lo tengo que revisar un poco, no sé si funciona bien en todos los casos.
        if self.signo() > otra.signo():
            return 1
        elif self.signo() < otra.signo():
            return -1
        else:
            if self.anno() > otra.anno():
                return 1
            elif self.anno() < otra.anno():
                return -1
            else:
                if self.mes() > otra.mes():
                    return 1
                elif self.mes() < otra.mes():
                    return -1
                else:
                    if self.dia() > otra.dia():
                        return 1
                    elif self.dia() < otra.dia():
                        return -1
                    else:
                        if self.hora() > otra.hora():
                            return 1
                        elif self.hora() < otra.hora():
                            return -1
                        else:
                            if self.segundo() > otra.segundo():
                                return 1
                            elif self.segundo() < otra.segundo():
                                return -1
                            else:
                                return 0


    def __sub__(self, otra):
        """Resta este intervalo menos el otro.
        
        Devuelve la diferencia como una FechaHoraMineRel.
        """
        # Restar es lo mismo que sumar el inverso.
        return self + otra.inverso()


    def __add__(self, otra):
        """Suma un intervalo a este.

        Devuelve otra fecha relativa.
        """
        if self.signo() == otra.signo():
            signo = self.signo()
        else:
            if self.abs().signo() >= otra.abs().signo():
                signo = self.signo()
            else:
                signo = otra.signo()

        if self.signo() == otra.signo():
            return FechaHoraMineRel(signo,
                                    self.anno() + otra.anno(),
                                    self.mes() + otra.mes(),
                                    self.dia() + otra.dia(),
                                    self.hora() + otra.hora(),
                                    self.segundo() + otra.segundo())
        else:
            return FechaHoraMineRel(signo,
                                    self.anno() - otra.anno(),
                                    self.mes() - otra.mes(),
                                    self.dia() - otra.dia(),
                                    self.hora() - otra.hora(),
                                    self.segundo() - otra.segundo())

    
def crear_rel(signo=1, annos=0, meses=0, dias=0, horas=0, segundos=0):
    """Devuelve una FechaHoraMineRel correspondiente a los datos pasados."""
    return FechaHoraMineRel(signo, annos, meses, dias, horas, segundos)


# ------------------------------------
# Código de inicialización del módulo.
# ------------------------------------

# Este valor representa el número de segundos entre la Fecha R de Unix y la Fecha R real
# de Minë. mktime() convierte una tupla de valores, en el número de segundos desde la 
# Fecha R de Unix. El número devuelto es float, así que lo convertimos a int redondeando.

FECHA_R_REAL_SEGUNDOS_TOTALES = int(time.mktime((FECHA_R_REAL_ANNO,
                                                 FECHA_R_REAL_MES,
                                                 FECHA_R_REAL_DIA,
                                                 FECHA_R_REAL_HORA,
                                                 FECHA_R_REAL_MINUTO,
                                                 FECHA_R_REAL_SEGUNDO,
                                                 -1, -1, -1)) + 0.5)

# Los siguientes valores son simplemente para acelerar los cálculos internos

SEGUNDOS_POR_DIA = SEGUNDOS_POR_HORA * HORAS_POR_DIA
SEGUNDOS_POR_MES = SEGUNDOS_POR_DIA * DIAS_POR_MES
SEGUNDOS_POR_ANNO = SEGUNDOS_POR_MES * MESES_POR_ANNO

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

if (__name__ == '__main__'):
    a = crear(anno=2500, mes=8, dia=14)
    b = crear(anno=2500, mes=9, dia=10)
    c = crear(anno=2502, mes=5, dia=20)

    print "De [%s]\n a [%s]\n va [%s]" % (str(a), str(b), str(b - a))
    print "De [%s]\n a [%s]\n va [%s]" % (str(c), str(a), str(a - c))

    r = crear_rel(signo=1, annos=4, meses=1, dias=40)

    print "[%s]\n mas [%s]\n es [%s]" % (str(a), str(r), str(a + r))

    while 1:
        f_ahora = ahora()
        print "%s (%s)" % (f_ahora, f_ahora.desc_seccion())
        time.sleep(1)

