"""$URL: svn://svn/repos/trunk/dulcinea/lib/history.py $
$Id: history.py 21683 2003-05-20 17:53:38Z dbinger $

event logging
"""

from types import StringType
from mx import DateTime
from dulcinea.errors import UsageError
from dulcinea.base import DulcineaPersistent
from dulcinea.typeutils import typecheck
from dulcinea.user import DulcineaUser

class Historical (DulcineaPersistent):
    """maintains a list of history events.

    Instance attributes:
      history : History
        the list of history events that comprise the object's history

    """

    def __init__ (self):
        self.history = History()

    def get_history (self):
        return self.history


class History (DulcineaPersistent):
    """A list of history events attached to a single object.

    Instance attributes:
      events : [HistoryEvent]
        the list of history events that comprise the object's history
    """

    def __init__ (self):
        self.events = []

    def __str__ (self):
        return "history (%d events)" % len(self.events)

    def dump (self, file=None, header=1, deep=1):
        file = self._startdump(file, header)
        file.write("events:")
        if deep and self.events:
              file.write("\n")
              file.indent_more()
              for e in self.events:
                  if e.user_id is None:
                      user = "(no user)"
                  else:
                      user = e.user_id
                  file.write("%s  %s  %s: %s\n" %
                             (e.timestamp, user, e.event_code, e.message))
              file.indent_less()
        elif self.events:
            file.write(" %d\n" % len(self.events))
        else:
            file.write(" none\n")

    def __len__ (self):
        return len(self.events)

    def __getitem__ (self, i):
        return self.events[i]

    def insert_event (self, event):
        i = 0
        for e in self.events:
            if e.timestamp > event.timestamp:
                break
            i += 1
        self.events.insert(i, event)
        self._p_changed = 1

    def add_event (self, user, event_code, message, timestamp=None):
        """(user : DulcineaUser, event_code : string, message : string,
            timestamp : DateTime = None)

        Create a new HistoryEvent instance.  If timestamp is None (the
        default), the new event is stamped with the current time and
        appended to the end of the history list.  If timestamp
        is supplied, it is used as the event's timestamp, and the
        event is inserted into the history list so that its ordering
        by timestamp is preserved.
        """
        new_event = HistoryEvent(user, event_code, message, timestamp)
        if timestamp is None:
            self.events.append(new_event)
            self._p_changed = 1
        else:
            self.insert_event(new_event)

    def add_unique_event (self, user, event_code, message):
        """(user : DulcineaUser, event_code : string, message : string)

        Create a new HistoryEvent instance.  If the last event on the
        list had the same event code and user, replace the last event with
        the new one.
        """
        new_event = HistoryEvent(user, event_code, message, timestamp=None)
        replace_event = False
        if len(self.events):
            last = self.events[-1]
            if (last.user_id == new_event.user_id and
                last.event_code == new_event.event_code):
                replace_event = True

        if replace_event:
            self.events[-1] = new_event
        else:
            self.events.append(new_event)
        self._p_changed = 1

    def find_event (self, user=None, event_code=None, latest=False,
                    message=None):
        """(user : DulcineaUser = None,
            event_code : string = None,
            latest : bool = False,
            message : string = None) -> HistoryEvent

        Find the earliest (or latest, if latest is true) event
        matching 'user' and/or 'event_code'.  If either search criterion
        is not supplied or None, it does not affect the search, thus the
        default behaviour is to return the first event in the history
        list.  Return None if no events match.
        """
        typecheck(user, DulcineaUser, allow_none=1)
        if latest:
            indices = xrange(len(self.events)-1, -1, -1)
        else:
            indices = xrange(len(self.events))
        for i in indices:
            event = self.events[i]
            if user is not None and user.id != event.user_id:
                continue
            if event_code is not None and event_code != event.event_code:
                continue
            if message is not None and message != event.message:
                continue
            return event
        else:
            return None


class HistoryEvent (DulcineaPersistent):
    """A single event in a history log.  A History instance is mainly a
    list of HistoryEvent instances.  Each history event has a user ID,
    a timestamp, an "event code" (short string giving the general class
    of event) and a message giving details about the event.

    Instance attributes:
      timestamp : DateTime
      user_id : string
      event_code : string
      message : string

    Feel free to access these directly; do *not* modify them!  (The
    whole point of HistoryEvent is to be immutable.)

    Also, HistoryEvent objects should never be created by outside code:
    always go through the 'add_event()' method of History.
    """

    def __init__ (self, user, event_code, message, timestamp=None):
        typecheck(timestamp, DateTime.DateTimeType, allow_none=1)
        if timestamp is None:
            self.timestamp = DateTime.now()
        else:
            self.timestamp = timestamp
        typecheck(user, DulcineaUser, allow_none=1)
        if user:
            self.user_id = user.get_id()
        else:
            self.user_id = None
        self.event_code = event_code
        self.message = message

    def __str__ (self):
        return "%s %s" % (self.timestamp, self.event_code)

    def __cmp__ (self, other):
        # DateTime objects can't be compared to None
        if self.timestamp is None and other.timestamp is not None:
            return -1
        if other.timestamp is None and self.timestamp is not None:
            return +1

        # Compare all attributes in turn, but stop as soon as cmp()
        # returns true (non-zero, ie. non-equal) for an attribute.
        return (cmp(self.timestamp, other.timestamp) or
                cmp(self.event_code, other.event_code) or
                cmp(self.user_id, other.user_id) or
                cmp(self.message, other.message))

