"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/calendar.py $
$Id: calendar.py 25570 2004-11-10 20:38:56Z rmasse $
"""

from datetime import datetime, timedelta
from dulcinea.base import DulcineaPersistent
from dulcinea.keyed import Keyed, KeyedMap
from dulcinea.permission import PermissionManager
from dulcinea.sort import method_sort
from dulcinea.user import DulcineaUser as User
from dulcinea.spec import require

def weekdays(start, end):
    """(start: datetime, end: datetime) -> int

    Return the number of weekdays between two dates.
    """
    days = (end - start).days
    if days < 0:
        raise ValueError, 'end date must be after start date'
    weeks, partial = divmod(days, 7)
    if partial > 0:
        sunday_distance = (6 - start.weekday()) % 7
        saturday_distance = (5 - start.weekday()) % 7
        if sunday_distance < partial:
            partial -= 1
        if saturday_distance < partial:
            partial -= 1
    return 5*weeks + partial


class Titled:
    """
    Instance attributes:
      title : str
      description : str

    """
    title_is = str
    description_is = str

    def __init__(self):
        self.title = None
        self.description = None

    def get_title(self):
        return self.title

    def set_title(self, title):
        require(title, self.title_is)
        self.title = title

    def get_description(self):
        return self.description

    def set_description(self, description):
        require(description, self.description_is)
        self.description = description


class Reservable(PermissionManager, Keyed, Titled):

    """
    Instance attributes:
      active : bool
    """

    active_is = bool

    valid_permissions = {
        'primary_contact' : 'primary contact',
        'control' : 'able to change any reservations of this item',
        'reserve' : 'able to make a reservation of this item',
        }

    def __init__(self):
        Keyed.__init__(self)
        Titled.__init__(self)
        PermissionManager.__init__(self)
        self.active = True

    def set_active(self, value):
        self.active = value

    def is_active(self):
        return self.active

    def allows_reserve(self, user):
        return self.grants('reserve', user)

    def allows_control(self, user):
        return (self.grants('control', user) or
                self.grants('primary_contact', user))


class Resource (DulcineaPersistent, Reservable):
    """
    Instance attributes: none
    """
    def __init__(self):
        Reservable.__init__(self)


class Resources (DulcineaPersistent, KeyedMap):
    """
    Instance attributes: none
    """
    def __init__(self):
        KeyedMap.__init__(self, Resource)

    def delete_resource(self, resource):
        del self.get_mapping()[resource.get_key()]


class Reservation (DulcineaPersistent, Keyed, Titled):
    """
    Instance attributes:
      start_time : datetime
      end_time : datetime
      reserved : Reservable
      reserver : User
    """

    start_time_is = datetime
    end_time_is = datetime
    reserved_is = Reservable
    reserver_is = User

    def __init__(self):
        Keyed.__init__(self)
        Titled.__init__(self)
        self.start_time = None
        self.end_time = None
        self.reserved = None
        self.reserver = None

    def __str__ (self):
        return "%s: %s" % (self.key, self.start_time)

    def set_start_time (self, time):
        require(time, self.start_time_is)
        time.replace(second=0, microsecond=0)
        self.start_time = time

    def set_end_time (self, time):
        require(time, self.start_time_is)
        assert time >= self.start_time
        time.replace(second=0, microsecond=0)
        self.end_time = time

    def set_reserved (self, reserved):
        require(reserved, self.reserved_is)
        self.reserved = reserved

    def set_reserver (self, user):
        require(user, self.reserver_is)
        self.reserver = user

    def get_start_time (self):
        return self.start_time

    def get_end_time (self):
        return self.end_time

    def get_reserved (self):
        return self.reserved

    def get_reserver (self):
        return self.reserver

    def is_active(self, start, end):
        return (self.get_end_time() > start and
                self.get_start_time() < end)

    def can_modify (self, user):
        """(user: User) -> bool
        """
        if user is self.reserver:
            return True
        if not user:
            return False
        if user.is_admin():
            return True
        if self.reserved.allows_control(user):
            return True
        return False


def create_reservation_table(reservations, time_increment,
                             start_time, end_time,
                             start_time_of_day, end_time_of_day):
    """(reservations : [Reservation]
        time_increment:timedelta,
        start_time:datetime,
        end_time:datetime,
        start_time_of_day:timedelta,
        end_time_of_day:timedelta) -> {
            (day:datetime, time_of_day:timedelta) :
            (reservation:Reservation, rowspan:int) | None }

    This calculates information useful for displaying a tabular view
    of a set of reservations.
    The time_increment is amount of time for each row of the table.
    The start_time and end_time determine the columns (days) of the table.
    The start_time_of_day and end_time_of_day limit the rows that are shown.

    The function returns a dictionary with an entry for each time block
    that is not vacant.  For the first block of each day for a given
    reservation, the dictionary maps (day, time_of_day) to
    (reservation, rowspan), meaning that this reservation appears first
    on this day at this time of day, and that it extends across rowspan
    time increments.

    The cells of the table that are continuations of a reservation that
    has already appeared above have entries that map (day, time_of_day)
    to None.
    """

    reservations = [reservation for reservation in reservations
                    if (reservation.get_start_time() < end_time and
                        reservation.get_end_time() > start_time)]
    reservations = method_sort(reservations, 'get_start_time')
    tableau = {}
    day_delta = timedelta(days=1)
    def gen_days():
        day = start_time
        while day < end_time:
            yield day
            day = day + day_delta
    for day in gen_days():
        day_reservations = [
            reservation for reservation in reservations
            if reservation.is_active(day + start_time_of_day,
                                     day + end_time_of_day)]
        time_of_day = start_time_of_day
        while time_of_day < end_time_of_day:
            if day_reservations:
                reservation = day_reservations[0]
                period_end = day + time_of_day + time_increment
                if reservation.get_start_time() < period_end:
                    start = (day, time_of_day)
                    reservation_end = reservation.get_end_time()
                    rows = 1
                    next_time = time_of_day + time_increment
                    while (next_time < end_time_of_day and
                           day + next_time < reservation_end):
                        rows += 1
                        time_of_day = next_time
                        next_time = next_time + time_increment
                        tableau[(day, time_of_day)] = None # mark continuation
                    tableau[start] = (reservation, rows)
                    day_reservations = day_reservations[1:]
            time_of_day = time_of_day + time_increment
    return tableau



class Calendar (DulcineaPersistent, KeyedMap):

    """
    Instance attributes: none
    """

    def __init__(self):
        KeyedMap.__init__(self, Reservation)

    def get_reservations(self):
        return self.get_mapping().values()

    def delete_reservation(self, reservation):
        del self.get_mapping()[reservation.get_key()]

    def get_reservations_for(self, resource):
        return [reservation for reservation in self.get_reservations()
                if resource is reservation.get_reserved()]

    def get_table_map(self, resource, time_increment,
                      start_time, end_time,
                      start_time_of_day, end_time_of_day):
        return create_reservation_table(self.get_reservations_for(resource),
                                        time_increment, start_time, end_time,
                                        start_time_of_day, end_time_of_day)
